天天看點

淺談Webpack與應用優化實踐

作者:閃念基因

起源

淺談Webpack與應用優化實踐

上圖是來自Webpack官網的宣傳圖,可以看出,左側是一大堆不同格式的資源檔案(子產品依賴),經過中間那個六棱形轉換後,變成了具備統一格式的靜态資源檔案。

這個六棱形即——Webpack,而這張圖也簡單明了的闡述了Webpack的主要作用——靜态子產品打包

什麼是Webpack?

webpack是一個用于建構現代JavaScript應用程式的靜态子產品打包工具,它能夠以一種一緻且開放的處理方式,加載應用中的所有資源檔案,如圖檔、CSS、視訊、字型檔案等,并将其合并打包成浏覽器相容的Web資源檔案。

在我看來,一緻且開放非常好的總結的Webpack的主要特性,那麼Webpack具備怎樣的特性?

  • 統一資源建構(一切皆子產品)

其他一些子產品打包器如Gulp、Grunt、RequireJs、Browserify等都缺乏相容處理所有資源,隻能應對不同資源,做不同的特化處理邏輯,且不同類型檔案之間無法資訊互通,而Webpack能很好做到這一點。忽略具體資源類型之間的差異,将所有代碼/非代碼檔案都統一看做Module-子產品對象,以相同的加載、解析、依賴管理、優化、合并流程實作打包,并借助Loader、Plugin兩種開發接口将資源差異處理邏輯轉交由社群實作,實作統一資源構模組化型。這樣做的好處有:

  1. 所有資源都是Module,是以可以用同一套代碼實作諸多特性,包括:代碼壓縮、Hot Module Replacement熱更新、緩存等。
  2. 打包時,資源之間資訊互換友善,例如HTML導入Base64格式的圖檔,放在以前是需要做處理邏輯的,不然也沒辦識别。
  3. 借助Loader、Plugin,Webpack幾乎可以用任意方式處理任意類型的資源,例如可以用Less、Stylus、Sass等預編譯CSS代碼。

可以用下面不同國家插座轉換圖來了解:

淺談Webpack與應用優化實踐
其他子產品打包器就像圖中的轉換器,不同型号需要不同轉換器,即不同資源需要不同處理邏輯,而Webpack能直接統一一個子產品格式标準,即全世界通用一個插座型号,做一層處理将不同型号轉成通用型号或者不需要額外處理就能直接通電。
  • 開放性

Webpack具備極強開放性,展現在能夠輕易接入一系列工程化工具,例如TypeScript、CoffeScript、Babel一類的JavaScript編譯工具;或者Less、Sass、Stylus、PostCss等CSS預處理器;或者Jest、Karma等測試架構等等。基于此,Webpack成為了現代前端工程化的基石。

為什麼需要Webpack?

Web是一個極其複雜的建構系統,它能夠融合多種工程化工具,将開發階段的應用代碼編譯、打包成适合網絡分發、用戶端運作的應用産物,同時其開放性的特點,讓整個社群環境活躍且龐大,光自己内置都有上百種配置項,更何況社群實作的數千種Loader、Plugin元件。

是以為什麼我們需要這種非常複雜的建構工具? 答案是:“大人,時代變了”

在最早的計算機時代,我們隻能用原生 JavaScript(ES5)、CSS、HTML 方式編寫頁面代碼,開發環境和生産環境通用一套代碼,開發與運作效率低下;

其次,過往可沒有現在開箱即用的腳手架,頁面的圖檔、代碼、CSS 等資源都能且隻能通過 img、 script、link 等标簽插入到頁面中,在精細處理上耗費我們太多精力。

直到 2009年 Node 與 RequireJS 打破僵局,讓我們在代碼被放到浏覽器運作起來之前,有機會做一些預處理工作 —— 開發與生産環境隔離管理實作方案。

