微内核架构应用研究

发布于 8 天前  1 次阅读


微内核架构(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(){
        //ourpluginlogic
    };
})(jQuery);

// 方法 2
jQuery.extend({
    myPlugin: funtion() {
        //ourpluginlogic
    }
})

这里我们通过两种方法,生成了 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 的整体概念设计包括 CompilerLoaderPlugin

  • 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 FrameworkTapable 是 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,以方便进行数据处理和收集。按照执行类型可以分为:SyncAsyncSeriesAsyncParallel。按照返回结果可以分为: BasicWaterfallBailLoop。这使得 Compiler 的编程模型得到了极大简化。更多细节这里就不赘述了,如果想要了解更多请移步 Tapable GithubTapable 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、总结

从以上的分析,我们可以看到。微内核架构作为一种常用的架构模式,无论是在前端和后端,都能极大的简化架构设计,并提供优越的扩展性和灵活性。对我们个人的设计和开发提供了很好的思路。


这个时间很美好,但它也很残酷