zoukankan      html  css  js  c++  java
  • 前端插件机制剖析及业界案例分析

    导语

    如果你的工具型面对的对象有很丰富的场景需求,或者不想再为频繁的增减需求而频繁迭代,是时候考虑为你的系统设计一款插件系统。

    插件机制

    插件机制:

    Core-Plugin 架构的组成

    • Core:基础功能,提供插件运行的环境,管理插件的注册与卸载(可拔插)以及运行,也即管理插件的生命周期。

    • PluginApi:插件运行的接口,由 Core 抽象出来的接口。(颗粒度尽可能小)

    • Plugin:每个插件都是一个独立的功能模块。

    Core-Plugin 模式的好处,总结几点:

    • 提高扩展性;

    • 减少因功能改变而引起的项目迭代,即使是需要扩展基础功能,也可以以插件的形式单独发包,配合 monorepo

    • 充分利用开发者 / 开源的力量,激发更多的想法;

    开箱即用的库 / 组件功能模块相比,可能比较明显的缺点就是,Plugin 的开发需要遵循规范,复杂一点的库(比如 Babel、webpack)还需要理解其运行原理,会相对有些门槛。

    插件机制在开源项目中的运用

    babel 插件机制

    官方定义:Babel 是一个 JavaScript 编译器。

    babel 大家都很熟悉,最重要的功能是将 ES6 版本的代码转换为 ES5 语法,使我们的代码能兼容不同的浏览器以及版本。随着 ES 语法的日渐丰富和扩展,对 babel 转换代码的规则也有更多的要求,babel 提供了一套插件机制支持开发者自定义插件来实现特殊的转换规则。在了解 babel 插件机制之前,需要掌握如下知识点:

    1. babel 转换流程。

    2. 如何开发 babel 插件。

    3. babel 插件的执行流程。

    babel 转换流程:

    推荐一个网站:https://astexplorer.net/,在线 AST 解析器。

    • 分析 (parse) 通过语法解析和词法分析生成 抽语法树 (AST);babel 用工具库 @babel/parser 来解析 ast

    例子:

    let tips = 'gun';
    
    const func1 = (a) => {
      console.log(a);
    };

    它的 AST 长这样:

    • 转换 (transform) 对解析得到的 AST 进行转换,就是在这一阶段利用各种插件规则对 AST 进行转换;babel-traverse 对 AST 树进行解析遍历出整个树的 path。

    • 生成 (generate) AST 转化为目标语法。

    bebel 插件开发 - es6 转换 es5

    这里以转换箭头函数和 let/const 为例:

    // 转化es6语法的babel插件
    // babel-types:https://github.com/babel/babel/tree/master/packages/babel-types
    // babel-types是babel的工具集之一,用于处理AST节点,包含了构造、验证以及变换AST节点的方法。
    exportdefaultfunction({ types: babelTypes }) {
        return {
          visitor: {
            Identifier(path, state) {},
            ASTNodeTypeHere(path, state) {},
            // 转换箭头函数
            ArrowFunctionExpression(path) {
              const node = path.node;
              if (!path.isArrowFunctionExpression()) return;
            
              path.arrowFunctionToExpression({ //... });
            },
            // 转换let/const -> var
            VariableDeclaration(path) {
              const node = path.node;
         
              if (node.kind === 'let' || node.kind === 'const') {
                // let a ,这里的 a 就是 node.declarations
                const varNode = type.variableDeclaration('var', node.declarations);
                path.replaceWith(varNode);
              }
            },
          }
        };
    }
    // .bebelrc
    {
        plugins: ['xxx']
    }
     

    执行流程:获取到插件的 vistor 对象,遍历 AST 节点(图 (5) AST 对象中标蓝的字段),如果遍历的节点 type 在 vistor 对象能找到对应的 key,则执行 vistor [key] 对应的逻辑(transform),遍历结束生成对应语法。单个 babel 插件的执行逻辑很清晰。然而实际开发中我们会在.babelrc 里配置很多 plugins,babel 是如何组织管理插件呢?我们知道,babel 会深度递归遍历 AST,代价很高,最好的方式是把插件组织起来,在一次遍历中全部执行完成。 传送门

    // bad case
    path.traverse({
      Identifier(path) {
        // ...
      }
    });
    
    path.traverse({
      BinaryExpression(path) {
        // ...
      }
    });
    
    // great case
    path.traverse({
      Identifier(path) {
        // ...
      },
      BinaryExpression(path) {
        // ...
      }
    });
     

    babel 内部为了提高效率,正是采用 merge visitors 的方式:

    // ...
    // 插件合并
    const visitor = traverse.visitors.merge(
      visitors,
      passes,
      file.opts.wrapPluginVisitorMethod,
    );
    // 一次性执行插件 visitor 中定义的方法
    traverse(file.ast, visitor, file.scope);
    // ...

    合并的原则是对于相同类型的节点,将处理方法组合成一个数组,当遇到该类型节点的时候,一次执行处理方法,合并的数据结构类似如下:

    {
      ArrowFunctionExpression: {
        enter: [...]
      },
      BlockStatement: {
        enter: [...],
        exit: [...]
      },
      DoWhileStatement: {
        enter: [...]
      }
    }

    webpack 插件机制

    webpack 插件

    webpack 插件的目的在于解决 loader 无法实现的其他事。除了自身提供的开箱即用的插件,还支持自定义插件。

    // 自定义插件
    class MyWebpackPlugin {
        const webpacksEventHook = 'emit';
        apply(compiler) {
            // 监听'emit'事件,同步方式
            compiler.hooks.emit.tap('MyWebpackPlugin', function(compilation) {
              // ...
            });
            // 监听webpacksEventHook(emit)事件,异步方式
            compiler.plugin(webpacksEventHook, function(compilation, callback) {
                // ...
                const compilationEvenetHook = 'xxx'
                compilation.plugin(compilationEvenetHook, function() {
                  console.log(`${compilationEvenetHook} done.`);
               });
               // 回调,插件功能完成后调用
               callback();
          });
        }
    }
    
    // 使用
    module.exports = {
      plugins: [
        new MyWebpackPlugin(options)
      ]
    };
     

    编写插件几个关键点:

    • 必须为插件实例提供 apply 方法。

      // webpack
      function webpack(options, callback) {
        // ...
        compiler = new Compiler();
        // 在初始化插件的时候是通过执行apply方法,并传入compiler对象。
        for (const plugin of options.plugins) {
            plugin.apply(compiler);
        }
        // ...
      }
       
    • 插件通过 compiler.plugin 注册 webpack 事件钩子 (webpacksEventHook)。

    • compiler.plugin 的回调可以拿到 complication 对象。

    前置知识

    1. webpack 插件的作用

    2. webpack 构建流程

    3. Tapable - 管理事件流的机制

    4. 理解 Compiler 和 Compilation 对象 - 开发插件必须要了解的

    webpack 构建流程

    这里我们目前只关注在 webpack 开始正式工作之前,会初始化生成 compiler 对象,之后可以理解为充当整个 webpack 的工作环境用于 plugins/loaders。

    Tapable-webpack 中的事件流机制

    webpack 的本质是处理事件流,在编译过程中会依据钩子执行不同的 plugin,如何将 plugin 与钩子对应起来正是 Tapable 要干的事,核心原理是发布订阅模式。Webpack 中的 Tapable 是独立的一个工具包,可以理解为 webpack 用来挂载插件的钩子(很形象了 (Ĭ ^ Ĭ)),暴露了不同的方法(异步 / 同步)来挂载:

    const {
        // 同步
        SyncHook, 
        SyncBailHook, 
        SyncWaterfallHook, 
        SyncLoopHook, 
        // 异步
        AsyncParallelHook,
        AsyncParallelBailHook, 
       AsyncSeriesHook, 
       AsyncSeriesBailHook, 
       AsyncSeriesWaterfallHook 
    } = require("tapable");

    简单看看 tapable 是怎么关联 webpack 和它的插件的,以上面的自定义插件为栗子:

    // 
    compiler.hooks.emit.tap('MyWebpackPlugin', function(compilation) {
      // ...
    });
    // 实现
    // 引入tapable
    const { SyncHook } = require('tapable');
    
    // Compiler类
    Class Compiler {
        constructor () {
            this.hooks = { 
              emit: new SyncHook(['arg1', 'arg2']), 
            };
        }
    }
    
    // webpack
    const compiler = new Compiler();
    // 绑定同步钩子 并传参
    compiler.hooks.emit.tap("MyWebpackPlugin", (arg1, arg2) =>console.log(`emit hook params: ${arg1}-${arg2}`));
    
    // webpack中执行MyWebpackPlugin插件挂载在emit钩子的函数 同步执行
    myCar.hooks.emit.call('hello', 'noaherzhang');

    SyncHook 为同步钩子,通过 tap/call 挂载和同步执行,Tapable 提供了同步和异步钩子,也会有对应的方法来进行挂载和执行:

     同步异步
    绑定 tap tapAsync/tapPromise
    执行 call callAsync/promise

    另外,我们自定义插件时,用到的 compiler 对象和 complication 对象都是继承自 Tapable 类,通过 apply/plugin 进行广播 / 监听事件。

    // 广播事件
    compiler.apply('事件名', params);
    compilation.apply('事件名', params);
    
    // 监听事件
    compiler.plugin('事件名', function(params){});
    compilation.plugin('事件名', function(params){});
     

    总结

    Tapable 就是 webpack 的一个工具库,在插件绑定对应的事件到对应的 webpack 暴露的钩子上,webapck 编译过程中触发事件,随后根据不同的 Tapable 方法执行绑定的函数。

    Compiler 对象 & Complication 对象

    从字面理解,compiler (v.) 表示运行时 (编译),complication (n.) 表示运行后产物 (bundles)。

    1. compiler 对象在 WebPack 构建过程中代表着整个 WebPack 环境,包含上下文、项目配置信息、执行、监听、统计等等一系列的信息,提供给 loader 和插件使用;compiler 对象在编译过程只会在初始化的时候创建一次,而 complication 在每次文件变化的时候都会重新创建一次,一个 Compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息,一个 complication 代表了一次资源版本构建。

    2. Compiler、Complication 对象都继承自 Tapable 对象;

    我们更多的是关注 webpack 通过 compiler、complication 对象暴露的钩子 (hook) 列举一些重要的,详细的参考:

    compiler.hooks

    钩子作用类型
    after-plugins 插件初始化之后 sync
    after-resolvers resolvers 初始化之后 sync
    run 在读取记录之前 async
    compile 【开始编译】在创建新 compilation 之前 sync
    compilation compilation 创建完成 sync
    emit 【编译完成】在生成资源并输出到目录之前 async
    after-emit 在生成资源并输出到目录之后 async
    done 编译完成 sync

    关于 webpack 的构建机制,个人比较推荐的文章:

    1. 《WebPack 插件机制探索》

    2. 《撸一个 webpack 插件》- 掘金

    同样结合 plugin-core 三要素,总结一下 webpack 插件设计:

    • Core:核心的构建流程,webpack 使用了 Tapable 管理插件;

    • PluginAPI:给开发者提供 complication、compiler 等对象,webpack 在 complication.hook、compiler.hook 暴露对应的事件钩子,插件开发者在 complication.hook、compiler.hook 进行挂载;

    • Plugin:带有 apply 方法的插件构造器;

    思考

    针对插件要素做个简要分类:

    1. 富 Plugin。功能很多,可扩展性也很强,像 babel 和 webpack 都是。这是相对最容易实现的插件系统,对于开发者来说也相对友好;如 bebel 和 webpack。

    2. 富 PluginApi。插件运行场景多样。移动滚动场景很多,better-scroll 要做的正是在 core 内做更多的逻辑去磨平这些场景之间的差异。

  • 相关阅读:
    linux常用命令
    ANAFI EXTENOED无人机(1)环境配置和基础开发
    无人机自主降落
    ROS开发(1)安装环境
    bebop无人机(1)环境配置和基础开发
    YOLO标注软件
    Python2与Python3之间切换
    python实现IOU计算
    读取多个(海康大华)网络摄像头的视频流 (使用opencv-python),解决实时读取延迟问题
    如何到外面的世界看看
  • 原文地址:https://www.cnblogs.com/cangqinglang/p/15791432.html
Copyright © 2011-2022 走看看