再往後,計算機世界陸續出現許多解決具體問題的工程化工具,如Babel(轉譯JS文法)、TypeScript(編譯時類型檢測)、CoffeeScript(ES6出來就不怎麼用了,社群也沒什麼人維護) 等,以及一些如 Less、Sass、Stylus 的預處理工具,為頁面樣式開發提供豐富的語言特性,如嵌套、繼承等。一定程度上很好的彌補浏覽器、語言、規範本身的設計缺陷,解放人工,讓我們可以更高效專注于業務代碼中。

持續到目前為止,網際網路依舊處于高速發展的階段,各種工具如春筍不斷冒出,前端不再局限于單調頁面的開發,大前端時代來臨,邊界被擴寬,頁面我能幹,背景我也行,微服務,小程式,app我也可以,Unity 3D遊戲肝一下我。但随之而來的問題是我們該如何管理這些工具?這時候我們就需要一套足夠開放,能融合諸多工程化工具,徹底抹平開發與生産環境差異的一體化工程方案,而Webpack可以很好的處理這一問題。

核心概念

Entry(入口)

訓示webpack以哪個檔案為入口起點開始打包,分析建構内部依賴圖

Output(輸出)

訓示webpack打包後的資源bundles輸出到哪裡去,以及如何命名

Loader(檔案加載器)

webpack隻能了解JavaScript和JSON檔案,這是webpack開箱即用的自帶能力;loader讓webpack能夠處理其他類型的檔案,并将它們轉換為有效子產品,以供應用程式使用,以及被添加到依賴圖中; 計算機世界檔案資源格式太多,Webpack不可能一一窮舉,部分處理工作開放出去由第三方處理勢在必行。目前Webpack内部隻需實作标準JavaScript代碼解析/處理,其他檔案資源解析邏輯将由第三方補充。

通常以mapping函數形式,接收原始代碼内容,傳回翻譯結果;

大多數 Loader 要做的事情就是将 source 轉譯為另一種形式的 output。

module.exports = function(source) {
    // 代碼計算
    return modifySource
}
複制代碼           
  • 流程

在Webpack進入建構階段後,首先會通過IO接口讀取檔案内容,之後調用LoaderRunner并将檔案内容以source參數形式傳遞到Loader數組,source資料在Loader數組内可能會經過若幹次形态轉換,最終以标準JavaScript代碼送出給Webpack主流程,以此實作内容翻譯功能。

  • 常見Loader

file-loader:把檔案輸出到一個檔案夾中,在代碼中通過相對 URL 去引用輸出的檔案

url-loader:和 file-loader 類似,但是能在檔案很小的情況下以 base64 的方式把檔案内容注入到代碼中去

source-map-loader:加載額外的 Source Map 檔案,以友善斷點調試

image-loader:加載并且壓縮圖檔檔案

babel-loader:把 ES6 轉換成 ES5

css-loader:加載 CSS,支援子產品化、壓縮、檔案導入等特性

style-loader:把 CSS 代碼注入到 JavaScript 中,通過 DOM 操作去加載 CSS。

eslint-loader:通過 ESLint 檢查 JavaScript 代碼

......

Plugins (插件)

Webpack對外提供了Loader與Plugin兩種擴充方式,其中Loader職責比較單一,而Plugin則功能強大,借助Webpack數量龐大的Hook,幾乎能改寫Webpack所有特性,執行範圍更廣的任務,從打包優化和壓縮,一直到重新定義環境中的變量等

  • 總體流程

在Webpack運作過程中,随着建構流程的推進會觸發各個鈎子回調,并傳入上下文參數(例如tap等方法回調函數中的compilation對象),插件可以通過調用上下文接口、修改上下文狀态等方式“篡改”建構代碼,進而将擴充代碼插入到Webpack建構流程中。

  • 觸發時機: 各建構流程暴露出來的Hook就是很好的觸發時機

Webpack内部幾個核心對象:

