起了一个吹牛逼的标题,但其实做的事情非常简单。
背景
在我们团队多数的产品中,需要做灰度发布或者说小流量分流的时候,通常是以机器或者以目录为单元分流,同时进行多次构建,利用不同的参数构建出不同的结果,比如:
npm run build -- --target=stable
# /assets
# app.xxx.js
# index.html
npm run build -- --target=insiders
# /assets
# app.yyy.js
# index.html
特点是每一次构建都会产出一样的HTML文件(index.html
),随后将这2份产物送到不同的服务器上,或者送到同一服务器的不同文件夹中,再进行分流,基本上就是这样的:
这种方案其实存在着一些不容易被感知到的缺陷:
- 要做多次构建,多次构建过程中的缓存利用等就会成为问题,可能拖慢速度。
- 多次构建放在多个产出目录下,部署的时候要将目录与线上的机器/目录对应上,这里引入了一个故障点(POF),如果上错了对应关系怎么办?
- 如果按机器分流,不同流量间的PV量是不一致的,要么预测PV的分布来准备不同数量的机器,要么就看到有些机器忙死有些闲死。
除了这些以外,我们面临着一个更糟糕的问题:使用Cookie进行分流的时候,没有Cookie时请求一个具体的文件(如app.xxx.js
)时,分流系统无法确切地找到这个文件(在哪台机器/哪个文件夹上)。
而我们使用的Sentry异常监控是需要下载Source Maps的,同样Firefox在下载Source Maps时也不会带上Cookie,这导致几乎所有的Source Maps下载都宣告失败,我们无法定位和分析问题。
解决方案
为了解决这一问题,我们决定把不同流量的构建产物直接放在一起。而为了能放在一起,我们亟待处理的问题就成了:index.html
这个文件的重复怎么办。
当然这是一个容易解决的问题,直接按照流量名来生成不同的文件就行,然后让分流直接分到不同的.html
文件上,即变成这样子:
所以我们的问题变成了,如何用一个统一的模式,来构建出上图中的全部内容。
技术实现
首先,我们发现webpack实际可以接受一个数组作为配置,所以我们就用一个文件来生成一个配置数组。
我们定义了一种模式来支持不同的开关,所有的开关被统一命名到features.flagName
下,然后我们就可以通过编写一个features.js
来定义流量以及对应的开关:
// features.js
exports.stable = {
allowEditOfUsername: false,
githubSignIn: false
};
exports.insiders = {
allowEditOfUsername: true,
githubSignIn: true
};
exports.dev = {
allowEditOfUsername: true,
githubSignIn: true
};
但是我们并不希望所有用到开关的地方都需要写import features from 'features';
这样的代码,同时即便写了其实也没办法选择到正确的流量。
因此我们选择了DefinePlugin来完成这一任务,根据当前的流量来生成一系列的features.flagName
常量,对源码进行替换。一个大概的代码就是:
const getFeatureFlagDefinitions = buildTarget => {
const flags = features[buildTarget];
return Object.entries(flags).reduce(
(output, [key, value]) => {
return {
...output,
['features.' + key]: JSON.stringify(value)
};
},
{}
);
};
在获得所有的流量名称后,用这个方法来创建webpack配置:
const createWebpackConfig = buildTarget => {
const featureFlags = getFeatureFlagDefinitions(buildTarget);
return {
...,
plugins: [
...,
new DefinePlugin(featureFlags),
new WebpackHtmlPlugin({filename: buildTarget + '.html'})
]
};
};
const featureNames = Object.keys(features);
module.exports = featureNames.map(createWebpackConfig);
具体效果
在构建的入口处,我们提供了可视化的当前流量-功能的对应关系:
同时一次构建2个流量并没有导致构建时间的增长,大致分析了一下,这是因为:
babel-loader
、eslint-loader
等的cache
配置存在,同一次构建过程中不会重复地编译。- webpack在接受了多个配置对象时,似乎会使用多线程的形式进行构建,因此多核的环境下并没有严重损失。
一些细节
首先有一个奇怪的小细节,cssnano
必须安装@next
的版本。当webpack进行多配置构建时,似乎会进入strict mode,而普通版本的cssnano依赖uniqid
这个库有一个全局变量泄露,会导致构建失败。
在设计之初,Feature Matrix的构建有一个额外的功能,当2个流量上的flag完全相同时,仅构建一次,随后通过多个 WebpackHtmlPlugin
实例来生成.html
文件,以节省构建的成本。但后续发现在代码中往往不仅仅用于flag,也需要流量的名称,如提供给Sentry监控平台来区分当前用户所在的流量。因此节省重复构建的功能在后续去除了。