【Blog】rax插件的編寫和原理

原由

因爲想分析打包的js構成,正好Rax的配置項裏有analyzer plugin來分析打包文件的選項,在build.json裏增長"analyzer": true就能夠開啓。一開始用的沒什麼問題,但以後在某些rax工程裏開啓這一選項卻報錯了!node

Error: listen EADDRINUSE: address already in use 127.0.0.1:8888
    at Server.setupListenHandle [as _listen2] (net.js:1301:14)
    at listenInCluster (net.js:1349:12)
    at doListen (net.js:1488:7)
    at processTicksAndRejections (internal/process/task_queues.js:81:21)
複製代碼

端口占用了,緣由是這些工程build-plugin-rax-apptargets包含了多個。Rax支持編譯到Web/Weex/Kraken/Ssr/小程序等目標代碼,不一樣的target會生成本身特有的webpackConfig,其中用不一樣的Driver來作到適應不一樣環境。所以當多個webpack運行時,analyzerPlugin也運行了多個,而端口都是默認的8888,沒有端口檢測,自動用新的端口,天然就報錯了。webpack

解決思路

Rax的構建腳本@alib/build-scripts經過讀取build.json,來動態生成webpackConfig。而生成config這一過程是經過webpack-chain這一工具來實現。git

爲了解決這個端口占用問題,其實只須要修改analyzerPlugin的端口設置就好了。github

經過在@alib/build-scripts/lib/commands/start.js中修改配置來進行測試:web

configArr.forEach((v, index) => v.chainConfig.plugin('BundleAnalyzerPlugin').tap(
  args => [...args, {analyzerPort: 8888 + index}]
))
複製代碼

正常運行,並同時生成了多個analyzer Server。可是這種方式直接是在node_modules裏修改,確定是不行的。npm

有兩種思路來處理,一種是寫一個插件來動態修改已有的analyzerPlugin配置項,一種是拋棄Rax集成的analyzerPlugin,本身在插件裏引入analyzerPlugin並進行配置項設置。json

插件編寫

修改已有的配置項

Rax插件須要 export 一個函數,函數會接收到兩個參數,第一個是 build-scripts 提供的 pluginAPI,第二個是用戶傳給插件的自定義參數。小程序

在src下新建一個fixAnalyzerPlugin,獲取webpackConfig並修改analyzer配置:api

module.exports = (api, options = {}) => {
    const { log, onGetWebpackConfig, getValue, context } = api;
    const targets = getValue('targets');
    const {analyzer = false} = context.userConfig
    let i = 0
    if (analyzer) {
        onGetWebpackConfig((config) => {
            log.info("reSet BundleAnalyzerPlugin", targets[i])
            config.plugin('BundleAnalyzerPlugin').tap(args => [...args, {
                analyzerPort: 8888 + i
            }])
            i++
        });
    }
};
複製代碼

代碼很簡單,onGetWebpackConfigpluginAPI提供的方法以前,此外還有contextonHook等。把這個插件配置到build.jsonpluginsBundleAnalyzerPlugin不會再報端口錯誤了。build.json:markdown

{
  "analyzer": true,
  "plugins": [
    [
      "build-plugin-rax-app",
      {
        "targets": [
          "web",
          "weex"
        ]
      }
    ],
    "@ali/build-plugin-rax-app-def",
    "./fixAnalyzerPlugin"
  ]
}
複製代碼

本身引入analyzerPlugin

上述pluginAPI中還提供registerUserConfig方法,能夠註冊 build.json 中的頂層配置字段,所以咱們新加一個配置項analyzer2,設置爲true,並新加一個newAnalyzerPlugin

const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = (api, options = {}) => {
    const { registerUserConfig } = api;
    let i = 0;

    registerUserConfig({
        name: 'analyzer2',
        validation: 'boolean',
        configWebpack: (config, value, context) => {
            if (value) {
                // reference: https://www.npmjs.com/package/webpack-bundle-analyzer
                config.plugin('BundleAnalyzerPlugin')
                  .use(BundleAnalyzerPlugin, [{analyzerPort: 8888+i}]);
                i++
            }
        }
    });
};
複製代碼

如今build.json改成了:

{
  "devServer": {
    "port": 9999
  },
  "analyzer2": true,
  "plugins": [
    [
      "build-plugin-rax-app",
      {
        "targets": ["web", "weex"]
      }
    ],
    "@ali/build-plugin-rax-app-def",
    "./newAnalyzerPlugin"
  ]
}
複製代碼

