作者:lzg9527
原文鏈接:https://segmentfault.com/a/1190000038180453
webpack ensure?有人稱它為異步加載,也有人稱為代碼切割,他其實(shí)就是將 js 模塊給獨(dú)立導(dǎo)出一個(gè).js 文件,然后使用這個(gè)模塊的時(shí)候,再創(chuàng)建一個(gè)?script?對(duì)象,加入到?document.head?對(duì)象中,瀏覽器會(huì)自動(dòng)幫我們發(fā)起請(qǐng)求,去請(qǐng)求這個(gè) js 文件,然后寫(xiě)個(gè)回調(diào)函數(shù),讓請(qǐng)求到的 js 文件做一些業(yè)務(wù)操作。
需求:main.js?依賴兩個(gè) js 文件:A.js?是點(diǎn)擊 aBtn 按鈕后,才執(zhí)行的邏輯,B.js?是點(diǎn)擊 bBtn 按鈕后,才執(zhí)行的邏輯。
webpack.config.js,我們先來(lái)寫(xiě)一下?webpack?打包的配置的代碼
constpath = require('path')// 路徑處理模塊
constHtmlWebpackPlugin = 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: [
newHtmlWebpackPlugin({
template: path.join(__dirname,'/index.html'),
? ? }),
newCleanWebpackPlugin(),// 所要清理的文件夾名稱
? ],
}
index.html?代碼如下
<!DOCTYPE html>
? <head>
? ? <title>webpack</title>
? </head>
? <body>
按鈕A
按鈕B
? ? </div>
? </body>
</html>
入口文件?main.js?如下
importA from'./A'
importB from'./B'
document.getElementById('aBtn').onclick = function () {
? alert(A)
}
document.getElementById('bBtn').onclick = function () {
? alert(B)
}
A.js?和?B.js?的代碼分別如下
// A.js
constA ='hello A'
module.exports = A
// B.js
constB ='hello B'
module.exports = B
此時(shí),我們對(duì)項(xiàng)目進(jìn)行?npm run build, 打包出來(lái)的只有兩個(gè)文件
index.html
index.js
由此可見(jiàn),此時(shí)?webpack?把?main.js?依賴的兩個(gè)文件都同時(shí)打包到同一個(gè) js 文件,并在 index.html 中引入。但是?A.js?和?B.js?都是點(diǎn)擊相應(yīng)按鈕才會(huì)執(zhí)行的邏輯,如果用戶并沒(méi)有點(diǎn)擊相應(yīng)按鈕,而且這兩個(gè)文件又是比較大的話,這樣是不是就導(dǎo)致首頁(yè)默認(rèn)加載的 js 文件太大,從而導(dǎo)致首頁(yè)渲染較慢呢?那么有能否實(shí)現(xiàn)當(dāng)用戶點(diǎn)擊按鈕的時(shí)候再加載相應(yīng)的依賴文件呢?
webpack.ensure?就解決了這個(gè)問(wèn)題。
下面我們將?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)
? })
}
此時(shí),我們?cè)龠M(jìn)行一下打包,發(fā)現(xiàn)多了?1.index.js?和?2.index.js?兩個(gè)文件。而我們打開(kāi)頁(yè)面時(shí)只引入了?index.js?一個(gè)文件,當(dāng)點(diǎn)擊按鈕 A 的時(shí)候才引入?1.index.js?文件,點(diǎn)擊按鈕 B 的時(shí)候才引入?2.index.js?文件。這樣就滿足了我們按需加載的需求。
require.ensure?這個(gè)函數(shù)是一個(gè)代碼分離的分割線,表示回調(diào)里面的?require?是我們想要進(jìn)行分割出去的,即?require('./A.js'),把 A.js 分割出去,形成一個(gè)?webpack?打包的單獨(dú) js 文件。它的語(yǔ)法如下
require.ensure(dependencies: String[], callback: function(require), chunkName: String)
我們打開(kāi)?1.index.js?文件,發(fā)現(xiàn)它的代碼如下
(window.webpackJsonp = window.webpackJsonp || []).push([
[1],
? [
? ? ,
? ? function (o, n) {
o.exports ='hello A'
? ? },
? ],
])
由上面的代碼可以看出:
異步加載的代碼,會(huì)保存在一個(gè)全局的?webpackJsonp?中。
webpackJsonp.push?的的值,兩個(gè)參數(shù)分別為異步加載的文件中存放的需要安裝的模塊對(duì)應(yīng)的 id 和異步加載的文件中存放的需要安裝的模塊列表。
在滿足某種情況下,會(huì)執(zhí)行具體模塊中的代碼。
webpack4 官方文檔提供了模塊按需切割加載,配合 es6 的按需加載?import()?方法,可以做到減少首頁(yè)包體積,加快首頁(yè)的請(qǐng)求速度,只有其他模塊,只有當(dāng)需要的時(shí)候才會(huì)加載對(duì)應(yīng) js。
import()的語(yǔ)法十分簡(jiǎn)單。該函數(shù)只接受一個(gè)參數(shù),就是引用包的地址,并且使用了?promise?式的回調(diào),獲取加載的包。在代碼中所有被?import()的模塊,都將打成一個(gè)單獨(dú)的包,放在?chunk?存儲(chǔ)的目錄下。在瀏覽器運(yùn)行到這一行代碼時(shí),就會(huì)自動(dòng)請(qǐng)求這個(gè)資源,實(shí)現(xiàn)異步加載。
下面我們將上述代碼改成?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)
? })
}
此時(shí)打包出來(lái)的文件和?webpack.ensure?方法是一樣的。
為什么需要懶加載?
像 vue 這種單頁(yè)面應(yīng)用,如果沒(méi)有路由懶加載,運(yùn)用 webpack 打包后的文件將會(huì)很大,造成進(jìn)入首頁(yè)時(shí),需要加載的內(nèi)容過(guò)多,出現(xiàn)較長(zhǎng)時(shí)間的白屏,運(yùn)用路由懶加載則可以將頁(yè)面進(jìn)行劃分,需要的時(shí)候才加載頁(yè)面,可以有效的分擔(dān)首頁(yè)所承擔(dān)的加載壓力,減少首頁(yè)加載用時(shí)。
vue 路由懶加載有以下三種方式
vue 異步組件
ES6 的?import()
webpack 的?require.ensure()
這種方法主要是使用了?resolve?的異步機(jī)制,用?require?代替了?import?實(shí)現(xiàn)按需加載
exportdefaultnewRouter({
? routes: [
? ? {
path:'/home',',
? ? ? component: (resolve) => require(['@/components/home'], resolve),
? ? },
? ? {
? ? ? path: '/about',',
component: (resolve) => require(['@/components/about'], resolve),
? ? },
? ],
})
這種模式可以通過(guò)參數(shù)中的?webpackChunkName?將 js 分開(kāi)打包。
exportdefaultnewRouter({
? routes: [
? ? {
path:'/home',
component: (resolve) => require.ensure([], () => resolve(require('@/components/home')),'home'),
? ? },
? ? {
path:'/about',
component: (resolve) => require.ensure([], () => resolve(require('@/components/about')),'about'),
? ? },
? ],
})
vue-router?在官網(wǎng)提供了一種方法,可以理解也是為通過(guò)?Promise?的?resolve?機(jī)制。因?yàn)?Promise?函數(shù)返回的?Promise?為?resolve?組件本身,而我們又可以使用?import?來(lái)導(dǎo)入組件。
exportdefaultnewRouter({
? routes: [
? ? {
path:'/home',
component: () =>import('@/components/home'),
? ? },
? ? {
path:'/about',
component: () =>import('@/components/home'),
? ? },
? ],
})
在 webpack 打包過(guò)程中,經(jīng)常出現(xiàn)?vendor.js,?app.js?單個(gè)文件較大的情況,這偏偏又是網(wǎng)頁(yè)最先加載的文件,這就會(huì)使得加載時(shí)間過(guò)長(zhǎng),從而使得白屏?xí)r間過(guò)長(zhǎng),影響用戶體驗(yàn)。所以我們需要有合理的分包策略。
在 Webapck4.x 版本之前,我們都是使用?CommonsChunkPlugin?去做分離
plugins: [
newwebpack.optimize.CommonsChunkPlugin({
name:'vendor',
? ? minChunks: function (module, count) {
return(
? ? ? ? module.resource &&
? ? ? ? /.js$/.test(module.resource) &&
module.resource.indexOf(path.join(__dirname,'./node_modules')) ===0
? ? ? )
? ? },
? }),
newwebpack.optimize.CommonsChunkPlugin({
name:'common',
chunks:'initial',
minChunks:2,
? }),
]
我們把以下文件單獨(dú)抽離出來(lái)打包
node_modules?文件夾下的,模塊
被 3 個(gè) 入口?chunk?共享的模塊
webpack 4 最大的改動(dòng)就是廢除了?CommonsChunkPlugin?引入了?optimization.splitChunks。如果你的?mode?是?production,那么 webpack4 就會(huì)自動(dòng)開(kāi)啟?Code Splitting。
它內(nèi)置的代碼分割策略是這樣的:
新的 chunk 是否被共享或者是來(lái)自?node_modules?的模塊
新的 chunk 體積在壓縮之前是否大于 30kb
按需加載 chunk 的并發(fā)請(qǐng)求數(shù)量小于等于 5 個(gè)
頁(yè)面初始加載時(shí)的并發(fā)請(qǐng)求數(shù)量小于等于 3 個(gè)
雖然在 webpack4 會(huì)自動(dòng)開(kāi)啟?Code Splitting,但是隨著項(xiàng)目工程的最大,這往往不能滿足我們的需求,我們需要再進(jìn)行個(gè)性化的優(yōu)化。
我們先找到一個(gè)優(yōu)化空間較大的項(xiàng)目來(lái)進(jìn)行操作。這是一個(gè)后臺(tái)管理系統(tǒng)項(xiàng)目,大部分內(nèi)容由 3-4 個(gè)前端開(kāi)發(fā),平時(shí)開(kāi)發(fā)周期較短,且大部分人沒(méi)有優(yōu)化意識(shí),只是寫(xiě)好業(yè)務(wù)代碼完成需求,日子一長(zhǎng),造成打包出來(lái)的文件較大,大大影響性能。
我們先用?webpack-bundle-analyzer?分析打包后的模塊依賴及文件大小,確定優(yōu)化的方向在哪。
然后我們?cè)倏聪麓虬鰜?lái)的 js 文件
看到這兩張圖的時(shí)候,我內(nèi)心是崩潰的,槽點(diǎn)如下
打包后生成多個(gè)將近 1M 的 js 文件,其中不乏?vendor.js?首頁(yè)必須加載的大文件
xlsx.js?這樣的插件沒(méi)必要使用,導(dǎo)出 excel 更好的方法應(yīng)該是后端返回文件流格式給前端處理
echart?和?iview?文件太大,應(yīng)該使用 cdn 引入的方法
吐槽完之后我們就要開(kāi)始做正事了。正是因?yàn)橛羞@么多槽點(diǎn),我們才更好用來(lái)驗(yàn)證我們優(yōu)化方法的可行性。
抽離 echart 和 iview
由上面分析可知,echart?和?iview?文件太大,此時(shí)我們就用到 webpack4 的?optimization.splitChunks?進(jìn)行代碼分割了,把他們單獨(dú)抽離打包成文件。(為了更好地呈現(xiàn)優(yōu)化效果,我們先把 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
? ? ? ? }
? ? ? }
? ? })
? },
此時(shí)我們?cè)儆?webpack-bundle-analyzer?分析一下
打包出來(lái)的 js 文件
從這里可以看出我們已經(jīng)成功把?echart?和?iview?單獨(dú)抽離出來(lái)了,同時(shí)?vendor.js?也相應(yīng)地減小了體積。此外,我們還可以繼續(xù)抽離其他更多的第三方模塊。
CDN 方式
雖然第三方模塊是單獨(dú)抽離出來(lái)了,但是在首頁(yè)或者相應(yīng)路由加載時(shí)還是要加載這樣一個(gè)幾百 kb 的文件,還是不利于性能優(yōu)化的。這時(shí),我們可以用 CDN 的方式引入這樣插件或者 UI 組件庫(kù)。
在?index.html?引入相應(yīng) cdn 鏈接
<head>
</head>
<body>
</body>
vue.config.js?配置?externals
configureWebpack: (config) => {
? config.externals = {
vue:'Vue',
xlsx:'XLSX',
iview:'iView',
iView:'ViewUI',
? }
}
刪除之前的引入方式并卸載相應(yīng) npm 依賴包
npm uninstall vue iview echarts xlsx --save
此時(shí)我們?cè)趤?lái)看一下打包后的情況

打包出來(lái)的 js 文件

well done ! 這時(shí)基本沒(méi)有打包出大文件了,首頁(yè)加載需要的?vendor.js?也只有幾十 kb,而且我們還可以進(jìn)一步優(yōu)化,就是把 vue 全家桶的一些模塊再通過(guò) cdn 的方法引入,比如?vue-router,vuex,axios?等。這時(shí)頁(yè)面特別是首頁(yè)加載的性能就得到大大地優(yōu)化了。