標籤:

使用Feature Matrix來構建應用

起了一個吹牛逼的標題,但其實做的事情非常簡單。

背景

在我們團隊多數的產品中,需要做灰度發布或者說小流量分流的時候,通常是以機器或者以目錄為單元分流,同時進行多次構建,利用不同的參數構建出不同的結果,比如:

npm run build -- --target=stable# /assets# app.xxx.js# index.htmlnpm 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.jsexports.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監控平台來區分當前用戶所在的流量。因此節省重複構建的功能在後續去除了。


推薦閱讀:

Hello Webpack.1
推薦閱讀 - 第16期
webpack 源碼解讀(1)--源碼結構
深入理解 webpack 文件打包機制
React-router4和webpack中output的publicPath相關關係

TAG:webpack |