纯前端的文档预览功能,是非常常见的需求,但就是这么简单的需求,难住了许多可爱的小伙伴们。别急,先访问一下解决方案,给你一个惊喜,再往下看:
项目最新进展(2023年4月4日):
1. Vue3+TypeScript+Vite版本发布,完全重构,性能飞跃,超高代码质量
2. 重构了Xlsx对于主题颜色的获取和计算,能够完美显示颜色
3. 重构了Pptx底层部分逻辑,解耦了图表部分,并优化了显示性能
4. 新版文档已经上线,demo部分替换部署了Vue3版本
2023年2月28日全面升级说明
鉴于很多朋友呼吁文档的问题,目前使用文档已更新,请参考
本次更新属于突破更新,完成了项目组件化改造,嵌入项目中使用更加容易,具体请拉取最新代码体验一下吧!
==更新日志==
1. 优化了pptx嵌套块溢出效果,比之前好很多
2. 增加了文件标题显示
3. 进行了组件化拆分设计,提供标准的Vue组件,方便接入
4. 优化了底层的一些代码,运行更加稳定
== 公众号上线!==
此外,博主的公众号上线了,大家在微信搜索 "飞鱼开源"。关注“飞鱼开源WorkShop”公众号,可以获取最新源码,同时不定期更新技术福利!
有大家的支持我才有更新的动力,感谢大家一直以来的支持!❤❤❤
仓库地址: https://git.flyfish.group,请下载过资源的大家注册后获取最新源码!
2022年8月1日更新,重大升级。
1. 重构大部分pptx逻辑,优化背景样式,块文字样式和图表。
2. 优化PDF展现逻辑,基于官网demo使用官方pdfViewer组件实现懒加载,虚拟滚动,大幅度提高性能,可以秒开超大PDF文件!
3. 优化框架和升级依赖版本
福利:注册git私库并发送账号给博主,博主会帮忙开放本项目的git仓库权限,永久更新!记得是下载过资源的小伙伴哦,开发不易,请予以点滴支持,不尽感激!
git仓库地址:飞鱼开源工作室
2022年5月31日更新。增加文件url输入预览,可以访问文件在线预览DEMO体验。由于很多小伙伴提的问题都是关于服务器URL预览文件怎么预览,这次的demo集成了这部分功能,大家可以参照源码进行理解和修改。
因demo使用ajax加载,在测试时请保证文件资源响应Header包含允许跨域的头部。建议头部如下:
Access-Control-Allow-Origin * Access-Control-Allow-Headers X-Requested-With Access-Control-Allow-Methods GET
功能入口如下:
word文档预览

Excel文档预览

PPT文档预览

PDF文档预览

图片预览

文本预览