complier: 全局建構管理器,負責管理配置資訊、Loader、Plugin等 compilation: 單次建構過程的管理器,負責周遊子產品,執行編譯操作,當watch=true,重新建立compilation. 此外還有Module、Resolver、Parser、Generator等關鍵類型,也都相應暴露了許多Hook。 複制代碼

  • 從形态上,插件通常是一個apply函數的類,Webpack 在啟動時會調用插件對象的 apply 函數,并以參數方式傳遞核心對象 compiler ,以此為起點,插件内可以注冊 compiler 對象及其子對象的鈎子(Hook)回調,例如:
class SomePlugin {
  apply(compiler) {
    compiler.hooks.thisCompilation.tap("SomePlugin", (compilation) => {
      compilation.addModule(/* ... */);
    });
  }
}
複制代碼           

示例中的 compiler 為 Hook 挂載的對象;thisCompilation 為 Hook 名稱;後面調用的 tap 為調用方式,支援 tap/tapAsync/tapPromise 等;

Hook和調用方式繁多,可以用到再去查找,熟悉常見的plugin及其應用,深入了解建議還是找常見的幾個看看源碼,因為社群太多了

  • 常見Plugin

HotModuleReplacementPlugin: 子產品熱更新插件,Webpack自帶

html-webpack-plugin: 生成 html 檔案

mini-css-extract-plugin: 将 CSS 提取為獨立的檔案的插件,對每個包含 css 的 js 檔案都會建立一個 CSS 檔案,支援按需加載 css 和 sourceMap

define-plugin:定義環境變量

commons-chunk-plugin:提取公共代碼

uglifyjs-webpack-plugin:通過UglifyES壓縮ES6代碼

Mode

模式(Mode)訓示 webpack 使用相應模式的配置,即上文提到的開發環境和生産環境隔離,不同mode對應不同配置,能夠進行有效環境區分,提高性能。

淺談Webpack與應用優化實踐

建構流程

淺談Webpack與應用優化實踐

可以總結為4個步驟:

  1. 輸入:從檔案系統配置檔案和 Shell 語句中讀取與合并最終參數,根據參數初始化 Compiler 對象,加載所有配置的插件,執行對象的 run 方法開始執行編譯。
  2. 子產品遞歸處理:根據entry找到所有入口檔案,調用Loader轉譯Module内容,并将結果轉換為AST(抽象文法樹),從中分析出子產品依賴關系,進一步遞歸子產品處理過程,直到所有依賴檔案都處理完畢。
  3. 後處理:所有子產品遞歸處理完畢後開始執行後處理,包括子產品合并、注入運作時、産物優化等,最終輸出Chunk集合。
  4. 輸出:根據入口和子產品之間的依賴關系,組裝成一個個包含多個子產品的 Chunk,再把每個 Chunk 轉換成一個單獨的檔案加入到輸出清單,根據配置确定輸出的路徑和檔案名,把檔案内容寫入到檔案系統

在以上過程中,Webpack 會在特定的時間點廣播出特定的事件鈎子,Plugin在監聽到感興趣的事件後會執行特定的邏輯,并且Plugin可以調用 Webpack 提供的 API 改變 Webpack 的運作結果。

應用

處理CSS資源

原生Webpack并不能識别CSS文法,假如不做額外配置直接導入.css檔案,會導緻編譯失敗。是以需要借助一些Loader來實作轉譯打包。

常見Loader和Plugin:

  • css-loader: 該Loader會将CSS等價翻譯形如:module.exports = "${css}" 的JavaScript代碼,使得Webpack能夠如同處理JS代碼一樣解析CSS内容與資源依賴。包含:CSS到JS轉譯(主要)、依賴解析、Sourcemap、css-in-module等,基于這些能力,Webpack才能像處理JS子產品一樣處理CSS子產品代碼。
  • style-loader: 該Loader将在産物中注入一系列runtime代碼,這些代碼會将CSS内容注入到頁面的标簽,使得樣式生效。
  • mini-css-extract-plugin: 該插件會将CSS代碼抽離到單獨的.css檔案,并将檔案通過标簽方式插入到頁面中

