How to Transform Build‑Scripts into a Flexible, Plugin‑Based Architecture for Frontend Projects
This article walks through the step‑by‑step evolution of a unified build‑scripts scaffold—from simple start/build/test commands to a modular, plugin‑driven system that supports shared configurations, user overrides, webpack‑chain, and multi‑task builds across multiple frontend projects.
一、写在前面
在 ICE、Rax 等项目研发中,我们或多或少都会接触到 build‑scripts 的使用。build‑scripts 是集团共建的统一构建脚手架解决方案,其除了提供基础的 start、build 和 test 命令外,还支持灵活的插件机制供开发者扩展构建配置。
本文尝试通过场景演进的方式,来由简至繁地讲解一下 build‑scripts 的架构演进过程,注意下文描述的演进过程意在讲清 build‑scripts 的设计原理及相关方法的作用,并不代表 build‑scripts 实际设计时的演进过程,如果文中存在理解错误的地方,还望指正。
二、架构演进
0. 构建场景
我们先来构建这样一个业务场景:假设我们团队内有一个前端项目 project‑a,项目使用 webpack 来进行构建打包。
项目 project‑a
<code>project-a
|- /dist
| |- main.js
|- /src
| |- say.js
| |- index.js
|- /scripts
| |- build.js
|- package.json
|- package-lock.json
</code>project‑a/src/say.js
<code>const sayFun = () => {
console.log('hello world!');
};
module.exports = sayFun;
</code>project‑a/src/index.js
<code>const say = require('./say');
say();
</code>project‑a/scripts/build.js
<code>const path = require('path');
const webpack = require('webpack');
// 定义 webpack 配置
const config = {
entry: './src/index',
output: {
filename: 'main.js',
path: path.resolve(__dirname, '../dist'),
},
};
// 实例化 webpack
const compiler = webpack(config);
// 执行 webpack 编译
compiler.run((err, stats) => {
compiler.close(() => {});
});
</code>过段时间由于业务需求,我们新建了一个前端项目 project‑b。由于项目类型相同,项目 project‑b 想要复用项目 project‑a 的 webpack 构建配置,此时应该怎么办呢?
1. 拷贝配置
为了项目快速上线,我们可以先直接从项目 project‑a 拷贝一份 webpack 构建配置到项目 project‑b,再配置一下 package.json 中的 build 命令,项目 project‑b 即可“完美复用”。
项目 project‑b
<code>project-b
|- /dist
| |- main.js
|- /src
| |- say.js
| |- index.js
|- /scripts
| |- build.js
|- package.json
|- package-lock.json
</code>拷贝只能临时解决问题,并不是长期方案。如果构建配置需要在多个项目间复用,我们可以考虑将其封装为一个 npm 包来独立维护。
2. 封装 npm 包
我们新建一个 npm 包 build‑scripts ,提供统一的构建入口。
<code>build-scripts
|- /bin
| |- build-scripts.js
|- /lib (ts 构建目录,文件同 src)
|- /src
| |- /commands
| | |- build.ts
| |- tsconfig.json
| |- package.json
| |- package-lock.json
</code>build‑scripts/bin/build‑scripts.js
<code>#!/usr/bin/env node
const program = require('commander');
const build = require('../lib/commands/build');
(async () => {
// build 命令注册
program.command('build').description('build project').action(build);
// 判断是否有运行的命令,如果有则退出已执行命令
const proc = program.runningCommand;
if (proc) {
proc.on('close', process.exit.bind(process));
proc.on('error', () => { process.exit(1); });
}
// 命令行参数解析
program.parse(process.argv);
// 如果无子命令,展示 help 信息
const subCmd = program.args[0];
if (!subCmd) {
program.help();
}
})();
</code>build‑scripts/src/commands/build.ts
<code>const path = require('path');
const webpack = require('webpack');
module.exports = async () => {
const rootDir = process.cwd();
// 定义 webpack 配置
const config = {
entry: path.resolve(rootDir, './src/index'),
module: {
rules: [
{
test: /\.ts?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: { extensions: ['.ts', '.js'] },
output: { filename: 'main.js', path: path.resolve(rootDir, './dist') },
};
// 实例化 webpack
const compiler = webpack(config);
// 执行 webpack 编译
compiler.run((err, stats) => {
compiler.close(() => {});
});
};
</code>项目 project‑a 和 project‑b 只需把 build 脚本改为调用
build‑scripts build,并在
devDependencies中加入
build‑scripts,即可共享统一的构建逻辑。
3. 添加用户配置
为满足不同项目的自定义需求,我们在项目根目录新增
build.json,用于覆盖默认的 entry 与 outputDir。
project‑c/build.json
<code>{
"entry": "./src/index1",
"outputDir": "./build"
}
</code>在 build‑scripts/src/commands/build.ts 中读取并合并用户配置:
<code>let userConfig = {};
try {
userConfig = require(path.resolve(rootDir, './build.json'));
} catch (error) {
console.log('Config error: build.json is not exist.');
return;
}
if (!userConfig.entry || typeof userConfig.entry !== 'string') {
console.log('Config error: userConfig.entry is not valid.');
return;
}
if (!userConfig.outputDir || typeof userConfig.outputDir !== 'string') {
console.log('Config error: userConfig.outputDir is not valid.');
return;
}
const config = {
entry: path.resolve(rootDir, userConfig.entry),
...
output: { filename: 'main.js', path: path.resolve(rootDir, userConfig.outputDir) },
};
</code>此方式仍存在配置校验分散、缺少默认值兜底等问题。
4. 添加插件机制
为解决更复杂的自定义需求,引入插件机制。用户在
build.json中声明插件列表,插件在执行时接收完整的 webpack 配置并自行修改。
project‑c/build.json
<code>{
"entry": "./src/index1",
"outputDir": "./build",
"plugins": ["build-plugin-xml"]
}
</code>build‑scripts/core/ConfigManager.ts (关键片段)
<code>private async runPlugins() {
for (const plugin of this.userConfig.plugins) {
const pluginPath = require.resolve(plugin, { paths: [process.cwd()] });
const pluginFn = require(pluginPath);
await pluginFn({
setConfig: this.setConfig,
registerUserConfig: this.registerUserConfig,
onGetWebpackConfig: this.onGetWebpackConfig,
});
}
}
</code>build‑plugin‑xml/index.js
<code>module.exports = async (webpackConfig) => {
if (!webpackConfig.module) webpackConfig.module = {};
if (!webpackConfig.module.rules) webpackConfig.module.rules = [];
webpackConfig.module.rules.push({
test: /\.xml$/i,
use: require.resolve('xml-loader'),
});
};
</code>5. 引入 webpack‑chain
使用
webpack‑chain将配置改写为链式 API,简化插件内部的对象操作。
build‑scripts/src/configs/build.ts
<code>const Config = require('webpack-chain');
const path = require('path');
const rootDir = process.cwd();
const buildConfig = new Config();
buildConfig.entry('index').add('./src/index');
buildConfig.module
.rule('ts')
.test(/\.ts?$/)
.use('ts-loader')
.loader(require.resolve('ts-loader'));
buildConfig.resolve.extensions.add('.ts').add('.js');
buildConfig.output.filename('main.js');
buildConfig.output.path(path.resolve(rootDir, './dist'));
module.exports = buildConfig;
</code>插件也改为链式写法:
<code>module.exports = async (webpackConfig) => {
webpackConfig.module
.rule('xml')
.before('ts')
.test(/\.xml$/i)
.use('xml-loader')
.loader(require.resolve('xml-loader'));
};
</code>6. 插件化默认构建配置
将默认构建配置抽离为插件
build‑plugin‑base,通过
registerTask注册任务。
build‑plugin‑base/index.js
<code>const Config = require('webpack-chain');
const path = require('path');
const rootDir = process.cwd();
module.exports = async ({ registerTask, registerUserConfig }) => {
const buildConfig = new Config();
buildConfig.entry('index').add('./src/index');
buildConfig.module
.rule('ts')
.test(/\.ts?$/)
.use('ts-loader')
.loader(require.resolve('ts-loader'));
buildConfig.resolve.extensions.add('.ts').add('.js');
buildConfig.output.filename('main.js');
buildConfig.output.path(path.resolve(rootDir, './dist'));
registerTask('base', buildConfig);
registerUserConfig([
{
name: 'entry',
validation: async (value) => typeof value === 'string',
configWebpack: async (defaultConfig, value) => {
defaultConfig.entry('index').clear().add(path.resolve(rootDir, value));
},
},
{
name: 'outputDir',
validation: async (value) => typeof value === 'string',
configWebpack: async (defaultConfig, value) => {
defaultConfig.output.path(path.resolve(rootDir, value));
},
},
]);
};
</code>对应插件
build‑plugin‑xml通过任务名称绑定:
<code>module.exports = async ({ onGetWebpackConfig }) => {
onGetWebpackConfig('base', (webpackConfig) => {
webpackConfig.module
.rule('xml')
.before('ts')
.test(/\.xml$/i)
.use('xml-loader')
.loader(require.resolve('xml-loader'));
});
};
</code>7. 添加多任务机制
为支持单项目多构建产物,引入任务列表。
ConfigManager维护
configArr,每个任务拥有独立的
WebpackChain实例和修改函数。
关键 API:
<code>public registerTask(name, chainConfig) {
if (this.configArr.find(v => v.name === name)) {
throw new Error(`[Error] config '${name}' already exists!`);
}
this.configArr.push({ name, chainConfig, modifyFunctions: [] });
}
public onGetWebpackConfig(name, fn) {
const task = this.configArr.find(v => v.name === name);
if (!task) throw new Error(`[Error] config '${name}' does not exist!`);
task.modifyFunctions.push(fn);
}
</code>在 build‑scripts/src/commands/build.ts 中执行所有任务:
<code>const compiler = webpack(
manager.configArr.map(task => task.chainConfig.toConfig())
);
compiler.run((err, stats) => {
compiler.close(() => {});
});
</code>三、写在最后
通过上述场景演进,build‑scripts 展示了一个具备灵活插件机制、支持用户配置、可通过 webpack‑chain 简化配置、并能执行多任务的前端构建解决方案。该思路同样适用于任何需要跨项目共享与扩展配置的场景。
注:示例代码可在仓库 build‑scripts‑demo 查看,完整源码请参考 build‑scripts 仓库。
参考资料
[1] 官方文档: https://github.com/neutrinojs/webpack-chain
[2] build‑scripts‑demo: https://github.com/CavsZhouyou/build-scripts-demo
[3] build‑scripts: https://github.com/ice-lab/build-scripts
Taobao Frontend Technology
The frontend landscape is constantly evolving, with rapid innovations across familiar languages. Like us, your understanding of the frontend is continually refreshed. Join us on Taobao, a vibrant, all‑encompassing platform, to uncover limitless potential.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.