Back
Featured image of post Cloudreve V3 重构总结

Cloudreve V3 重构总结

对 Cloudreve V3 开发过程一些零碎的总结

前言

Cloudreve 作为一个从2017年开始写的项目,其后端一直延续着最初版本的架构。而 Cloudreve 最初版本是我学习 ThinkPHP 时边学边写的项目,项目的整体结构及逻辑较为混乱,随着新特性的不断加入,很多时候不得不为了兼并 “Legacy Code” 花费很多功夫。重构后端的想法我很早就产生了,但是因为工作量很大,一直没能实施。2019年10月起,因为手头没什么别的大项目、学校的课程相对的轻松了一些,所以就有了用 Golang 完全重构后端的想法。重构过程最终花费了 6 个月,在后端完全重构的同时,也针对前端做了大量优化、改进,最终在 2020 年 3 月 正式完成第一个 V3 的 RC 版本。这篇文章将对这个过程,以及整个 V3 版本的开发做一个总结。

改善的问题

清晰的整体架构

V2 版本中,MVC 模式的规划并不严谨。V3 版本使用的 Web 框架是 gin,其显著特点是轻量,不会在项目架构上做出限制,你可以按照自己的喜好组织整体结构。V3 的后端主要由 Controller、Service、Model、Serializer 组成。 其中 Serializer 负责返回 JSON 的序列化;Model 是 GROM 中数据库模型的扩展,封装了每个模型的常用数据库操作。

异步任务支持

Cloudreve 本身有某些动作需要异步地在后台执行,比如文件的转存,即将文件数据由服务端传输到存储端。在 V2 版本中,用户需要额外部署“任务队列”辅助程序,负责在后台执行这些任务,但问题是这种方案稳定性差、部署繁琐。V3 版本中将异步任务的处理和主程序融合,并且新增了很多需要异步任务的特性,比如打包下载、文件压缩/解压缩等。

更统一的存储策略支持

V2 本身就支持了多种类型的存储策略,V3 所支持的存储策略种类基本与 V2 保持一致。但是 V2 中对各个存储策略的支持比较割裂,使用不同存储策略得到的体验差别较大。比如:离线下载只支持本机存储,不同存储策略对 缩略图、WebDAV 等特性的支持差异很大。在 V3 中,通过对不同存储策略接口的统一抽象,不同存储策略的差异进一步缩小。离线下载、WebDAV 都覆盖了全部类型的存储策略,新存储策略的开发也变得更加简单。即便如此,存储策略间的差异仍然不能完全消除,具体对比可参考【存储策略对比】,但我们仍在不断探索缩小差异的可能。

减少用户出错率

在部署流程上,通过内嵌静态资源、Web 服务器、SQLite 驱动等组件,V3 把流程缩短到了“开箱即用”的状态,不再需要填写各种数据库信息、检查权限、配置 URL Rewrite 规则等操作,将用户在部署流程中的出错率降到尽可能的低。

把用户当傻子 – 在设计 V3 的交互时我一直谨记这句话。在 V2 版本中,用户反馈上来的很多“Bug”其实都是流程操作不当导致,而造成这一现象的根本原因是设计交互时过于想当然了,很多情况下用户并不会按照你的逻辑思路去走。V3 中强化了表单填写的指引、检查,并且将之前出错率最高的添加存储策略的表单改为了向导式的结构,将教程本身和表单结合,一步步地引导用户完成存储策略的添加。

向导式的表单
向导式的表单

不同的上传请求处理思路

在 V2 中,本地存储策略采用的是传统的分片上传合并的方式。V3 中我探索使用了新的流式上传模式:客户端将数据全部装到正文里,服务端采用类似管道-过滤器模式,把请求正文当作数据流处理。这样做虽然牺牲了断点续传的功能,但是来自请求正文的文件数据直接写入硬盘,可以大幅提高上传流程的 I/O 效率。更重要的是,我们可以把所有支持流式输入的操作加入到上传流程中。比如:在处理上传数据的同时计算 CRC ;又或者是将接收到的数据实时中转传输到远程的存储端(这也就使得 WebDAV 覆盖全部存储策略成为了可能)。

同时,V3 把上传处理部分的构建进一步封装,在不同生命周期都暴露出 Hooks 方便不同场景下定义上传处理,进一步提高了代码复用度。

单页应用

