缘起
笔者使用飞书文档已有一年多,期间再也没有换过文档类工具。飞书文档提供的协同、高效的创作体验深深吸引着我。 再加上我所在的团队也十分喜欢飞书的产品设计,因此我对飞书的接触足够多。
今年一整年,我都在创作自己的软件--reduce。一开始的定位是一款笔记软件,随着后面逐渐开发,它渐渐换了产品形态。不过,reduce的研发过程中却有一项设计让我十分在意,那就是rps的设计。
rps
rps , 全称 -- reduce plugin system。皆在解决reduce功能少的问题。
在我的世界观中非常喜欢unix的设计哲学。把一件事情划分到很小,然后做好它,不同部分之间完美配合,简单且高效。
因此,reduce一开始就是以一个十分微小的核心去构建它的功能的。自然而然,在reduce 0.2的过程中就产生了插件的概念,当时并不知道微内核这个概念,因此浪费了4个月的时间研究怎么做插件系统,然而却最终只是表象,看不透里面的本质。
随着后面积累的深入,我逐步产生了构建一个微内核的想法,并通过了自己的技术上的最小可行性验证。
微内核
提到微内核可能有些陌生,然而你一定听说过插件。
插件的概念在前端的世界里面早已深入人心。
像我们最常用的编辑器vscode,就提供了非常多的插件。
还有webpack, vite, umi, vue, babel, rollup ...
前端的世界早已经被插件所包围。
微内核本质是一种架构, 所谓架构,其实就是为了减少代码的重复编写,方便我们程序员管理代码的内心功法。
按照微内核的架构可以实现什么呢? 可以实现想要的功能可以灵活定制,不断地加入到插件系统中去。
然而,按照这样肤浅的理解貌似还远远不够~~~
飞书文档的微内核架构
我们直击本质,看到飞书文档的底层逻辑是什么?
飞书文档的本质在于由一个个“微内核”构成。
飞书文档中的微内核称作“块”。
其实稍有研究就可以发现,块和块之间本身没有很大的强耦合关系,这是因为在我们人类使用东西的时候,也是喜欢将一些事情分类聚合,所以,一个承载着业务的块,在飞书文档中处处被体现。
明白了飞书文档中的基本结构,我们再看每个块它内部的实现细节。
hover到某个块上都会弹出一个小浮窗的操作菜单,这些就是块的“插件”。
这也就不难理解为什么飞书文档这款产品中好多块它的操作逻辑都是一致的,就是因为它安装了对应的插件。
这样,原本什么功能都没有的块, 在插件的加持下有了渲染菜单的功能。
现在我们继续给块添加功能,添加一个小组件插件,可以将小组件渲染进块中。
小组件就是react写的一个UI组件,本身从外界接收props渲染,并对外暴露api。这样,一个插件就保证了它的纯净。
块通过将自己这个对象当做ctx传递给组件,让组件给自己挂载额外的属性和方法。
好了,现在块只需要从数据库里取出业务数据传递给组件去渲染就可以了。
假设我想实现对块的协同编辑,怎么做?
在我们上面的块的实现中,插件的value和onChange均被内核所劫持,然而,内核只提供最基本的功能,协同难道要集成到内核中去吗?
不,我们换种思路,只需要一个函数去在分发value的时候拦截他做点事情,然后在onChange->value改变的时候再劫持一下做点事情,协同这个难题就解决了嘛。
本身插件的value和api都挂载到内核对象上,做点事情绝对不是内核去干的事情,而是内核需要引入第三方的对象去统筹处理。
这就引入一种设计模式--策略模式。
详细的策略模式可以去了解一下,这里不做赘述。然后,我们在块中可以将该策略的接口对象注入进来,由ioc容器去选择合适的实现类去实现对应的策略。
这样,一个基于block构建的微内核就完成了。
block对应的后端将每个block对应成单例,然后对业务方提供block的统一抽象,业务方自己的业务实现对block的进一层封装管理,搭配block后端,将block嵌入到自己的业务系统中。例如:文档、白板、网盘等等。