在构建业务系统的时候,经常会涉及到对附件的支持,继而又会引申出对附件在线预览、在线编辑、多人协同编辑等种种能力的诉求。
对于人力不是特别充裕、或者项目投入预期规划不是特别大的公司或者项目而言,通常会选择基于一些开源方案来实现,但是开源组件选择之后,如何将其无缝对接融入到自己的业务系统中并完全支持自身诉求的实现,**不仅要能用、而且要好用,**其实也是一个需要好好思量的问题。
在正式开始构建在线的文件管理服务前,首先是分析下需要支持的功能诉求:
选型确定之后,就是如何与现有业务系统进行整合了。因为开源组件往往都是通用逻辑设计的,而业务系统的逻辑又各不相同,所以如何去整合并方便扩展出自己需要的定制化能力,成了下一步摆在眼前需要处理的问题。
系统边界划定,对业务系统整体的接入配合而言就简单了:
预览编辑服务,作为业务系统的边缘代理适配器模块,需要保证提供给左侧业务系统的接口的稳定,而右侧具体对接的开源方案、内部处理逻辑等,则可以随意调整。
所以在实施的时候,在服务端的文件预览编辑服务中进行了封装,对外提供服务端API接口,服务端自带一个简单HTML界面(基于SpringBootThymeleaf实现),业务请求对应服务端提供的独立html界面,并在界面中完成使用OnlyOffice的JSapi请求的操作。
具体步骤说明如下:
这样就实现整体的交互封装,业务可以代码无耦合的方式来直接使用预览能力。具体的office文档在线预览与编辑的能力实现,由开源的OnlyOffice来提供。
具体使用的时候,交互逻辑如下:
看似很复杂的逻辑,但是经过封装之后,对于业务使用而言其实很简单,只要在发送给文件预览服务的请求中,给定一个文件下载地址与文件保存回调地址即可。
前面有提过,采用OnlyOffice来实现office文档的在线协同编辑,关于OnlyOffice在线编辑的原理,其官网给出的介绍如下:
对上述步骤解释如下:
也即当用户关闭文档编辑界面之后,会触发文档的保存事件,回调callback接口,将保存事件推送给服务端,并告知服务端变更后的文档地址,这样服务端可以从给定的地址下载变更后的文档,然后更新到自己的存储中。
结合到我们具体的项目使用中,其具体的交互过程展开阐述下,就是下图的过程:
这里,一个在线编辑操作的回调请求内容示例如下:
字段
字段类型
含义说明
actions
List
每个用户加入或者退出此文档的编辑的动作信息。其中具体type的取值0表示断开连接,1表示建立连接
key
String
目标文档在OnlyOffice中处理的唯一标识ID,注意这里的key与业务系统中目标文件实际的唯一ID并非一个概念,不能混为一谈,因为业务系统中某个文件的ID需要保持不变,但是在OnlyOffice中编辑的时候,这个key需要不停的变。
status
Integer
文档当前的操作状态类型,取值说明:1:文档正在被编辑2:文件已准备好保存3:文档保存发生错误4:文件关闭,没有变化6:文档正在被编辑,但是当前状态已经被保存7:强制保存文档时发生错误
url
改动后的文档的下载地址,可以从这个地址下载到变更后的文件,然后存储更新业务系统中实际的文档
实际测试的时候发现,此处的回调接口被调用的情况非常的频繁,务必要注意当且仅当actions中所有的对象的type都等于0的时候,也即所有用户均已经退出编辑且文档已经准备好保存的时候,回调接口被调用的时候才需要去更新key值。
这里是在实际构建的时候踩坑较久的一个地方,下面章节中展开详细说下踩坑过程。
看了下官网的问题原因解释,就是因为文档编辑之后,原来的key对应的文档已经被编辑过,已经不能被打开了(可以把key理解为不同的version,文档被编辑之后,version变更了,原来老的version就不允许操作了)。最后官网还很贴心地提示:别忘了每次编辑之后要重新生成一个新的key!
按照官网的介绍,在callback接口被调用的时候,重新为文件生成一个key,后续新的用户想要加入此文档的编辑的时候,都是拿到新生成的一个key,这样不就可以了吗?
try{//如果redis里面有缓存此文档对应的key值,则直接使用fileUniqueKey=redisCacheOperateService.getFileUniqueKeyDetail(fileId);}catch(Exceptione){//如果redis里面没有缓存此文档对应的key值,则生成对应的key并加入缓存中fileUniqueKey=FileUniqueKey.builder().build();redisCacheOperateService.saveOrUpdateFileUniqueKeyDetail(fileUniqueKey);}//获取本次在线操作对应的key值document.setKey(fileUniqueKey.generatekey());//编辑成功后,重新生成随机码,实现key值变化的目的fileUniqueKey.updateRandomUniqueKey();redisCacheOperateService.saveOrUpdateFileUniqueKeyDetail(fileUniqueKey);按照上述思路改完后,再次尝试,发现:
难道只有让大家都约好了一起加入进去再开始编辑才行吗?那这个在线编辑功能显然就是个鸡肋了——显然OnlyOffice也不太可能会是这种实现。再全面复盘了下测试的现象,分析了下可能原因:
基于上述分析:
所以说,如果每次只要有用户还在线的时候,这个文档的key就不应该变,只有等某篇文档的所有用户都关闭编辑窗口的时候,再去处理文档key的变更,这样不就解决问题了吗?
那问题就简单了,按照这个思路修改了下callback的代码逻辑,判断下某篇文档的所有用户都退出编辑之后,再去重新生成新的key值。
代码演示如下:
为了保障预览服务的可靠,在生产环境上规划实施集群化部署。从上一章的阐述中,我们知道OnlyOffice的功能实现严重依赖单机本地的缓存数据信息,在集群部署的场景下,过度依赖本地缓存的弊端就显现出来了。
集群化部署,本以为会很简单,直接部署多个docker节点,然后使用Nginx做一下反向代理以及负载均衡不就可以了嘛?但是实际实施的时候却发现在协同编辑场景下出现了预期之外的问题。因为多人在线协同编辑的能力要求所有人对某篇文档的编辑请求都在同一个OnlyOffice服务节点上才行,而Nginx随机负载分发,会导致同一篇文档的编辑请求分发到不同节点上,这样就会导致编辑的内容相互覆盖。
因为用户的请求并不是直接打到OnlyOffice地址上的,而是先打到文件预览服务中,然后由文件预览服务经过某种策略处理后,再将请求重定向到OnlyOffice服务上进行文档操作的,所以这里我们可以通过增加一个简单的分发策略,保证对同一个文档的所有的请求操作,都被分发到固定的一个OnlyOffice服务上处理即可。
这里的分发策略,考虑有2种方案:
具体代码逻辑如下:
kkFileView作为一个基于JAVA构建的可独立集成部署的文件预览开源组件,其在各种文件的预览上表现非常的优异,集成起来也非常的简单,直接提供下文件下载的地址就可以了。支持Office文档、图片、视频、音频、压缩包等各种文档的预览。
对于kkFileView的集成,我们采用了与OnlyOffice集成截然不同的处理策略,因为kkFileView基于JAVASpringBoot技术栈构建,与我们业务系统技术栈一致,所以我们基于kkFileView的源码进行了深度的定制整改。主要包括几方面:
基于前面整体的规划策略,Office文档使用OnlyOffice进行预览操作,非Office文档则由kkFileView实现预览操作(业务调用方无感知,都是统一一个url地址)。开发完成部署上线之后,功能也都一切正常。
虽然系统支持了Office文档的在线预览与编辑能力,但是统计了下,其实近乎95%的Office文档操作都是预览操作,考虑到kkFileView预览PDF的速度非常的快,因此决定通过kkFileView来支持Office文档的预览操作,而OnlyOffice只用来做Office文档的在线协同编辑,或者用于某些kkFileView预览效果不够好的Office文档的兜底预览场景。
因为kkFileView预览Office文档的策略是先将Office文档转换为PDF,然后采用预览PDF的策略来实现的,为了进一步的提升速度,避免每次都实时去进行Office文档转PDF的操作,所以设计采用异步事件的方式进行预处理转换,异步转化Office文档为PDF,然后对于Office文档只读场景直接使用PDF预览即可。
当业务系统中的文件内容有新增或者变更的时候,具体的异步转换处理的时序操作逻辑如下:
在线协同编辑的时候,需要监听下每个文件的变更,如果编辑后的话,需要异步重新转换下文档缓存内容。
到这里呢,对于快速预览office文档的逻辑,就算基本完成了。按照当前的策略,对于office文档预览的场景,默认都会使用转换后的缓存PDF文档进行预览。在实际验证的时候,偶尔会遇到一些转换后PDF预览效果不佳的情况,所以为了解决此类问题,又对处理流程的逻辑进行了一点优化,请求参数中,预留了个字段,可以用于调用方设定是否禁用本地转换缓存结果文件进行预览:
到此呢,整个文档的在线预览与编辑能力的构建,就算完成了。在处理具体的文档的预览或者在线编辑请求的时候,对应的处理判断总体逻辑如下:
回顾下构建之初规划的功能诉求,也已经全部支持:
功能点
支持情况
常规文档在线预览
office文档在线预览
office文档协同编辑
集群部署
业务解耦
整体系统层面的网元模块架构情况如下图所示,整个预览服务中,所有内部逻辑均封装在内部,统一由预览编辑服务对外提供API接口,供业务服务进行调用与交互。后续如果需要对预览服务的实现策略进行调整,也无需变更外部业务侧的逻辑,实现与业务逻辑解耦的效果。