在 ICE、Rax 等项目研发中,我们或多或少都会接触到 build-scripts 的使用。build-scripts 是集团共建的统一构建脚手架解决方案,其除了提供基础的 start、build 和 test 命令外,还支持灵活的插件机制供开发者扩展构建配置。
本文尝试通过场景演进的方式,来由简至繁地讲解一下 build-scripts 的架构演进过程,注意下文描述的演进过程意在讲清 build-scripts 的设计原理及相关方法的作用,并不代表 build-scripts 实际设计时的演进过程,如果文中存在理解错误的地方,还望指正。
我们先来构建这样一个业务场景:
假设我们团队内有一个前端项目 project-a,项目使用 webpack 来进行构建打包。
项目 project-a
project-a
|- /dist
|- main.js
|- /src
|- say.js
|- index.js
|- /scripts
|- build.js
|- package.json
|- package-lock.json
project-a/src/say.js
const sayFun = () => {
console.log('hello world!');
};
module.exports = sayFun;
project-a/src/index.js
const say = require('./say');
say();
project-a/scripts/build.js
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((closeErr) => {});
});
project-a/package.json
{
"name": "project-a",
"version": "1.0.0",
"description": "",
"main": "dist/main.js",
"scripts": {
"build": "node scripts/build.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"webpack": "^5.74.0"
}
}
过段时间由于业务需求,我们新建了一个前端项目 project-b。由于项目类型相同, 项目 project-b 想要复用项目 project-a 的 webpack 构建配置, 此时应该怎么办呢?
图 1.png
为了项目快速上线,我们可以先直接从项目 project-a 拷贝一份 webpack 构建配置到项目 project-b ,再配置一下 package.json 中的 build 命令,项目 project-b 即可“完美复用”。
图 2.png
项目 project-b
project-b
|- /dist
+ |- main.js
|- /src
|- say.js
|- index.js
+ |- /scripts
+ |- build.js
|- package.json
|- package-lock.json
project-b/package.json
{
"name": "project-b",
"version": "1.0.0",
"description": "",
"main": "dist/main.js",
"scripts": {
+ "build": "node scripts/build.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
+ "devDependencies": {
+ "webpack": "^5.74.0"
+ }
}
下面我们的场景先来演进一下:
项目 project-b 上线一段时间后,团队内推行项目 TS 化,我们首先对项目 project-a 进行了如下改造:
项目 project-a
project-a
|- /dist
|- main.js
|- /src
- |- say.js
- |- index.js
+ |- say.ts
+ |- index.ts
|- /scripts
|- build.js
+ |- tsconfig.json
|- package.json
|- package-lock.json
project-a/scripts/build.js
const path = require('path');
const webpack = require('webpack');
// 定义 webpack 配置
const config = {
entry: './src/index',
+ module: {
+ rules: [
+ {
+ test: /\.ts?$/,
+ use: 'ts-loader',
+ exclude: /node_modules/,
+ },
+ ],
+ },
+ resolve: {
+ extensions: ['.ts', '.js'],
+ },
...
};
...
// 执行 webpack 编译
compiler.run((err, stats) => {
compiler.close((closeErr) => {});
});
project-a/package.json
{
"name": "project-a",
...
"devDependencies": {
+ "ts-loader": "^9.3.1",
+ "typescript": "^4.8.2",
+ "@types/node": "^18.7.14",
"webpack": "^5.74.0"
}
}
由于项目 project-b 也需要完成 TS 化,所以我们不得不按照项目 project-a 的修改,在项目 project-b 里也重复修改一次。此时通过拷贝在项目间复用配置的问题就暴露出来了:构建配置更新时,项目间需要同步手动修改,配置维护成本较高,且存在修改不一致的风险。
一般来说,拷贝只能临时解决问题,并不是一个长期的解决方案。如果构建配置需要在多个项目间复用,我们可以考虑将其封装为一个 npm 包来独立维护。下面我们新建一个 npm 包 build-scripts 来做这件事:
npm 包 build-scripts
build-scripts
|- /bin
|- build-scripts.js
|- /lib (ts 构建目录,文件同 src)
|- /src
|- /commands
|- build.ts
|- tsconfig.json
|- package.json
|- package-lock.json
build-scripts/bin/build-scripts.js
#!/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();
}
})();
build-scripts/src/commands/build.ts
import * as path from 'path';
import * as webpack from 'webpack';
export = async () => {
const rootDir = process.cwd();
// 定义 webpack 配置
const config = {
entry: path.resolve(rootDir, './src/index'),
module: {
rules: [
{
test: /\.ts?$/,
use: require.resolve('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((closeErr) => {});
});
};
build-scripts/package.json
{
"name": "build-scripts",
"version": "1.0.0",
"description": "",
"bin": {
"build-scripts": "bin/build-scripts.js"
},
"scripts": {
"build": "tsc",
"start": "tsc -w",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"commander": "^9.4.0",
"ts-loader": "^9.3.1",
"webpack": "^5.74.0"
},
"devDependencies": {
"@types/webpack": "^5.28.0",
"typescript": "^4.8.2"
}
}
我们将项目的构建配置抽离到 npm 包 build-scripts 里进行统一维护,同时以脚手架的方式来提供项目调用,降低项目的接入成本。项目 project-a 和项目 project-b 只需做如下改造:
项目 project-a
project-a
|- /dist
|- main.js
|- /src
|- say.ts
|- index.ts
- |- /scripts
- |- build.js
|- tsconfig.json
|- package.json
|- package-lock.json
project-a/package.json
{
"name": "project-a",
...
"scripts": {
- "build": "node scripts/build.js",
+ "build": "build-scripts build",
"test": "echo \"Error: no test specified\" && exit 1"
},
...
"devDependencies": {
- "ts-loader": "^9.3.1",
+ "build-scripts": "^1.0.0",
"typescript": "^4.8.2",
"@types/node": "^18.7.14",
- "webpack": "^5.74.0"
}
}
项目 project-b 改造同项目 project-a
改造完成后,项目 project-a 和项目 project-b 不再需要在项目里独立维护构建配置,而是通过统一脚手架的方式调用 build-scripts 的 build 命令进行构建打包。后续构建配置更新时,各个项目也只需要升级 npm 包 build-scripts 版本即可,避免了之前手动拷贝带来的修改维护问题。
图 3.png
下面我们的场景再来演进一下:
由于业务需求,我们又新建了一个前端项目 project-c。项目 project-c 想要接入 build-scripts 进行构建打包,但它的打包入口并不是默认的 src/index
,构建目录也不是 /dist
,此时应该怎么办呢?
一般来说,不同项目对构建配置都会有一定的自定义需求,所以我们需要将一些常用的配置开放给项目进行设置,例如 entry、outputDir 等。基于这个目的,我们下面来对 build-scripts 进行一下改造:
我们首先来为项目 project-c 新增一个用户配置文件 build.json。
项目 project-c
project-c
|- /build
|- main.js
|- /src
|- say.ts
|- index1.ts
+ |- build.json
|- tsconfig.json
|- package.json
|- package-lock.json
project-c/build.json
{
"entry": "./src/index1",
"outputDir": "./build"
}
然后我们来对 build-scritps 里的执行逻辑进行一下改造,让 build-scripts 在执行构建命令时,先读取当前项目下的用户配置 build.json,然后使用用户配置来覆盖默认的构建配置。
build-scripts/src/commands/build.ts
import * as path from 'path';
import * as webpack from 'webpack';
export = async () => {
const rootDir = process.cwd();
+ // 获取用户配置
+ let userConfig: { [name: string]: any } = {};
+ try {
+ userConfig = require(path.resolve(rootDir, './build.json'));
+ } catch (error) {
+ console.log('Config error: build.json is not exist.');
+ return;
+ }
+ // 用户配置非空及合法性校验
+ if (!userConfig.entry) {
+ console.log('Config error: userConfig.entry is not exist.');
+ return;
+ }
+ if (typeof userConfig.entry !== 'string') {
+ console.log('Config error: userConfig.entry is not valid.');
+ return;
+ }
+ if (!userConfig.outputDir) {
+ console.log('Config error: userConfig.outputDir is not exist.');
+ return;
+ }
+ if (typeof userConfig.outputDir !== 'string') {
+ console.log('Config error: userConfig.outputDir is not valid.');
+ return;
+ }
// 定义 webpack 配置
const config = {
- entry: path.resolve(rootDir, './src/index'),
+ entry: path.resolve(rootDir, userConfig.entry),
...
output: {
filename: 'main.js',
- path: path.resolve(rootDir, './dist'),
+ path: path.resolve(rootDir, userConfig.outputDir),
},
};
...
};
通过上面的改造,我们就可以基本实现项目 project-c 对于构建配置的自定义需求。
但仔细观察后,我们可以发现上面的改造方式存在一些问题:
基于以上问题,我们再来对 build-scripts 进行一下改造:
npm 包 build-scripts
build-scripts
|- /bin
|- build-scripts.js
|- /lib (ts 构建目录,文件同 src)
|- /src
|- /commands
|- build.ts
+ |- /configs
+ |- build.ts
+ |- /core
+ |- ConfigManager.ts
|- tsconfig.json
|- package.json
|- package-lock.json
我们首先将默认的构建配置抽离到一个独立的文件 configs/build.ts
进行维护。
build-scripts/src/configs/build.ts
const path = require('path');
const rootDir = process.cwd();
const buildConfig = {
entry: path.resolve(rootDir, './src/index'),
module: {
rules: [
{
test: /\.ts?$/,
use: require.resolve('ts-loader'),
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.ts', '.js'],
},
output: {
filename: 'main.js',
path: path.resolve(rootDir, './dist'),
},
};
export default buildConfig;
然后我们新增一个 ConfigManager 类来进行构建配置的管理,负责用户配置和默认构建配置的合并。
build-scripts/src/core/ConfigManager.ts
import _ = require('lodash');
import path = require('path');
import assert = require('assert');
// 配置类型定义
interface IConfig {
[key: string]: any;
}
// 用户配置注册信息类型定义
interface IUserConfigRegistration {
[key: string]: IUserConfigArgs;
}
interface IUserConfigArgs {
name: string;
defaultValue?: any;
validation?: (value: any) => Promise<boolean>;
configWebpack?: (defaultConfig: IConfig, value: any) => void;
}
class ConfigManager {
// webpack 配置
public config: IConfig;
// 用户配置
public userConfig: IConfig;
// 用户配置注册信息
private userConfigRegistration: IUserConfigRegistration;
constructor(config: IConfig) {
this.config = config;
this.userConfig = {};
this.userConfigRegistration = {};
}
/**
* 注册用户配置
*
* @param {IUserConfigArgs[]} configs
* @memberof ConfigManager
*/
public registerUserConfig = (configs: IUserConfigArgs[]) => {
configs.forEach((conf) => {
const configName = conf.name;
// 判断配置属性是否已注册
if (this.userConfigRegistration[configName]) {
throw new Error(
`[Config File]: ${configName} already registered in userConfigRegistration.`
);
}
// 添加配置的注册信息
this.userConfigRegistration[configName] = conf;
// 如果当前项目的用户配置中不存在该配置值,则使用该配置注册时的默认值
if (
_.isUndefined(this.userConfig[configName]) &&
Object.prototype.hasOwnProperty.call(conf, 'defaultValue')
) {
this.userConfig[configName] = conf.defaultValue;
}
});
}
/**
* 获取用户配置
*
* @private
* @return {*}
* @memberof ConfigManager
*/
private getUserConfig = () => {
const rootDir = process.cwd();
try {
this.userConfig = require(path.resolve(rootDir, './build.json'));
} catch (error) {
console.log('Config error: build.json is not exist.');
return;
}
}
/**
* 执行注册用户配置
*
* @param {*} configs
* @memberof ConfigManager
*/
private runUserConfig = async () => {
for (const configInfoKey in this.userConfig) {
const configInfo = this.userConfigRegistration[configInfoKey];
// 配置属性未注册
if (!configInfo) {
throw new Error(
`[Config File]: Config key '${configInfoKey}' is not supported.`
);
}
const { name, validation } = configInfo;
const configValue = this.userConfig[name];
// 配置值校验
if (validation) {
const validationResult = await validation(configValue);
assert(
validationResult,
`${name} did not pass validation, result: ${validationResult}`
);
}
// 配置值更新到默认 webpack 配置
if (configInfo.configWebpack) {
await configInfo.configWebpack(this.config, configValue);
}
}
}
/**
* webpack 配置初始化
*/
public setup = async () => {
// 获取用户配置
this.getUserConfig();
// 用户配置校验及合并
await this.runUserConfig();
}
}
export default ConfigManager;
然后修改 build 命令执行逻辑,通过初始化 ConfigManager 实例对构建配置进行管理。
build-scripts/src/commands/build.ts
import * as path from 'path';
import * as webpack from 'webpack';
+ import defaultConfig from '../configs/build';
+ import ConfigManager from '../core/ConfigManager';
export = async () => {
const rootDir = process.cwd();
- // 获取用户配置
- let userConfig: { [name: string]: any } = {};
- try {
- userConfig = require(path.resolve(rootDir, './build.json'));
- } catch (error) {
- console.log('Config error: build.json is not exist.');
- return;
- }
- // 用户配置非空及合法性校验
- if (!userConfig.entry) {
- console.log('Config error: userConfig.entry is not exist.');
- return;
- }
- if (typeof userConfig.entry !== 'string') {
- console.log('Config error: userConfig.entry is not valid.');
- return;
- }
- if (!userConfig.outputDir) {
- console.log('Config error: userConfig.outputDir is not exist.');
- return;
- }
- if (typeof userConfig.outputDir !== 'string') {
- console.log('Config error: userConfig.outputDir is not valid.');
- return;
- }
- // 定义 webpack 配置
- const config = {
- entry: path.resolve(rootDir, userConfig.entry),
- module: {
- rules: [
- {
- test: /\.ts?$/,
- use: require.resolve('ts-loader'),
- exclude: /node_modules/,
- },
- ],
- },
- resolve: {
- extensions: ['.ts', '.js'],
- },
- output: {
- filename: 'main.js',
- path: path.resolve(rootDir, userConfig.outputDir),
- },
- };
+ // 初始化配置管理类
+ const manager = new ConfigManager(defaultConfig);
+
+ // 注册用户配置
+ manager.registerUserConfig([
+ {
+ // entry 配置
+ name: 'entry',
+ // 配置值校验
+ validation: async (value) => {
+ return typeof value === 'string';
+ },
+ // 配置值合并
+ configWebpack: async (defaultConfig, value) => {
+ defaultConfig.entry = path.resolve(rootDir, value);
+ },
+ },
+ {
+ // outputDir 配置
+ name: 'outputDir',
+ // 配置值校验
+ validation: async (value) => {
+ return typeof value === 'string';
+ },
+ // 配置值合并
+ configWebpack: async (defaultConfig, value) => {
+ defaultConfig.output.path = path.resolve(rootDir, value);
+ },
+ },
+ ]);
+
+ // webpack 配置初始化
+ await manager.setup();
// 实例化 webpack
- const compiler = webpack(config);
+ const compiler = webpack(manager.config);
// 执行 webpack 编译
compiler.run((err, stats) => {
compiler.close((closeErr) => {});
});
};
通过上面的改造,我们将用户配置的覆盖逻辑和默认构建配置进行了解耦,同时通过 ConfigManager 类的 registerUserConfig 方法将用户配置的校验、覆盖等逻辑等聚合在一起进行管理。
改造完成后,整体的执行流程如下:
图 4.png
下面我们的场景再来演进一下:
由于业务需求,项目 project-c 需要处理 xml 文件, 所以项目的构建配置中需要增加 xml 文件的处理 loader,但是 build-scripts 并不支持 config.module.rules
的扩展,此时应该怎么办呢?
我们之前新增的用户配置方案只适用于一些简单的配置覆盖,如果项目涉及到复杂的构建配置自定义操作,就无能为力了。
社区中一般的做法是将构建配置 eject 到项目中,由用户自行修改,比如 react-scripts 。但是 eject 操作是不可逆的,如果后续构建配置有更新,项目就无法直接通过升级 npm 包的方式完成更新,同时单个项目对于构建配置的扩展也无法在多个项目间复用。
理想的方式是设计一种插件机制,能够让用户可插拔式地对构建配置进行扩展,同时这些插件也可以在项目间复用。基于这个目的,我们来对 build-scripts 进行一下改造:
用户配置 build.json 中新增 plugins 字段,用于配置自定义插件列表。
project-c/build.json
{
"entry": "./src/index1",
"outputDir": "./build",
+ "plugins": ["build-plugin-xml"]
}
然后我们再来改造一下 ConfigManager 里的执行逻辑,让 ConfigManager 在执行完用户配置和默认配置的合并后,去依次执行项目 build.json 中定义的插件列表,并将合并后的配置以参数的形式传入插件。
build-scripts/core/ConfigManager.ts
import _ = require('lodash');
import path = require('path');
import assert = require('assert');
...
class ConfigManager {
// webpack 配置
public config: IConfig;
...
/**
* 执行注册用户配置
*
* @param {*} configs
* @memberof ConfigManager
*/
private runUserConfig = async () => {
for (const configInfoKey in this.userConfig) {
+ if (configInfoKey === 'plugins') return;
const configInfo = this.userConfigRegistration[configInfoKey];
...
}
}
+ /**
+ * 执行插件
+ *
+ * @private
+ * @memberof ConfigManager
+ */
+ private runPlugins = async () => {
+ for (const plugin of this.userConfig.plugins) {
+ const pluginPath = require.resolve(plugin, { paths: [process.cwd()] });
+ const pluginFn = require(pluginPath);
+ await pluginFn(this.config);
+ }
+ }
/**
* webpack 配置初始化
*/
public setup = async () => {
// 获取用户配置
this.getUserConfig();
// 用户配置校验及合并
await this.runUserConfig();
+ // 执行插件
+ await this.runPlugins();
}
}
export default ConfigManager;
通过插件执行时传入的构建配置,我们就可以直接在插件内部完成构建配置对于 xml-loader 的扩展。
build-plugin-xml/index.js
module.exports = async (webpackConfig) => {
// 空值属性判断
if (!webpackConfig.module) webpackConfig.module = {};
if (!webpackConfig.module.rules) webpackConfig.module.rules = [];
// 添加 xml-loader
webpackConfig.module.rules.push({
test: /\.xml$/i,
use: require.resolve('xml-loader'),
});
};
基于以上的插件机制,项目可以对构建配置实现任意的自定义扩展,同时插件还可以 npm 包的形式在多个项目间复用。
改造完成后,整体的执行流程如下:
图 5.png
下面我们的场景再来演进一下:
由于构建性能问题(仅为场景假设),插件 build-plugin-xml 需要将 xml-loader 的匹配规则调整到 ts-loader 的匹配规则之前,所以我们对插件 build-plugin-xml 进行了如下改造:
module.exports = async (webpackConfig) => {
// 空值属性判断
if (!webpackConfig.module) webpackConfig.module = {};
if (!webpackConfig.module.rules) webpackConfig.module.rules = [];
// 定义 xml-loader 规则
const xmlRule = {
test: /\.xml$/i,
use: require.resolve('xml-loader'),
};
// 找到 ts-loader 规则位置
const tsIndex = webpackConfig.module.rules.findIndex(
(rule) => String(rule.test) === '/\\.ts?$/'
);
// 添加 xml-loader 规则
if (tsIndex > -1) {
webpackConfig.module.rules.splice(tsIndex - 1, 0, xmlRule);
} else {
webpackConfig.module.rules.push(xmlRule);
}
};
改造完成后,插件 build-plugin-xml 针对 xml-loader 的扩展一共做了四件事:
观察上面的改造我们可以发现,虽然我们的构建配置并不复杂,但针对于它的修改和扩展还是比较繁琐的。这主要是由于 webpack 构建配置是以一个 JavaScript 对象的形式来进行维护的,一般项目中的配置对象往往很大,且内部属性间存在层层嵌套,针对配置对象的修改和扩展会涉及到各种判空、遍历、分支处理等操作,所以逻辑会显得比较复杂。
为了解决插件中构建配置修改和扩展逻辑复杂的问题,我们可以在项目中来引入 webpack-chain :
webpack-chain 是一种 webpack 的流式配置方案,通过链式调用的方式来操作配置对象。其核心是 ChainedMap 和 ChainedSet 两个对象类型,借助 ChainedMap 和 ChainedSet 提供的操作方法,我们能够很方便地对配置对象进行修改和扩展,可以避免之前手动操作 JavaScript 对象时带来的繁琐。这里不做过多介绍,感兴趣的同学可以查看官方文档[1]。
我们先来将默认的构建配置修改为 webpack-chain 的方式。
build-scripts/src/configs/build.ts
+ import * as Config from 'webpack-chain';
const path = require('path');
const rootDir = process.cwd();
- const buildConfig = {
- entry: path.resolve(rootDir, './src/index'),
- module: {
- rules: [
- {
- test: /\.ts?$/,
- use: require.resolve('ts-loader'),
- exclude: /node_modules/,
- },
- ],
- },
- resolve: {
- extensions: ['.ts', '.js'],
- },
- output: {
- filename: 'main.js',
- path: path.resolve(rootDir, './dist'),
- },
- };
+ 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'));
export default buildConfig;
然后我们将 ConfigManager 中涉及到构建配置的地方也切换为 webpack-chain 的方式。
src/core/ConfigManager.ts
import _ = require('lodash');
import path = require('path');
import assert = require('assert');
+ import WebpackChain = require('webpack-chain');
...
interface IUserConfigArgs {
name: string;
defaultValue?: any;
validation?: (value: any) => Promise<boolean>;
- configWebpack?: (defaultConfig: IConfig, value: any) => void;
+ configWebpack?: (defaultConfig: WebpackChain, value: any) => void;
}
class ConfigManager {
// webpack 配置
- public config: IConfig;
+ public config: WebpackChain;
// 用户配置
public userConfig: IConfig;
// 用户配置注册信息
private userConfigRegistration: IUserConfigRegistration;
- constructor(config: IConfig) {
+ constructor(config: WebpackChain) {
this.config = config;
this.userConfig = {};
this.userConfigRegistration = {};
}
...
}
export default ConfigManager;
同时用户配置中涉及到构建配置的地方也切换为 webpack-chain 的方式。
src/commands/build.ts
...
export = async () => {
...
// 注册用户配置
manager.registerUserConfig([
{
...
// 配置值合并
configWebpack: async (defaultConfig, value) => {
- defaultConfig.entry = path.resolve(rootDir, value);
+ defaultConfig.entry('index').clear().add(path.resolve(rootDir, value));
},
},
{
...
// 配置值合并
configWebpack: async (defaultConfig, value) => {
- defaultConfig.output.path = path.resolve(rootDir, value);
+ defaultConfig.output.path(path.resolve(rootDir, value));
},
},
]);
// webpack 配置初始化
await manager.setup();
// 实例化 webpack
- const compiler = webpack(manager.config);
+ const compiler = webpack(manager.config.toConfig());
...
};
借助 webpack-chain ,插件 build-plugin-xml 针对 xml-loader 的扩展逻辑可以简化为:
module.exports = async (webpackConfig) => {
- // 空值属性判断
- if (!webpackConfig.module) webpackConfig.module = {};
- if (!webpackConfig.module.rules) webpackConfig.module.rules = [];
-
- // 定义 xml 规则
- const xmlRule = {
- test: /\.xml$/i,
- use: require.resolve('xml-loader'),
- };
-
- // 找到 ts 规则位置
- const tsIndex = webpackConfig.module.rules.findIndex(
- (rule) => String(rule.test) === '/\\.ts?$/'
- );
-
- // 添加 xml 规则
- if (tsIndex > -1) {
- webpackConfig.module.rules.splice(tsIndex - 1, 0, xmlRule);
- } else {
- webpackConfig.module.rules.push(xmlRule);
- }
+ webpackConfig.module
+ .rule('xml')
+ .before('ts')
+ .test(/\.xml$/i)
+ .use('xml-loader')
+ .loader(require.resolve('xml-loader'));
};
相对之前复杂的空值判断和对象遍历逻辑,webpack-chain 极大地简化了插件内部对于配置对象的修改和扩展操作,无论是代码质量,还是开发体验,相对于之前来说都有不小的提升。
下面我们的场景再来演进一下:
假设现在接入 build-scripts 的项目都是 react 项目, 由于业务方向的调整,后续团队的技术栈会切换到 rax,新增的 rax 项目想继续使用 build-scripts 进行项目间构建配置的复用,此时应该怎么办呢?
由于 build-scripts 里默认的构建配置是基于 react 的,所以 rax 项目是没办法直接基于插件进行扩展的,难道需要基于 rax 构建配置再新建一个 build-scritps 项目吗?这样显然是没办法做到核心逻辑复用的。我们来换个思路想想,既然插件可以修改构建配置,那么能不能将构建配置的初始化也放在插件里?这样就能够实现构建配置和 build-scripts 的解耦,任意类型的项目都能够基于 build-scripts 来进行构建配置的管理和扩展。
基于这个目的,我们下面来对 build-scripts 进行一下改造:
我们首先对 ConfigManager 里的逻辑进行一下调整,新增 setConfig 方法提供给插件进行构建配置的初始化,由于插件还承担修改和扩展构建配置的职责,而这部分逻辑的调用是在初始配置和用户配置合并后的,所以我们通过 onGetWebpackConfig 方法注册回调函数的方式来执行这部分逻辑。
src/core/ConfigManager.ts
import _ = require('lodash');
import path = require('path');
import assert = require('assert');
import WebpackChain = require('webpack-chain');
...
+ // webpack 配置修改函数类型定义
+ type IModifyConfigFn = (defaultConfig: WebpackChain) => void;
class ConfigManager {
// webpack 配置
public config: WebpackChain;
// 用户配置
public userConfig: IConfig;
// 用户配置注册信息
private userConfigRegistration: IUserConfigRegistration;
+ // 已注册的 webpack 配置修改函数
+ private modifyConfigFns: IModifyConfigFn[];
- constructor(config: WebpackChain) {
- this.config = config;
+ constructor() {
this.userConfig = {};
this.userConfigRegistration = {};
+ this.modifyConfigFns = [];
}
+ /**
+ * 设置 webpack 配置
+ *
+ * @param {WebpackChain} config
+ * @memberof ConfigManager
+ */
+ public setConfig = (config: WebpackChain) => {
+ this.config = config;
+ };
+ /**
+ * 注册 webpack 配置修改函数
+ *
+ * @param {(defaultConfig: WebpackChain) => void} fn
+ * @memberof ConfigManager
+ */
+ public onGetWebpackConfig = (fn: (defaultConfig: WebpackChain) => void) => {
+ this.modifyConfigFns.push(fn);
+ };
/**
* 注册用户配置
*
* @param {IUserConfigArgs[]} configs
* @memberof ConfigManager
*/
public registerUserConfig = (configs: IUserConfigArgs[]) => {
...
};
/**
* 获取用户配置
*
* @private
* @return {*}
* @memberof ConfigManager
*/
private getUserConfig = () => {
...
};
/**
* 执行注册用户配置
*
* @param {*} configs
* @memberof ConfigManager
*/
private runUserConfig = async () => {
...
};
/**
* 执行插件
*
* @private
* @memberof ConfigManager
*/
private runPlugins = async () => {
for (const plugin of this.userConfig.plugins) {
const pluginPath = require.resolve(plugin, { paths: [process.cwd()] });
const pluginFn = require(pluginPath);
- await pluginFn(this.config);
+ await pluginFn({
+ setConfig: this.setConfig,
+ registerUserConfig: this.registerUserConfig,
+ onGetWebpackConfig: this.onGetWebpackConfig,
+ });
}
};
+ /**
+ * 执行 webpack 配置修改函数
+ *
+ * @private
+ * @memberof ConfigManager
+ */
+ private runWebpackModifyFns = async () => {
+ this.modifyConfigFns.forEach((fn) => fn(this.config));
+ };
/**
* webpack 配置初始化
*/
public setup = async () => {
// 获取用户配置
this.getUserConfig();
+ // 执行插件
+ await this.runPlugins();
// 用户配置校验及合并
await this.runUserConfig();
- // 执行插件
- await this.runPlugins();
+ // 执行 webpack 配置修改函数
+ await this.runWebpackModifyFns();
};
}
export default ConfigManager;
然后我们将 build-scripts 里默认配置相关的逻辑给抽离出来。
npm 包 build-scripts
build-scripts
|- /bin
|- build-scripts.js
|- /lib (ts 构建目录,文件同 src)
|- /src
|- /commands
|- build.ts
- |- /configs
- |- build.ts
|- /core
|- ConfigManager.ts
|- tsconfig.json
|- package.json
|- package-lock.json
由于用户配置一般是跟默认构建配置走的,所以我们也抽离出来。
src/commands/build.ts
- import * as path from 'path';
import * as webpack from 'webpack';
- import defaultConfig from '../configs/build';
import ConfigManager from '../core/ConfigManager';
export = async () => {
- const rootDir = process.cwd();
// 初始化配置管理类
- const manager = new ConfigManager(defaultConfig);
+ const manager = new ConfigManager();
- // 注册用户配置
- manager.registerUserConfig([
- {
- // entry 配置
- name: 'entry',
- // 配置值校验
- validation: async (value) => {
- return typeof value === 'string';
- },
- // 配置值合并
- configWebpack: async (defaultConfig, value) => {
- defaultConfig.entry('index').clear().add(path.resolve(rootDir, value));
- },
- },
- {
- // outputDir 配置
- name: 'outputDir',
- // 配置值校验
- validation: async (value) => {
- return typeof value === 'string';
- },
- // 配置值合并
- configWebpack: async (defaultConfig, value) => {
- defaultConfig.output.path(path.resolve(rootDir, value));
- },
- },
- ]);
// webpack 配置初始化
await manager.setup();
// 实例化 webpack
const compiler = webpack(manager.config.toConfig());
// 执行 webpack 编译
compiler.run((err, stats) => {
compiler.close((closeErr) => {});
});
};
我们将抽离的默认构建配置的相关逻辑,封装到插件 build-plugin-base 里。
build-plugin-base/index.js
const Config = require('webpack-chain');
const path = require('path');
const rootDir = process.cwd();
module.exports = async ({ setConfig, 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'));
setConfig(buildConfig);
/**
* 注册用户配置
*/
registerUserConfig([
{
// entry 配置
name: 'entry',
// 配置值校验
validation: async (value) => {
return typeof value === 'string';
},
// 配置值合并
configWebpack: async (defaultConfig, value) => {
defaultConfig.entry('index').clear().add(path.resolve(rootDir, value));
},
},
{
// outputDir 配置
name: 'outputDir',
// 配置值校验
validation: async (value) => {
return typeof value === 'string';
},
// 配置值合并
configWebpack: async (defaultConfig, value) => {
defaultConfig.output.path(path.resolve(rootDir, value));
},
},
]);
};
同时我们还需要调整一下 build-plugin-xml 里的逻辑,将构建配置扩展的逻辑通过 onGetWebpackConfig 方法改为回调函数的方式调用。
build-plugin-xml/index.js
- module.exports = async (webpackConfig) => {
+ module.exports = async ({ onGetWebpackConfig }) => {
+ onGetWebpackConfig((webpackConfig) => {
webpackConfig.module
.rule('xml')
.test(/\.xml$/i)
.use('xml-loader')
.loader(require.resolve('xml-loader'));
+ });
};
通过以上的改造,我们实现了默认构建配置和 build-scripts 的解耦,理论上任意类型的项目均可基于 build-scripts 来实现构建配置的项目间复用及扩展。
改造完成后,整体的执行流程如下:
图 6.png
最后我们的场景再来扩展一下:
假设单个项目的构建产物不止一种,例如 Rax 项目需要打包构建为 H5 和 小程序两种类型,两种类型对应的是不同的构建配置,但 build-scripts 只支持一份构建配置, 此时应该怎么办呢?
webpack 其实默认是支持多构建配置执行的,我们只需要向 webpack 的 compiler 实例传入一个数组就行:
const webpack = require('webpack');
webpack([
{ entry: './index1.js', output: { filename: 'bundle1.js' } },
{ entry: './index2.js', output: { filename: 'bundle2.js' } }
], (err, stats) => {
process.stdout.write(stats.toString() + '\n');
})
基于 webpack 的多配置执行能力,我们可以来考虑为 build-scripts 设计一种多任务机制。 基于这个目的,我们下面来对 build-scripts 进行一下改造:
首先我们来调整一下 ConfigManager 里的逻辑,将 webapck 的默认配置改为数组形式,同时新增 registerTask 方法来进行 webpack 默认配置的注册,同时调整一下 webpack 默认配置引用的相关逻辑。
build-scripts/src/commands/ConfigManager.ts
import _ = require('lodash');
import path = require('path');
import assert = require('assert');
import WebpackChain = require('webpack-chain');
...
// webpack 配置修改函数类型定义
type IModifyConfigFn = (defaultConfig: WebpackChain) => void;
+ // webpack 任务配置类型定义
+ export interface ITaskConfig {
+ name: string;
+ chainConfig: WebpackChain;
+ modifyFunctions: IModifyConfigFn[];
+ }
class ConfigManager {
- // webpack 配置
- public config: WebpackChain;
+ // webpack 配置列表
+ public configArr: ITaskConfig[];
// 用户配置
public userConfig: IConfig;
// 用户配置注册信息
private userConfigRegistration: IUserConfigRegistration;
- // 已注册的 webpack 配置修改函数
- private modifyConfigFns: IModifyConfigFn[];
constructor() {
+ this.configArr = [];
this.userConfig = {};
this.userConfigRegistration = {};
- this.modifyConfigFns = [];
}
- /**
- * 设置 webpack 配置
- *
- * @param {WebpackChain} config
- * @memberof ConfigManager
- */
- public setConfig = (config: WebpackChain) => {
- this.config = config;
- };
+ /**
+ * 注册 webpack 任务
+ *
+ * @param {string} name
+ * @param {WebpackChain} chainConfig
+ * @memberof ConfigManager
+ */
+ public registerTask = (name: string, chainConfig: WebpackChain) => {
+ const exist = this.configArr.find((v): boolean => v.name === name);
+ if (!exist) {
+ this.configArr.push({
+ name,
+ chainConfig,
+ modifyFunctions: [],
+ });
+ } else {
+ throw new Error(`[Error] config '${name}' already exists!`);
+ }
+ };
/**
* 注册 webpack 配置修改函数
*
+ * @param {string} name
* @param {(defaultConfig: WebpackChain) => void} fn
* @memberof ConfigManager
*/
- public onGetWebpackConfig = (fn: (defaultConfig: WebpackChain) => void) => {
- this.modifyConfigFns.push(fn);
- };
+ public onGetWebpackConfig = (
+ name: string,
+ fn: (defaultConfig: WebpackChain) => void
+ ) => {
+ const config = this.configArr.find((v): boolean => v.name === name);
+
+ if (config) {
+ config.modifyFunctions.push(fn);
+ } else {
+ throw new Error(`[Error] config '${name}' does not exist!`);
+ }
+ };
/**
* 注册用户配置
*
* @param {IUserConfigArgs[]} configs
* @memberof ConfigManager
*/
public registerUserConfig = (configs: IUserConfigArgs[]) => {
...
};
/**
* 获取用户配置
*
* @private
* @return {*}
* @memberof ConfigManager
*/
private getUserConfig = () => {
...
};
/**
* 执行注册用户配置
*
* @param {*} configs
* @memberof ConfigManager
*/
private runUserConfig = async () => {
for (const configInfoKey in this.userConfig) {
...
// 配置值更新到默认 webpack 配置
if (configInfo.configWebpack) {
- await configInfo.configWebpack(this.config, configValue);
+ // 遍历已注册的 webapck 任务
+ for (const webpackConfigInfo of this.configArr) {
+ await configInfo.configWebpack(
+ webpackConfigInfo.chainConfig,
+ configValue
+ );
+ }
}
}
};
/**
* 执行插件
*
* @private
* @memberof ConfigManager
*/
private runPlugins = async () => {
for (const plugin of this.userConfig.plugins) {
const pluginPath = require.resolve(plugin, { paths: [process.cwd()] });
const pluginFn = require(pluginPath);
await pluginFn({
- setConfig: this.setConfig,
+ registerTask: this.registerTask,
registerUserConfig: this.registerUserConfig,
onGetWebpackConfig: this.onGetWebpackConfig,
});
}
};
/**
* 执行 webpack 配置修改函数
*
* @private
* @memberof ConfigManager
*/
private runWebpackModifyFns = async () => {
- this.modifyConfigFns.forEach((fn) => fn(this.config));
+ for (const webpackConfigInfo of this.configArr) {
+ webpackConfigInfo.modifyFunctions.forEach((fn) =>
+ fn(webpackConfigInfo.chainConfig)
+ );
+ }
};
/**
* webpack 配置初始化
*/
public setup = async () => {
// 获取用户配置
this.getUserConfig();
// 执行插件
await this.runPlugins();
// 用户配置校验及合并
await this.runUserConfig();
// 执行 webpack 配置修改函数
await this.runWebpackModifyFns();
};
}
export default ConfigManager;
build 命令执行时的构建配置获取也需要改为数组的形式。
build-scripts/src/commands/build.ts
import * as webpack from 'webpack';
import ConfigManager from '../core/ConfigManager';
export = async () => {
// 初始化配置管理类
const manager = new ConfigManager();
// webpack 配置初始化
await manager.setup();
// 实例化 webpack
- const compiler = webpack(manager.config.toConfig());
+ const compiler = webpack(
+ manager.configArr.map((config) => config.chainConfig.toConfig())
+ );
// 执行 webpack 编译
compiler.run((err, stats) => {
compiler.close((closeErr) => {});
});
};
插件 build-plugin-base 也需要调整默认构建配置的注册方式。
build-plugin-base/index.js
const Config = require('webpack-chain');
const path = require('path');
const rootDir = process.cwd();
- module.exports = async ({ setConfig, registerUserConfig }) => {
+ module.exports = async ({ registerTask, registerUserConfig }) => {
/**
* 设置默认配置
*/
const buildConfig = new Config();
...
- setConfig(buildConfig)
+ registerTask('base', buildConfig);
/**
* 注册用户配置
*/
registerUserConfig([
...
]);
};
插件 build-plugin-xml 也需要添加上对应的 webpack 任务名称参数。
build-plugin-xml/index.js
module.exports = async ({ onGetWebpackConfig }) => {
- onGetWebpackConfig((webpackConfig) => {
+ onGetWebpackConfig('base', (webpackConfig) => {
webpackConfig.module
.rule('xml')
.before('ts')
.test(/\.xml$/i)
.use('xml-loader')
.loader(require.resolve('xml-loader'));
});
};
通过以上的改造,我们为 build-scripts 增加了多任务执行的机制,可以实现单个项目下的多构建任务执行。
改造完成后,整体的执行流程如下:
图 7.png
以上我们通过场景演进的方式,对 build-scripts 核心的设计原理和相关方法进行了讲解。通过以上的分析,我们可以看出 build-scripts 本质上是一个具有灵活插件机制的配置管理方案,不仅仅局限于 webpack 配置,任何有跨项目间配置复用及扩展的场景,都可以借助 build-scripts 的设计思路。
注:文中涉及示例代码可通过仓库 _ build-scripts-demo_[2] 查看,同时 build-scripts 中未介绍到的相关方法,感兴趣的同学也可以通过仓库 _build-scripts_[3] 阅读相关源码。
[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
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/x_5wCbmx_vsbjLRTsAoTMA
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。