1. 程式人生 > 其它 >使用yarn 升級node_Webpack 5 升級實驗

使用yarn 升級node_Webpack 5 升級實驗

技術標籤:使用yarn 升級node

今天嘗試把我們團隊的通用構建工具reskript做了webpack 5的升級,使用最新的5.0.0-alpha.23版本。

先發一個實測的構建效能資料,測試環境為我自己的MacBook Pro 13寸高配:

專案腳手架,構建後產出544KB的指令碼,首次構建用時39.62秒,第二次構建用時22.84秒。
一個經典的單頁應用,構建後產出3.5MB的指令碼,首次構建用時299.33秒,第二次構建用時82.70秒。

總結果來看,Webpack 5的長效快取效果非常明顯

這本不是一個容易的事情,我們在構建上做了大量的定製化的工作,包括:

  1. 使用一大堆外掛,比如case-sensitive-paths-webpack-plugin
    等。
  2. 通過DefinePlugin進行了大量的“動態”內容的處理,包括整個process.env、構建時可宣告的Feature Matrix資訊,以及構建時的git版本、時間等內容。
  3. 封裝了core-js版本、react-hot-loader版本等,有很多包的路徑不在產品開發者想象的位置,以及包的版本固定的問題。

由於升級的過程比較枯燥,無非就是除錯、修改、繼續除錯,所以這邊只簡單的羅列幾個升級時的典型問題。

外掛不相容

case-sensitive-paths-webpack-plugin外掛並不相容新版本,在GitHub上已經有相應的Issue。好在這個功能並不影響實際的構建,純粹是一個防禦性的措施,直接去掉就好了。

優化配置不同

  • optimization. occurrenceOrder已經廢棄了,如有配置可以直接刪掉。
  • HashedModuleIdsPlugin也已經沒用了,新的模組ID生成演算法應該是優於這個的,具體穩定性沒喊待測試

HTML外掛不可用

html-webpack-plugin用不了,可以參考這個Issue。這個問題可大了,總不能構建完沒有HTML頁面吧。

不過好在這問題不難修復,具體程式碼是index.js中的145行,把這一行中的compilation.compilationDependencies.add修改為compilation.fileDependencies.add就可以正常運行了。

考慮到就是升級做個實驗,所以沒有把修改後的版本發包,等官方修改吧。

複雜規則失效

我們有一個規則是這樣的:

