FileViewer

GitHub License

View in Github

简介

FileViewer 是一款自用的纯前端的文件预览工具,无需后端支持,即可在浏览器中直接预览多种常见文件类型。
该工具专为 NJUST-docs 项目开发,可能并不适用于所有使用场景,且代码极其混乱,但仍然开放源码供参考。
目前仍在开发中,已支持以下文件格式:

功能特性

使用方法

使用以下格式访问 FileViewer 进行文件预览:

http://example.com/autoviewer.html?url={文件链接}

{文件链接} 替换为实际文件的 URL,即可在浏览器中打开进行预览。

关于 PDF.js 的使用

这里引入 PDF.js 的方式可能不太优雅,直接把 buildweb 目录全部复制过来了。同时可能缺少对音视频格式和其他 Office 格式的预览,但问题不大。

跨域限制与调整 由于 PDF.js 具有跨域限制,如果预览器和 PDF 文件的来源不同,可能会遇到加载失败的问题。这是由于 /pdfjs/web/viewer.mjs 中的 validateFileURL 代码所致:

const HOSTED_VIEWER_ORIGINS = new Set(["null", "http://mozilla.github.io", "https://mozilla.github.io"]);
var validateFileURL = function (file) {
    if (!file) return;
    const viewerOrigin = URL.parse(window.location)?.origin || "null";
    if (HOSTED_VIEWER_ORIGINS.has(viewerOrigin)) return;
    const fileOrigin = URL.parse(file, window.location)?.origin;
    if (fileOrigin === viewerOrigin) return;

    throw new Error("file origin does not match viewer's");
};

为解决此问题,可直接修改该函数,使其跳过验证:

var validateFileURL = function (file) {
    return; // 直接跳过验证,允许所有来源的 PDF 加载
};

部署步骤

  1. 下载代码
    git clone https://github.com/NJUST-OpenLib/FileViewer.git
    
  2. 上传文件
    将项目文件上传到 Web 服务器的指定目录。

  3. 注意跨域问题
    autoviewer 不支持直接通过本地文件访问,否则会遇到 CORS 问题。请使用 HTTP 服务器访问。

  4. 访问预览器
    在浏览器中访问以下链接,即可预览文件:
    http://example.com/autoviewer.html?url={文件链接}
    

iframe 嵌入示例

这个工具主要是方便我直接 iframe 嵌入使用,以下是示例代码:

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>URL 弹窗预览</title>
    <style>
        body {
            font-family: sans-serif;
            margin: 20px;
        }
        input[type="text"] {
            width: 300px;
            padding: 8px;
        }
        button {
            padding: 8px 12px;
            margin-left: 8px;
            cursor: pointer;
        }
        .modal {
            display: none;
            position: fixed;
            z-index: 1000;
            left: 0;
            top: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.5);
        }
        .modal-content {
            background: #fff;
            margin: 5% auto;
            width: 80%;
            height: 80%;
            border: 1px solid #888;
            display: flex;
            flex-direction: column;
        }
        .modal-header {
            background-color: #f0f0f0;
            padding: 10px;
            border-bottom: 1px solid #888;
            position: relative;
        }
        .close {
            position: absolute;
            top: 50%;
            right: 15px;
            transform: translateY(-50%);
            font-size: 20px;
            font-weight: bold;
            cursor: pointer;
        }
        .modal-iframe-container {
            flex: 1;
        }
        .modal iframe {
            width: 100%;
            height: 100%;
            border: none;
        }
    </style>
</head>
<body>
    <input type="text" id="urlInput" placeholder="请输入 URL">
    <button id="openBtn">打开弹窗</button>
    <div id="modal" class="modal">
        <div class="modal-content">
            <div class="modal-header">
                <span class="close" id="closeBtn">&times;</span>
            </div>
            <div class="modal-iframe-container">
                <iframe id="modalIframe" src=""></iframe>
            </div>
        </div>
    </div>
    <script>
        const openBtn = document.getElementById('openBtn');
        const closeBtn = document.getElementById('closeBtn');
        const modal = document.getElementById('modal');
        const modalIframe = document.getElementById('modalIframe');
        const urlInput = document.getElementById('urlInput');

        openBtn.addEventListener('click', function () {
            const url = urlInput.value.trim();
            if (url) {
                modalIframe.src = 'http://example.com/autoview.html?url=' + encodeURIComponent(url);
                modal.style.display = 'block';
            } else {
                alert('请输入有效的 URL');
            }
        });

        closeBtn.addEventListener('click', function () {
            modal.style.display = 'none';
            modalIframe.src = '';
        });

        window.addEventListener('click', function (event) {
            if (event.target === modal) {
                modal.style.display = 'none';
                modalIframe.src = '';
            }
        });
    </script>
</body>
</html>

许可证

本项目基于 MIT 许可证 进行开源。

第三方库的使用

本项目使用以下库和资源来实现文件预览功能:

本项目使用 subframe7536 提供的字体 Maple Mono NF-CN _

— 这是优化后的完整版本,结合了你的新内容,使整体表达更严谨、清晰:

注意

本项目在预览 PDF 文件时可能会产生额外的网络请求,进而增加流量消耗。

pdf.js 的请求行为

在加载 PDF 文件时,pdf.js 首先需要读取 文件尾部 以解析 交叉引用表(Cross-Reference Table, XRef),从而定位 根对象(Root Object) 并构建文档结构。随后,它会遍历 /Pages 树,以获取所有页面的元数据(如页面尺寸、资源引用等)。

如果 所有 /Page 对象均直接位于 /Pages 树的顶层,解析器需要一次性读取这些对象的位置信息。而由于 交叉引用表通常存储在文件末尾,pdf.js 可能需要下载整个文件,才能完成元数据解析。这种情况下,即便启用了 分片请求(Range Request),仍可能导致整份 PDF 被完整下载,从而增加流量消耗。

在解析出足够的元数据后,pdf.js 会根据 disableRangedisableStream 这两个参数决定是否继续使用 分片请求流式传输

若服务器正确暴露了 Accept-Ranges 头部,则 pdf.js 会发送 Range 请求,其中每个请求的大小由 default_chunk_size 参数控制。此外,disableAutoFetch 参数决定是否自动获取所有分片。

导致额外流量消耗的情况

pdf.js 的 分片加载机制 在某些情况下可能导致额外的流量消耗。例如:

在相关讨论中,pdf.js 贡献者最初认为该问题是由于错误的配置(尽管我们使用的是官方 Demo),随后又认为可能是 “格式不规范的 PDF 文件” 导致的。然而,我对多个电子书 PDF 进行了测试,均存在类似问题。因此,这可能是 pdf.js 在处理某些 扫描类 PDF 时的兼容性问题,正如贡献者所述:

“All /Page-objects are placed directly at the top-level of the /Pages-tree and trying to improve this one (bad case).”

究竟是 pdf.js 的问题,还是 PDF 文件本身的问题,目前尚无定论,甚至可能二者都不是。然而可以确定的是,该问题与本项目无关

建议

为尽量减少无用流量消耗,建议制作标准的PDF。同时保持 pdf.js(或 PDF.mjs)的 default_chunk_size 值较小,例如 65536(默认值)。即便发生额外请求,最多也只会增加 约 5MB 的流量消耗。
如果 default_chunk_size 被增大到 65536 × 16,则流量消耗会更高。

原始讨论