大家好,又见面了。

在构建业务零碎的时候,常常会波及到对附件的反对,继而又会引申出对附件在线预览在线编辑多人协同编辑等种种能力的诉求。

对于人力不是特地富余、或者我的项目投入预期布局不是特地大的公司或者我的项目而言,通常会抉择基于一些开源计划来实现,然而开源组件抉择之后,如何将其无缝对接融入到本人的业务零碎中并齐全反对本身诉求的实现,不仅要能用、而且要好用,其实也是一个须要好好思量的问题。

此前在我的项目中就曾遇到过这么个场景,上面一起分享下具体的架构设计调整演进与最终计划落地策略,以及过程中遇到的一些问题。

开源组件的抉择

在正式开始构建在线的文件治理服务前,首先是剖析下须要反对的性能诉求:

  • 须要反对office文档在线预览、在线协同编辑能力
  • 须要反对常见的支流文件的在线预览,比方图片视频文本文档PDF压缩包之类的
  • 须要反对文件的存储管理能力

对于文件的存储管理,间接采纳了公司外部公有云的OSS文件托管服务进行实现,实现起来比较简单。文件在线预览与Office文件在线编辑的能力,则选用相干的开源计划来实现。通过一番比照剖析,最终选定了两个开源组件:

  • OnlyOffice
    用于反对office文档的在线协同编辑、预览等能力。
  • kkFileView
    用于反对惯例文档的在线预览能力

选型确定之后,就是如何与现有业务零碎进行整合了。因为开源组件往往都是通用逻辑设计的,而业务零碎的逻辑又各不相同,所以如何去整合并不便扩大出本人须要的定制化能力,成了下一步摆在眼前须要解决的问题。

整体适配对接策略

为了保障业务零碎的稳固,防止业务零碎中强耦合文件预览相干的开源模块,同时也为了不便业务层的调用,所以布局构建一个对立的入口代理转接服务,对立由此服务对业务零碎提供预览与在线编辑相干能力,对业务层屏蔽掉底层具体的开源计划整合逻辑。这样的益处是,不论预览与编辑服务这边如何调整,甚至前面更换实现计划,都不会影响到业务层的调用逻辑。

零碎边界划定,对业务零碎整体的接入配合而言就简略了:

  • 业务零碎只须要与预览编辑服务之间进行接口与实现层面的约定对接即可,其实也是零碎外部的模块间标准定义
  • 预览编辑服务负责残缺的业务零碎申请的鉴权、与开源组件之间的适配转换、业务定制化的预览与编辑能力扩大等等。

预览编辑服务,作为业务零碎的边缘代理适配器模块,须要保障提供给左侧业务零碎的接口的稳固,而右侧具体对接的开源计划、外部解决逻辑等,则能够随便调整。

整合OnlyOffice实现Office文档在线预览与编辑

让业务代码无耦合的形式应用预览能力

OnlyOffice作为一个负责office在线预览的性能组件,其提供了一个JS API办法。具体应用的时候,须要在HTML页面中援用其提供的JS文件并调用对应API办法将申请参数传递给OnlyOffice进行解决。这些申请参数外面,既含有对文档在线显示相干的一些属性约定,还蕴含一个重要的参数,也即须要操作的指标Office文件的获取地址url。在OnlyOffice收到申请之后,须要去给定的地址下载指标Office文件,而后外部解析解决之后,依照申请参数的指定信息,渲染展现到界面上。

在理论的零碎布局中,为了便于后续版本升级保护,以及防止OnlyOffice强耦合到各个业务零碎中,所以不太偏向于让前端界面间接去集成与调用OnlyOffice相干的JS文件

所以在施行的时候,在服务端的文件预览编辑服务中进行了封装,对外提供服务端API接口,服务端自带一个简略HTML界面(基于SpringBoot + Thymeleaf实现),业务申请对应服务端提供的独立html界面,并在界面中实现应用OnlyOffice的JS api申请的操作。

具体步骤阐明如下:

  • 对外提供服务端HttpGet接口,借助Thymeleaf框架,界面跳转呈现对应html界面

  • 提供简略的HTML界面,用于引入OnlyOffice JS文件,作为最终显示界面外壳:

  • 在独立的JS文件中,接管从JAVA逻辑中传入的参数信息,而后转换封装为OnlyOffice须要的格局,而后调用OnlyOfficeAPI接口发送申请

