一、webpack 異步加載原理
webpack ensure 有人稱它為異步加載,也有人稱為代碼切割,他其實就是將 js 模塊給獨立導出一個.js 文件,然后使用這個模塊的時候,再創建一個 script 對象,加入到 document.head 對象中,瀏覽器會自動幫我們發起請求,去請求這個 js 文件,然后寫個回調函數,讓請求到的 js 文件做一些業務操作。
1、問題背景
需求:main.js 依賴兩個 js 文件:A.js 是點擊 aBtn 按鈕后,才執行的邏輯,B.js 是點擊 bBtn 按鈕后,才執行的邏輯。
webpack.config.js,我們先來寫一下 webpack 打包的配置的代碼
const path = require('path') // 路徑處理模塊
const HtmlWebpackPlugin = require('html-webpack-plugin') const { CleanWebpackPlugin } = require('clean-webpack-plugin') // 引入CleanWebpackPlugin插件
module.exports = { entry: { index: path.join(__dirname, '/src/main.js'), }, output: { path: path.join(__dirname, '/dist'), filename: 'index.js', }, plugins: [ new HtmlWebpackPlugin({ template: path.join(__dirname, '/index.html'), }), new CleanWebpackPlugin(), // 所要清理的文件夾名稱
], }
// index.html 代碼如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>webpack</title>
</head>
<body>
<div id="app">
<button id="aBtn">按鈕A</button>
<button id="bBtn">按鈕B</button>
</div>
</body>
</html>
// 入口文件 main.js 如下
import A from './A' import B from './B' document.getElementById('aBtn').onclick = function () { alert(A) } document.getElementById('bBtn').onclick = function () { alert(B) } // A.js 和 B.js 的代碼分別如下 // A.js
const A = 'hello A' module.exports = A // B.js
const B = 'hello B' module.exports = B
此時,我們對項目進行 npm run build, 打包出來的只有兩個文件:index.html、index.js。
由此可見,此時 webpack 把 main.js 依賴的兩個文件都同時打包到同一個 js 文件,並在 index.html 中引入。
但是 A.js 和 B.js 都是點擊相應按鈕才會執行的邏輯,如果用戶並沒有點擊相應按鈕,而且這兩個文件又是比較大的話,這樣是不是就導致首頁默認加載的 js 文件太大,從而導致首頁渲染較慢呢?那么有能否實現當用戶點擊按鈕的時候再加載相應的依賴文件呢?
webpack.ensure 就解決了這個問題。
2、require.ensure 異步加載
下面我們將 main.js 改成異步加載的方式
document.getElementById('aBtn').onclick = function () { //異步加載A
require.ensure([], function () { let A = require('./A.js') alert(A) }) } document.getElementById('bBtn').onclick = function () { //異步加載b
require.ensure([], function () { let B = require('./B.js') alert(B) }) }
此時,我們再進行一下打包,發現多了 1.index.js 和 2.index.js 兩個文件。而我們打開頁面時只引入了 index.js 一個文件,當點擊按鈕 A 的時候才引入 1.index.js 文件,點擊按鈕 B 的時候才引入 2.index.js 文件。這樣就滿足了我們按需加載的需求。
require.ensure 這個函數是一個代碼分離的分割線,表示回調里面的 require 是我們想要進行分割出去的,即 require('./A.js'),把 A.js 分割出去,形成一個 webpack 打包的單獨 js 文件。它的語法如下:
require.ensure(dependencies: String[], callback: function(require), chunkName: String)
我們打開 1.index.js 文件,發現它的代碼如下
(window.webpackJsonp = window.webpackJsonp || []).push([ [1], [ , function (o, n) { o.exports = 'hello A' }, ], ])
由上面的代碼可以看出:
-
異步加載的代碼,會保存在一個全局的
webpackJsonp中。 -
webpackJsonp.push的的值,兩個參數分別為異步加載的文件中存放的需要安裝的模塊對應的 id 和異步加載的文件中存放的需要安裝的模塊列表。 -
在滿足某種情況下,會執行具體模塊中的代碼。
3、import() 按需加載
webpack4 官方文檔提供了模塊按需切割加載,配合 es6 的按需加載 import() 方法,可以做到減少首頁包體積,加快首頁的請求速度,只有其他模塊,只有當需要的時候才會加載對應 js。
import()的語法十分簡單。該函數只接受一個參數,就是引用包的地址,並且使用了 promise 式的回調,獲取加載的包。
在代碼中所有被 import()的模塊,都將打成一個單獨的包,放在 chunk 存儲的目錄下。在瀏覽器運行到這一行代碼時,就會自動請求這個資源,實現異步加載。
下面我們將上述代碼改成 import()方式。
document.getElementById('aBtn').onclick = function () { //異步加載A
import('./A').then((data) => { alert(data.A) }) } document.getElementById('bBtn').onclick = function () { //異步加載b
import('./B').then((data) => { alert(data.B) }) }
此時打包出來的文件和 webpack.ensure 方法是一樣的。
二、路由懶加載
1、為什么需要懶加載?
像 vue 這種單頁面應用,如果沒有路由懶加載,運用 webpack 打包后的文件將會很大,造成進入首頁時,需要加載的內容過多,出現較長時間的白屏,運用路由懶加載則可以將頁面進行划分,需要的時候才加載頁面,可以有效的分擔首頁所承擔的加載壓力,減少首頁加載用時。
vue 路由懶加載有以下三種方式
-
vue 異步組件
-
ES6 的
import() -
webpack 的
require.ensure()
2、vue 異步組件
這種方法主要是使用了 resolve 的異步機制,用 require 代替了 import 實現按需加載
export default new Router({ routes: [ { path: '/home',',
component: (resolve) => require(['@/components/home'], resolve), }, { path: '/about',',
component: (resolve) => require(['@/components/about'], resolve), }, ], })
3、require.ensure
這種模式可以通過參數中的 webpackChunkName 將 js 分開打包。
export default new Router({ routes: [ { path: '/home', component: (resolve) => require.ensure([], () => resolve(require('@/components/home')), 'home'), }, { path: '/about', component: (resolve) => require.ensure([], () => resolve(require('@/components/about')), 'about'), }, ], })
4、ES6 的 import()
vue-router 在官網提供了一種方法,可以理解也是為通過 Promise 的 resolve 機制。因為 Promise 函數返回的 Promise 為 resolve 組件本身,而我們又可以使用 import 來導入組件
export default new Router({ routes: [ { path: '/home', component: () => import('@/components/home'), }, { path: '/about', component: () => import('@/components/home'), }, ], })
三、webpack 分包策略
在 webpack 打包過程中,經常出現 vendor.js, app.js 單個文件較大的情況,這偏偏又是網頁最先加載的文件,這就會使得加載時間過長,從而使得白屏時間過長,影響用戶體驗。所以我們需要有合理的分包策略。
1、CommonsChunkPlugin
在 Webapck4.x 版本之前,我們都是使用 CommonsChunkPlugin 去做分離
plugins: [ new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', minChunks: function (module, count) { return ( module.resource &&
/.js$/.test(module.resource) && module.resource.indexOf(path.join(__dirname, './node_modules')) === 0 ) }, }), new webpack.optimize.CommonsChunkPlugin({ name: 'common', chunks: 'initial', minChunks: 2, }), ]
我們把以下文件單獨抽離出來打包
-
node_modules文件夾下的,模塊 -
被 3 個 入口
chunk共享的模塊
2、optimization.splitChunks
webpack 4 最大的改動就是廢除了 CommonsChunkPlugin 引入了 optimization.splitChunks。如果你的 mode 是 production,那么 webpack4 就會自動開啟 Code Splitting。
它內置的代碼分割策略是這樣的:
-
新的 chunk 是否被共享或者是來自
node_modules的模塊 -
新的 chunk 體積在壓縮之前是否大於 30kb
-
按需加載 chunk 的並發請求數量小於等於 5 個
-
頁面初始加載時的並發請求數量小於等於 3 個
雖然在 webpack4 會自動開啟 Code Splitting,但是隨着項目工程的最大,這往往不能滿足我們的需求,我們需要再進行個性化的優化。
3、應用實例
我們先找到一個優化空間較大的項目來進行操作。這是一個后台管理系統項目,大部分內容由 3-4 個前端開發,平時開發周期較短,且大部分人沒有優化意識,只是寫好業務代碼完成需求,日子一長,造成打包出來的文件較大,大大影響性能。
我們先用 webpack-bundle-analyzer 分析打包后的模塊依賴及文件大小,確定優化的方向在哪。

