Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

plugin-anything 插件化改造工具 #150

Open
hoperyy opened this issue Aug 5, 2020 · 0 comments
Open

plugin-anything 插件化改造工具 #150

hoperyy opened this issue Aug 5, 2020 · 0 comments

Comments

@hoperyy
Copy link
Owner

hoperyy commented Aug 5, 2020

github: plugin-anything

前言

前端团队在实现工程化的 Cli 套件、Node Server 等系统时,为了满足功能的开放性,通常有三种方式:配置化、插件化、配置与插件的结合。

三者均有各自的优劣势:

  • 配置化

    • 优势

      顾名思义,用户在使用的时候,通过工具暴露出的配置文件进行各类快捷配置,实现对工具的影响。

    • 劣势

      而工具的核心功能,内置于工具模块内部,其逻辑外部无法干预。

  • 插件化

    • 优势

      工具内部只维护一系列生命周期及任务调度,所有业务功能以插件的形式对接,用户可以在尽可能大的自由度下,订制自己需要的功能。

    • 劣势

      上手成本不会像配置文件那样开箱即用,需要用户理解插件开发规范。

  • 配置与插件的结合

    这种是现在主流的方式 Webpack、Babel 等工具均采用该方式,插件化将这类工具的生态极大完善。

Webpack 的插件化思路

Webpack 维护了一个生命周期,主要做两件事情:1. 核心代码运行;2. 暴露生命周期中的各个钩子。

文档地址

用户引入插件的方式:

webpackConfig: {
    plugins: [
        new PluginA(),
        new PluginB(),
        // ...
    ]
}

开发插件的方式:

class PluginA {
    constructor() {
        
    }
    apply(compiler) {
        compiler.hooks.someHook.tap(...);
    }
}

在插件开发时,开发者拦截了 someHook 钩子,对 Webpack 在该时刻的生命周期进行干预。

上述代码中,出现了 compiler.hooks.someHook,背后的原因,是 Webpack 将暴露出的生命周期钩子(如 someHook 钩子)挂载到内部的 hooks 对象上,举例如下:

hooks = {
    hookA: new WaterfallHook(),
    hookB: new BailHook(),
}

WaterfallHook: 流水式事件集,注册的处理逻辑会串行执行
BailHook: 瀑布式事件级,注册的处理逻辑会并行执行
Webpack 的事件机制依赖 Tappable,其有众多类型的事件级,这里不赘述

总结而言:Webpack 内部执行核心逻辑的同时,暴露繁多的生命周期钩子,交给插件干预,实现灵活的功能。

Babel 的插件化思路

Babel 的插件化机制和 Webpack 的插件化又有一些不同。

Babel 本质上是 StringA -> AST -> AST change -> StringB 的转换器,其插件是为该核心功能服务的。

Babel 运行时,会先将 String 转为 AST,遍历 AST,并在遍历图中对 AST 节点进行改造,最终将 AST 转为新 String。

Babel 的插件机制主要影响的是上述中 “遍历 AST,并在遍历图中对 AST 节点进行改造” 过程。

开发 Babel 插件

开发 Babel 插件的方式,是提供 Vistor 对象,也就是针对各个 AST 节点的回调处理方法,拦截节点的遍历行为,并对节点进行改造。

const Visitor = {
    ASTNode1() {
        // change node
    },
    ASTNode2() {
        // change node
    }
}

使用 Babel 插件

这一点是值得借鉴的,Babel 接受众多的插件引入方式,如:

  • .babelrc.json / babel.config.json

    {
        "plugins": [
            "transform-runtime",
            "class-properties"
        ]
    }
  • babel.config.js

    module.exports = {
        plugins: [
            'transform-runtime',
            'class-properties'
        ]
    };
  • packge.json

    {
      "name": "my-package",
      "version": "1.0.0",
      "babel": {
        "presets": [ ... ],
        "plugins": [ ... ],
      }
    }
  • JS API

    const babel = require('babel-core');
    
    babel.transform(code, {
        plugins: [ ... ]
    });
    

