写在前面
纯前端的文档预览功能,是非常常见的需求,但就是这么简单的需求,难住了许多可爱的小伙伴们。别急,先访问一下解决方案,给你一个惊喜,再往下看:
文件在线预览DEMO
服务器文件预览DEMO
实现效果
word文档预览
Excel文档预览
PPT文档预览
PDF文档预览
图片预览
文本预览
视频预览
看完了之后,废话不多说,来给大家梳理梳理实现思路。
现存的方案和不足
笔者在接到这个功能需求后,对市面上目前的实现方案进行了归纳和梳理,不外乎就三种:
- PDF预览使用pdfjs,Office文档使用微软的提供的预览URL。该方案确实省事,而且效果是最好的,但是有个很大的问题,文件链接必须是公网链接,这对于在企业网或局域网部署的系统来说,基本上是不可行的方案,pass
- 使用Java后端统一转换为PDF,然后在前端预览。该方案兼容性较好,效果仅次于在线Office,但是对于服务器的压力比较大,在动辄要“高并发,高可用,高吞吐量”的互联网场景下总是不那么合适,基于OpenOffice的文件转换非常耗费IO,pass
- 客户端本地安装Office,利用浏览器的Office插件进行预览。这种实现方式对客户端要求较高,基本上不考虑,毕竟我们是web预览嘛,谁知道客户用的是啥浏览器,pass
意外的收获
到此为止,所有的方案都被pass掉了,非常绝望。无果后,我搭上梯子,疯狂Google,终于找到了一个jquery的开源插件,叫做officeToHtml,出于对开源的尊重,这里提供一下人家的访问链接(https://officetohtml.js.org/):OfficeJs | Demos
这个开源项目非常好用,引用它的demo就能直接预览主流格式,但是它是基于JQuery的。事实上,当时我都已经通过这个方案实现了,结果我们领导说不是Vue,而且用的组件也太老了,强行pass掉了。现实总是残酷的,看着我头顶所剩不多的秀发,深深叹了口气,准备自己再次开整。
有了国外大佬的思路提供,我的思路也渐渐清晰:
- 要解决这个问题,还是得用Vue实现
- 大佬的项目是jquery写的,我用Vue实现,也没说不让引用jQuery呀
- 分析一下大佬使用的开源组件,去GitHub上找最新的或者效果最好的,说不定有Vue版本呢
- 自己封装渲染入口,根据扩展名动态匹配渲染器,解析需要的格式。
OK,思路清晰了,我们开始撸代码。
开始实现
一、找替代框架
大佬的框架已经老得不被待见了,大致整理后,笔者找到的最贴近且效果最好的框架都在下面的表里了:
文档格式 | 老的开源组件 | 替代开源组件 |
word(docx) | mammoth | docx-preview(npm) |
powerpoint(pptx) | pptxjs | pptxjs改造开发 |
excel(xlsx) | sheetjs、handsontable | exceljs(npm)、handsontable(npm) |
pdf(pdf) | pdfjs | pdfjs(npm) |
图片 | jquery.verySimpleImageViewer | v-viewer(npm) |
升级后的组件完全兼容npm,唯一不兼容的pptxjs也被我改造了,能够完美兼容。以下是package.json中相关的依赖。
-
"@handsontable/vue": "^11.1.0",
-
"docx-preview": "^0.1.8",
-
-
"handsontable": "^11.1.0",
-
"pdfjs-dist": "^2.12.313",
-
-
二、搭建简单的视图组件
框架找好了,接下来我们开工。老样子,用vue-cli创建一个hello-world项目,把脚手架初始化出来。如果没安装过,先全局安装一下:
npm install -g @vue/cli-service-global
创建项目,名字就叫file-viewer吧!
-
-
然后我们在 src/components/HelloWorld.vue中,给他加一个容器,用于承载文档视图。再弄一个简单的loading容器,ok。
注意,这里的 @/components/util 是一些常用工具类,主要做二进制数据和字节码、字符串互转的。当然,文档渲染入口也在里面,我们后面说。
-
-
-
-
-
<h1><a href="/">Vue在线文档查看器<input class="file-select" type="file" @change="handleChange"/></a></h1>
-
-
-
-
<div v-show="loading" class="well loading">正在加载中,请耐心等待...</div>
-
<div v-show="!loading" class="well" ref="output"></div>
-
-
-
-
-
-
import { getExtend, readBuffer, render } from '@/components/util';
-
import { parse } from 'qs';
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
const [ file ] = e.target.files;
-
const arrayBuffer = await readBuffer(file);
-
-
this.last = await this.displayResult(arrayBuffer, file)
-
-
-
-
-
-
-
displayResult(buffer, file) {
-
-
-
-
const extend = getExtend(name);
-
-
const { output } = this.$refs;
-
-
const node = document.createElement('div');
-
-
-
output.removeChild(this.last.$el);
-
-
-
const child = output.appendChild(node);
-
-
return new Promise((resolve, reject) => render(buffer, extend, child)
-
.then(resolve).catch(reject));
-
-
-
-
-
-
-
-
-
-
background-color: #12b6ff;
-
-
-
-
-
-
-
-
-
height: calc(100vh - 14px);
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
background-color: #f2f2f2;
-
-
-
width: calc(100% - 14px);
-
height: calc(100vh - 73px);
-
-
-
-
-
-
-
-
-
-
-
-
三、实现渲染入口
写好容器后,下一步就是重头戏,笔者这里使用匹配模式简单实现了一个渲染入口,代码如下:
-
-
import renders from './renders';
-
-
-
export async function render(buffer, type, target) {
-
const handler = renders[type];
-
-
return handler(buffer, target);
-
-
return renders.error(buffer, target, type);
-
具体渲染逻辑我们用声明式的方式进行配置,统一放置在vendors目录下,像这样:
之后我们写一个策略配置器,去统一导入这些模块:
-
import { defaultOptions, renderAsync } from 'docx-preview';
-
import renderPptx from '@/vendors/pptx';
-
import renderSheet from '@/vendors/xlsx';
-
import renderPdf from '@/vendors/pdf';
-
import renderImage from '@/vendors/image';
-
import renderText from '@/vendors/text';
-
import renderMp4 from '@/vendors/mp4';
-
-
-
const VueWrapper = el => ({
-
-
-
-
-
-
-
-
-
-
-
handler: async (buffer, target) => {
-
const docxOptions = Object.assign(defaultOptions, {
-
-
-
-
await renderAsync(buffer, target, null, docxOptions)
-
return VueWrapper(target);
-
-
-
-
-
-
handler: async (buffer, target) => {
-
await renderPptx(buffer, target, null);
-
window.dispatchEvent(new Event('resize'));
-
return VueWrapper(target);
-
-
-
-
-
-
handler: async (buffer, target) => {
-
return renderSheet(buffer, target);
-
-
-
-
-
-
handler: async (buffer, target) => {
-
return renderPdf(buffer, target);
-
-
-
-
-
accepts: [ 'gif', 'jpg', 'jpeg', 'bmp', 'tiff', 'tif', 'png', 'svg' ],
-
handler: async (buffer, target) => {
-
return renderImage(buffer, target);
-
-
-
-
-
accepts: [ 'txt', 'json', 'js', 'css', 'java', 'py', 'html', 'jsx', 'ts', 'tsx', 'xml', 'md', 'log' ],
-
handler: async (buffer, target) => {
-
return renderText(buffer, target)
-
-
-
-
-
-
handler: async (buffer, target) => {
-
renderMp4(buffer, target)
-
return VueWrapper(target);
-
-
-
-
-
-
handler: async (buffer, target, type) => {
-
target.innerHTML = `<div style="text-align: center; margin-top: 80px">不支持.${type}格式的在线预览,请下载后预览或转换为支持的格式</div>
-
<div style="text-align: center">支持docx, xlsx, pptx, pdf, 以及纯文本格式和各种图片格式的在线预览</div>`;
-
return VueWrapper(target);
-
-
-
-
-
-
export default handlers.reduce((result, { accepts, handler }) => {
-
accepts.forEach(type => result[type] = handler)
-
-
ok,大功告成!😄
四、运行调试
还好我们前期做足了工夫,一下子就运行起来了,但是很快就遇到了问题:
- pptxjs的npm版本无法使用,有很多bug,只能下载源码自己改bug,作者已经不维护了
- exceljs解析sheet的时候有超级多坑,踩到吐🤮
- 性能问题,遇到几十兆的pdf,打开超级慢,页面会假死(等待一会就好了)。这个也需要进一步优化
好不容易修改好了这些,终于跑起来了。大家可以在我的在线demo看到效果file-viewerhttp://viewer.flyfish.group/
五、总结一下
实现这个功能总体来说还是非常困难的,除了有很多坑,找到可用的开源组件也是耗费了我大量的精力。好在前期做的努力没有白费,成功上线了产品,也得到了领导的认可罒ω罒,嘿嘿。现在我把我的项目共享出来,我已经把源码上传了。本着对技术尊重的态度,大家帮忙打赏一两块钱就可以拿到完整的源码。
此外,我也会不断的优化更新,修改bug,提升性能,希望大家持续关注我,有人关注我就一定会一直努力的!谢谢大家!最后附上链接:
Web端文件预览,纯前端Vue实现的file-viewer,不需要后端,支持所有主流格式,附带接入文档和嵌入式引用demo-Web开发文档类资源-CSDN下载
最后的最后,希望大家写代码都能无bug!如果文章确实帮到了你,麻烦给个关注,谢谢!
该文章在 2022/4/15 12:14:59 编辑过