構建效率大幅提高,webpack5 在企鵝輔導的升級實踐

| 導語 2020 年 10 月 10 日,webpack5 正式發佈,並帶來了諸多重大的變動,將會使前端的構建效率與質量大爲提高。其實如今各大博客網站已經有不少關於 webpack5 的文章,但真正經過業務實踐並得到第一手數據的並很少,因此今天就給你們介紹一下 webpack5 在企鵝輔導業務中的升級與實踐ad下面是企鵝輔導h5項目分別在 webpack4 和 webpack5 版本下的構建實測數據,測試環境爲個人 MacBook Pro 15 寸高配。css

在上表打包的結果基礎之上,修改項目中的代碼後,從新進行打包獲得以下結果:html

打包後文件的大小:前端

從上表的測試結果能夠看出,webpack5 構建性能相對於 webpack4 提高不少,但在打包完成的 bundle 大小上,與 v4 差距不大。由此能夠看出 webpack5 的新特性帶來了一些優化,下面結合這些新的特性來分析爲何可以作到這些優化。node

webpack5 新特性react

webpack5 的發佈帶來了不少新的特性,例如優化持久緩存、優化長期緩存、Node Polyfill 腳本的移除、更優的 tree-shaking 以及 Module Federation 等。下面針對這些新的特性做出分析。webpack

一、編譯緩存git

顧名思義,編譯緩存就是在首次編譯後把結果緩存起來,在後續編譯時複用緩存,從而達到加速編譯的效果。github

1.一、webpack4 緩存方案web

webpack4 及以前的版本自己是沒有持久化緩存的能力的,只能藉助其餘的插件或 loader 來實現,例如:npm

  • 使用 cache-loader 來緩存編譯結果到硬盤,再次構建時在緩存的基礎上增量編譯長期緩存。

  • 使用自帶緩存的 loader,如:babel-loader,能夠配置 cacheDirectory 來將 babel 編譯的結果緩存下來。

  • 使用 hard-source-webpack-plugin 來爲模塊提供中間緩存。

以下圖所示,使用以上緩存方案的結果,默認存儲在 node_modules/.cache 目錄下:

1.二、webpack5 緩存方案

webpack5 統一了持久化緩存的方案,有效下降了配置的複雜性。另外因爲 webpack 提供了構建的 runtime,全部被 webpack 處理的模塊都能獲得有效的緩存,大大提升了緩存的覆蓋率,所以 webpack5 的持久化緩存方案將會比其餘第三方插件緩存性能要好不少。

webpack5 緩存的開啓能夠經過如下配置來實現:

module.exports = {    
    cache: {      
        // 將緩存類型設置爲文件系統      
        type: "filesystem",       
        buildDependencies: {        
            /* 將你的 config 添加爲 buildDependency,           
               以便在改變 config 時得到緩存無效*/        
            config: [__filename],        
            /* 若是有其餘的東西被構建依賴,           
               你能夠在這裏添加它們*/        
            /* 注意,webpack.config,           
               加載器和全部從你的配置中引用的模塊都會被自動添加*/      
        },      
        // 指定緩存的版本      
        version: '1.0'     
    }
}
複製代碼

以下圖所示,webpack5 默認將構建的緩存結果放在 node_modules/.cache 目錄下,能夠經過配置更改目錄:

注意事項:

  • cache 的屬性 type 會在開發模式下被默認設置成 memory,並且在生產模式中被禁用,因此若是想要在生產打包時使用緩存須要顯式的設置。

  • 爲了防止緩存過於固定,致使更改構建配置無感知,依然使用舊的緩存,默認狀況下,每次修改構建配置文件都會致使從新開始緩存。固然也能夠本身主動設置 version 來控制緩存的更新。

更多緩存的配置能夠參考官方文檔:

webpack.js.org/configurati…

二、長效緩存

長效緩存指的是能充分利用瀏覽器緩存,儘可能減小因爲模塊變動致使的構建文件 hash 值的改變,從而致使文件緩存失效。