这样就实现整体的交互封装,业务能够代码无耦合的形式来间接应用预览能力。具体的office文档在线预览与编辑的能力实现,由开源的OnlyOffice来提供。

具体应用的时候,交互逻辑如下:

  1. 向文件预览服务发送申请,指定要操作某个文档;
  2. 文件预览服务通过对申请的鉴权以及其余解决逻辑之后,浏览器会跳转出OnlyOffice在线文档预览编辑界面,此步骤也会携带上具体的文档操作属性数据(比方文件下载地址、文件更新保留回调地址等)、以及操作的用户信息、容许以后用户执行的具体操作权限等等信息;
  3. 在关上的界面上,用户能够执行查看或者编辑等操作;
  4. OnlyOffice会通过指定的接口地址,获取要操作的文件的数据,以及编辑之后调用指定的回调接口,将更新后的内容保留。

看似很简单的逻辑,然而通过封装之后,对于业务应用而言其实很简略,只有在发送给文件预览服务的申请中,给定一个文件下载地址与文件保留回调地址即可。

协同在线编辑能力的关注点

后面有提过,采纳OnlyOffice来实现office文档的在线协同编辑,对于OnlyOffice在线编辑的原理,其官网给出的介绍如下:

对上述步骤解释如下:

也即当用户敞开文档编辑界面之后,会触发文档的保留事件,回调callback接口,将保留事件推送给服务端,并告知服务端变更后的文档地址,这样服务端能够从给定的地址下载变更后的文档,而后更新到本人的存储中

联合到咱们具体的我的项目应用中,其具体的交互过程开展论述下,就是下图的过程:

这里,一个在线编辑操作的回调申请内容示例如下:

{    "actions": [{"type": 0, "userid": "78e1e841"}],    "changesurl": "https://documentserver/url-to-changes.zip",    "history": {        "changes": changes,        "serverVersion": serverVersion    },    "filetype": "docx",    "key": "Khirz6zTPdfd7",    "status": 2,    "url": "https://documentserver/url-to-edited-document.docx",    "users": ["6d5a81d0"]}

对于回调申请的各个参数的具体含意,能够参见官网介绍,须要特地关注的几个字段梳理如下:

| 字段 | 字段类型 | 含意阐明 |
|---|---| --- |
| actions | List<Object> | 每个用户退出或者退出此文档的编辑的动作信息。其中具体type的取值0示意断开连接,1示意建设连贯 |
| key | String | 指标文档在OnlyOffice中解决的惟一标识ID,留神这里的key与业务零碎中指标文件理论的惟一ID并非一个概念,不能一概而论,因为业务零碎中某个文件的ID须要放弃不变,然而在OnlyOffice中编辑的时候,这个key须要不停的变。 |
| status | Integer |文档以后的操作状态类型,取值阐明:
1: 文档正在被编辑
2:文件已筹备好保留
3:文档保留产生谬误
4:文件敞开,没有变动
6:文档正在被编辑,然而以后状态曾经被保留
7:强制保存文档时产生谬误|
| url | String | 改变后的文档的下载地址,能够从这个地址下载到变更后的文件,而后存储更新业务零碎中理论的文档 |

理论测试的时候发现,此处的回调接口被调用的状况十分的频繁,务必要留神当且仅当actions中所有的对象的type都等于0的时候,也即所有用户均曾经退出编辑且文档曾经筹备好保留的时候,回调接口被调用的时候才须要去更新key值

这里是在理论构建的时候踩坑较久的一个中央,上面章节中开展具体说下踩坑过程。

OnlyOffice协同编辑踩坑记

在借助OnlyOffice构建在线协同编辑能力的时候,遇到一个很奇怪的问题,关上一篇文档,在线对其内容进行编辑,而后编辑实现后敞开窗口,过了一段时间尝试再次打开文档编辑的时候,却会报错:

看了下官网的问题起因解释,就是因为文档编辑之后,原来的key对应的文档曾经被编辑过,曾经不能被关上了(能够把key了解为不同的version,文档被编辑之后,version变更了,原来老的version就不容许操作了)。最初官网还很贴心的提醒:别忘了每次编辑之后要从新生成一个新的key

