本周精读内容是 《插件化思维》。没有参考文章,资料源自 webpack、fis、egg 以及笔者自身开发经验。 ## 1 引言 用过构建工具的同学都知道,`grunt`, `webpack`, `gulp` 都支持插件开发。后端框架比如 `egg` `koa` 都支持插件机制拓展,前端页面也有许多可拓展性的要求。插件化无处不在,所有的框架都希望自身拥有最强大的可拓展能力,可维护性,而且都选择了插件化的方式达到目标。 我认为插件化思维是一种极客精神,而且大量可拓展、需要协同开发的程序都离不开插件机制支撑。 没有插件化,核心库的代码会变得冗余,功能耦合越来越严重,最后导致维护困难。插件化就是将不断扩张的功能分散在插件中,内部集中维护逻辑,这就有点像数据库横向扩容,结构不变,拆分数据。 ## 2 精读 理想情况下,我们都希望一个库,或者一个框架具有足够的可拓展性。这个可拓展性体现在这三个方面: * 让社区可以贡献代码,而且即使代码存在问题,也不会影响核心代码的稳定性。 * 支持二次开发,满足不同业务场景的特定需求。 * 让代码以功能为纬度聚合起来,而不是某个片面的逻辑结构,在代码数量庞大的场景尤为重要。 我们都清楚插件化应该能解决问题,但从哪下手呢?这就是笔者希望分享给大家的经验。 做技术设计时,最好先从使用者角度出发,当设计出舒服的调用方式时,再去考虑实现。所以我们先从插件使用者角度出发,看看可以提供哪些插件使用方式给开发者。 ### 2.1 插件化分类 插件化许多都是从设计模式演化而来的,大概可以参考的有:命令模式,工厂模式,抽象工厂模式等等,笔者根据个人经验,总结出三种插件化形式: * 约定/注入插件化。 * 事件插件化。 * 插槽插件化。 最后还有一个不算插件化实现方式,但效果比较优雅,姑且称为分形插件化吧。下面一一解释。 #### 2.1.1 约定/注入插件化 按照某个约定来设计插件,这个约定一般是:**入口文件/指定文件名作为插件入口,文件形式.json/.ts 不等,只要返回的对象按照约定名称书写,就会被加载,并可以拿到一些上下文。** 举例来说,比如只要项目的 `package.json` 的 `apollo` 存在 `commands` 属性,会自动注册新的命令行: ```json { "apollo": { "commands": [{ "name": "publish", "action": "doPublish" }] } } ``` 当然 json 能力很弱,定义函数部分需要单独在 ts 文件中完成,那么更广泛的方式是直接写 ts 文件,但按照文件路径决定作用,比如:项目的 `./controllers` 存在 ts 文件,会自动作为控制器,响应前端的请求。 这种情况根据功能类型决定对 ts 文件代码结构的要求。比如 node 控制器这层,一个文件要响应多个请求,而且逻辑单一,那就很适合用 class 的方式作为约定,比如: ```typescript export default class User { async login(ctx: Context) { ctx.json({ ok: true }); } } ``` **如果功能相对杂乱,没有清晰的功能入口规划,比如 gulp 这种插件,那用对象会更简洁,而且更倾向于用一个入口**,因为主要操作的是上下文,而且只需要一个入口,内部逻辑种类无法控制。所以可能会这样写: ```typescript export default (context: Context) => { // context.sourceFiles.xx }; ``` > 举例:`fis`、`gulp`、`webpack`、`egg`。 #### 2.1.2 事件插件化 顾名思义,通过事件的方式提供插件开发的能力。 这种方式的框架之间跨界更大,比如 dom 事件: ```typescript document.on("focus", callback); ``` 虽然只是普通的业务代码,但这本质上就是插件机制: * 可拓展:可以重复定义 N 个 focus 事件相互独立。 * 事件相互独立:每个 callback 之间互相不受影响。 也可以解释为,事件机制就是在一些阶段放出钩子,允许用户代码拓展整体框架的生命周期。 `service worker` 就更明显,业务代码几乎完全由一堆事件监听构成,比如 `install` 时机,随时可以新增一个监听,将 `install` 时机进行 delay,而不需要侵入其他代码。 在事件机制玩出花样的应该算 `koa` 了,它的中间件洋葱模型非常有名,换个角度理解,可以认为是**能控制执行时机的事件插件化**,也就是只要想把执行时机放在所有事件执行完毕时,把代码放在 `next()` 之后即可,如果想终止插件执行,可以不调用 `next()`。 > 举例:`koa`、`service worker`、`dom events`。 #### 2.1.3 插槽插件化 这种插件化一般用在对 UI 元素的拓展。**react 的内置数据流是符合组件物理结构的,而 redux 数据流是符合用户定义的逻辑结构**,那么对于 html 布局来说也是一样:**html 默认布局是物理结构,那插槽布局方式就是 html 中的 redux。** 正常 UI 组织逻辑是这样的: ```tsx
``` 插槽的组织方式是这样的: ```tsx { position: "root", View: {insertPosition("layout")} } ``` ```tsx { position: "layout", View: [
{insertPosition("header")}
, ] } ``` ```tsx { position: "header", View: } ``` ```tsx { position: "footer", View: } ``` 这样插件中的代码可以不受物理结构的约束,直接插入到任何插入点。 更重要的是,实现了 UI 解耦,父元素就不需要知道子元素的具体实例。一般来说,决定一个组件状态的都是其父元素而不是子元素,比如一个按钮可能在 `` 中表现为一种组合态的样式。但不可能说 `` 因为有了 `