因爲想分析打包的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-app
的targets
包含了多個。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++
});
}
};
複製代碼
代碼很簡單,onGetWebpackConfig
是pluginAPI
提供的方法以前,此外還有context
,onHook
等。把這個插件配置到build.json
的plugins
中BundleAnalyzerPlugin
不會再報端口錯誤了。build.json:markdown
{
"analyzer": true,
"plugins": [
[
"build-plugin-rax-app",
{
"targets": [
"web",
"weex"
]
}
],
"@ali/build-plugin-rax-app-def",
"./fixAnalyzerPlugin"
]
}
複製代碼
上述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
還提供了一系列生命週期,start
和build
的生命週期略有不一樣,主要是在commands
下的build.js
和start.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包解決一下這個問題:
tnpm i -D @ali/fix-analyzer-plugin
複製代碼
build.json中plugins加入
"plugins": [
...
"@ali/fix-analyzer-plugin"
]
複製代碼