依照官网的介绍,在callback接口被调用的时候,从新为文件生成一个key,后续新的用户想要退出此文档的编辑的时候,都是拿到新生成的一个key,这样不就能够了吗?

  • Step1: 文档关上的时候,先尝试获取已存在的key值,如果不存在则新生成一个key并缓存起来
try {    // 如果redis外面有缓存此文档对应的key值,则间接应用    fileUniqueKey = redisCacheOperateService.getFileUniqueKeyDetail(fileId);} catch (Exception e) {    // 如果redis外面没有缓存此文档对应的key值,则生成对应的key并退出缓存中    fileUniqueKey = FileUniqueKey.builder().build();    redisCacheOperateService.saveOrUpdateFileUniqueKeyDetail(fileUniqueKey);}//获取本次在线操作对应的key值document.setKey(fileUniqueKey.generatekey());
  • Step2: 文件编辑保留回调解决中,从新生成新的key值并更新缓存的key值
// 编辑胜利后,从新生成随机码,实现key值变动的目标fileUniqueKey.updateRandomUniqueKey();redisCacheOperateService.saveOrUpdateFileUniqueKeyDetail(fileUniqueKey);

依照上述思路改完后,再次尝试,发现:

  1. 当用户A打开文档未做任何改变的时候,用户B也去打开文档,而后两个用户A、B都能够退出到同一个文档的协同编辑中,也能够进行协同编辑了;
  2. 当用户A或B做了改变之后,再有一个新的用户C退出此文档编辑的时候,却没有方法和A、B退出到同一个协同编辑会话中,C的改变会笼罩到A和B的改变,同理A或者B的改变也会笼罩掉C的改变。

难道只有让大家都约好了一起退出进去再开始编辑才行吗?那这个在线编辑性能显然就是个鸡肋了 —— 显然OnlyOffice也不太可能会是这种实现。再全面复盘了下测试的景象,剖析了下可能起因:

  • 因为A、B应用的同一个key,所以A和B能够退出到同一个协同编辑会话中
  • A或者B批改了文档之后,在callback触发的逻辑中,将此文档对应的key更新成了一个新的值
  • C尝试进行同一篇文档的在线编辑的时候,因为应用的key和A、B应用的key不雷同,所以这个时候对于OnlyOffice而言,其实C是在编辑一篇与A、B齐全独立的文档

所以问题还是出在了key的解决策略上。在网上找了一圈的文档没找到答案,受限于工夫束缚,也没有去看过OnlyOffice的源码,只能依据景象剖析OnlyOffice外部是基于本地缓存来解决的,而key是是否让申请打到同一份本地缓存的要害,猜想了下OnlyOffice外部的大抵解决思路是上面这个样子:

基于上述剖析:

  • 要想多人参加到同一个合作编辑会话中,必须要保障所有人操作的key都是雷同的一个
  • 要想编辑后的文档可能下次再被关上,必须保障下次关上的时候key应用新的值
  • key不变更的状况下,用户A关上编辑的时候,窗口未敞开的状况下,用户B能够退出,但如果用户A敞开,用户B再用同一个key拜访的时候,就会报错。

所以说,如果每次只有有用户还在线的时候,这个文档的key就不应该变,只有等某篇文档的所有用户都敞开编辑窗口的时候,再去解决文档key的变更,这样不就解决问题了吗?

那问题就简略了,依照这个思路批改了下callback的代码逻辑,判断下某篇文档的所有用户都退出编辑之后,再去从新生成新的key值。

代码演示如下:

@PostMapping("/callback")public DocumentEditCallbackResponse saveDocumentFile(@RequestBody DocumentEditCallback callback) throws IOException {    try {        // 当且仅当所有用户都退出后,才须要将key从新生成一下,否则下次再关上的时候,就打不开了        if (callback.getStatus() == DocumentStatus.READY_FOR_SAVING.getCode()                || callback.getStatus() == DocumentStatus.BEING_EDITED_STATE_SAVED.getCode()) {            // 保留文件内容            documentService.saveDocumentFile(callback.getKey(), callback.getUrl());            // 如果所有用户都已退出,则更新此文件对应的预览key值            boolean allUserExits = callback.getActions()                    .stream().anyMatch(actionsBean -> actionsBean.getType() == 0);            if (allUserExits) {                fileUniqueKey.updateRandomUniqueKey();                redisCacheOperateService.saveOrUpdateFileUniqueKeyDetail(fileUniqueKey);            }        }        return DocumentEditCallbackResponse.success();    } catch (Exception e) {        return DocumentEditCallbackResponse.failue();    }}

代码改变实现后,再次测试,果然问题隐没,在线预览性能恢复正常。

OnlyOffice集群化部署

为了保障预览服务的牢靠,在生产环境上布局施行集群化部署。 从上一章的论述中,咱们晓得OnlyOffice的性能实现重大依赖单机本地的缓存数据信息,在集群部署的场景下,适度依赖本地缓存的弊病就显现出来了。

集群化部署,本认为会很简略,间接部署多个docker节点,而后应用Nginx做一下反向代理以及负载平衡不就能够了嘛?然而理论施行的时候却发现在协同编辑场景下呈现了预期之外的问题。因为多人在线协同编辑的能力要求所有人对某篇文档的编辑申请都在同一个OnlyOffice服务节点上才行,而Nginx随机负载散发,会导致同一篇文档的编辑申请散发到不同节点上,这样就会导致编辑的内容互相笼罩。

因为用户的申请并不是间接打到OnlyOffice地址上的,而是先打到文件预览服务中,而后由文件预览服务通过某种策略解决后,再将申请重定向到OnlyOffice服务上进行文档操作的,所以这里咱们能够通过减少一个简略的散发策略,保障对同一个文档的所有的申请操作,都被散发到固定的一个OnlyOffice服务上解决即可。

这里的散发策略,思考有2种计划:

  1. 依据每个文档的惟一ID计算hashcode值,而后与OnlyOffice节点数取余,决定每个文档别离有哪个OnlyOffice服务解决。此计划实现起来最为简略,然而存在的问题也不少(比方节点新增或者删除的时候存在问题,须要上一致性hash算法)。
  2. 通过随机散发+Redis记住文档与节点映射的形式,先随机抉择一个节点,而后记录下此文件与OnlyOffice节点之间的映射关系,而后前面对此文件的申请始终散发到该OnlyOffice节点上。

这里咱们实现的时候采纳了第2种计划,借助redis缓存来实现,整体策略如上图示意。具体实现的时候对缓存数据减少了肯定的过期与续期策略,既保证同一文档申请散发到同一节点,又保障肯定工夫之后文档散发缓存隐没,能够重新分配闲暇的OnlyOffice服务器(因为开源版本OnlyOffice只反对最大20并发量,所以能够在此层级进行调配调整)。

具体代码逻辑如下:

public NodeServerInfo getOnlyOfficeServer(String fileUniqueId) {    // 从redis中先看下是否有调配过,如果有,持续应用    NodeServerInfo existServer = redisCacheOperateService.getExistOnlyOfficeServerByFileId(fileUniqueId);    if (existServer != null) {        if (serverAvailable(existServer)) {            // 缩短有效期            redisCacheOperateService.renewalOnlyOfficeMapExpireDays(fileUniqueId, onlyOfficeServerCacheDays);            return existServer;        } else {            // 删除有效的缓存            redisCacheOperateService.deleteExistOnlyOfficeServerMapping(fileUniqueId);        }    }    // 从新抉择一个可用的server    NodeServerInfo nodeServerInfo = chooseAvaliableServer();    // 将文件与服务器之间映射关系存储redis中    redisCacheOperateService.saveFileAndOnlyOfficeServerMapping(fileUniqueId, nodeServerInfo,            onlyOfficeServerCacheDays);    return nodeServerInfo;}

至此呢,集群化部署的问题解决,可用性上失去的无效保障。并且通过定期探测机制,及时将不可用的OnlyOffice节点从候选列表中剔除掉,保障了申请始终在可用节点上,无效防止了单点问题的呈现,也肯定水平上缓解单个节点的压力(社区版本同时仅反对20并发数、通过肯定策略能够扩散不同文件的申请到不同节点上)。

整合kkFileView实现其余文件的在线预览

kkFileView作为一个基于JAVA构建的可独立集成部署的文件预览开源组件,其在各种文件的预览上体现十分的优异,集成起来也十分的简略,间接提供下文件下载的地址就能够了。反对Office文档图片视频音频压缩包等各种文档的预览。

对于kkFileView的集成,咱们采纳了与OnlyOffice集成截然不同的解决策略,因为kkFileView基于JAVA SpringBoot技术栈构建,与咱们业务零碎技术栈统一,所以咱们基于kkFileView的源码进行了深度的定制整改。次要包含几方面:

  • 曾经采纳了OnlyOffice来提供Office文档的预览与编辑能力,这样kkFileView就不须要此局部能力,去掉此局部能力之后,整个kkFileView部署包体积放大300M左右
  • kkFileView打包的时候是打成了zip包,而后通过start.sh脚本来进行启动的,咱们适配了下公司内CI构建工具的特点,改为了经典的SpringBoot的部署状态,即1个jar搞定
  • 因为咱们的文件获取接口波及到权限校验,咱们定制了下此局部的逻辑,对接了下对立的鉴权核心。

两者交融:缓解OnlyOffice加载慢问题

基于后面整体的布局策略,Office文档应用OnlyOffice进行预览操作,非Office文档则由kkFileView实现预览操作(业务调用方无感知,都是对立一个url地址)。开发实现部署上线之后,性能也都一切正常。

然而自从上线之后,用户广泛吐槽在线Office文档预览的加载速度太慢,难以忍受。因为首次应用的时候OnlyOffice会在浏览器本地加载一个30M左右的缓存数据,而咱们的服务部署在公司内网机房外面,通过多层代理凋谢到公网中,用户在公司办公网络中拜访的时候,相当于绕了多层网络代理,且因为公司办公网络对客户端单机上行速率有限度,导致这个第一次加载缓存数据的工夫须要10-15s左右能力加载出文件。

尽管仅仅是第一次的关上速度比较慢(如果清理了浏览器缓存之后,首次加载还是会慢),然而期待的工夫的确也有点久,所以思考进行优化,晋升下用户的体验感知。

异步Office转PDF进行预览

尽管零碎反对了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预览成果不佳的状况, 所以为了解决此类问题,又对解决流程的逻辑进行了一点优化,申请参数中,预留了个字段,能够用于调用方设定是否禁用本地转换缓存后果文件进行预览:

@ApiModelProperty(value = "是否禁止应用转换后的格局来预览文件以晋升速度,默认false", required = false)private boolean notUseConvertedResultForPreview;

这样呢,在预览界面上能够提供个切换按钮。如果预览成果不称心,能够间接切换到原始文档采纳OnlyOffice服务进行预览,尽管速度慢些、然而能够解决预览成果的问题。

整体实现全貌

到此呢,整个文档的在线预览与编辑能力的构建,就算实现了。在解决具体的文档的预览或者在线编辑申请的时候,对应的解决判断总体逻辑如下:

回顾下构建之初布局的性能诉求,也曾经全副反对:

性能点反对状况
惯例文档在线预览
office文档在线预览
office文档协同编辑
集群部署
业务解耦

整体零碎层面的网元模块架构状况如下图所示,整个预览服务中,所有外部逻辑均封装在外部,对立由预览编辑服务对外提供API接口,供业务服务进行调用与交互。后续如果须要对预览服务的实现策略进行调整,也无需变更内部业务侧的逻辑,实现与业务逻辑解耦的成果。

总结

好啦,对于基于开源计划构建对立的文件在线预览与Office协同编辑平台的架构考量与实现过程关键点,这里就给大家分享到这里咯。看到这里,不晓得你是否也有过此方面的经验呢?针对文中的实现策略,是否还有什么更好的见解呢?欢送多多留言切磋交换。

须要补充一下:

  • 因为对OnlyOffice的源码实现或者框架具体实现理解也不是很深刻,所以本文论述的相干计划,次要是基于其社区版本,在应用层面进行额定的封装,来达到本身诉求。
  • 如有足够的精力或者能力,也能够思考间接基于其源码进行二次开发定制来实现目标 —— 这块受限于业务交付的急切性,没有尝试。

我是悟道,聊技术、又不仅仅聊技术~

如果感觉有用,请点赞 + 关注让我感触到您的反对。也能够关注下我的公众号【架构悟道】,获取更及时的更新。

期待与你一起探讨,一起成长为更好的本人。