2.一、webpack4 長效緩存方案

webpack4 及以前的版本 moduleIdchunkId 默認是自增的,更改模塊的數量,容易致使緩存的失效。

使用腳手架建立一個簡單的項目,構建結果以下:

import React from 'react';
import ReactDOM from 'react-dom';

ReactDOM.render(  
    <React.StrictMode>    
        <div />  
    </React.StrictMode>,  
document.getElementById('root'));
複製代碼

註釋掉入口文件 test.js 裏引用的 css 文件,如上代碼,構建結果以下:

由上圖可知,僅僅改了其中一個文件,結果構建出來的全部 js 文件的 hash 值都變了,不利於瀏覽器進行長效緩存。v4 以前的解決辦法是使用 HashedModuleIdsPlugin 固定 moduleId,它會使用模塊路徑生成的 hash 做爲 moduleId;使用 NamedChunksPlugin 來固定 chunkId

其中 webpack4 中能夠根據以下配置來解決此問題:

optimization.moduleIds = 'hashed'
optimization.chunkIds = 'named'
複製代碼

2.二、webpack5 長效緩存方案

webpack5 增長了肯定的 moduleIdchunkId 的支持,以下配置:

optimization.moduleIds = 'deterministic'
optimization.chunkIds = 'deterministic'
複製代碼

此配置在生產模式下是默認開啓的,它的做用是以肯定的方式爲 modulechunk 分配 3-5 位數字 id,相比於 v4 版本的選項 hashed,它會致使更小的文件 bundles。

因爲 moduleIdchunkId 肯定了,構建的文件的 hash 值也會肯定,有利於瀏覽器長效緩存。同時此配置有利於減小文件打包大小。

在開發模式下,建議使用:

optimization.moduleIds = 'named'
optimization.chunkIds = 'named'
複製代碼

此選項生產對調試更友好的可讀的 id

三、Node Polyfill 腳本被移除

webpack4 版本中附帶了大多數 Node.js 核心模塊的 polyfill,一旦前端使用了任何核心模塊,這些模塊就會自動應用,可是其實有些是沒必要要的。

webpack5 將不會自動爲 Node.js 模塊添加 polyfill,而是更專一的投入到前端模塊的兼容中。所以須要開發者手動添加合適的 polyfill。

import sha256 from 'crypto-js/sha256';

const hashDigest = sha256('hello world1');
console.log(hashDigest);
複製代碼

上面代碼在v4中打包結果以下:

使用 wepack4 打包,主動添加了crypto 的 polyfill,即 crypto-browserify,打包大小爲 441k。在 wepack5 中打包這樣的代碼,構建會提示開發者進行確認是否須要 node polyfill,以下圖:

若是確認不須要 polyfill,可根據提示設置 fallback,以下:

resolve: {  
    fallback: { 
        "crypto": false 
    }
}
複製代碼

打包結果爲:

打包後 js 文件小了 305k,去除掉項目不須要的 node polyfill,對於減少打包大小收益很可觀。

四、更優的 tree-shaking

// const.js
export const a = 'hello';
export const b = 'world';

// module.js
export * as module from './const';

// index.js
import * as main from './module';

console.log(main.module.a)
複製代碼

有如上的一段代碼,在 v4 構建中打包後的結果以下:

從上圖能夠看出,const.js 導出的 a,b 變量都被打包了,但實際上咱們只用到了 a,期待的是b 應該不被打包進去。

webpack5 對 tree-shaking 進行了優化,分析模塊的 exportimport 的依賴關係,去掉未被使用的模塊,打包結果以下:

!function(){"use strict"; console.log("hello")}();
複製代碼

能夠看出代碼很是簡潔。

五、Module Federation

Module Federation 使得使 JavaScript 應用得以從另外一個 JavaScript 應用中動態地加載代碼 —— 同時共享依賴。至關於 webpack 提供了線上 runtime 的環境,多個應用利用 CDN 共享組件或應用,不須要本地安裝 npm 包再構建了,這就有點雲組件的概念了。

