构建化工具 Webpack,glup,grunt 等都支持插件化开发,后端框架 koa, egg 等也支持插件化拓展,所有框架都希望自身有高拓展性,所以纷纷选择插件化作为增强框架自身的功能
- 插件化作为 geek 的一部分,协同开发的程序都离不开插件机制的支撑,没有插件化,核心库的代码就会变得冗余,导致功能耦合相对严重,维护成本增加,插件化就是将不断扩张的功能分散到插件中,内部进行集中维护逻辑。
-
理想状态下,一个插件应该满足三个拓展性要求:
- 社区可以贡献代码,即使代码存在问题,也不影响核心代码的稳定性
- 支持二次开发,满足不同的业务场景需求
- 让代码以功能为维度聚合,而不是用片面的逻辑结构组成,代码量一旦上来此优势便十分明显
-
做插件设计,最好先从使用者角度出发,设计舒服的调用方式再去考虑实现。
-
插件化分类
-
参考设计模式: 命令模式, 工厂模式,抽象工厂模式等等总结三种:
- 约定/注入插件化 (2.1)
- 事件插件化 (2.2)
- 插槽插件化 (2.3)
-
2.1 约定一般是入口文件/指定文件名作为插件入口,文件形式.json/.ts 不等,返回对象根据约定名称书写,就会被加载并获取到其上下文
// e.g. package.json 中的appollo只要存在commands属性,就会自动注册新命令行 { "appollo": { "commands": [{"name": "publish", "action": "doPublish"}] } }
- 如果功能相对杂乱,没有清晰的功能入口规划,用对象形式会更加简洁,并更倾向使用一个入口,因为主要操作的是上下文,只需要一个入口,内部逻辑种类无法控制:(Temp)
export default class User { // context.sourceFiles.xx } // fis, gulp, webpack, egg
- 如果功能相对杂乱,没有清晰的功能入口规划,用对象形式会更加简洁,并更倾向使用一个入口,因为主要操作的是上下文,只需要一个入口,内部逻辑种类无法控制:(Temp)
-
2.2 事件插件化就是通过事件的能力提供插件开发的能力:
- 比如 dom 事件
docment.on("focus", callback)
本质上他可拓展,可以重复定义多个 focus 事件相互独立,事件间的 callback 执行任务也不受影响,可以理解为事件机制在某些阶段释放出一些钩子,允许用户代码拓展整体框架的生命周期,事件机制比较出彩的就数 koa 了,其中间件洋葱模型非常出色,,可以认为是控制执行时机的事件插件化,把想要执行的事件插件放在 next()后即可,终止插件执行也可以不调用 next()
- 比如 dom 事件
-
2.3 插槽插件化
-
这种插件化一般用在 UI 元素的拓展,react 的内置数据流是符合组件物理结构的, 而 redux 数据流是符合用户定义的逻辑结构,对于 html 布局也是一样,html 为物理结构,插槽布局方式就是 html 中的 redux
// 插槽组织形式 { position: "root", View: <Layout>{insertionPosition("Layout")}</Layout> }
-
重要点: 实现 UI 解耦,父元素不需要知道子元素的具体实现,一般决定一个组件状态的是其父元素而不是子元素,比如一个 button 在中表现为一种组合态的样式,但不能说有了另外一个组件而影响自身逻辑发生变化。
<Tabs>{insertionPosition(`tabs-${this.state.selectedTabs}`)}</Tabs>
-
-
2.4 分型插件化
- 特点: 插件结构与项目结构分型,组成大项目的小插件,自身结构与项目结构相同
-
-
核心代码怎么加载组件,支持插件化的框架核心功能是整合插件以及定义生命周期,与功能相关的代码可以通过插件实现
-
确定插件加载形式
-
确定插件注册形式
- npm 注册,符合每个前缀会自动注册为插件
- 通过文件名注册, 存在
xx.plugin.ts
会自动做到插件引用,一般作为辅助方案使用 - 代码形式注册,在代码中 require 即可,如
babel-polyfill
不过要求插件执行逻辑在浏览器中运行,场景受限。 - 通过描述注册: package.json 描述一个属性,表明加载插件,如
.babelrc {"presets": ["es2015"]}
- 自动注册:比较暴力,通过遍历可能存在的位置,主要满足插件约定的自动注册为组件
-
确定生命周期: 组件注册完成后,第一件事就是加载组件,根据框架业务逻辑不同而执行不同的生命周期,插件在生命周期中扮演不同的功能,通过插件影响这些过程的执行。
-
插件对生命周期的拦截:一般通过事件,回调函数等方式,支持插件生命周期的拦截
-
插件之间的依赖和通信,两种方式处理:依赖关系定义在业务项目中,与依赖关系定义在插件中,如依赖关系定义的项目中,webpack 配置举例:
{ "use": ["babel-loader", "ts-loader"] }
- 执行逻辑是 ts-loader -> babel-loader, 规则由框架自身决定,但是插件加载规则有顺序,跟配置写法相关。配置得写在插件中。
- 另一种行为将插件依赖写在插件中,webpack-preload-plugin 依赖 html-webpack-plugin
- 两种场景一是与业务有关的顺序,插件无法做主的业务逻辑问题 ,需要把殊勋交给业务项目配置;二是插件内部顺序,业务无需关心的顺序问题,插件自身定义执行顺序,框架核心一般可能需要同时支持者两种配置方式,最终决定插件的加载顺序。
- 插件间通信可以通过 hook 或者 context 支持,hook 主要传递的是时机信息,context 主要传递的是数据信息,是否生效取决于上面说到的插件加载顺序
-
核心功能的插件化
-
插件化框架核心代码主要是对插件的加载,生命周期的梳理,以及实现 hook 让插件影响生命周期,最后补充上插件的加载顺序以及通信,就比较完备了。
-
衡量代码质量点: 是否所有核心业务逻辑都可以由插件完成,因为只有插件实现核心业务逻辑,才能检验插件的能力,进而推导出第三方插件是否拥有足够的拓展能力。
-
核心逻辑有一部分代码没有通过插件机制编写,第三方插件无法拓展此逻辑,也不利于框架的维护。
-
从此思想出发,开发者得明确哪些功能应该做成插件,以及将哪些插件固化为内置插件,核心三点:
-
哪些插件需要内置:开源的,基础功能以及体现核心竞争力的可以内置,如基础功能,缺少基础功能的框架做不了任何事
create-react-app
babel
-
插件是依赖型还是完全正交的:正交插件(不影响其他插件,也不依赖其他任何插件,自身也不需要被任何插件拓展)
- 非正交插件也是三点:
- 依赖其他插件,需要补充使用顺序
- 依赖并拓展其他插件的插件,此插件最好不要减少其他插件的功能,可以新增一个规则单独对插件执行顺序进行控制
- 非正交插件也是三点:
-
可能被其他插件拓展的插件:难点在设计拓展的颗粒度
-
入口文件举例:
import * as React from 'react'; import * as ReactDOM from "react-dom"; import { App } from "./app"; ReactDOM.render(<App />, document.getElementById("root"));
-
潜在的拓展需求:
- 某处插入其他代码支持
- 如何保住插入代码的顺序
- 用
react-lite
替换react
怎么支持 - dev 模式用 hot(App)替换 App 作为入口,怎么支持
- 模板入口 div 的 id 可能不是 root,怎么支持
- 模板入口 div 是自动生成的,怎么支持
- 用于 ReactNative,没有 document,怎么支持
- 后端渲染时,需要用
ReactDOM.hydrate
而不是ReactDOM.render
怎么支持 - 八种场景组合出现,需要保证任意组合正确运行,无法全量替换模板,怎么办?
-
-
-
插件化场景 -- 举例
- 前后端框架 react 的生命周期 koa 的中间件, 业务代码用到的 request 处理
- 脚手架 功能可插拔
- 工具库 redux 提供的中间件机制
- 需要多人协同的复杂业务场景
- 插件化系统化集成思考引出对框架开发心得分享
- 插件化带来更好的分工协作,同时框架提供的可拓展性得到了提高。
-