简介:这份资源面向需要在Web项目中集成文件在线预览能力的开发者,提供一套基于HTML与JavaScript的轻量实现方案,覆盖PDF、Excel、PPT、DOC、JPG、PNG等常见格式,适合前端入门到中级水平、希望快速落地预览功能的读者参考。压缩包共6个文件,约238KB,包含示例网页、jQuery及媒体预览脚本、测试文档与图片素材,以及一份使用说明,便于对照理解页面结构与脚本调用方式。资源已有27285人学习下载,具备一定的实践参考基础。读者可从中获取可直接运行的预览示例页面,理解PDF借助object/iframe嵌入、图片通过img标签展示、Office文档依赖第三方查看服务或服务端转换的差异化处理思路,并借助说明文档与测试样本完成本地验证与二次改造,为构建用户友好的文件预览系统提供可复用的起点。
1. 浏览器里直接打开 PDF、Excel、PPT:一个纯前端文件预览方案能解决什么
后台管理系统里最容易被低估的需求,就是“点一下附件就能看”。用户上传了合同 PDF、报价 Excel、汇报 PPT、扫描件 JPG,你不可能要求每个使用者本地装 Office 或阅读器,也不该为了预览把文件先下载到本地再打开。这个项目用 HTML + JavaScript 在浏览器端完成 pdf、excel、ppt、doc、jpg、png 的在线预览,核心思路是把不同格式分派给对应的渲染路径:PDF 交给浏览器原生或 PDF.js,图片直接走<img>,Office 文档则依赖在线转换服务或前端解析库兜底。它适合正在做 OA、合同管理、教学资料库、工单附件预览的前端和全栈开发者,尤其是那些不想引入重型文档中台、希望先跑通一条预览链路的小团队。下面按“能干什么 → 怎么跑 → 怎么改”拆开讲。
2. 技术选型与整体架构:一个入口如何分派五种文件格式
2.1 为什么不做“一个库通吃”,而是按格式分派
很多人第一反应是找一个万能预览库,但现实里没有哪个前端库能同时高质量渲染 PDF、Excel、PPT、Word 和图片。PDF 有 PDF.js 这种成熟方案,图片浏览器原生支持,而 Office 三件套(doc/docx、xls/xlsx、ppt/pptx)在前端直接渲染的复杂度极高,常见做法是走“转换后预览”或“服务端转 PDF/图片再展示”。这个项目的架构本质是一个格式路由器:拿到文件扩展名或 MIME 类型,决定走哪条渲染分支。
我一般会把预览层拆成三层:
| 层级 | 职责 | 典型实现 |
|---|---|---|
| 入口层 | 接收文件 URL / File 对象,识别类型 | 扩展名 + MIME 双重判断 |
| 分派层 | 按类型选择渲染器 | switch / 策略表 |
| 渲染层 | 具体格式的展示逻辑 | iframe、img、PDF.js、转换服务 |
这样拆的好处是:以后要加 Markdown、视频、CAD 预览,只需要在分派层注册一个新策略,入口和页面结构不用动。血泪经验是,很多项目一开始把判断逻辑写死在点击事件里,后面加一种格式就要改三处,维护成本直接翻倍。
2.2 目录结构长什么样
一个能跑起来的最小工程,目录通常是这样组织的:
file-preview-demo/ ├── index.html # 预览入口页,含文件列表和预览容器 ├── css/ │ └── preview.css # 预览容器、工具栏、加载态样式 ├── js/ │ ├── main.js # 页面初始化、文件点击事件绑定 │ ├── dispatcher.js # 格式识别与渲染器分派 │ └── renderers/ │ ├── pdf.js # PDF 渲染(iframe 或 PDF.js) │ ├── image.js # 图片渲染 │ └── office.js # Office 文档渲染(转换服务 / 兜底下载) ├── lib/ │ └── pdfjs/ # PDF.js 静态资源(可选) └── assets/ └── samples/ # 示例文件,便于本地验证这个结构不追求工程化脚手架,目的是让读者下载后直接双击index.html或起一个静态服务就能看到效果。dispatcher.js是整个项目的“大脑”,renderers/下每个文件只关心自己那一种格式怎么画出来。
2.3 格式识别的核心代码
先看入口层怎么判断文件类型。不要只信扩展名,用户把.xlsx改成.pdf的情况并不少见,所以扩展名和 MIME 要一起看:
// dispatcher.js const RENDERER_MAP = { pdf: 'pdf', jpg: 'image', jpeg: 'image', png: 'image', gif: 'image', webp: 'image', doc: 'office', docx: 'office', xls: 'office', xlsx: 'office', ppt: 'office', pptx: 'office' }; /** * 根据文件名和可选的 MIME 类型返回渲染器名称 * @param {string} fileName 文件名,如 "合同.pdf" * @param {string} [mimeType] 浏览器给出的 MIME,如 "application/pdf" * @returns {string} 渲染器 key,未知类型返回 'unknown' */ export function resolveRenderer(fileName, mimeType) { const ext = (fileName.split('.').pop() || '').toLowerCase(); if (RENDERER_MAP[ext]) return RENDERER_MAP[ext]; // 扩展名不可靠时,用 MIME 兜底 if (mimeType?.startsWith('image/')) return 'image'; if (mimeType === 'application/pdf') return 'pdf'; if (mimeType?.includes('officedocument') || mimeType?.includes('msoffice')) { return 'office'; } return 'unknown'; }这段代码的关键点有三个:第一,RENDERER_MAP用对象做策略表,比一长串if/else好扩展;第二,扩展名优先、MIME 兜底,兼顾速度和准确性;第三,未知类型统一返回unknown,交给上层决定是提示“暂不支持”还是直接给下载链接。参数上fileName必须传,mimeType可选,实际项目里可以从File.type或响应头Content-Type拿到。
2.4 渲染器分派与容器挂载
识别出类型后,下一步是把对应渲染器挂到预览容器里。这里用一个统一的render函数收口:
// dispatcher.js import { renderPdf } from './renderers/pdf.js'; import { renderImage } from './renderers/image.js'; import { renderOffice } from './renderers/office.js'; const RENDERERS = { pdf: renderPdf, image: renderImage, office: renderOffice }; /** * 把文件渲染到指定容器 * @param {HTMLElement} container 预览容器 * @param {string} fileUrl 文件地址(同源或允许跨域) * @param {string} fileName 文件名 */ export async function renderFile(container, fileUrl, fileName) { container.innerHTML = '<div class="loading">加载中…</div>'; const key = resolveRenderer(fileName); const renderer = RENDERERS[key]; if (!renderer) { container.innerHTML = ` <div class="unsupported"> 暂不支持该格式预览,<a href="${fileUrl}" download>点击下载</a> </div>`; return; } try { await renderer(container, fileUrl, fileName); } catch (err) { console.error('[preview] render failed:', err); container.innerHTML = ` <div class="error"> 预览失败,<a href="${fileUrl}" download>点击下载原文件</a> </div>`; } }逻辑说明:先清空容器并显示加载态,避免用户以为页面卡死;然后查策略表,查不到就给下载兜底;查到就await渲染器执行,任何异常都落到统一的错误提示,并且始终保留“下载原文件”这条退路。参数container是 DOM 节点,fileUrl建议用同源地址或已配置 CORS 的地址,否则 PDF.js 和转换服务都可能被浏览器拦截。
3. 三种渲染路径落地:PDF、图片、Office 各自怎么跑通
3.1 PDF 渲染:iframe 与 PDF.js 怎么选
PDF 是最容易做好的格式,因为现代浏览器基本内置了 PDF 阅读器。最省事的做法是直接塞 iframe:
// renderers/pdf.js export function renderPdf(container, fileUrl) { const iframe = document.createElement('iframe'); iframe.src = fileUrl; iframe.className = 'pdf-frame'; iframe.title = 'PDF 预览'; container.innerHTML = ''; container.appendChild(iframe); }这段代码没有任何依赖,fileUrl指向 PDF 文件即可,浏览器会用自己的阅读器打开,支持缩放、翻页、搜索。缺点是样式不可控,移动端体验参差,部分浏览器会强制下载而不是内嵌显示。
如果要求统一 UI、支持页码跳转和文本层,就上 PDF.js。常见做法是把pdfjs-dist的静态资源放到lib/pdfjs/,然后:
// renderers/pdf.js(PDF.js 版本) import * as pdfjsLib from '../lib/pdfjs/pdf.mjs'; pdfjsLib.GlobalWorkerOptions.workerSrc = '../lib/pdfjs/pdf.worker.mjs'; export async function renderPdf(container, fileUrl) { const pdf = await pdfjsLib.getDocument(fileUrl).promise; const canvas = document.createElement('canvas'); container.innerHTML = ''; container.appendChild(canvas); const page = await pdf.getPage(1); // 先渲染第一页 const viewport = page.getViewport({ scale: 1.5 }); canvas.width = viewport.width; canvas.height = viewport.height; await page.render({ canvasContext: canvas.getContext('2d'), viewport }).promise; }参数说明:scale控制清晰度,1.5 在普通屏幕上够用,打印场景可以调到 2 以上;getPage(1)只渲染首页,多页预览需要循环或加翻页控件。注意workerSrc必须指向真实存在的 worker 文件,路径写错时控制台会报 “Setting up fake worker failed”,这是最常见的翻车点。
3.2 图片渲染:别小看一个 img 的边界情况
图片分支看起来最简单,但实际项目里坑不少:大图撑破容器、透明 PNG 背景看不清、加载失败没有提示。一个稳妥的实现:
// renderers/image.js export function renderImage(container, fileUrl, fileName) { const img = new Image(); img.className = 'preview-image'; img.alt = fileName; img.onload = () => { container.innerHTML = ''; container.appendChild(img); }; img.onerror = () => { container.innerHTML = ` <div class="error">图片加载失败,<a href="${fileUrl}" download>点击下载</a></div>`; }; img.src = fileUrl; }逻辑上先绑定onload/onerror再赋src,避免缓存命中时事件不触发。样式侧用max-width: 100%; max-height: 100%; object-fit: contain;保证不溢出容器。如果图片来自不同源且服务端没开 CORS,onerror会触发,这时要么让后端加响应头,要么走同源代理,别在前端硬扛。
3.3 Office 文档渲染:前端做不了的部分怎么兜底
doc、xls、ppt 是这套方案里最需要说清楚的部分。纯前端直接解析并高保真渲染 Office 文档,工程量极大,常见做法有三条路:
- 服务端转 PDF/图片:上传后异步转成 PDF,前端统一按 PDF 预览。保真度最高,但需要后端和转换组件。
- 在线转换服务:把文件地址交给第三方转换接口,拿回一个可嵌入的预览页。接入快,但涉及文件外发,敏感场景要谨慎。
- 前端轻量解析:用 SheetJS 读 xlsx 表格数据、用 mammoth.js 把 docx 转 HTML。适合只需要看内容、不要求版式的场景。
这个项目作为纯前端 Demo,Office 分支我建议先做“能看内容”的降级方案,代码上留好扩展点:
// renderers/office.js export async function renderOffice(container, fileUrl, fileName) { const ext = fileName.split('.').pop().toLowerCase(); // xlsx 用 SheetJS 转成 HTML 表格,适合数据类文件 if (ext === 'xlsx' || ext === 'xls') { const resp = await fetch(fileUrl); const buf = await resp.arrayBuffer(); const wb = XLSX.read(buf, { type: 'array' }); const firstSheet = wb.Sheets[wb.SheetNames[0]]; const html = XLSX.utils.sheet_to_html(firstSheet); container.innerHTML = `<div class="sheet-wrap">${html}</div>`; return; } // docx 用 mammoth 转 HTML,只保留内容结构 if (ext === 'docx') { const resp = await fetch(fileUrl); const buf = await resp.arrayBuffer(); const result = await mammoth.convertToHtml({ arrayBuffer: buf }); container.innerHTML = `<article class="doc-body">${result.value}</article>`; return; } // ppt/ppt 及旧版 doc/xls:前端不硬扛,给下载或接转换服务 container.innerHTML = ` <div class="fallback"> 该格式建议通过服务端转换后预览, <a href="${fileUrl}" download>先下载原文件</a> </div>`; }参数说明:XLSX.read的type: 'array'表示传入 ArrayBuffer;sheet_to_html只渲染第一个工作表,多 sheet 需要加标签页切换。mammoth.convertToHtml返回的value是 HTML 字符串,样式需要自己补。注意旧版.doc、.ppt二进制格式这两个库支持有限,别指望前端一把梭,老老实实走转换服务或下载兜底,这是踩过坑之后的结论。
3.4 页面入口怎么串起来
最后把文件列表和预览容器串起来,index.html里放一个列表和一个右侧容器,main.js绑定点击:
// main.js import { renderFile } from './dispatcher.js'; const list = document.querySelector('#file-list'); const viewer = document.querySelector('#viewer'); list.addEventListener('click', (e) => { const item = e.target.closest('[data-url]'); if (!item) return; const { url, name } = item.dataset; renderFile(viewer, url, name); });>class FilePreview { constructor(container) { this.container = container; this.current = null; } async open(url, fileName) { this.destroy(); // 先清理上一个渲染结果 this.current = { url, fileName }; await renderFile(this.container, url, fileName); } destroy() { this.container.innerHTML = ''; // 释放 DOM 引用 this.current = null; } }
改造要点:destroy里除了清空 DOM,如果用了 PDF.js 还要调用pdf.destroy()释放 worker 资源;如果用了 blob URL,记得URL.revokeObjectURL。这些清理动作在单页应用里尤其重要,否则来回切换几十个文件后内存会明显上涨。
5.2 加一层文件类型白名单和大小限制
生产环境不能什么文件都往预览器里塞。建议在入口处加白名单和大小校验,超限直接提示下载:
const MAX_PREVIEW_SIZE = 20 * 1024 * 1024; // 20MB function canPreview(file) { const allowed = ['pdf', 'jpg', 'jpeg', 'png', 'docx', 'xlsx']; const ext = file.name.split('.').pop().toLowerCase(); if (!allowed.includes(ext)) return { ok: false, reason: '格式不支持预览' }; if (file.size > MAX_PREVIEW_SIZE) return { ok: false, reason: '文件过大,请下载查看' }; return { ok: true }; }白名单和大小阈值都应该是配置项,不同业务线要求不一样。合同系统可能允许 50MB 的 PDF,而工单附件 10MB 就够。把阈值写死在代码里,后面每来一个需求就要改一次,不如一开始就抽成配置。
5.3 验证方法:三类文件各跑一遍
改完之后怎么确认没坏?我的习惯是准备一组固定样本:一个多页 PDF、一张带透明通道的 PNG、一个多 sheet 的 xlsx、一个带图片的 docx、一个旧版 doc。每次改动后依次点一遍,重点看控制台有没有报错、切换文件后上一个预览是否被清理、失败时下载链接是否可用。再补一个弱网场景,把浏览器网络调成 Slow 3G,确认加载态能正常显示而不是白屏。
5.4 一个具体技巧:用统一错误边界兜住所有渲染器
所有渲染器都可能抛异常,与其在每个渲染器里写 try/catch,不如在分派层统一兜。前面renderFile里已经这么做了,但还可以再进一步:把错误信息上报到一个回调,方便排查线上问题:
export async function renderFile(container, fileUrl, fileName, onError) { try { // ...分派逻辑 } catch (err) { onError?.(err, { fileUrl, fileName }); container.innerHTML = `<div class="error">预览失败,<a href="${fileUrl}" download>下载原文件</a></div>`; } }onError是可选参数,业务侧可以传一个上报函数,把文件名、错误堆栈、浏览器信息一起发出去。这样线上用户说“打不开”时,你手里有据可查,而不是靠猜。从那以后我每次接预览类需求,都强制先把这个错误边界和下载兜底写进去,再谈渲染效果——预览可以降级,但用户永远要有路可走。希望帮到你。
本文还有配套的精品资源,点击获取