diff --git "a/53.\347\262\276\350\257\273\343\200\212\346\217\222\344\273\266\345\214\226\346\200\235\347\273\264\343\200\213.md" "b/53.\347\262\276\350\257\273\343\200\212\346\217\222\344\273\266\345\214\226\346\200\235\347\273\264\343\200\213.md" new file mode 100644 index 00000000..ce5b3cdb --- /dev/null +++ "b/53.\347\262\276\350\257\273\343\200\212\346\217\222\344\273\266\345\214\226\346\200\235\347\273\264\343\200\213.md" @@ -0,0 +1,423 @@ +本周精读内容是 《插件化思维》。没有参考文章,资料源自 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 解耦,父元素就不需要知道子元素的具体实例。一般来说,决定一个组件状态的都是其父元素而不是子元素,比如一个按钮可能在 `` 中表现为  一种组合态的样式。但不可能说 `` 因为有了 `