因为历史遗留问题,V2 中虽然使用了 React,但是不同页面间仍是独立的存在,并且前后端未完全分离,某些字段仍会以模板变量的形式传递给前端。 V3 借助 React Router,将全部页面整合,并且对前后端进行了完全分离,通过 Lazy Load 提升前端模块加载效率。在初期,我曾尝试将各个子页面全部 Lazy Load,但是这样会产生大量过度分离的碎片 Chunk,一次请求几十个只有几 KB 的 JS 文件显然不如一次请求几个几百 KB 的。所以最终只把模块分离成了前台、后台、某些预览组件(比如 PDF.js),并进行 Lazy Load。

黑暗模式

2019 年的 UI 设计方向好像聚焦在了黑暗模式上。借助于 Material-UI 自带的调色板机制,黑暗模式的适配可以很轻松的完成。除了让用户自己选择外,还可以借助媒体查询 prefers-color-scheme 跟随系统的黑暗模式设定。

单元测试

V2 中没有单元测试,所以后期进行版本迭代时会引入很多很难发现的潜在问题。V3 在 Service 层以下做到了 85% 的 Coverage。在写测试的过程中经常需要重读一部分代码,而许多 Bug 是在这个过程中发现的,果然小黄鸭调试法还是很有用的🤣。

有意思的东西

在重构过程中学到的、用到的一些有意思的东西:

sync.Pool

最初看到这个是在 gin 的源代码中,gin.Context 在初始化时是直接从一个 sync.Pool 中取出来的。这个包的作用是在高并发场景下减少 Go 的 GC 对性能带来的影响。随后我也在 Cloudreve 的 FileSystem 对象中用到了这个包,因为每个与文件管理相关的请求都需要初始化一个FileSystem 对象,而FileSystem 本身的体积还是挺大的。

一般情况下,当FileSystem 被用完后需要等待 GC 回收,而高并发场景下可能产生大量待回收的FileSystem 资源。而sync.Pool能把这些被丢弃的FileSystem 复位并回收暂存起来,下次有需要用到时,直接从sync.Pool 中取出一个就行了。当然,如果池子里暂存的不够用了,还是需要重新 new 一个的。

Hashids

Hashids 是一个包含了多种语言实现的库集合,可以根据整型生成唯一的字符串ID。一个典型的应用场景就是生成类似百度云分享链接结尾的 Key,理想场景下,生成的方式需要具有以下特性:

  • 得到的 Key 唯一
  • 服务端可通过 Key 找到对应的 Integer(在这个场景下是数据库主键)
  • 用户无法通过 Key 还原对应的 Integer
  • Key 尽可能短,长度可随着 Integer 的增加适当增加

Hashids 可以完美满足上述条件,它支持通过加盐得到不同组的 Key,用户不知道具体 Salt 值的情况下是无法复原 Integer 的。

WebAuthn

WebAuthn 是一个能让网站支持浏览器级别的认证方案的协议。借助 WebAuthn ,我们可以实现用 YubiKey、Windows Hello、Face ID 等这种物理的认证方式登录网站。V3 版本也实验性的加入了 WebAuthn 支持。

使用 Windows Hello 登录 Cloudreve
使用 Windows Hello 登录 Cloudreve

有关 WebAuthn 的更多信息,推荐阅读『谈谈 WebAuthn』,解释的非常详尽,还包含了交互式的 Demo。

在 Go 的二进制包中嵌入静态资源

为了实现”开箱即用“,Cloudreve 的前端静态资源需要在编译时嵌入最终的二进制包。这种工具的轮子有很多,基本原理都是基于 Code Generation,需要手动执行嵌入命令,这些工具将前端资源用 ZIP 打包后的数据放到一个slice 里面,并对外提供了FileSystem接口方便读取文件。

任务追踪

因为重构过程的工作量较大,所以我需要一个任务管理工具方便我有条理的规划工作。因为不涉及团队开发,所以对工具的多人协作的特性要求不高。最开始我使用的是 Microsoft To Do,优点是使用起来很方便,有什么新灵感可以直接打开手机记录下来;但缺点是没法对任务结构化的划分,只支持设立二级的子任务。后来尝试了 GitHub 自带的看板,体验下来还挺不错的,可以跟 issue 深度结合,自由度也很高。

Microsoft To Do
Microsoft To Do
GitHub Project Boards
GitHub Project Boards