因为想分析打包的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"
]
复制代码