最主要的,是它提供了 plugins 入口,读取这些入口,可以干预 Babel 的运行状态,满足众多的新功能需求。

总结而言,Babel 提供了众多的插件接入方式,涵盖了配置文件、命令行、JS API 等。

如何实现一个插件化工具

那么,如果我们要实现一个插件化的工具,如命令行、构建套件呢,怎么提供插件化机制,将该工具的功能尽可能开放呢?

  • 工具本身

    工具在运行核心逻辑的过程中,暴露必要的生命周期钩子。

    工具内部需要维护一套事件处理逻辑,将插件拦截声明周期时提供的回调函数

  • 插件开发者

    • 插件开发者可以拦截生命周期钩子,并为该钩子提供回调函数
    • 插件开发者可以自定义新钩子,为后续插件使用
  • 插件使用者

    插件使用者,也就是用户,需要尽可能简单的引入插件,并能够自由地开发插件。

    我们可以借鉴 Babel 的插件引入方式,接收 plugins 选项,并提供多种多样的配置方式(配置文件 / JS API)。

反馈在代码上,就是三个主要步骤:

  1. 初始化工具生命周期钩子
  2. 执行用户提供的插件
  3. 执行核心逻辑,并在某个时机执行某个生命周期钩子

以下是该工具的实现逻辑伪代码:

class PluginedTool {
    constructor(options) {
        this.options = {
            plugins: options.plugins
        };
        
        // step1: init hooks
        
        this.hooks = {
            hookA: new WaterfallHook(),
            hookB: new WaterfallHook(),
        };
        
        // step2: run plugins
        
        for (let i = 0, len = this.options.plugins.length; i < len; i++) {
            // find plugin function
            const pluginFunction = this.findPluginFunction(this.options.plugins[i]);
            
            // run plugin and supply context object
            pluginFunction({ ... });
        }
        
        // step3: run core code and flush hooks
        
        this.hooks.hookA.fire();
        // do something
        this.hooks.hookB.fire();
    }
    
    private options: {};
}

这样,一个简单的插件化工具就实现了,我们可以在 step3 执行工具的核心逻辑时,尽可能地触发各类生命周期钩子,实现极致的灵活性。

这套机制可以应用在命令行、构建套件、Node Server 等各类形态上。

如何实现多个插件化工具

诚然,上文中提到,大部分的插件化工具的实现逻辑是类似的:

  1. 初始化工具生命周期钩子
  2. 执行用户提供的插件
  3. 执行核心逻辑,并在某个时机执行某个生命周期钩子

而该工具需要维护:事件注册、消费机制、自己的生命周期等等。

很快,我们在用同样的逻辑将多个工具插件化时,发现上述通用逻辑是可以抽象出来的,不需要每个工具都重新实现一遍各个配套设施。

为此,一个插件化工厂被写了出来:plugin-anything

用法如下:

const { runPluginAnything } = require('plugin-anything');

runPluginAnything(
    {
        // Array< string >
        searchList: [
            // string: absolute folder path
        ],

        // Array< string | FunctionContructor | Array<string | FunctionContructor, object> >
        plugins: [
            // string: plugin name
            // FunctionContructor: Plugin Constructor
            // Array: [ string | FunctionContructor, options object ]
        ],
    }, 
    {
        // init something like: hooks, customs config
        async init({ hooks, Events, customs }) {
            hooks.done = new Events();
            customs.myConfig = {};
        },

        // run lifecycle
        async lifecycle({ hooks, Events, customs }) {
            // flush hooks
            await hooks.done.flush('waterfall');

            // do something
            // ...
            // console.log(customs.myConfig);
        }
    }
);

image

开发者可以快速的创建一个插件化的工具,想想也是不错的。

感谢阅读,比心。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant