ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Vue项目实现PDF、Word、Excel在线预览的完整方案与踩坑记录

Vue项目实现PDF、Word、Excel在线预览的完整方案与踩坑记录 前阵子给一个政务类的后台管理系统做文件预览模块需求听起来很常规把服务器上的PDF、Word、Excel在页面上直接打开看点击下载按钮才进入下载流程。我一开始也没当回事觉得PDF随手就能弄Word和Excel顶多找两个库的事。结果真东西做下来从选型到兼容性再到性能折腾了小半个月。这篇文章就当一次复盘把我在Vue项目里集成 PDF、Word、Excel 预览的完整思路、核心代码片段和踩坑经历梳理出来给同样被这个需求砸中的朋友一点参考。不管你是刚接到这种需求的半小白还是已经踩过几个坑的初级前端按这条路线走至少能少走几段弯路。1. 接到页面里直接预览文件需求我先做了三件事1.1 摸清浏览器到底能白嫖哪一步很多人第一反应是PDF 浏览器自己就能开啊iframe或者直接给个链接不就完事了这话对了一半。桌面端 Chrome、Edge、Firefox 确实原生支持 PDF 预览你只要丢一个.pdf地址给浏览器它自己就会调起内置阅读器。但这个方案有三个致命问题移动端体验完全不可控。安卓手机浏览器对 PDF 的处理策略五花八门有的直接唤起下载有的甩给第三方App有的白屏没反应。无法做精细控制。你要实现只能看正文、不能下载基本没戏用户随手一按 CtrlS 文件就跑走了。样式和交互不可控。你不会想让用户停留在浏览器的内置阅读器里翻页、放大缩小、页码跳转全都不可控跟业务页面的设计语言完全割裂。Word 和 Excel 就更不用说了浏览器原生压根不认。所以实际情况是PDF 必须上 pdf.js 这类渲染库Word 和 Excel 必须借助前端解析库转成 HTML 或数据表格这是绕不过去的第一步认知。1.2 确认文件来源接口流、静态URL、本地上传预览逻辑完全不同我在动手前先问了后端同事一个问题文件从哪来这直接决定了整个组件的输入设计。常见的三种场景后端返回一个可访问的静态URL比如https://files.example.com/contract/2024/xxx.pdf。这种情况最简单直接把 URL 丢给预览组件就行。后端返回二进制流前端用固定接口地址去请求一般是通过/api/file/download?fileIdxxx这样的接口返回application/octet-stream或对应 MIME 类型。这种场景下前端 fetch 或 axios 拿到的是Blob/ArrayBuffer需要自己拼接对象地址或直接传给解析库。用户本地选择文件后本地预览比如用户先上传到服务器前想先确认内容对不对。这种直接用FileReader把File对象转成ArrayBuffer再喂给解析库。我最后的设计是预览组件同时支持传入 URL 和本地 File 对象内部统一处理。组件接收的文件地址先走一次fetch如果是本地文件就直接转ArrayBuffer这样前端调用方只需要关心我有个地址还是我有个文件。1.3 技术选型pdf.js docx-preview SheetJS 的组合怎么确定的选型的时候我列了一个对比表格逐个过了一遍每个方案在 Vue3 Vite 项目里的接入成本文件类型方案优点缺点PDFpdf.js官方库渲染质量高、可精细控制交互、社区庞大需要自己封装组件配置 Worker 有门槛PDFvue-pdf上手快API简单更新慢底层 pdf.js 版本旧多页渲染需要循环处理PDFiframe / embed0成本可控性差移动端兼容差不能防下载Worddocx-preview还原度高支持页眉页脚、表格、图片只认 .docx文件大时渲染较慢Wordmammoth.js轻量输出 HTML 干净还原度一般复杂版式会丢ExcelSheetJSxlsx社区版免费解析能力强样式还原有限大数据量需优化Excelexceljs读写都强偏向数据处理渲染表格仍需自建ExcelLuckysheet / Univer接近在线表格体验体积大重依赖仅限 Excel 场景最后定下来的组合是PDF 部分用 pdf.js 官方库自己封装Word 用 docx-previewExcel 用 SheetJS 解析成二维数组后配合 el-table 渲染。这套组合的通用性最强不依赖任何 UI 框架换到 Element 之外的其他组件库也能用。2. PDF预览放弃iframe用pdf.js做到可翻页、可缩放2.1 pdf.js 的 Worker 配置是第一个坑pdf.js 是 Mozilla 团队维护的官方 PDF 渲染库核心思路是用 Worker 线程解析 PDF 文件主线程负责把每一页画到 Canvas 上。直接安装依赖npm install pdfjs-dist装完以后第一个坑就来了必须告诉 pdf.js 去哪里找 Worker 文件。import * as pdfjsLib from pdfjs-dist import workerUrl from pdfjs-dist/build/pdf.worker.min.js?url pdfjsLib.GlobalWorkerOptions.workerSrc workerUrl在 Vite 里?url后缀会把这个 worker 文件作为静态资源打包并返回最终部署路径。这一步千万不能省忘记配置 Worker 的话pdf.js 会退回到主线程解析轻则卡顿重则直接白屏报错。如果你的项目用的是 Webpack要写成import workerUrl from pdfjs-dist/build/pdf.worker.min.js然后配合file-loader或者worker-loader。内网部署环境特别要注意别用 CDN 链接一定要把 worker 文件放到自己的静态资源目录不然生产环境一旦访问不了公网 CDN整个预览功能就挂了。2.2 两段核心代码加载文档 渲染页面pdf.js 的 API 设计得很清晰核心就是getDocument加载 PDF 文件然后逐页渲染。下面是一个最小可用的封装import * as pdfjsLib from pdfjs-dist import workerUrl from pdfjs-dist/build/pdf.worker.min.js?url pdfjsLib.GlobalWorkerOptions.workerSrc workerUrl export async function loadPdf(urlOrBuffer) { const loadingTask pdfjsLib.getDocument({ data: urlOrBuffer instanceof ArrayBuffer ? urlOrBuffer : undefined, url: typeof urlOrBuffer string ? urlOrBuffer : undefined }) const pdf await loadingTask.promise return pdf }拿到pdf对象后渲染指定页码到 Canvasexport async function renderPdfPage(pdf, pageNumber, canvas) { const page await pdf.getPage(pageNumber) const baseViewport page.getViewport({ scale: 1 }) // 适配高清屏 const dpr window.devicePixelRatio || 1 const scale dpr const viewport page.getViewport({ scale }) canvas.width viewport.width canvas.height viewport.height canvas.style.width ${baseViewport.width}px canvas.style.height ${baseViewport.height}px const context canvas.getContext(2d) const renderContext { canvasContext: context, viewport } await page.render(renderContext).promise return baseViewport }这里有个细节值得说明Canvas 的物理像素和 CSS 像素要区分开。如果直接用viewport.width设置 Canvas 宽度在 Retina 屏上会模糊。我上面的写法是用devicePixelRatio放大 Canvas 的物理尺寸再用style.width固定 CSS 显示尺寸这样在高分屏上渲染的 PDF 文字边缘是清晰的。还有一个更隐蔽的问题渲染大页面时Canvas 支持的最大面积是有限制的尤其移动端。如果 PDF 页面尺寸异常比如画报类的超宽页面Canvas 可能直接报错。我通常会在渲染前加一个保护const maxCanvasArea 16384 * 16384 if (viewport.height * viewport.width maxCanvasArea) { // 缩小 scale或提示用户该页面过大 }2.3 翻页、缩放、页码显示一个最小封装就够了把上面两个函数组合起来就能做一个简单的 PDF 预览组件。核心状态就是pageNum和totalPagestemplate div classpdf-preview div refcanvasWrap classpdf-canvas-wrap canvas refcanvas/canvas /div div classpdf-toolbar button :disabledpageNum 1 clickpageNum--上一页/button span{{ pageNum }} / {{ totalPages }}/span button :disabledpageNum totalPages clickpageNum下一页/button button clickzoomOut缩小/button span{{ Math.round(scaleRatio * 100) }}%/span button clickzoomIn放大/button /div /div /template缩放逻辑我建议用重新渲染当前页的方式而不是用 CSStransform去缩放 Canvas。因为 CSS 缩放会让人觉得字是糊的重新用page.getViewport({ scale: newScale })渲染出来的才是矢量级清晰度。需要提醒的是翻页时最好加一个渲染中的状态因为 pdf.js 每一页渲染都是异步任务用户快速点击翻页时容易产生竞态——上一次渲染还没结束下次渲染就开始了画面会闪跳。一个简单的处理是在渲染前用Promise判断当前渲染任务是否是最新的或者直接用一个renderId标记渲染完成后只有最新的一次才更新 UI。3. Word预览docx-preview 的实现和它解决不了的.doc3.1 为什么选 docx-preview 而不是 mammoth.jsWord 预览的方案我在 mammoth.js 和 docx-preview 之间犹豫了很久。mammoth.js 主打的是把 docx 转成干净的 HTML它的设计哲学是只保留内容结构丢掉复杂的版式。如果你的 Word 文档主要是纯文字报告mammoth 生成的 HTML 非常轻量解析速度也快。但一旦文档里有页眉页脚、批注、修订、复杂表格、嵌入图片mammoth 基本就力不从心了。docx-preview 的思路更贴合预览这个需求——它尽力去还原 docx 在 Word 里的排版效果包括分页、页边距、表格边框、段落间距等。虽然做不到 100% 像素级还原但对用户来说看到的东西和本地打开 Word 差不多这就足够了。最终我选 docx-preview原因就一句话预览工具的首要目标是像而不是轻。我们的用户里有很多是业务部门的同事他们判断预览功能好不好用的第一标准就是跟我电脑上打开像不像。安装命令npm install docx-preview3.2 renderAsync 从拿到 Blob 到页面展示的完整链路docx-preview 的使用方式和 pdf.js 完全不同它只需要一个容器 DOM 和一个Blob/ArrayBuffer剩下的渲染全自动完成。完整代码如下import { renderAsync } from docx-preview export async function previewWord(fileSource, container) { let blob if (fileSource instanceof Blob) { blob fileSource } else if (fileSource instanceof ArrayBuffer) { blob new Blob([fileSource]) } else if (typeof fileSource string) { const res await fetch(fileSource) if (!res.ok) throw new Error(Word 文件加载失败) blob await res.blob() } else { throw new Error(不支持的 Word 文件来源) } await renderAsync(blob, container, null, { className: docx-preview-wrapper, inWrapper: true, ignoreWidth: false, ignoreHeight: false, ignoreFonts: false, breakPages: true, experimental: false }) }渲染时几个配置项值得理解breakPages: true表示按分页符切分这样容器里能看到文档的分页效果否则所有内容会连成一长条。ignoreWidth: false表示尊重 Word 文档里设定的页面宽度如果想强制占满容器宽度可以设成true。inWrapper: true会在容器外包裹一层带上默认的内边距样式方便做预览卡片效果。还有一个很容易被忽略的点docx-preview 不是数据驱动而是直接往 DOM 里塞内容。这带来的问题是如果同一个容器渲染了两次前一次渲染的内容不会自动清空会叠加在一起。所以在渲染前一定要先清空容器container.innerHTML await renderAsync(blob, container)这个问题看似不起眼但我在实际开发里真的遇到过——用户先预览了一个 A 文档再点另一个 B 文档页面上 A 的残留内容没清掉新版内容直接堆在了下面看着跟排版错乱一样。3.3 .doc 老格式的三个处理方向这里必须说清楚一个残酷的事实docx-preview 只支持 .docx不支持老版本的 .doc。.doc 是 Word 97-2003 时代的二进制格式解析难度比 OpenXML 的 .docx 高一个数量级前端几乎没有成熟的开源解决方案。我在项目里处理 .doc 的方案有三个方向按优先级排序后端转换推荐后端用 LibreOffice 或 OnlyOffice 把 .doc 转成 .docx 或 PDF再返回给前端预览。这是最稳妥的方案转换质量高而且后端可以缓存转换结果。缺点是依赖后端同事配合。前端降级提示检测到文件后缀是 .doc 时直接提示该文件为旧版 Word 格式暂不支持在线预览请下载后查看并给一个明显的下载按钮。转存为 .docx引导用户用 WPS 或 Word 把 .doc 另存为 .docx 再上传。我最后项目里做的是组合拳前端识别 .doc 后缀先尝试请求后端转换接口转换接口不可用或者超时就降级为下载提示。这样既保证了对老文件的基本可用性又不会把前端搞成一个笨重的格式转换器。4. Excel预览SheetJS 解析成数据再渲染比原样还原更实用4.1 sheet_to_html 和 sheet_to_json 两条路线的取舍Excel 的预览方案比前两类复杂因为 Excel 文件本质上是一个容器里面装的是工作表、单元格、公式、样式、合并单元格等各种元素。到底该怎么在网页里呈现它业界分两派一派是原样还原路线代表是 Luckysheet 和 Univer 这类在线表格引擎。它们在网页里模拟 Excel 的完整交互能看公式、能拖拽、能合并单元格体验确实好。代价是体积巨大、依赖复杂、上手门槛高而且如果只是做只读预览这个重武器有点杀鸡用牛刀。另一派是数据转化路线代表就是 SheetJSxlsx库。它先把 xlsx 解析成 JSON、二维数组或 HTML 字符串然后再用前端表格组件渲染。这条路的好处是灵活、轻量数据可以加工、可以做搜索排序组件风格也可以跟项目 UI 统一。坏处是原样还原能力有限带复杂样式的 xlsx 可能显示得不够像。我的取舍逻辑很简单后台管理系统的用户看 Excel 主要是看数据内容比如核对报表、查看名单、确认导入模板而不是在网页里做表格编辑。所以数据可读、性能稳定、结合 UI 框架比像素级还原重要得多。最后选了 SheetJS 解析 el-table 渲染的方案。4.2 解析 xlsx 到二维数组的完整封装安装 SheetJS 社区版npm install xlsx核心解析逻辑如下import * as XLSX from xlsx export async function parseExcel(fileSource, { maxRows 500 } {}) { let data if (fileSource instanceof ArrayBuffer) { data fileSource } else if (typeof fileSource string) { const res await fetch(fileSource) if (!res.ok) throw new Error(Excel 文件加载失败) data await res.arrayBuffer() } else { throw new Error(不支持的 Excel 文件来源) } const workbook XLSX.read(data, { type: array, cellDates: true, cellNF: false }) const sheetName workbook.SheetNames[0] const sheet workbook.Sheets[sheetName] const rows XLSX.utils.sheet_to_json(sheet, { header: 1, defval: , range: maxRows }) return { sheets: workbook.SheetNames, activeSheet: sheetName, rows, totalRows: sheet[!ref] ? XLSX.utils.decode_range(sheet[!ref]).e.r 1 : 0 } }这里有几个参数我必须解释因为它们直接影响解析结果type: array告诉 SheetJS 输入的原始数据是二进制数组。cellDates: true让日期单元格解析成 JS 的Date对象否则会得到一个 Excel 序列号数字比如45293这样的值前端还得自己换算成日期。header: 1让sheet_to_json返回二维数组而不是对象数组。二维数组的好处是渲染成表格时每一行都好处理。defval: 空白单元格默认值设为空字符串避免后续渲染出现undefined。range: maxRows只读取前 N 行这是性能优化的关键。一个几十万行的 Excel 文件如果你全量解析浏览器直接卡死。限制行数后解析速度和渲染速度都能保持在秒级。拿到二维数组后在 Vue 模板里渲染就简单了template el-table :datatableData border stripe sizesmall el-table-column v-for(_, colIndex) in columns :keycolIndex :propcol_${colIndex} :labelcolumnLabel(colIndex) min-width120 show-overflow-tooltip / /el-table /template需要特别处理的是列数据。因为sheet_to_json返回的二维数组长度可能参差不齐某行有 10 列另一行只有 5 列是常态。我在渲染前会先遍历所有行算出最大列数然后每一行再补齐到统一长度。这样可以避免 el-table 渲染时某行少了一列而错位。4.3 大数据量预览的降级策略SheetJS 的解析速度本身很快但真正卡的是浏览器渲染表格 DOM。3万行数据全量塞进 el-table页面基本等着白屏。我的降级策略是分三档总行数策略 500 行直接渲染全部500 - 5000 行只渲染前 500 行页面提示仅展示前 500 行完整内容请下载查看 5000 行不解析文件内容直接弹窗提示下载也就是说上面的maxRows参数我用的是一个动态值。先根据文件大小和 SheetJS 读取到的总行数判断档位再决定传给sheet_to_json的行数上限。实际体验下来500 行以内渲染毫无压力用户看着也不会觉得怎么才这么点数据。还有个细节是合并单元格。如果 Excel 里有合并单元格sheet_to_json解析出的二维数组只在左上角单元格有值其余都是空字符串。如果业务上需要保留合并表现可以读取sheet[!merges]拿到合并区域信息再映射到 el-table 的span-method里。但这一步会显著增加代码复杂度我的建议是先跟需求方确认有没有需要看清合并单元格的场景没有就不用做。5. 统一封装成 FilePreview 预览组件按扩展名自动分发5.1 props 设计url、file、扩展名整个功能开发到这里我发现一个问题三个预览模块PDF、Word、Excel的加载状态、错误处理、组件入口是互相独立的如果直接在业务页面里各用各的调用方要写三份判断逻辑。所以我决定做一个统一的FilePreview组件对外只暴露两个核心 propsscript setup defineProps({ // 文件访问地址二选一 src: { type: String, default: }, // 本地 File 对象二选一 file: { type: File, default: null }, // 可选不传时根据文件名后缀自动识别 ext: { type: String, default: } }) /scriptext这个 prop 很多人可能觉得多余——从src里截取后缀不就行了但实际项目中有些后端返回的文件 URL 不带后缀比如/api/file/download?id123或者后缀被?tokenxxx污染了。这种情况下就必须给组件一个显式的文件类型提示。所以我的逻辑是优先用ext没有ext再从文件名或 URL 里解析后缀。5.2 内部按 type 分发到三个渲染模块组件内部的逻辑核心是确定预览类型。我把确定类型的逻辑独立成一个函数function resolvePreviewType(src, ext, file) { if (ext) return ext.toLowerCase() const fileName file ? file.name : (src || ) const match fileName.match(/\.([^.?#])($|[?#])/) if (match) return match[1].toLowerCase() return }然后根据类型分发到不同的子组件。模板部分长这样template div classfile-preview div v-ifloading classfile-preview-loading文件加载中.../div div v-else-iferror classfile-preview-error p{{ errorMessage }}/p /div template v-else PdfPreview v-ifpreviewType pdf :urlresolvedSrc loadonInnerLoad erroronInnerError / WordPreview v-else-ifpreviewType docx :urlresolvedSrc :filefile loadonInnerLoad erroronInnerError / ExcelPreview v-else-if[xlsx, xls, csv].includes(previewType) :urlresolvedSrc :filefile loadonInnerLoad erroronInnerError / div v-else classfile-preview-unsupported 暂不支持 {{ previewType || 该 }} 文件类型的在线预览 /div /template /div /template这里有个交互细节值得说一下每个子组件都是用自己的方式加载文件流的pdf.js 直接接受 URLdocx-preview 需要先 fetch 成 BlobSheetJS 需要 ArrayBuffer所以父组件不提前统一 fetch而是把原始src或file传给子组件让子组件自己处理。这样做的好处是职责清晰坏处是每个子组件都要写一遍加载逻辑。我的折中方案是封装一个loadFileSource的公共函数放在子组件里共用。5.3 加载态、错误态、空态的统一治理预览组件最容易被忽视的就是异常反馈。一个文件预览不了如果只是白屏用户会以为功能坏了实际上可能是网络问题、文件损坏、格式不支持、跨域被拦。我在这版组件里把状态机统一成三个loading、error、success。子组件加载成功后发load事件失败后发error事件父组件负责切换状态并展示错误信息。错误信息尽量做到人话可读// PDF.js 加载失败 PDF 文件解析失败文件可能已损坏或不是合法的 PDF 格式 // docx-preview 渲染失败 Word 文件渲染失败请检查文件是否为有效的 .docx 格式 // fetch 网络层错误 文件加载失败请检查网络连接或稍后重试有一个隐藏的状态也必须处理加载超时。大文件在弱网环境下fetch 可能要等很久。我给 fetch 包了一层AbortController设置 30 秒超时超时后主动中断并提示用户重新尝试。这个体验优化虽然只是一个小细节但在实际项目里避免了用户盯着转圈等两分钟的尴尬。6. 从内网环境到生产环境我踩过的一些坑6.1 文件流没拿到先检查 responseType先说一个前端新人特别容易踩的经典坑用 axios 请求文件接口忘记设置responseType: blob导致拿到的数据是一串乱码字符串。因为 axios 默认把响应当成 JSON 解析二进制流被强行转成了文本后面无论是传给 pdf.js 还是 docx-preview都会解析失败。正确的写法是const res await axios.get(/api/file/download, { params: { fileId: xxx }, responseType: blob, timeout: 30000 })如果用原生 fetch不需要设置 responseType但要注意拿到的res.blob()和res.arrayBuffer()只能用一次不能先用 blob 再转 arrayBuffer流只能消费一次。有些同事会写const data await res.arrayBuffer()之后又调用res.blob()去拿 Word 渲染用的 Blob结果后者直接报错。正确的做法是按需取一次如果要同时用 Blob 和 ArrayBuffer就const buf await res.arrayBuffer(); const blob new Blob([buf])。6.2 iframe 和 pdf.js 的跨域表现完全不同如果你的 PDF 地址是跨域的iframe方案大概率会翻车。跨域情况下 iframe 和父页面之间的交互是受限的虽然单纯展示 PDF 内容可能能看但如果你在父页面上调 iframe 的contentWindow去控制翻页会被浏览器拒绝访问。pdf.js 的getDocument({ url })本身是走fetch加载的受 CORS 策略限制。如果后端没配 CORS 或者内网网关有安全策略跨域请求一样会失败。我在项目里遇到过一种典型场景前端部署在 A 域名文件存储在 B 域名的对象存储上B 域名没有开 CORS导致 PDF、Word、Excel 全部加载失败。这种问题有三个解决方向让后端网关加一个反向代理接口前端通过同源路径访问由后端转发。这个最省事。对象存储或文件服务配置 CORS 白名单。适合文件和前端域名都相对固定的内网场景。前端在后端接口返回文件时用 URL.createObjectURL 生成一个临时对象地址这样子组件拿到的就是一个同源地址不涉及跨域。这个方案我在项目里用了很多次前提是文件接口本身在同域。6.3 文件名中文乱码与 Content-Disposition 的解析当文件接口返回的响应头里有Content-Disposition时前后端配合容易出问题。后端如果不做处理直接返回filename文件名.pdf中文会被编码成%E6%96%87%E4%BB%B6.pdf前端拿到以后不能直接用解码后的字符串当文件名。我写了一个比较健壮的解析函数function getFileNameFromDisposition(disposition) { if (!disposition) return // 处理 filename*UTF-8xxxx 的格式 const utf8Match disposition.match(/filename\*UTF-8([^;])/i) if (utf8Match) { try { return decodeURIComponent(utf8Match[1]) } catch (e) { // ignore decode error } } // 处理 filenamexxxx 的格式 const asciiMatch disposition.match(/filename?([^;])?/i) return asciiMatch ? asciiMatch[1] : }需要注意decodeURIComponent可能因为编码格式不标准而抛异常所以必须包一层try/catch。这个函数我一般在用户点击下载和预览成功后展示文件名两个场景都会用到。6.4 大文件内存控制与移动端注意点最后说两个生产环境才暴露的问题。第一个是内存暴涨。pdf.js 渲染 PDF 时每一页都会生成一个巨大的 Canvas 对象如果一个 PDF 有 200 页用户全部翻一遍内存占用会非常可观。我的处理方式是用一个WeakMap或普通对象缓存当前页和前后两页的渲染结果翻页时主动释放更远页码的 Canvas 引用而不是把 200 页全部渲染出来放内存里。Excel 那边同理上面说了限制行数就是最直接的手段。第二个是移动端 Canvas 面积限制。某些安卓浏览器的 Canvas 最大尺寸有限制超过之后渲染会静默失败。我的处理是在渲染前计算viewport的宽高当超过一个安全阈值时比如单边超过 8000px直接缩小渲染scale让 Canvas 尺寸降下来然后用style.width撑大显示。虽然清晰度会略降但至少页面不会白屏。从最初的这需求不难到最终交付前后经历了选型调研、方案推翻、内网环境联调、生产环境踩坑几个阶段。回过头看预览 PDF、Word、Excel 这个功能没有银弹核心就是按文件类型选对库 把交互状态管好 提前想清楚异常降级路径。我在这套方案里最大的体会是先搞清楚你的用户到底要看到内容还是还原原貌这决定了整个技术路线的走向。如果你是做合同、公文类预览还原原貌优先级高docx-preview 和 pdf.js 是不错的选择如果你只是让用户快速核对表格数据SheetJS 加 el-table 就足够了别把系统做成一个重型的 Office Online。最后再分享一个小建议给预览组件加上统一的文件大小限制提示超过 50MB 的文件一律引导下载别指望浏览器在前端能无压力处理这种巨型文件这是真实环境里最能保平安的一条规则。
返回列表