前端渲染文件实现

Posted by mjTree on August 19, 2024

更新于:2024-08-20 14:00

一、业务场景介绍

本篇文件会提到关于 word、excel、pdf 文件如何在前端渲染以及涉及到的编辑功能,但不是多人在线编辑的功能,请注意。关于前端渲染文件的需求与场景,涉及的跨平台访问、实时协作、安全性等等不做讨论,我们主要讨论的和文档结构化的需求关联。在上篇文章提到的文档比对场景中,当后端完成了解析和比对的工作后,如何让前端展示呢?

如果不还原源文件和目标文件的版面样式,自行输出自定义样式展示,那比对工作算是失去了它很大的存在意义了(类似 llm 应用的输出是可以自定义样式,交互方式不同而已)。除了比对外,还有很多业务场景需要完整的展示原文件的样式,这里不做过多描述。

二、相关技术路线

1. word 渲染

如果只是在页面展示一个 docx 文件的内容,这个会相对简单,开源可选的方案很多。但是需要记住一点,不要设想自己的组件渲染出来的和客户在自己工具上看到的百分比一致。熟悉 word 的朋友们应该知道,从根本上来看 word 文档不过是一个巨大的字符流,不同厂商遵循 ooxml 协议来实现的工具来渲染一份样本可能会出现差异性的,可能不同设备上字体不一,可能是不同工具的渲染逻辑实现不同。所以要记住这一点,不要和自己死磕。

如果既要渲染又要在线编辑怎么办?从零开始自研开发编辑功能难度大成本高,做在线编辑的国内厂商有很多(金山、石墨、腾讯、飞书、冰蓝、永中等等),也有开源的组件(OnlyOffice、Canvas-Editor等)。没有时间的前端选择了开源的 OnlyOffice 工具,并应用于公司的产品上。经历过二次开发的魔改,可用但是体验较差,打开变得慢了些,可能是希望生活也能如此吧。这里推荐一个 OnlyOffice 二开指南 ,可以联系作者加群,关于开源的坑很多群友都踩过而且群活跃程度较高的。

技术路线:原计划后端解析 word 文件,按照字符range的偏移量来处理,前端工具也遵循这个偏移量,快速实现业务上审核纠错的高亮。但不同工具的偏移量定义逻辑不同,导致做高亮时就很困难,虽然基于 word 的部分特性做到了高亮,但出现了上面提到的慢的问题。后续有人继续调研吗,答案是没人了。

2. excel 渲染

下面直接贴出一些开源的工具,能对开源工具有个初步的了解。

工具名 描述 功能 扩展性
Handsontable 国外开源,专注于数据表格,对其他功能支持较少 2 1
OnlyOffice 国外开源,功能强大,但定制化功能受限于GPL协议,商业部署有限制,存在商业风险 5 2
X-Spreadsheet 国内开源,偏向于数据表格,对其他功能支持较少 2 1
LuckySheet 国内开源,更新不稳定,存在大量未修复问题,不支持多例,定制化能力有限 3 4
FortuneSheet 国内开源,LuckySheet的 React+TS 版本,优化了结构,功能尚不完善 2 3
Univer 国内开源,LuckySheet的3.0版本,代码优化,功能尚不完善 2 1

技术路线:早期的前端选择了 LuckySheet (24年5月份停止更新)作为基底,然后二次开发制作一个初版。现在的话可以推荐使用Univer来当作基底,进行 excel 的信息解析,然后再基于canvas渲染并还原 excel-sheet 结构。最后再基于canvas作为编辑器,然后再把编辑操作同步到源文件中(如果业务上不需要同步到源文件就更好了)。这条技术路线相比 word 是简单一些的,主要是基于办公的 excel 的复杂度较低的前提来完成,但是很复杂的 excel (包含vba、内嵌第三方插件的渲染工具等) LuckySheet就很难处理,另外网页展示上提供的编辑操作非常有限。

个人想法:可基于 后端服务 解析,前端若选择 onlyoffice 工具进行渲染的话,可通过的单元格坐标(行列)来定位,这是比 word 要方便的多。

3. pdf 渲染

在文档结构化的场景中,左边是展示原始 pdf 文件信息,右边展示对 pdf 文件结构化的信息(基于canvas渲染),具体可参考 OpenSourceTools 的展示设计,如下图所示。

display_demo

iframe
iframe渲染 pdf 时主要依赖于浏览器对 pdf 文件的内置支持,由于这个原因一般不会选择用这个方案的(字体、浏览器兼容性等),下面有一段简单的代码可以测试。

<!DOCTYPE html>
<html>
<head>
    <title>PDF Viewer</title>
