前端插件设计

1,873 阅读4分钟

【本文主旨】仅探讨插件系统的实现,不针对某个具体功能的插件。
【关键词】依赖与被依赖者的管理赋能、vue、webpack、babel

对插件最直观的认识,顾名思义,即插即用,具有很高的灵活性;另外插件完全解耦于使用者,只要满足对接口的约定即可。 本文主要从目前使用较多的几个工具入手,简单介绍下插件的使用,以及它们对插件的约定格式。

  • 前言
    从简单的例子开始:

    function pluginA(App) {
        App.protoType.a = 1;
    }
    function pluginB(App) {
        App.protoType.b = 1;
    }
    class App { }
    
    //对App原型进行增强
    pluginA(App);
    pluginB(App);
    

    这种写法,从语义上看,plugin是主动施与者,为了统一对外接口,较多的写法为:

    class App { 
        use(plugins) {
            let type = Object.protoType.toString.call(plugins);
            if(type === '[object Function]') {
                plugins.call(null, this);
            }else if(type === '[object Array]') {
                plugins.forEach(plugin => {
                   pulgin.call(null, this); 
                });
            }
        }
    }
    //用法一
    App.use(pluginA);
    App.use(pluginB);
    //用法二
    App.use([pluginA, pluginB]);
    

    由使用者提供接口,语义上更友好。App:"我需要pluginA"。pluginA便为之所用。是不是有些IOC的意思?

  • webpack插件
    插件向第三方开发者提供了 webpack 引擎中完整的能力。使用阶段式的构建回调,开发者可以引入它们自己的行为到 webpack 构建流程中。

    • 插件写法

      function MyPlugin(options) {
      
      };
      
      // 在插件函数的 prototype 上定义一个 `apply` 方法。
      MyPlugin.prototype.apply = function(compiler) {
          // 指定一个挂载到 webpack 自身的事件钩子。
          compiler.plugin('webpacksEventHook', function(compilation, callback) {
      
              // 功能完成后调用 webpack 提供的回调。
              callback();
          });
      };
      
      module.exports = MyPlugin;
      

      顺便一提:
      compiler:代表的是不变的webpack环境,是针对webpack的;
      compilation:针对的是随时可变的项目文件,只要文件有改动,compilation就会被重新创建。

    • 用法

      var MyPlugin = require('my-plugin');
      
      var webpackConfig = {
          ...,
          plugins: [
              new MyPlugin({options: true})
          ]
      };
      
      
    • 深入理解

      • 插件的初始化时机
        为了让插件对webpack构建生命周期的事件节点,做出相应的反应,在读取webpack.config.js文件后,会首先执行配置文件中插件(plugin)的实例化,为webpack事件流挂上自定义钩子。
      • apply方法的约定
        查看源码,插件的应用如下:
        if (options.plugins && Array.isArray(options.plugins)) {
        	for (const plugin of options.plugins) {
        	    //调用plugin的apply方法
            	plugin.apply(compiler);
            }
        }
        
        由此看出,webpack内部对插件的统一处理:获取插件的apply方法,将compiler注入,以完成事件钩子的注册。此处apply便是webpack处理插件的通用接口,因此编写webpack插件,必须提供apply方法。
  • Vue插件

    • 插件写法
      //1. 若插件为对象,必须提供install方法
      MyPlugin.install = function (Vue, options) { }
      export default MyPlugin;
      
      //2. 若为函数,会被作为install方法
      export default function(Vue) {
      }
      
    • 用法
      var MyPlugin = require('my-plugin');
      
      Vue.use(MyPlugin, options);
      
      use的内部处理和webpack类似:MyPlugin.install(Vue),都是使用约定的函数,将使用者注入,进行功能增强。 另外提一下,Regular中的Component也是由一个use函数来统一'使用'插件。
  • Babel插件
    Babel 虽然开箱即用,但是什么动作都不做。它基本上类似于 const babel = code => code; ,将代码解析之后再输出同样的代码。如果想要 Babel 做一些实际的工作,就需要为其添加插件。

    • 插件写法
      //babel-plugin-demo/index
      module.exports = function() {
          return {
              //访问者  
              visitor: {
                  BinaryExpression(path, state) {
                      //对AST对象进行变换操作
                  }
                  /**
                  *等价于
                  * BinaryExpression: {
                  *    enter() {
                  *        //进入节点,相应的存在exit()
                  *    }
                  * }
                  *
                  **/
              }
          }
      }
      
      上面定义了一个简单的访问者(visitor),当遍历AST树的过程中遇到typeBinaryExpression的节点,就会调用BinaryExpression()方法。
    • 使用
      //.babelrc
      {
          ...
          "plugin": [
              [
                  "demo",  //等价于babel-plugin-demo
                  {
                      ...   //state.opts
                  }
              ]
          ]
          
      }
      
  • Eslint插件

    • 创建规则
    //使用yo和generator-eslint生成模板
    ...
    1. lib/rules/x.js
    module.exports = {
        meta: {
            type: '',
            docs: {},
            fixable: '',
            schema:[],
            message: {
                //mcontext.report中的essageId对应的值配置
            }
        },
        create: function(context) {
            return {
                //键名是AST的选择器(和babel的写法类似)
                //使用context.report抛出问题以及fix设置
            }
        }
    }
    2. lib/index.js
    module.exports = {
        rule: {
            'x': require('./rules/x')
        },
        config: {
            recommended: {
                rules: {
                    'demo/x': 2  //或者eslint-plugin-demo
                }
            }
        }
    }
    
    • 使用
    //安装
    npm i eslint-plugin-demo -D
    
    //.eslintrc.js配置
    //使用1:presets
    "extends": [
        "eslint:recommended",
        "plugin:eslint-plugin-demo/recommended"  //使用插件的recommended配置
    ],
    //使用2
    "plugin": [
        "demo"
    ],
    "rules": [
        "demo/recommended": "error"  //针对插件的每个规则自定义处理
    ]
    
  • 总结

    通过上面的总结,可以看到不同框架对插件的实现大同小异:不固化依赖,而是通过对外提供"插槽"的方式,由使用者灵活的按需注入。在其中,看到了依赖注入装饰者模式的影子。
    日常开发中,随处可见插件的应用,比如redux的中间件等,这里不做赘述。如果能够灵活处理依赖与被依赖两者的关系,有利于提高代码的复用性,对编程思维也是一种提升。

欢迎关注公众号,不定时更新哦~

【欢迎留言】本文是否对你有帮助,亦或有所遗漏笔误等,烦请告知。