以 github 上的例子爲例,basic-host-remote

上圖是項目的目錄結構,能夠看出存在 2 個應用 app一、app2。其中 app1 使用了 app2 的代碼,那麼 app1 是如何引用 app2 的代碼呢?看下面的代碼:

// app1
import React from "react";
const RemoteButton = React.lazy(() => import("app2/Button"));
const App = () => (  
<div>    
    <h1>Basic Host-Remote</h1>    
    <h2>App 1</h2>    
    <React.Suspense fallback="Loading Button">      
    <RemoteButton />    
    </React.Suspense>  
</div>);
export default App;
複製代碼

其中最重要的就是

const RemoteButton = React.lazy(() => import("app2/Button"));
複製代碼

直接在 app1 的項目中引用了 app2 項目的代碼。是如何作到的?咱們看下構建配置:

先看提供組件 Button 的 app2 的配置:

const HtmlWebpackPlugin = require("html-webpack-plugin");
const { ModuleFederationPlugin } = require("webpack").container;

const path = require("path");
module.exports = { 
    // 有刪減  
    plugins: [    
        new ModuleFederationPlugin({      
            name: "app2",      
            library: { 
                type: "var", 
                name: "app2" 
            },      
            filename: "remoteEntry.js",      
            exposes: {        
                "./Button": "./src/Button",      
            },      
            shared: { 
                react: { 
                    singleton: true 
                }, 
                "react-dom": { 
                    singleton: true 
                } 
            },    
        }),    
        new HtmlWebpackPlugin({      
        template: "./public/index.html",    
        }),  
    ],
};
複製代碼

依賴共享主要是由插件 ModuleFederationPlugin 來提供的,由上面的配置能夠看出 app2 暴露出了 Button 組件,依賴 react、react-dom,生成入口文件爲 remoteEntru.js。下面再來看下 app1的配置:

const HtmlWebpackPlugin = require("html-webpack-plugin");
const { ModuleFederationPlugin } = require("webpack").container;
const path = require("path");
module.exports = {  
    //http://localhost:3002/remoteEntry.js  
    plugins: [    
        new ModuleFederationPlugin({      
            name: "app1",      
            remotes: {        
                app2: "app2@http://localhost:3002/remoteEntry.js",      
            },      
            shared: { 
                react: { 
                    singleton: true 
                }, 
                "react-dom": { 
                    singleton: true 
                } 
            },    
        }),  
    ],
};
複製代碼

結合以前 app2 的配置來看,app1 加載遠程的 app2 模塊,依賴 react、react-dom。

瀏覽器裏運行效果如圖:

Module Federation 還有不少的潛力能夠挖掘,例如能夠將咱們項目中經常使用的依賴包 react 全家桶等打成一個包,作成一個 runtime,開發環境和生產環境依賴一個 runtime,這樣能夠大大減小項目的大小,提升編譯速度。

一些更實用的用法須要咱們在實際使用中繼續探索,發揮 webpack5 更大的價值。

六、其餘新特性

一、在 webpack4 中標記過時的功能都已經在 webpack5 移除了。

二、開發環境下默認使用可讀的名稱爲 module 命名,不須要使用以下語法:

import(/* webpackChunkName: "name" */ "module")
複製代碼

三、原生 worker 支持

......

本文針對 webpack5 的比較重要的特性進行了說明,具體的一些變動能夠去參考官方文檔。

升級踩坑

升級的過程比較枯燥,基本上就是調試、修改、繼續調試的過程,下面列出幾個比較典型的問題。

一、升級 webpack 及相關包的版本

這個過程是比較耗時的,須要將 webpack 的版本及相關 loader 和 plugin 的版本進行升級,現在 webpack5 已正式發佈,相關插件基本上都兼容了 webpack5,因此大部分問題都能經過升級包版本解決。