视频预览
看完了之后,废话不多说,来给大家梳理梳理实现思路。
笔者在接到这个功能需求后,对市面上目前的实现方案进行了归纳和梳理,不外乎就三种:
到此为止,所有的方案都被pass掉了,非常绝望。无果后,我搭上梯子,疯狂Google,终于找到了一个jquery的开源插件,叫做officeToHtml,出于对开源的尊重,这里提供一下人家的访问链接:OfficeJs | Demos
这个开源项目非常好用,引用它的demo就能直接预览主流格式,但是它是基于JQuery的。事实上,当时我都已经通过这个方案实现了,结果我们领导说不是Vue,而且用的组件也太老了,强行pass掉了。现实总是残酷的,看着我头顶所剩不多的秀发,深深叹了口气,准备自己再次开整。
有了国外大佬的思路提供,我的思路也渐渐清晰:
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",
"exceljs": "^4.3.0",
"handsontable": "^11.1.0",
"pdfjs-dist": "^2.12.313",
"v-viewer": "^1.6.4",
"vue": "^2.6.11"
框架找好了,接下来我们开工。老样子,用vue-cli创建一个hello-world项目,把脚手架初始化出来。如果没安装过,先全局安装一下:
npm install -g @vue/cli-service-global
创建项目,名字就叫file-viewer吧!
cd ~/Projects
vue create file-viewer
然后我们在 src/components/HelloWorld.vue中,给他加一个容器,用于承载文档视图。再弄一个简单的loading容器,ok。
注意,这里的 @/components/util 是一些常用工具类,主要做二进制数据和字节码、字符串互转的。当然,文档渲染入口也在里面,我们后面说。
<template>
<div :class="{hidden}">
<div class="banner">
<div class="container">
<h1><a href="/">Vue在线文档查看器<input class="file-select" type="file" @change="handleChange"/></a></h1>
</div>
</div>
<div class="container">
<div v-show="loading" class="well loading">正在加载中,请耐心等待...</div>
<div v-show="!loading" class="well" ref="output"></div>
</div>
</div>
</template>
<script>
import { getExtend, readBuffer, render } from '@/components/util';
import { parse } from 'qs';
/**
* 支持嵌入式显示,基于postMessage支持跨域
* 示例代码:
*
*/
export default {
name: 'HelloWorld',
props: {
msg: String
},
data() {
return {
// 加载状态跟踪
loading: false,
// 上个渲染实例
last: null,
// 隐藏头部,当基于消息机制渲染,将隐藏
hidden: false,
}
},
methods: {
async handleChange(e) {
this.loading = true;
try {
const [ file ] = e.target.files;
const arrayBuffer = await readBuffer(file);
this.loading = false
this.last = await this.displayResult(arrayBuffer, file)
} catch (e) {
console.error(e)
} finally {
this.loading = false
}
},
displayResult(buffer, file) {
// 取得文件名
const { name } = file;
// 取得扩展名
const extend = getExtend(name);
// 输出目的地
const { output } = this.$refs;
// 生成新的dom
const node = document.createElement('div');
// 添加孩子,防止vue实例替换dom元素
if (this.last) {
output.removeChild(this.last.$el);
this.last.$destroy();
}
const child = output.appendChild(node);
// 调用渲染方法进行渲染
return new Promise((resolve, reject) => render(buffer, extend, child)
.then(resolve).catch(reject));
}
}
}
</script>
<style scoped>
.banner {
overflow: auto;
text-align: center;
background-color: #12b6ff;
color: #fff;
}
.hidden .banner {
display: none;
}
.hidden .well {
height: calc(100vh - 12px);
}
.file-select {
position: absolute;
left: 5%;
top: 17px;
margin-left: 20px;
}
.banner a {
color: #fff;
}
.banner h1 {
font-size: 20px;
line-height: 2;
margin: 0.5em 0;
}
.well {
display: block;
background-color: #f2f2f2;
border: 1px solid #ccc;
margin: 5px;
width: calc(100% - 12px);
height: calc(100vh - 73px);
overflow: auto;
}
.loading {
text-align: center;
padding-top: 50px;
}
.messages .warning {
color: #cc6600;
}
</style>
写好容器后,下一步就是重头戏,笔者这里使用匹配模式简单实现了一个渲染入口,代码如下:
// 导入渲染器
import renders from './renders';
// 渲染入口函数,包含字节数组、文件类型、目标容器
export async function render(buffer, type, target) {
const handler = renders[type];
if (handler) {
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';
// 假装构造一个vue的包装,让上层统一处理销毁和替换节点
const VueWrapper = el => ({
$el: el,
$destroy() {
// 什么也不需要 nothing to do
},
});
const handlers = [
// 使用docxjs支持,目前效果最好的渲染器
{
accepts: [ 'docx' ],
handler: async (buffer, target) => {
const docxOptions = Object.assign(defaultOptions, {
debug: true,
experimental: true,
});
await renderAsync(buffer, target, null, docxOptions)
return VueWrapper(target);
}
},
// 使用pptx2html,已通过默认值更替
{
accepts: [ 'pptx' ],
handler: async (buffer, target) => {
await renderPptx(buffer, target, null);
window.dispatchEvent(new Event('resize'));
return VueWrapper(target);
},
},
// 使用sheetjs + handsontable,无样式
{
accepts: [ 'xlsx' ],
handler: async (buffer, target) => {
return renderSheet(buffer, target);
},
},
// 使用pdfjs,渲染pdf,效果最好
{
accepts: [ 'pdf' ],
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)
},
},
// 视频预览,仅支持MP4
{
accepts: [ 'mp4' ],
handler: async (buffer, target) => {
renderMp4(buffer, target)
return VueWrapper(target);
},
},
// 错误处理
{
accepts: [ 'error' ],
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)
return result;
}, {});
ok,大功告成!😄
还好我们前期做足了工夫,一下子就运行起来了,但是很快就遇到了问题:
好不容易修改好了这些,终于跑起来了。大家可以在我的在线demo看到效果 file-viewerhttp://viewer.flyfish.group/
实现这个功能总体来说还是非常困难的,除了有很多坑,找到可用的开源组件也是耗费了我大量的精力。好在前期做的努力没有白费,成功上线了产品,也得到了领导的认可罒ω罒,嘿嘿。现在我把我的项目共享出来,我已经把源码上传了。本着对技术尊重的态度,大家帮忙打赏一两块钱就可以拿到完整的源码。
此外,我也会不断的优化更新,修改bug,提升性能,希望大家持续关注我,有人关注我就一定会一直努力的!谢谢大家!最后附上链接:
Web端文件预览,纯前端Vue实现的file-viewer,不需要后端,支持所有主流格式,附带接入文档和嵌入式引用demo-Web开发文档类资源-CSDN下载
最后的最后,希望大家写代码都能无bug!如果文章确实帮到了你,麻烦给个关注,谢谢!
我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h
我试图获取一个长度在1到10之间的字符串,并输出将字符串分解为大小为1、2或3的连续子字符串的所有可能方式。例如:输入:123456将整数分割成单个字符,然后继续查找组合。该代码将返回以下所有数组。[1,2,3,4,5,6][12,3,4,5,6][1,23,4,5,6][1,2,34,5,6][1,2,3,45,6][1,2,3,4,56][12,34,5,6][12,3,45,6][12,3,4,56][1,23,45,6][1,2,34,56][1,23,4,56][12,34,56][123,4,5,6][1,234,5,6][1,2,345,6][1,2,3,456][123
当我的预订模型通过rake任务在状态机上转换时,我试图找出如何跳过对ActiveRecord对象的特定实例的验证。我想在reservation.close时跳过所有验证!叫做。希望调用reservation.close!(:validate=>false)之类的东西。仅供引用,我们正在使用https://github.com/pluginaweek/state_machine用于状态机。这是我的预订模型的示例。classReservation["requested","negotiating","approved"])}state_machine:initial=>'requested
我有这个html标记:我想得到这个:我如何使用Nokogiri做到这一点? 最佳答案 require'nokogiri'doc=Nokogiri::HTML('')您可以通过xpath删除所有属性:doc.xpath('//@*').remove或者,如果您需要做一些更复杂的事情,有时使用以下方法遍历所有元素会更容易:doc.traversedo|node|node.keys.eachdo|attribute|node.deleteattributeendend 关于ruby-Nokog
我想获取模块中定义的所有常量的值:moduleLettersA='apple'.freezeB='boy'.freezeendconstants给了我常量的名字:Letters.constants(false)#=>[:A,:B]如何获取它们的值的数组,即["apple","boy"]? 最佳答案 为了做到这一点,请使用mapLetters.constants(false).map&Letters.method(:const_get)这将返回["a","b"]第二种方式:Letters.constants(false).map{|c
这个问题在这里已经有了答案:Railsformattingdate(4个答案)关闭4年前。我想格式化Time.Now函数以显示YYYY-MM-DDHH:MM:SS而不是:“2018-03-0909:47:19+0000”该函数需要放在时间中.现在功能。require‘roo’require‘roo-xls’require‘byebug’file_name=ARGV.first||“Template.xlsx”excel_file=Roo::Spreadsheet.open(“./#{file_name}“,extension::xlsx)xml=Nokogiri::XML::Build
我喜欢使用Textile或Markdown为我的项目编写自述文件,但是当我生成RDoc时,自述文件被解释为RDoc并且看起来非常糟糕。有没有办法让RDoc通过RedCloth或BlueCloth而不是它自己的格式化程序运行文件?它可以配置为自动检测文件后缀的格式吗?(例如README.textile通过RedCloth运行,但README.mdown通过BlueCloth运行) 最佳答案 使用YARD直接代替RDoc将允许您包含Textile或Markdown文件,只要它们的文件后缀是合理的。我经常使用类似于以下Rake任务的东西:
给定一个复杂的对象层次结构,幸运的是它不包含循环引用,我如何实现支持各种格式的序列化?我不是来讨论实际实现的。相反,我正在寻找可能会派上用场的设计模式提示。更准确地说:我正在使用Ruby,我想解析XML和JSON数据以构建复杂的对象层次结构。此外,应该可以将该层次结构序列化为JSON、XML和可能的HTML。我可以为此使用Builder模式吗?在任何提到的情况下,我都有某种结构化数据-无论是在内存中还是文本中-我想用它来构建其他东西。我认为将序列化逻辑与实际业务逻辑分开会很好,这样我以后就可以轻松支持多种XML格式。 最佳答案 我最
是否有简单的方法来更改默认ISO格式(yyyy-mm-dd)的ActiveAdmin日期过滤器显示格式? 最佳答案 您可以像这样为日期选择器提供额外的选项,而不是覆盖js:=f.input:my_date,as::datepicker,datepicker_options:{dateFormat:"mm/dd/yy"} 关于ruby-on-rails-事件管理员日期过滤器日期格式自定义,我们在StackOverflow上找到一个类似的问题: https://s
matlab打开matlab,用最简单的imread方法读取一个图像clcclearimg_h=imread('hua.jpg');返回一个数组(矩阵),往往是a*b*cunit8类型解释一下这个三维数组的意思,行数、数和层数,unit8:指数据类型,无符号八位整形,可理解为0~2^8的数三个层数分别代表RGB三个通道图像rgb最常用的是24-位实现方法,即RGB每个通道有256色阶(2^8)。基于这样的24-位RGB模型的色彩空间可以表现256×256×256≈1670万色当imshow传入了一个二维数组,它将以灰度方式绘制;可以把图像拆分为rgb三层,可以以灰度的方式观察它figure(1