使用Feature Matrix来构建应用

378 阅读4分钟
原文链接: zhuanlan.zhihu.com

起了一个吹牛逼的标题,但其实做的事情非常简单。

背景

在我们团队多数的产品中,需要做灰度发布或者说小流量分流的时候,通常是以机器或者以目录为单元分流,同时进行多次构建,利用不同的参数构建出不同的结果,比如:

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份产物送到不同的服务器上,或者送到同一服务器的不同文件夹中,再进行分流,基本上就是这样的:



这种方案其实存在着一些不容易被感知到的缺陷:


  1. 要做多次构建,多次构建过程中的缓存利用等就会成为问题,可能拖慢速度。
  2. 多次构建放在多个产出目录下,部署的时候要将目录与线上的机器/目录对应上,这里引入了一个故障点(POF),如果上错了对应关系怎么办?
  3. 如果按机器分流,不同流量间的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-loadereslint-loader等的cache配置存在,同一次构建过程中不会重复地编译。
  • webpack在接受了多个配置对象时,似乎会使用多线程的形式进行构建,因此多核的环境下并没有严重损失。

一些细节

首先有一个奇怪的小细节,cssnano必须安装@next的版本。当webpack进行多配置构建时,似乎会进入strict mode,而普通版本的cssnano依赖uniqid这个库有一个全局变量泄露,会导致构建失败。

在设计之初,Feature Matrix的构建有一个额外的功能,当2个流量上的flag完全相同时,仅构建一次,随后通过多个 WebpackHtmlPlugin实例来生成.html文件,以节省构建的成本。但后续发现在代码中往往不仅仅用于flag,也需要流量的名称,如提供给Sentry监控平台来区分当前用户所在的流量。因此节省重复构建的功能在后续去除了。