</head>
<body>
    <iframe src="./demo.pdf" width="100%" height="600"></iframe>
</body>
</html>

pdfjs
pdfjs 是一个目前大多数的选择方案,也是早期我们的选择方案。但是 pdfjs 也是有缺陷的,不少场景下加载会变得很慢,这边有 官方说明

将上门的写入一个 html 文件中,通过python3 -m http.server 8000命令启动,在浏览器中打开 html 文件即可观察(保证pdf路径正确)。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>PDF.js Viewer</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.9.359/pdf_viewer.css">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.9.359/pdf.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.9.359/pdf.worker.js"></script>
    <style>
        body {
            font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
            margin: 0;
            padding: 0;
        }
        #pdf-canvas {
            display: block;
        }
    </style>
</head>
<body>
<div id="pdf-canvas"></div>
<script>
    document.addEventListener('DOMContentLoaded', function () {
        var pdfCanvas = document.getElementById('pdf-canvas');
        var pdfUrl = './demo.pdf';
        pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.9.359/pdf.worker.js';

        var loadingTask = pdfjsLib.getDocument(pdfUrl);
        loadingTask.promise.then(function (pdf) {
            var pageNumber = 1;
            pdf.getPage(pageNumber).then(function (page) {
                var scale = 1.5;
                var viewport = page.getViewport({ scale: scale });
                var canvas = document.createElement('canvas');
                var context = canvas.getContext('2d');
                canvas.width = viewport.width;
                canvas.height = viewport.height;
                pdfCanvas.appendChild(canvas);
                var renderContext = {
                    canvasContext: context,
                    viewport: viewport
                };
                var renderTask = page.render(renderContext);
                renderTask.promise.then(function () {
                    console.log('Page rendered');
                });
            });
        }).catch(function (reason) {
            console.error('Error rendering PDF:', reason);
        });
    });
</script>
</body>
</html>

图片渲染
这个方案是基于上面pdfjs方案改进过来的,使用图片方式渲染,这个需要后端服务将 pdf 按页转换成图片,提升首次加载速度的同时,对于用户向下翻页时也相当丝滑(滑选和搜索字符等正常)。但是缺点也比较明显,增加了不必要的存储,图片的 dpi 过大前端会渲染模糊。该方案目前是我们的默认方式,两种渲染方式可以相互切换。

4. 小结

关于 pdf 渲染,还有一些渲染优化的方向可以调研尝试。

  1. 分割的 pdf 文件,渲染多个pdf.js实例(分割会增加不必要的存储,也不建议用分割文件替换原文件)。
  2. 参考pdf.js的 webviewer 优化渲染队列机制来尝试提示渲染速度(未尝试,不确定)。
  3. 检查并优化 pdf 文件协议码信息,保证文件不出现上面官方提到的相关问题(提升很大,完全做到相对困难)。
  4. 参考该篇文章,优化pdf.js底层 canvas渲染机制,把上下文类型从 2d 升级为 WebGL2(要显卡gpu资源,客户环境无法保证)。
  5. mupdf.js,一个用于节点和浏览器的Webassembly PDF呈现器(未尝试,不确定)。

针对 word、excel、powerpoint 这类 ooxml 协议文件,一开始看到office.js感觉是很无敌的存在,但是发现官方很久不维护,实现逻辑是转换成 html 再展示。在官方提供的 demo 上测试效果很差,还有mammoth.js等方案都不太可取。转换的方案基本不太可取,转换过程的本身是一个信息丢失的过程,不可能做到百分百转换的。目前看通用并且省时省力的方案是各类文件转换成 pdf,再用上面的 pdf 渲染处理。但这句话和上一句话有冲突,转换是信息丢失的过程,word2pdf 是一个在展示方面丢失率最低的(丢失的是元素类型信息),excel2pdf 某些场景不是一个可取的方案,还有其他格式例如 ppt、ofd、epub 等等。

三、开源工具推荐

file-online-preview ,官方支持了太多太多的文件类型在线渲染了,具体可以查看官方提供的 展示平台 。对于非语音、非视频的版面文件,主要分为图片渲染和转换成 pdf 文件渲染两种模式,pdf 则是使用上面提到的pdf.js方案;excel 文件也是上面提到的Luckysheet方案。由于是开源的工具,大家直接使用来减少项目前期的研究时间。

四、总结

前端渲染文件的技术不仅提供了更好的用户体验,还提高了跨平台的兼容性和实时协作的效率。在网页端实现文件渲染以及编辑功能是一件比较困难的事情,本篇文章这边提供了当前团队在用的一些技术路线和方案供大家参考。在未来的 Web 应用中,这种基于前端的文档处理方式将变得越来越普遍,并为更多的业务场景提供便利。