「转」微内核架构应用研究
转载自: https://yunsong0922.github.io/2018/12/09/微内核架构应用研究/
微内核架构(Microkernel Architecture)也叫 Plugin Architecture,是一种基于插件的架构方式,通过编写精简的微内核来支撑以 plugin 的方式来添加更多丰富的功能。微内核架构在我们常用的应用和框架里面非常常见,比如工具有 IntelliJ、Chrome、Sublime、Photoshop 等, 前端框架有 jQuery、Babel、Webpack 等,基本比较流行的应用和框架都采用了微内核架构,虽然具体技术实现不同,但从思想上,它们都利用了插件机制带来的扩展性和灵活性。
微内核架构具体来说是什么样子的架构?为何具有如此大的威力?有哪些框架在使用它和架构以及如何使用的?下面的内容会进行具体的介绍。
1、什么是微内核架构?
微内核架构包含两个核心概念:内核系统和插件模块。应用的逻辑被切分到内核系统和插件模块中,以提供很好的扩展性、灵活性和逻辑隔离性。内核系统是将系统所要完成的业务逻辑进行高度的抽象,在高度抽象概念的基础上以实现通用业务逻辑。插件模块是独立的组件,包含特定的处理逻辑和自定义代码,旨在增强或扩展微核心以产生额外的业务功能。通常,插件模块应独立于其他插件模块,当然也可以设计需要其他插件的插件。无论哪种方式,将插件之间的通信保持在最低限度以避免依赖性问题非常重要。
内核系统在运行时候需要知道可用的插件,并获取它们的引用。比较常见的方式是微内核实现一种类注册表的机制,插件会注册到注册表中,从而微内核在适当的时机完成对插件的调用。微内核和插件之间的具体通信协议在架构模式层面并没做具体限制,可以是在同一个进程内,也可以是分布式的,可以通过 Socket 通信,也可以通过 HTTP 通信。关键的是插件可以扩展微内核,并且各个插件之间的功能各自独立。
下面让我们看看现有的框架是如何实现微内核架构的。
2、 微内核架构的前端应用
2.1、 jQuery 的微内核架构实现
jQuery 是前端流行的综合性框架,为前端的发展做出了不朽的贡献,在 MV* 框架流行之前,jQuery 扛起了整个前端大旗。为什么 jQuery 能如此流行呢?其中一个重要的原因是 jQuery 的简单和非常容易扩展。jQuery 的插件规则非常简单,几乎没有具体的规则,这是它能在整个社区中实现的难以置信的多样性的原因之一。
我们可以简单地通过向 jQuery 的 jQuery.fn 对象添加一个新的函数属性来编写一个插件
// 方法 1
(function($){
$.fn.myPlugin=function(){
//our plugin logic
};
})(jQuery);
// 方法 2
jQuery.extend({
myPlugin: funtion() {
//our plugin logic
}
})
这里我们通过两种方法,生成了 myPlugin myPlugin 中可以做任何我们想做的事情。
具体插件使用方法如下
$("#elem").myPlugin({
key: "value",
});
这么简单的插件机制,jQuery 是如何实现的呢?jQuery 采用了原型设计模式
// jQuery 入口函数
var jQuery = function (selector, context) {
return new jQuery.init(selector, context);
};
// jQuery 核心原型定义,也是 jQuery plugin 的扩展接口
jQuery.fn = jQuery.prototype = {
hello: function hello() {
console.log("hello world");
},
//... 其他定义
};
// 另一种扩展 plugin 的便捷方法,接收一个对象
jQuery.extend = jQuery.fn.extend = function (targetObj) {
// 克隆 targetObj
};
// jQuery 真正的实例化构造函数
var init = function (selector, context) {
// init dom elements
};
init.prototype = jQuery.fn;
jQuery.init = init;
$ = jQuery;
我们所有的扩展其实赋值给了 jQuery.prototype,这样在生成真正的 jQuery 对象的时候,相应的扩展就都可以使用了。jQuery 中自带的大量函数都是基于这种方式实现的。
2.2、 Webpack 的微内核架构实现
Webpack 是前端领域熟知的打包框架,可以将各种资源打包到 js 文件中,统一进行管理。Webpack 之所以能灵活的加载各种类型的资源,并将这些资源以灵活的形式进行打包,得益于 Webpack 优雅的微内核架构设计。
概念和架构设计
Webpack 的整体概念设计包括 Compiler、Loader 和 Plugin,
Compiler - 从业务上来讲, Webpack 本质上就是一个编译器,Compiler 实现了核心的微内核架构,将 Loader 和 Plugin 合理的组织在一起
Loader - 顾名思义是不同类型的资源加载器,比如 css-loader,babel-loader
Plugin - Compiler 在整个编译过程中,以 Hook 的形式暴露出了一系列回调,以供开发者编写 Plugin 来接收 Hook 并处理
三者的关系如下图
Loader 设计
Loader 和 Compiler 之间的关系非常灵活。Compiler 根据文件后缀筛选出相应的 Loader 来加载文件,比如用 css-loader 来加载 css 文件,用 babel-loader 来加载 jsx 文件。
Loader 接收 Compiler 传递过来的字符串形式的文件,经过编译和转换成为 javascript 之后将结果返还给 webpack 的 Compiler。Compiler 会从返回的结果中解析需要继续加载的 module,继续逐级加载,直至加载完成整个 module 依赖图。这个记载过程中可能会遇到各种各样的资源类型,会分别找到相应的 Loader 来加载。
Tapable Plugin Framework
在讲 Webpack 的 Plugin 实现之前,需要提一下 webpack 中衍生出来的 Tapable Plugin Framework。Tapable 是 webpack 插件架构的核心,极大简化了 webpack 的整体架构。虽然它在为 webpack 服务,但庆幸的是 Tapable 的优雅抽象使得我们能用它来编写其他微内核架构。
这里简单介绍一下 Tapable 的核心概念,更多细节可以参考 Tapable Github
Tapable 中核心概念包括 Hook 和 Tap 。 Hook 是 Compiler 编译过程中主动释放出的接口,Tap 可以为理解 HookHandler。这看起来很像是 Event 和 EventHandler 的关系,但细细思考和看代码之后,其实差距还挺大的。
在整个执行过程中,Tap 执行后会返回结果,并且这个返回结果会在后续的编译处理中使用,不论 Tap 是同步执行还是异步执行。而 Event 和 EventHandler 的关系,EventHandler 更像是 Event 发生后的一种副作用,不论是语义和编程模型都和 Hook 与 Tap 不同。
Tapable 提供了多种类型的 Hook,以方便进行数据处理和收集。按照执行类型可以分为:Sync
、AsyncSeries
、AsyncParallel
。按照返回结果可以分为: Basic、Waterfall、Bail、Loop。这使得 Compiler 的编程模型得到了极大简化。更多细节这里就不赘述了,如果想要了解更多请移步 Tapable Github 和 Tapable Type Definition
Plugin 设计
在讲完了 Loader 和 Tapable 之后,Plugin 的逻辑也就更好理解了,Compiler 使用 Loader 加载完成 javascript 之后,会在 Compiler 中调用各种 Hook 来完成核心的打包编译逻辑,而这些核心的打包和处理逻辑全都是 Plugin 实现了 Hook 回调来完成的。下面举一个例子:
const { SyncHook } = require("tapable");
class Car {
constructor() {
this.hooks = {
accelerate: new SyncHook(["newSpeed"]),
brake: new SyncHook(),
};
}
setSpeed(newSpeed) {
// 实现真正的加速
this.hooks.accelerate.call(newSpeed);
}
}
const myCar = new Car();
// Use the tap method to add a plugin
myCar.hooks.accelerate.tap("LoggerPlugin", (newSpeed) =>
console.log(`Accelerating to ${newSpeed}`)
);
myCar.setSpeed(13);
// 打印出 Accelerating to 13
这里 Car 这个实体使用 SyncHook 暴露了 accelerate Hook,当汽车真正开始加速之后,调用加速 Hook,相应的 Tap 也会被执行。
2.3、 Babel 的微内核架构实现
Babel 是现代前端领域必备的 JavaScript 编译器,特别是用 ES6 来编写前端,它是一个源码到源码的编译器,通常也叫做 “转换编译器(transpiler)”。面对快速变化而异构的前端环境,Babel 也使用了 Plugin 微内核架构来扩展自身的编译生命周期。
Babel 的生命周期主要抽象为 3 个核心阶段:
- 解析(parse)- 进行词法分析(Lexical Analysis)和语法分析(Syntactic Analysis)以生成 AST(抽象语法树)
- 转换 (transform) - 对 AST 进行相应的转换操作,此处实现了丰富的 Plugin 机制
- 生成 (generate) - 根据 AST 生成目标代码
AST 是编译器的核心数据结构,所有的源代码在经历解析阶段之后转换为 AST,转换阶段的核心代码都在操作 AST。更多 AST 细节移步 AST Explorer 和 AST Wiki
目前看来,Babel 由于解析阶段的复杂性,解析阶段暂时没有微内核架构支撑,但转换阶段实现了清晰微内核架构,可以方便的编写 plugin 来支撑语法转换,生成阶段比较简单,直接根据转换阶段生成的 AST 输出即可,不需要微内核架构。
解析阶段
解析阶段将源代码解析为 AST(抽象语法树), Babel 暂时并没有对普通开发者暴露 plugin 开发接口,而是使用继承的方式来进行扩展,实现不同语言的解析器。所以如果想要支持新的语法,此阶段需要直接往 Babel 中提交相应的 PR,但相应的转换阶段代码可以用 plugin 的方式进行开发。
这里列举了 Babel 中常用的 Parser 类型
// 通用解析器
class Parser extends StatementParser {}
// Typescript 解析器
class TypescriptParser extends Parser {}
// Flow 解析器
class FlowParser extends Parser {}
// Jsx 解析器
class JsxParser extends Parser {}
// Es 解析器
class EsTreeParser extends Parser {}
转换阶段
在解析阶段结束后,会生成 AST,转换阶段主要就是对 ASTNode 进行操作。所有的转换代码都可以用 plugin 的方式实现相应的 Visitor 来提供。Visitor 是操作 ASTNode 的实体定义。解析阶段所有的 Visitor 会尝试遍历所有感兴趣的 ASTNode。举个简单的例子:
这里是一个非常简单的 Babel 插件,用于处理 === 的表达式。
假设要将语句 foo === bar
,解析为 yun === song
。
// example plugin
export default function (babel) {
return {
visitor: {
BinaryExpression(path) {
if (path.node.operator !== "===") {
return;
}
path.node.left = babel.types.identifier("yun");
path.node.right = babel.types.identifier("song");
},
},
};
}
此函数在运行后,会返回 plugin 对象。plugin 对象中的变量 Visitor 包含核心 transform 逻辑,通过提供要处理的表达式类型,比如这里是 BinaryExpression,即可以处理相应的语句。这里判断如果 BinaryExpression 是一个 === 操作,就将变量的左右进行替换。
Visitor 主要是在操作 AST,比如上述 foo === bar
对应的语法分析树为:
{
type: "BinaryExpression",
operator: "===",
left: {
type: "Identifier",
name: "foo"
},
right: {
type: "Identifier",
name: "bar"
}
}
上述 Visitor 就是在将 left 和 right 换掉。
这里附上 Visitor 的接口定义,
// Scope 是作用域,Binding 是变量和作用域的关系
export class Scope {}
export class Binding {}
// plugin 编写核心接口
export interface Visitor<S = Node> extends VisitNodeObject<Node> {
ArrayExpression?: VisitNode<S, t.ArrayExpression>;
AssignmentExpression?: VisitNode<S, t.AssignmentExpression>;
// ...
}
export type VisitNode<T, P> = VisitNodeFunction<T, P> | VisitNodeObject<T>;
export type VisitNodeFunction<T, P> = (this: T, path: NodePath<P>, state: any) => void;
export interface VisitNodeObject<T> {
enter?(path: NodePath<T>, state: any): void;
exit?(path: NodePath<T>, state: any): void;
}
// 代表一个 Node,但比 Node 拥有更丰富的信息,方便快速处理
export class NodePath<T = Node> {}
生成阶段
暂无微内核架构
3、微内核架构的服务端应用
TODO
4、总结
从以上的分析,我们可以看到。微内核架构作为一种常用的架构模式,无论是在前端和后端,都能极大的简化架构设计,并提供优越的扩展性和灵活性。对我们个人的设计和开发提供了很好的思路。
参考资料
Learning Javascript Design Patterns jQuery 源代码 Webpack API Tapable Plugin Framework Tapable Type Definition AST Explorer Babel 插件手册 @babel/parser @types/babel-traverse