三個元件各司其職:css-loader讓Webpack能夠正确了解CSS代碼、分析資源依賴;style-loader、mini-css-extract-plugin則通過适當方式将CSS插入到頁面,對頁面樣式産生影響。

  • 經css-loader處理後,CSS代碼會轉譯為等價JS字元串,但這些字元串還不會對頁面樣式産生影響,還需要:
  1. 開發環境: 使用style-loader将樣式代碼注入到頁面的
  2. 生産環境:使用mini-css-extract-plugin将樣式代碼抽離到單獨産物檔案,并以标簽方式引入到頁面中
淺談Webpack與應用優化實踐

淺談Webpack與應用優化實踐
npm i -D style-loader css-loader
module.exports = {
    module: {
        rules: [
            {
                test: /\.css$/i,
                use: ['style-loader', 'css-loader']            
            }        
        ]    
    }
}
複制代碼           

注意: 注意保持 style-loader 在前,css-loader 在後,Webpack執行Loader是基于compose(函數式程式設計)方式實作的,是以是從後往前,即css-loader -> style-loader

優化點

經過style-loader + css-loader處理後,樣式代碼最終會被寫入Bundle檔案,并在運作時通過style标簽注入到頁面。這種将JS、CSS代碼合并進同一個産物檔案的方式有幾個問題:

  • JS、CSS資源無法并行加載,進而降低頁面性能
  • 資源緩存顆粒變大,JS、CSS任意一種變更都會緻使緩存失效

是以,生産環境通常會用mini-css-extract-plugin插件替代style-loader,将樣式代碼抽離成單獨CSS檔案

npm i -D mini-css-extract-plugin

const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const HTMLWebpackPlugin = require('html-webpack-plugin')

module.exports = {
    module: {
            rules: [{
                test: '/\.css$/',
                use: [
                    // 根據運作環境判斷使用哪個loader
                    (process.env.NODE_ENV === 'development' ? 'style-loader' : MiniCssExtractPlugin.loader),
                    'css-loader'                
                ]            
            }]
    },
    plugins: [
        new MiniCssExtractPlugin(),
        new HTMLWebpackPlugin()    
    ]
}
複制代碼           

注意 :

  • mini-css-extract-plugin 庫同時提供 Loader、Plugin 元件,需要同時使用
  • mini-css-extract-plugin 不能與 style-loader 混用,否則報錯,是以上述示例中第 9 行需要判斷 process.env.NODE_ENV 環境變量決定使用那個 Loader
  • mini-css-extract-plugin 需要與 html-webpack-plugin 同時使用,才能将産物路徑以 link 标簽方式插入到 html 中

style-loader與mini-css-extract-plugin差別

  • style-loader主要用于開發環境,會在javascript主程式中注入runtime代碼,将CSS抽取注入HTML的,支援hmr,但由于混合在js中,代碼顆粒度大,無論js或css變動都會導緻緩存失效,并且内嵌css代碼無法異步并行加載,導緻頁面卡頓。
  • mini-css-extract-plugin主要用于生産環境,将CSS打包成單個獨立檔案,不支援hmr,然後以<link>的形式引入頁面,是并行加載資源,這個插件必須與html-webpack-plugin同時使用才能生效。

預處理器Less、Sass、Stylus等同理,就不多說了。

處理圖像

原生 Webpack 4 隻能處理标準 JavaScript 子產品,是以需要借助 Loader —— 例如 file-loader、url-loader、raw-loader 等完成圖像加載操作,實踐中我們通常需要按資源類型選擇适當加載器

  • file-loader: 将圖檔引用轉換成url語句,經過轉換後生成圖檔檔案,并在代碼中插入圖檔的url位址
  • url-loader: 兩種表現,小于門檻值轉為base64編碼,大于門檻值調用file-loader進行加載
  • raw-loader: 不做任何轉譯,簡單的将檔案内容弄複制到産品中,适用SVG場景。