{
    test: /.[jt]sx?$/,
    oneOf: [
        {
            resource: {
                and: [
                    {include: projectDirectory},
                    {exclude: projectDirectory + 'externals'},
                    {exclude: //node_modules//},
                ],
            }
            oneOf: [
                {
                    test: /.worker.[jt]sx?$/,
                    use: use('worker', 'babel', 'eslint'),
                },
                {
                    use: use('babel', styleName && usage === 'devServer' && 'styleName', 'eslint'),
                },
            ],
        },
        {
            test: testOfPackages(sourceCompilePackages, cwd),
            use: use('babel', 'eslint'),
        },
        {
            test: thirdParties,
            exclude: //node_modules/monaco-editor//,
            use: use(usage === 'devServer' && hot === 'all' && 'hot', 'sourceMap'),
        },
    ],
}

在Webpack 5中,oneOf.resource.and下面不能用includeexclude了(不知道為什麼,沒有查到任何和這有關的變更記錄)。解決的方法是把resource改成函式:

{
    resource(resource) {
        return resource.includes(projectDirectory)
            && !resource.includes(projectDirectory + '/externals')
            && !resource.includes('/node_modules/');
}

沒有Node相容

在Webpack 5中移除了Node模組的相容。

webpack 5 stops automatically polyfilling these core modules and focuses on frontend-compatible modules.

在實際構建時,如果遇到類似於const {Buffer} = require('buffer')的程式碼,會提示新版本不會再對它進行自動的相容,由你來選擇是否安裝相應的庫並通過resolve.alias配置:

{
    resolve: {
        alias: {
            buffer: 'buffer',
        },
    },
}

但是,Webpack只解決引入模組的程式碼, 不解決全域性變數的檢測,這是和之前版本最大的區別。比如有程式碼是這樣的:

exports.isBuffer = Buffer.isBuffer;

Webpack 5並不會認為這裡用到了Buffer這個物件需要處理相容性,而是正常地進行打包,也不提示開發者。直到系統執行時,才會出現Buffer is not defined這樣的錯誤。

同時,由於內建的NodeSourcePlugin已經修改了實現,現在只會處理global這一個變數,所以即便把這個外掛找回來也不會再幫你修復這些全域性變數的使用了,我們只能自己想辦法去處理。

在這裡推薦babel-plugin-import-globals這個babel外掛,它可以找到相關的全域性變數並進行處理。我們至今發現的只有Bufferprocess這兩個被使用,所以配置是這樣的:

{
    loader: require.resolve('babel-loader'),
    options: {
        sourceType: 'unambiguous', // 這個一定要配,自動處理es和js模組
        compact: false, // 這個建議配,能提升效能
        plugins: [
            [
                require.resolve('babel-plugin-import-globals'),
                {
                    'process': require.resolve('process'),
                    'Buffer': {moduleName: require.resolve('buffer'), exportName: 'Buffer'},
                },
            ],
        ],
    },
}

將這個配置加到node_modules下的JavaScript檔案上就行,如:

{
    test: //jsx?$/,
    include: /node_modules/,
    use: [loader],
}

當然這會讓所有第三方的程式碼也過babel的處理(雖然只有一個外掛),會被babel解析,一定程度上會影響構建的速度。babel處理的速度與原來的NodeSourcePlugin的處理孰優孰劣我也無法再做比較了。

除此之外,在resolve.alias下也需要配上對應的一些相容庫:

{
    crypto: 'crypto-browserify',
    stream: 'stream-browserify',
    vm: 'vm-browserify',
}

配置快取

Webpack 5最令我期待的功能就是長效快取,通過相關的配置開啟:

{
    cache: {
        type: 'filesystem'
    }
}

但這樣開啟後,快取會過於固定,引起一系列問題:

  • mode之類的變化無法響應,快取不會變。
  • 如果根據不同的場景,有不同的babel配置等,也同樣不會感知,依然會用舊的快取。
  • 使用DefinePlugin注入的動態內容,全部不會變化。

而要處理這些“動態性”,我們需要2個東西。

第一個是cache.version的配置,這個配置可以告訴webpack內容有了變化,需要重新處理快取,如mode或babel配置之類的,可以通過不同的version隔離開來。

最簡單的cache.version的演算法是webpack.config.jsnode_modules/.yarn-integrity做一下雜湊,但我們封裝了webpack的能力,所以並不存在一個固定的webpack.config.js,就必須手動實現它,我們當前的演算法是:

const computeCacheKey = (entry: BuildEntry): string => {
    const hash = crypto.createHash('sha1');
    hash.update(entry.usage); // 使用場景,如build、dev等
    hash.update(entry.mode);
    hash.update(entry.hostPackageName); // 主包名,會用在一些import語句上
    hash.update(fs.readFileSync(path.join(entry.cwd, 'settings.js'))); // 使用者的定製化配置
    hash.update(fs.readFileSync(path.join(entry.cwd, 'node_modules', '.yarn-integrity'))); // 依賴資訊
    return hash.digest('hex');
};

要保持這個演算法穩定,並且在動態的資訊變化時一定會發生改變。

cache.version不能用來處理注入的內容,如果把DefinePlugin消費的東西都放進去,比如我們的構建還有當時的時間戳,這就會讓版本號就會永遠變化,起不了快取的作用。

解決的辦法是使用DefinePlugin.runtimeValue函式。這個函式其實V4的時候就有,藏得挺隱蔽的,甚至@types/webpack的定義資訊中都沒有它,以至於為了用它我們不得不這麼搞:

const runtimeValue: (compute: () => string, dynamic: boolean) => any = (DefinePlugin as any).runtimeValue;

使用的方法是這樣:

const defines = {
  'process.env.foo': DefinePlugin.runtimeValue(() => JSON.stringify(process.env.foo), true),
};

new DefinePlugin(defines);

注意runtimeValue呼叫的第2個引數,此處用true表示“這是一個始終會變的值”,它也可以傳一些檔案的路徑來讓值和檔案是否變化建立關係。這樣做了以後,有遇到process.env.foo的檔案會在構建時排除在快取外,而其它上下游的檔案依然可以快取,這裡不會出現“因為入口檔案有一個動態內容,所以下面其它檔案都不能快取”這樣尷尬的情況,並不怎麼影響快取的使用和效能。

總結

Webpack 5的升級並不難,一些細節和外掛的相容性是主要問題,也可以將外掛相容性的修復再反饋回社群,與社群一起成長。

當做現在Webpack 5還存在一些問題,比如構建我們一個單頁系統用掉了2.5GB記憶體,不設定--max-old-space-size引數都跑不下去。因此建議做一下升級的嘗試,處理好相容問題,以便正式版釋出的時候能快速遷移,但不要直接用在生產環境上。