Webpack之模組化優化
開發中,模組化可以防止變數和方法被汙染,只需要關注一部分的邏輯實現,有效地減少了與全域性的耦合,也便於後期的維護和拓展
當然,相信瞭解過前端模組化發展歷史的童鞋,都應該聽過 IIFE
、 AMD
、 CommonJS
等等,它們都是能夠實現模組化的規範,直到 ES2015
出來後,才正式把模組化納入其標準中。在談到今天主題前,我們先簡單講解一下上面幾種模組化方式的實現以及區別,對於後面將要講到的 webpack模組化優化
有一定幫助。
IIFE
在各種模組化規範出來之前, ES5
是不支援模組化開發,但當時也有一些大牛們為了更好地避免函式的 副作用
和封裝,就開始巧妙地想到了使用 IIFE
來實現模組化(注:JS本身是不存在塊級作用域):

可以看到,在原來的 ES5
基礎上可以封裝部分邏輯模組,也就是一個簡單的 閉包行為
,避免內部變數收到外部環境的影響。
但是,缺陷也是很明顯滴暴露出來,它無法實現模組間的依賴,同時代碼是分配到主流程中,對於後期的維護和修改帶來了困難。
CommonJS
CommonJS規範
實現的是同步載入方式,常用於服務端,其終極目標是提供一個類似Python,Ruby和Java模組化標準庫。
為此, NodeJS
的出現,也就正式標誌著 Javascript模組化程式設計
誕生。在服務端,模組需要與作業系統或者應用程式進行互動,而 NodeJS的模組系統
就是參照 CommonJS規範
進行編寫的。
該規範指出,需通過 exports
或者 module.exports
(注:這兩個匯出方法的使用區別,有興趣的童鞋可以看看 ofollow,noindex">阮一峰對於該API的講解 )來匯出對外的變數或介面,通過 require方法
匯入其他模組的輸出到當前模組作用域中。直接上栗子:

這時候,也許有童鞋會提出疑問,那麼 CommonJS
能用於客戶端嗎?
答案是肯定的。由於客戶端由於缺少四個Node.js環境的變數: module、exports、require、global
,導致客戶端無法使用 CommonJS規範
。為此,需第三方工具或庫(例如 require1k 、 tiny-browser-require 等等)才能讓客戶端實現 CommonJS規範
。
AMD
鑑於 ES5
內部通過 IIFE
實現的模組化無法真正意思上(類似Java、Python等)的模組化,因此有些大牛們就提出了適用於客戶端的 AMD規範
,它可以非同步引入模組,同時模組可以很好地將某些邏輯功能封裝在一個檔案中以便主流程需要時引入。其中 RequireJS
庫很好滴實現了 AMD規範
,直接上例子:

RequireJS API暴露了 require
和 define
兩個全域性方法,對於主流程或者模組中需要依賴其他模組時,都可以傳進 require
和 define
兩個全域性方法第一個引數中。同時,我們可以看到, AMD規範
實現的是非同步載入模組方式(多個模組引入時會並行載入,有效滴加快執行效率並且無阻塞頁面的載入)。對於每個模組只需要做該模組該做的事情即可,模組與主流程之間有效地減低了耦合度。
CMD
CMD規範
也叫通用模組定義(Common Module Definition),實現的也是非同步載入模組方式, SeaJS庫
就很好滴實現該規範,它主要具有以下特點:
- 使用上:和
AMD規範
類似,都是使用require
和define
兩個全域性方法,但使用require
方法時卻是同步執行模組程式碼的,這和CommonJS規範
很類似; - 實現上:
CMD規範
核心是提前載入,延遲執行,而AMD規範
核心是提前載入,提前執行,當然CommonJS規範
核心則是延遲載入,延遲執行;
需要注意的是, CMD規範
使用 require
方法的原因是因為在提前載入模組過程中,會把載入下來的模組儲存在記憶體中,以至於客戶端執行主流程按需引入模組時是同步執行記憶體中儲存的模組。(有興趣的童鞋,可以看看這邊文章——SeaJS是如何工作的)
在我看來, CMD規範
的懶執行機制可有效地提高頁面互動效能,因為在頁面互動過程完成前不需要執行其他暫時沒用到的模組(當然這只是我個人觀點,如果有童鞋有不同的看法,可以在評論區上寫上來一起探討學習一下,對於效能對比上,童鞋們也可以看看 SeaJS的github上關於AMD和CMD對比,挺有趣的 )。而這種機制,也對於下面我要提到 webpack模組化優化
密切相關。
現在就直接上一個 SeaJS
栗子領略一下 CMD規範
(有興趣的童鞋,也可以看下SeaJS的API):

UMD
UMD規範
可以看成是一種方案,用於解決前後端跨平臺模組載入,支援 AMD規範
和 CommonJS規範
。說白了,就是致力於用一種實現方式能夠把模組載入相容前後端。
話不多說,有興趣的童鞋可以看看 UMD的官方介紹和栗子 。下面也用一個簡單的栗子來領略 UMD規範
的寫法:

可以看到, UMD規範
實現方式就是先判斷是否支援 AMD規範
,然後再判斷是否支援 CommonJS規範
,當兩者均不支援時則直接把模組定義在全域性物件上
ES6 Module
由於 ES5
缺少模組化載入理念,因此在 ES6
中正式把模組化載入納入其標準
ES6
中模組是在編譯時輸出介面,而上面提到的各種模組載入規範都是在執行時輸出介面,可以看到, ES6 Module
在一定程度上對效能進行了優化。有興趣的童鞋可以看看 阮一峰的對ES6 Module的介紹 。下面就舉個栗子:

目前,客戶端基本都是使用 ES6 Module
模組載入方式來對模組進行載入,而對於 Node
服務端尚在逐漸向該模組載入方式靠攏,但是大部分情況下依然還是使用 CommonJS規範
。
相信大家看完上面模組載入的各種實現方式,都應該對模組化有一定的瞭解。好啦,下面就進入今天的主題,在 Webpack
中可以怎樣去對 Application
中模組化進行優化呢?
遇到的問題
在日常模組化開發中,一個頁面會有很多個元件所構成,而這些元件是需要我們按需引入的,毫無疑問,現在我們以 Vue
作為栗子,專案結構( 下面只展示主要的目錄和檔案,至於其他目錄和檔案就不展示了 )如下:
|- src |--- components |------ message.vue// 詳細資訊框元件 |------ main.vue// 首頁需要的元件 |------ goods.vue// 商品頁面元件 |--- router |------ index.js// 路由檔案 |--- App.vue// 入口vue檔案 複製程式碼
正常情況下,我們是這樣編寫頁面的:
App.vue

index.js 路由檔案

main.vue 首頁元件

goods.vue 商品頁面元件

message.vue 商品資訊元件

上面的栗子,執行時在網址上輸入 localhost:8080/#/
會直接使用 Main.vue
首頁元件,當輸入 localhost:8080/#/goods
時會展示 goods.vue
商品元件,點選按鈕會直接展示 message.vue
詳細資訊元件。
到這裡,我會想問,上面的簡單 SPA
栗子是否還有更加優化的方案?倘若我想加快首頁載入的速度以使使用者有個更好的體驗,該如何處理?
對於上面的問題,我們會經常遇到,各位童鞋也可以各抒己見在評論區說說自己的看法。在這裡,就不賣關子,換作是我,首先想到要下手的就是 Webpack
,而這也是今天所要提及的核心內容: 用Webpack如何更好地優化複雜程式的模組化
。
Dynamic Import
目前嘗處於 stage 3
階段的 ECMAScript提案的import()語法 ,相信很多童鞋都有了解過或聽過,那它究竟是幹嘛的?按照官方的說法就是:
ECMAScript modules are completely static, import() enables dynamic loading of ECMAScript modules.
簡單滴說,就是目前模組載入都是靜態載入的,而import()可讓我們按需載入對應模組。那麼問題來了,上面栗子也是按需載入,首頁只需要 Main
首頁元件,而商品頁面也只加載了 goods
和 message
元件。那麼再細心看看,上面的栗子是不是真的做到了 按需載入
?
答案是否定的,正如我上面提及的 ES6 Module
是在編譯階段就輸出介面,因此當我們使用 Webpack
打包後,所有需要的模組都會打進一個 js檔案
中。因此,首頁在載入過程中,就需要載入完整個 js檔案
才能讓使用者進行體驗效果,當然我們都知道,這個 js檔案
也有一些模組邏輯是我們暫時並不需要用到,而這也恰好是我們接下來要處理的問題。
import()
語法的出現,再結合 webpack 4
(也可以選擇 webpack 3
),就可以很優雅地處理上述問題。當然也少不了 babel
的轉化。下面就是處理實現:
webpack.config.js 部分配置

再將路由檔案進行修改後如下:

可以看到,當我們再次訪問 localhost:8080/#/
時,會發現載入 js檔案
比之前小了,而且最重要的該檔案裡是沒有 Goods
商品元件程式碼的。當再訪問 localhost:8080/#/goods
時,會非同步從服務端載入 0.js
檔案,而改檔案是包含 Goods
商品元件程式碼的。由此可見,不僅減少了主流程 js
檔案的大小,加快頁面的載入體驗,而且還可以按需非同步下載必要的模組檔案。
好了,到了這裡,如果我想再優化一下有 Goods
商品元件中 Message
詳細資訊元件,因為它是隻有在點選按鈕才去載入的,那我們是不是也可以讓其進行懶載入,讓 localhost:8080/#/goods
下的頁面載入更加快?
答案是肯定的,但是需要用到 Vue
中 component
語法,事不宜遲,我們就來動手改改看:
goods.vue 商品頁面元件

當我們再去訪問 localhost:8080/#/goods
時,會發現 js檔案
也比之前少了 Message
詳細資訊元件程式碼,加快載入體驗,同時點選按鈕後,會動態引入 1.js
檔案,這也是從服務端非同步引入載入 Message
詳細資訊元件。
這時候,也許會有童鞋提測疑問,當關閉 Message
詳細資訊元件彈窗時,再重新點選按鈕,就需要重新 render
,那這樣豈不是需要消耗一定的效能?答案是肯定會有所損耗,那麼如果想優化它,可以怎麼做?很簡單,只需要使用 Vue API
暴露的 <keep-alive>元件
來包裹我們的 <component>
即可,這樣就會有效滴把元件儲存在記憶體中,下次訪問時可直接從記憶體中讀取。
至此,也許有童鞋又會提出疑問,上面 0.js
檔案和 1.js
檔案到底是什麼鬼?再耐心點往下看,說不定有你想要的驚喜哈哈:speak_no_evil:
Magic Comments
上面之所以會出現 0
和 1
,原因是因為對於動態引入 import()
不指定模組名稱 chunkName
時, Webpack
會預設從0到n來按序命名接下來動態引入的檔案。
但是,對於這些數字0或者n,我們是無法知道該動態引入的 js檔案
是屬於哪一個模組,為此 Webpack
也為我們提供了 Magic Comments
,以此可以自定義模組名稱。
下面就拿上面路由檔案動態引入作為栗子,使用上很簡單:

這時候,從 localhost:8080/#/
中訪問 localhost:8080/#/goods
時,會發現載入不是 0.js
,而是 goods.js
,這樣就會很容易知道哪些模組是動態引入的。當然有興趣的童鞋,也可以想想指定 Message詳細資訊元件
動態引入時的 chunkName
。
至此,上面講到的方案,可以有效滴優化我們現有階段的模組化開發,當然童鞋們也可以嘗試一下。
Extension
其實,在我看來,還可以更深入點優化一下。就拿上面的 Goods
商品元件,當點選按鈕時,如果載入的模組比較大,使用者就不得不去等待載入完整個 message.js
檔案才能展示彈窗出來(當然這樣的情況出現不是特別多,由於打包後的js檔案一般都會存放到cdn上,而請求時會採取就近原則載入,這也有效避免這個問題,但我們還是可以探討一下這個問題可以使用什麼方式避免)。為此我們有木有方案可以將這個時延有效滴去掉?在這裡我就不賣關子,在 Webpack 4.6
中,指出明確支援 prefetching and preloading
,而這兩個東西恰好就是可以解決我們剛剛要面對問題。
Preload And PreFetch
Preload
是預載入, PreFetch
是預測將要載入的模組,這兩者都是 link
標籤下的屬性。有興趣的童鞋,可以看看 關於這兩者在客戶端執行的優先順序
簡單來說, Preload
優先順序為 Height
,而 PreFetch
則為 Low
。這兩者是有區別的:
-
preload
:主要是用於當前頁面的預載入,會和主檔案bundle.js
並行下載,且優先獲取,可用於預載入某些必要模組 -
prefetch
: 主要是用於下一步操作或者頁面,會在瀏覽器空閒時間才去下載,優先順序最低
另外,需要說明的是, preload
和 prefetch
目前對於現有的瀏覽器的支援程度並不是那麼的友好 。具體可以看看兩個相容性:Preload的相容性和 Prefetch的相容性
雖然相容性不是那麼友好, preload
和 prefetch
都是申明性質的,所以就算不支援,也不會影響現有頁面的任何功能
看到這裡,估計大家應該也可以想到,使用 prefetch
可以解決我們上面遇到的時延等待問題,那麼具體怎麼寫?在這裡,我們還需要藉助 Webpack
的 preload-webpack-plugin
外掛,配置編寫如下:
webpack.config.js 部分配置

這時候,就要在 Webpack 4.6+
版本上,修改商品頁元件如下:
goods.vue 商品頁面元件

可以看到,只需要加上 webpackPrefetch: true
的註釋再結合 preload-webpack-plugin
外掛即可把動態引入的模組進行 Prefetch
。
當我們再次訪問 localhost:8080/#/goods
時,會發現,載入完主流程的 js檔案
後,然後在瀏覽器的空閒時間,就會自動載入 message.js檔案
,並且可以看到其優先順序為 Low
的。另外,我們也可以看到在 <head>
標籤,是通過 <link>
標籤使用 rel="prefetch"
來載入 message.js檔案
。這時候再點選按鈕,就會不再需要等待時延直接載入 Message元件
。
至於上述執行的效果圖,我就不截圖了,有興趣的童鞋可以親自去嘗試驗證一下,這樣才能有更加深刻的印象。如有不便,在這裡我就說聲不好意思哈:see_no_evil:
對於 Webpack模組化載入優化
,也許還會有一些更好更優的方案,也歡迎:clap:大神們在評論區分享分享來一起學習。而上述的優化方案,其實在日常的模組化開發中是可以使用到的,不過還請大家要結合自己需求的應用場景分析再來優化,一旦使用不當,優化也許就是一個消耗效能的體驗。