上述 file-loader、url-loader、raw-loader 都并不局限于處理圖檔,它們還可以被用于加載任意類型的多媒體或文本檔案,使用頻率極高,幾乎已經成為标配元件!是以 Webpack5 直接内置了這些能力,開箱即可使用。

環境治理政策

在現代前端工程化實踐中,通常需要将同一個應用項目部署在不同環境(如生産環境、開發環境、測試環境)中,以滿足項目參與各方的不同需求。這就要求我們能根據部署環境需求,對同一份代碼執行各有側重的打包政策,例如:

  • 開發環境需要使用 webpack-dev-server 實作 Hot Module Replacement;
  • 測試環境需要帶上完整的 Soucemap 内容,以幫助更好地定位問題;
  • 生産環境需要盡可能打包出更快、更小、更好的應用代碼,確定使用者體驗。

拿常用腳手架vue-cli配置流程舉例:

  1. 首先在項目根目錄下(與package.json同級)建立三個".env"檔案
淺談Webpack與應用優化實踐

如上,三個".env"檔案字尾名為development、production、test,分别對應為開發環境、生産環境和測試環境

淺談Webpack與應用優化實踐
淺談Webpack與應用優化實踐
  1. 配置package.json檔案

在 vue-cli-service 指令後加上對應".env"檔案名字。配置完成後,當我們運作npm run xxx指令時會執行對應的".env"檔案。進而實作環境變量配置功能。

淺談Webpack與應用優化實踐
  1. 使用配置的環境變量 我們可以通過對不同環境變量适配不同配置
淺談Webpack與應用優化實踐

注意:隻有.env.xxx檔案接受鍵值對形式參數,但隻要有NODE_ENV,BASE_URL和以VUE_APP_開頭的變量将會通過webpack.DefinePlugin靜态嵌入用戶端,其他一些命名是無效的。

還有許多應用,例如熱更新等,用到就查即可

優化

使用最新穩定版本的Webpack與Node

看起來算是句廢話,但是确實真的非常使用,也是成本效益最高的優化手段之一,每個版本的疊代會帶來許多新特性和對性能的優化提升,特别是Webpack團隊還特别重視優化方面。可能現在苦惱的優化點,需要繞幾個圈實作,下版本就開箱即用也不一定,是以緊跟穩定版本就對了。

圖像優化

  • 圖像壓縮:減少網絡傳輸流量, 推薦image-webpack-loader
  • 雪碧圖:減少 HTTP 請求次數,将許多細小圖檔合并成一張大圖,進而将多次請求合并成一次請求,結合CSS background-position控制顯示視圖位置達到效果,僅使用于那種同屏需要請求許多張圖檔的場景。
  • 響應式圖檔:根據用戶端裝置情況下發适當分辨率的圖檔,有助于減少網絡流量;推薦webpack-spritesmith
  • CDN:緩存資源,中轉站,有效提升傳輸效率。
  • 等等。

注意:雪碧圖曾經是一種使用廣泛的性能優化技術,但 HTTP2 實作 TCP 多路複用之後,雪碧圖的優化效果已經微乎其微 —— 甚至是反優化。

SplitChunk分包優化

Webpack 預設會将盡可能多的子產品代碼打包在一起,優點是能減少最終頁面的 HTTP 請求數,但缺點也很明顯:

  • 頁面初始代碼包過大,導緻資源備援,影響首屏渲染性能
  • 無法有效應用浏覽器緩存,特别對于 NPM 包這類變動較少的代碼,業務代碼哪怕改了一行都會導緻 NPM 包緩存失效.

為此,從webpack v4 開始就提供了開箱即用的SplitChunksPlugin ,專門用于根據産物包的體積、引用次數等做分包優化,規避上述問題,特别适合生産環境使用。

參考

掘金課程《Webpack5 核心原理與應用實踐》

作者:JLong

連結:https://juejin.cn/post/7187378439182090299

來源:稀土掘金