槽點如下:
-
打包后生成多個將近 1M 的 js 文件,其中不乏
vendor.js首頁必須加載的大文件 -
xlsx.js這樣的插件沒必要使用,導出 excel 更好的方法應該是后端返回文件流格式給前端處理 -
echart和iview文件太大,應該使用 cdn 引入的方法
正是因為有這么多槽點,我們才更好用來驗證我們優化方法的可行性。
1)抽離 echart 和 iview
由上面分析可知,echart 和 iview 文件太大,此時我們就用到 webpack4 的 optimization.splitChunks 進行代碼分割了,把他們單獨抽離打包成文件。(為了更好地呈現優化效果,我們先把 xlsx.js 去掉)
vue.config.js 修改如下:
chainWebpack: config => { config.optimization.splitChunks({ chunks: 'all', cacheGroups: { vendors: { name: 'chunk-vendors', test: /[/]node_modules[/]/, priority: 10, chunks: 'initial' }, iview: { name: 'chunk-iview', priority: 20, test: /[/]node_modules[/]_?iview(.*)/ }, echarts: { name: 'chunk-echarts', priority: 20, test: /[/]node_modules[/]_?echarts(.*)/ }, commons: { name: 'chunk-commons', minChunks: 2, priority: 5, chunks: 'initial', reuseExistingChunk: true } } }) },
此時我們再用 webpack-bundle-analyzer 分析一下