二、配置 webpack5 編譯緩存不生效

這個問題就比較坑了,腳手架建立一個簡單項目後,根據官網文檔配置 cache,啓動構建:

webpack --config webpack-dist.config.js

cache: {   
    type: 'filesystem'
}
複製代碼

結果構建是成功,可是相應的緩存卻一直沒有生成,其中構建提示以下:

提示說 webpack-dist.config.js 找不到,當時就很懵了,這個文件明明是存在的,並且配置緩存策略時,並無這個文件。查閱大量文檔以後開始翻看源碼,其中部分以下:

// webpack/lib/cache/PackFileCacheStrategy.js
if (newBuildDependencies.size > 0 || !this.buildSnapshot) {    
    if (reportProgress) 
        reportProgress(0.5, "resolve build dependencies");    
    this.logger.debug(`Capturing build dependencies... (${Array.from(newBuildDependencies).join(", ")})`);   
    promise = new Promise((resolve, reject) => {       
        this.logger.time("resolve build dependencies");       
        this.fileSystemInfo.resolveBuildDependencies(this.context,newBuildDependencies,) 
        ...
複製代碼

打印 newBuildDependencies 獲得結果:

發現還真有這個文件,並且相比於其餘絕對路徑,這個相對路徑可能沒法找到。

繼續斷點調試,追溯這裏的 newBuildDependencies 的值,發現webpack-dist.config.js 這個文件是在 webpack-cli 裏寫入的,

const cacheDefaults = (finalConfig, parsedArgs) => {    
    // eslint-disable-next-line no-prototype-builtins    
    const hasCache = finalConfig.hasOwnProperty('cache');    
    let cacheConfig = {};    
    if (hasCache && parsedArgs.config) {       
         if (finalConfig.cache && finalConfig.cache.type === 'filesystem') { 
           cacheConfig.buildDependencies = {       
             config: parsedArgs.config,          
           };      
         }       
         console.log(3333, cacheConfig)        
         return { 
            cache: cacheConfig 
         };  
    }    
    return cacheConfig;
};
複製代碼

從這裏看出當配置持久緩存時,使用命令行自動的給 cache 加上 config 後面的參數。因爲找不到這個相對路徑,從而致使緩存邏輯執行報錯,緩存失敗。

個人解決辦法:

const path = require('path');
const exec = require('child_process').exec;
const config = path.resolve(__dirname, 'webpack-dist.config.js');
const cmdStr = `webpack --config ${config}`;

exec(cmdStr, function(err,stdout,stderr){  
    if(err) {      
        console.log('get weather api error:'+stderr);  
    } else {      
        console.log(stdout);  
    }
});
複製代碼

獲取 webpack-dist.config.js 的絕對路徑,傳給命令行,就能夠解決。可能還有更優雅的解決方法,後面繼續探索。

三、loader 配置參數修改

出現以下報錯時,表示 webpack5 不兼容之前的 webpack 的寫法了,須要按最新版的規則來修改:

{  
    test: /\.css$/,  
    loaders: ['css-loader'],        
    // 提取出css
}
loaders改成use 
{  
    test: /\.css$/,  
    use: ['css-loader'],        
    // 提取出css
}
複製代碼

四、去掉 node polyfill

因爲 webpack5 會自動去掉 polyfill,所以會出現以下提示

解決辦法是按照提示修改,確認是否須要添加 polyfill

resolve: {  
    fallback: { 
        "domain": false 
    }
}
複製代碼

總結

webpack5 正式發佈已經有一段時間了,總的來講:

  1. 構建性能大幅度提高,依賴核心代碼層面的持久緩存,覆蓋率更高,配置更簡單。

  2. 打包後的代碼體積減小。

  3. 默認支持瀏覽器長期緩存,下降配置門檻。

  4. 使人激動的新特性 Module Federation,蘊含極大的可能性。

相關文章
相關標籤/搜索