插件原理

解決這個端口占用問題當然是一個目的,但更多的是爲了更好的熟悉rax的構建原理,並實踐一些自定義的能力。經過plugin這種方式,咱們能夠集成各類已有的webpackPlugin到Rax的構建中。那麼這種插件機制是怎麼實現的呢?

核心無疑是 build-scripts ,其中core/Context.js封裝了一個class Context,初始化時this.resolvePlugins方法來獲取this.plugins:

this.plugins = this.resolvePlugins(builtInPlugins);
this.resolvePlugins = (builtInPlugins) => {
    const userPlugins = [...builtInPlugins, ...(this.userConfig.plugins || [])]
      .map((pluginInfo) => {
        let fn;
        // 從build.json獲取插件信息
        const plugins = Array.isArray(pluginInfo) ? pluginInfo : [pluginInfo, undefined];
        // plugin 文件路徑
        const pluginPath = require.resolve(plugins[0], { paths: [this.rootDir] }); 
        // 插件設置項
        const options = plugins[1];
        // 插件module.exports的函數
        fn = require(pluginPath);
        return {
            name: plugins[0],
            pluginPath,
            fn: fn.default || fn || (() => { }),
            options,
        };
      });
    return userPlugins;
};
複製代碼

以後會在setUp中運行執行this.runPlugins,運行註冊的插件,生成webpackChain配置:

this.runPlugins = async () => {
    for (const pluginInfo of this.plugins) {
      const { fn, options } = pluginInfo;
      const pluginContext = _.pick(this, PLUGIN_CONTEXT_KEY);
      // pluginAPI 即上述提供了一系列方法的api
      const pluginAPI = {
        //script 統一的 log 工具
        log,
        // 包含運行時的各類環境信息
        context: pluginContext,
        // 註冊多 webpack 任務
        registerTask: this.registerTask,
        // 獲取所有 webpack 任務
        getAllTask: this.getAllTask,
        // 獲取所有 Rax Plugin
        getAllPlugin: this.getAllPlugin,
        // 獲取webpack配置,能夠用 webpack-chain 形式修改 webpack 配置
        onGetWebpackConfig: this.onGetWebpackConfig,
        // 獲取Jest測試配置
        onGetJestConfig: this.onGetJestConfig,
        // 用 onHook 監聽命令運行時事件
        onHook: this.onHook,
        // 用來在context中註冊變量,以供插件之間的通訊
        setValue: this.setValue,
        // 用來獲取context中註冊的變量
        getValue: this.getValue,
        // 註冊 build.json 中的頂層配置字段
        registerUserConfig: this.registerUserConfig,
        // 註冊各命令上支持的 cli 參數
        registerCliOption: this.registerCliOption,
        // 註冊自定義方法
        registerMethod: this.registerMethod,
        // 運行註冊的自定義方法
        applyMethod: this.applyMethod,
        // 修改build.json 中的配置
        modifyUserConfig: this.modifyUserConfig,
      };
      // 運行插件,傳入pluginAPI, options,用pluginAPI中的方法修改相應配置
      await fn(pluginAPI, options);
    }
};
複製代碼

從這裏看,上面咱們寫的插件做用原理就很明顯了。

除此以外,onHook還提供了一系列生命週期,startbuild的生命週期略有不一樣,主要是在commands下的build.jsstart.js中在相應階段運行context實例applyHook方法,來執行註冊好的事件隊列。而事件隊列就是插件用onHook註冊的。

// 執行
// start.js
const context = new Context(...);
const { applyHook } = context;
await applyHook(`before.start.load`);

// Context.js
// 實際執行方法
this.applyHook = async (key, opts = {}) => {
    const hooks = this.eventHooks[key] || [];
    for (const fn of hooks) {
        await fn(opts);
    }
};
//註冊
this.onHook = (key, fn) => {
    if (!Array.isArray(this.eventHooks[key])) {
        this.eventHooks[key] = [];
    }
    this.eventHooks[key].push(fn);
};
複製代碼

看到這,感受build-scripts整個插件機制和生命週期都有種似曾相識的感受。沒錯,和webpack中的插件和生命週期很像,估計是有所參考吧。

npm包

暫時發了個npm包解決一下這個問題:

tnpm i -D @ali/fix-analyzer-plugin
複製代碼

build.json中plugins加入

"plugins": [
   ...
    "@ali/fix-analyzer-plugin"
  ]
複製代碼
相關文章
相關標籤/搜索