從這里可以看出我們已經成功把 echart 和 iview 單獨抽離出來了,同時 vendor.js 也相應地減小了體積。此外,我們還可以繼續抽離其他更多的第三方模塊。
2)CDN 方式
雖然第三方模塊是單獨抽離出來了,但是在首頁或者相應路由加載時還是要加載這樣一個幾百 kb 的文件,還是不利於性能優化的。這時,我們可以用 CDN 的方式引入這樣插件或者 UI 組件庫。
(1)在 index.html 引入相應 cdn 鏈接
<head>
<link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/iview/3.5.4/styles/iview.css" />
</head>
<body>
<div id="app"></div>
<script src="https://cdn.bootcss.com/vue/2.6.8/vue.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/iview/3.5.4/iview.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/xlsx/0.16.8/xlsx.mini.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/xlsx/0.16.8/cpexcel.min.js"></script>
</body>
(2)vue.config.js 配置 externals
configureWebpack: (config) => { config.externals = { vue: 'Vue', xlsx: 'XLSX', iview: 'iView', iView: 'ViewUI', } }
(3)刪除之前的引入方式並卸載相應 npm 依賴包
npm uninstall vue iview echarts xlsx --save
此時我們在來看一下打包后的情況


這時基本沒有打包出大文件了,首頁加載需要的 vendor.js 也只有幾十 kb,而且我們還可以進一步優化,就是把 vue 全家桶的一些模塊再通過 cdn 的方法引入,比如 vue-router,vuex,axios 等。這時頁面特別是首頁加載的性能就得到大大地優化了。
轉載至作者:lzg9527
原文: webpack異步加載原理及分包策略
