原文地址:https://segmentfault.com/a/1190000022846552
前言
說起路由懶加載帮毁,大家很快就知道怎么實現(xiàn)它,但是問到路由懶加載的原理屹徘,怕有一部分小伙伴是一頭霧水了吧。下面帶大家一起去理解路由懶加載的原理。
路由懶加載也可以叫做路由組件懶加載,最常用的是通過import()
來實現(xiàn)它究流。
function load(component) {
return () => import(`views/${component}`)
}
然后通過Webpack編譯打包后寝殴,會把每個路由組件的代碼分割成一一個js文件,初始化時不會加載這些js文件善茎,只當激活路由組件才會去加載對應(yīng)的js文件券册。
在這里先不管Webpack是怎么按路由組件分割代碼,只管在Webpack編譯后垂涯,怎么實現(xiàn)按需加載對應(yīng)的路由組件js文件烁焙。
一、準備工作
1耕赘、搭建項目
想要理解路由懶加載的原理骄蝇,建議從最簡單的項目開始,用Vue Cli3搭建一個項目操骡,其中只包含一個路由組件九火。在main.js只引入vue-router,其它統(tǒng)統(tǒng)不要册招。
main.js
import Vue from 'vue';
import App from './App.vue';
import Router from 'vue-router';
Vue.use(Router);
//路由懶加載
function load(component) {
return () => import(`views/${component}`)
}
// 路由配置
const router = new Router({
mode: 'history',
base: process.env.BASE_URL,
routes: [
{
path: '/',
name: 'home',
component: load('Home'),
meta: {
title: '首頁'
}
},
]
});
new Vue({
router,
render: h => h(App)
}).$mount('#app')
views/Home.vue
<template>
<div>
{{tip}}
</div>
</template>
<script>
export default {
data(){
return {
tip:'歡迎使用Vue項目'
}
}
}
</script>
2岔激、webpackChunkName
利用webpackChunkName,使編譯打包后的js文件名字能和路由組件一一對應(yīng),修改一下load函數(shù)是掰。
function load(component) {
return () => import(/* webpackChunkName: "[request]" */ `views/${component}`)
}
3虑鼎、去掉代碼壓縮混淆
去掉代碼壓縮混淆,便于我們閱讀編譯打包后的代碼。在vue.config.js中配置
module.exports={
chainWebpack:config => {
config.optimization.minimize(false);
},
}
4炫彩、npm run build
執(zhí)行命令npm run build
匾七,編譯打包后的dist文件結(jié)構(gòu)如下所示
[圖片上傳失敗...(image-e06cd6-1619759897206)]
其中Home.67f3cd34.js就是路由組件Home.vue編譯打包后對應(yīng)的js文件。
二媒楼、分析index.html
[圖片上傳失敗...(image-1b5e76-1619759897205)]
從上面我們可以看到乐尊,先用link定義Home.js、app.js划址、chunk-vendors.js這些資源和web客戶端的關(guān)系扔嵌。
-
ref=preload
:告訴瀏覽器這個資源要給我提前加載。 -
rel=prefetch
:告訴瀏覽器這個資源空閑的時候給我加載一下夺颤。 -
as=script
:告訴瀏覽器這個資源是script痢缎,提升加載的優(yōu)先級。
然后在body里面加載了chunk-vendors.js世澜、app.js這兩個js資源独旷。可以看出web客戶端初始化時候就加載了這個兩個js資源寥裂。
三嵌洼、分析chunk-vendors.js
chunk-vendors.js可以稱為項目公共模塊集合,代碼精簡后如下所示封恰,
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["chunk-vendors"],{
"01f9":(function(module,exports,__webpack_require__){
...//省略
})
...//省略
}])
從代碼中可以看出麻养,執(zhí)行chunk-vendors.js,僅僅把下面這個數(shù)組push
到window["webpackJsonp"]
中诺舔,而數(shù)組第二項是個對象鳖昌,對象的每個value值是一個函數(shù)表達式,不會執(zhí)行低飒。就這樣結(jié)束了许昨,當然不是,我們帶著window["webpackJsonp"]
去app.js中找找褥赊。
四糕档、分析app.js
app.js可以稱為項目的入口文件。
app.js里面是一個自執(zhí)行函數(shù)拌喉,通過搜索window["webpackJsonp"]
可以找到如下相關(guān)代碼翼岁。
(function(modules){
//省略...
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
var parentJsonpFunction = oldJsonpFunction;
//省略...
}({
0:(function(module, exports, __webpack_require__) {
module.exports = __webpack_require__("56d7");
})
//省略...
}))
- 先把
window["webpackJsonp"]
賦值給jsonpArray
。 - 把
jsonpArray
的push
方法賦值給oldJsonpFunction
司光。 - 用
webpackJsonpCallback
函數(shù)攔截jsopArray
的push
方法,也就是說調(diào)用window["webpackJsonp"]
的push
方法都會執(zhí)行webpackJsonpCallback
函數(shù)悉患。 - 將
jsonpArray
淺拷貝一下再賦值給jsonpArray
残家。 - 因為執(zhí)行chunk-vendors.js中的
window["webpackJsonp"].push
時push
方法還未被webpackJsonpCallback
函數(shù)攔截,所以要循環(huán)jsonpArray
售躁,將每項作為參數(shù)傳入webpackJsonpCallback
函數(shù)并調(diào)用坞淮。 - 將
jsonpArray
的push
方法再賦值給parentJsonpFunction
茴晋。
1、webpackJsonpCallback函數(shù)
接下來我們看一下webpackJsonpCallback
這個函數(shù)回窘。
(function(modules){
function webpackJsonpCallback(data) {
var chunkIds = data[0];
var moreModules = data[1];
var executeModules = data[2];
var moduleId, chunkId, i = 0, resolves = [];
for (; i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if (Object.prototype.hasOwnProperty.call(installedChunks, chunkId)
&& installedChunks[chunkId]) {
resolves.push(installedChunks[chunkId][0]);
}
installedChunks[chunkId] = 0;
}
for (moduleId in moreModules) {
if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}
if (parentJsonpFunction) parentJsonpFunction(data);
while (resolves.length) {
resolves.shift()();
}
deferredModules.push.apply(deferredModules, executeModules || []);
return checkDeferredModules();
};
var installedChunks = {
"app": 0
};
//省略...
}({
0:(function(module, exports, __webpack_require__) {
module.exports = __webpack_require__("56d7");
})
//省略...
}))
想知道webpackJsonpCallback
函數(shù)有什么作用诺擅,要先弄明白modules
、installedChunks
啡直、deferredModules
這三個變量的作用烁涌。
- module是指任意的代碼塊,chunk是webpack處理過程中被分組的module的合集酒觅。
-
modules
緩存所有的module(代碼塊)撮执,調(diào)用modules
中的module就可以執(zhí)行里面的代碼。 -
installedChunks
緩存所有chunk的加載狀態(tài)舷丹,如果installedChunks[chunk]
為0抒钱,代表chunk已經(jīng)加載完畢。 -
deferredModules
中每項也是一個數(shù)組颜凯,例如[module,chunk1,chunk2,chunk3]
,其作用是如果要執(zhí)行module谋币,必須在chunk1、chunk2症概、chunk3都加載完畢后才能執(zhí)行蕾额。
if (parentJsonpFunction) parentJsonpFunction(data)
這句代碼在多入口項目中才有作用,在前面提到過jsonpArray
的push
方法被賦值給parentJsonpFunction
穴豫,調(diào)用parentJsonpFunction
是真正把chunk中push方法中的參數(shù)push到window["webpackJsonp"]
這個數(shù)組中凡简。
比如說現(xiàn)在項目有兩個入口,app.js和app1.js精肃,app.js中緩存一些module秤涩,在app1.js就可以通過window["webpackJsonp"]
來調(diào)用這些module,調(diào)用代碼如下司抱。
for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
再來理解webpackJsonpCallback
函數(shù)是不是清楚了很多筐眷,接下來看一下checkDeferredModules
這個函數(shù)。
2习柠、checkDeferredModules函數(shù)
var deferredModules = [];
var installedChunks = {
"app": 0
}
function checkDeferredModules() {
var result;
for (var i = 0; i < deferredModules.length; i++) {
var deferredModule = deferredModules[i];
var fulfilled = true;
for (var j = 1; j < deferredModule.length; j++) {
var depId = deferredModule[j];
if (installedChunks[depId] !== 0) fulfilled = false;
}
if (fulfilled) {
deferredModules.splice(i--, 1);
result = __webpack_require__(__webpack_require__.s = deferredModule[0]);
}
}
return result;
}
- 循環(huán)
deferredModules
匀谣,創(chuàng)建變量fulfilled
表示deferredModule
中的chunk加載情況,true
表示全部加載完畢资溃,false
表示未全部加載完畢武翎。 - 從
j=1
開始循環(huán)deferredModule
中的chunk,因為deferredModule[0]
是module溶锭,如果installedChunks[chunk]!==0
宝恶,則這個chunk未加載完畢,把變量fulfilled
設(shè)置為false
。循環(huán)結(jié)束后返回result垫毙。 - 經(jīng)循環(huán)
deferredModule
中的chunk并判斷chunk的加載狀態(tài)后霹疫,fulfilled
還是為true,則調(diào)用__webpack_require__
函數(shù)综芥,將deferredModule[0]
(module)作為參數(shù)傳入執(zhí)行丽蝎。 -
deferredModules.splice(i--, 1)
,刪除滿足條件的deferredModule,并將i減一,其中i--
是先使用i,然后在減一藐握。
因為在webpackJsonpCallback
函數(shù)中deferredModules
為[]
,所以回到主體函數(shù)繼續(xù)往下看栏笆。
deferredModules.push([0, "chunk-vendors"]);
return checkDeferredModules();
按上面邏輯分析后,會執(zhí)行__webpack_require__(0)
,那么來看一下__webpack_require__
這個函數(shù)臊泰。
3蛉加、webpack_require函數(shù)
var installedModules = {};
function __webpack_require__(moduleId) {
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
module.l = true;
return module.exports;
}
從代碼可知__webpack_require__
就是一個執(zhí)行module的方法。
-
installedModules
用來緩存module的執(zhí)行狀態(tài)缸逃。 - 通過moduleId在modules(在
webpackJsonpCallback
函數(shù)中緩存所有module的集合)獲取對應(yīng)的module用call方法執(zhí)行针饥。 - 將執(zhí)行結(jié)果賦值到module.exports并返回。
所以執(zhí)行__webpack_require__(0)
,其實就是執(zhí)行下面的代碼需频。
(function (module, exports, __webpack_require__) {
module.exports = __webpack_require__("56d7");
}),
在里面又用__webpack_require__
執(zhí)行id為56d7的module丁眼,我們找到對應(yīng)的module繼續(xù)看,看一下里面關(guān)鍵的代碼片段昭殉。
function load(component) {
return function () {
return __webpack_require__("9dac")("./".concat(component));
};
}
var routes = [{
path: '/',
name: 'home',
component: load('Home'),
meta: {
title: '首頁'
}
}, {
path: '*',
redirect: {
path: '/'
}
}];
看到這里是不是非常熟悉了苞七,就是配置路由的地方。load
還是作為加載路由組件的函數(shù)挪丢,里面用__webpack_require__("9dac")
返回的方法來執(zhí)行加載路由組件蹂风,我們來看一下__webpack_require__("9dac")
。
(function (module, exports, __webpack_require__) {
var map = {
"./Home": [
"bb51",
"Home"
],
"./Home.vue": [
"bb51",
"Home"
]
};
function webpackAsyncContext(req) {
if (!__webpack_require__.o(map, req)) {
return Promise.resolve().then(function () {
var e = new Error("Cannot find module '" + req + "'");
e.code = 'MODULE_NOT_FOUND';
throw e;
});
}
var ids = map[req], id = ids[0];
return __webpack_require__.e(ids[1]).then(function () {
return __webpack_require__(id);
});
}
webpackAsyncContext.keys = function webpackAsyncContextKeys() {
return Object.keys(map);
};
webpackAsyncContext.id = "9dac";
module.exports = webpackAsyncContext;
})
4乾蓬、webpackAsyncContext函數(shù)
其中的關(guān)鍵函數(shù)為webpackAsyncContext
,調(diào)用load('Home')
時惠啄,req
為'./Home'
,__webpack_require__.o
方法為
__webpack_require__.o = function (object, property) {
return Object.prototype.hasOwnProperty.call(object, property);
};
這個方法就是判斷在變量map
中有沒有key為./Home
的項任内,如果沒有拋出Cannot find module './Home'
的錯誤撵渡。有執(zhí)行__webpack_require__.e
方法,參數(shù)為Home
死嗦。
5趋距、webpack_require.e方法
var installedChunks = {
"app": 0
}
__webpack_require__.p = "/";
function jsonpScriptSrc(chunkId) {
return __webpack_require__.p + "js/" + ({ "Home": "Home" }[chunkId] || chunkId) +
"." + { "Home": "37ee624e" }[chunkId] + ".js"
}
__webpack_require__.e = function requireEnsure(chunkId) {
var promises = [];
var installedChunkData = installedChunks[chunkId];
if (installedChunkData !== 0) {
if (installedChunkData) {
promises.push(installedChunkData[2]);
} else {
var promise = new Promise(function (resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
promises.push(installedChunkData[2] = promise);
var script = document.createElement('script');
var onScriptComplete;
script.charset = 'utf-8';
script.timeout = 120;
if (__webpack_require__.nc) {
script.setAttribute("nonce", __webpack_require__.nc);
}
script.src = jsonpScriptSrc(chunkId);
var error = new Error();
onScriptComplete = function (event) {
// 避免IE內(nèi)存泄漏。
script.onerror = script.onload = null;
clearTimeout(timeout);
var chunk = installedChunks[chunkId];
if (chunk !== 0) {
if (chunk) {
var errorType = event &&
(event.type === 'load' ? 'missing' : event.type);
var realSrc = event && event.target && event.target.src;
error.message = 'Loading chunk ' + chunkId
+ ' failed.\n(' + errorType + ': ' + realSrc + ')';
error.name = 'ChunkLoadError';
error.type = errorType;
error.request = realSrc;
chunk[1](error);
}
installedChunks[chunkId] = undefined;
}
};
var timeout = setTimeout(function () {
onScriptComplete({ type: 'timeout', target: script });
}, 120000);
script.onerror = script.onload = onScriptComplete;
document.head.appendChild(script);
}
}
return Promise.all(promises);
};
__webpack_require__.e
方法是實現(xiàn)懶加載的核心越除,在這個方法里面處理了三件事情节腐。
- 使用JSONP模式加載路由對應(yīng)的js文件靠欢,也可以稱為chunk。
- 設(shè)置chunk加載的三種狀態(tài)并緩存在
installedChunks
中铜跑,防止chunk重復加載。 - 處理chunk加載超時和加載出錯的場景骡澈。
chunk加載的三種狀態(tài)
-
installedChunks[chunkId]
為0
锅纺,代表該chunk已經(jīng)加載完畢。 -
installedChunks[chunkId]
為undefined
肋殴,代表該chunk加載失敗囤锉、加載超時、從未加載過护锤。 -
installedChunks[chunkId]
為Promise
對象官地,代表該chunk正在加載。
chunk加載超時處理
script.timeout = 120;
var timeout = setTimeout(function () {
onScriptComplete({ type: 'timeout', target: script });
}, 120000);
script.timeout = 120
代表該chunk加載120秒后還沒加載完畢則超時烙懦。
用setTimeout
設(shè)置個120秒的計時器驱入,在120秒后執(zhí)行onScriptComplete({ type: 'timeout', target: script })
。
在看一下onScriptComplete
函數(shù)
var onScriptComplete = function (event) {
// 避免IE內(nèi)存泄漏氯析。
script.onerror = script.onload = null;
clearTimeout(timeout);
var chunk = installedChunks[chunkId];
if (chunk !== 0) {
if (chunk) {
var errorType = event && (event.type === 'load' ? 'missing' : event.type);
var realSrc = event && event.target && event.target.src;
error.message = 'Loading chunk ' + chunkId
+ ' failed.\n(' + errorType + ': ' + realSrc + ')';
error.name = 'ChunkLoadError';
error.type = errorType;
error.request = realSrc;
chunk[1](error);
}
installedChunks[chunkId] = undefined;
}
};
此時chunkId為Home
亏较,加載是Home.js,代碼是
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["Home"],{
"bb51":(function(module, __webpack_exports__, __webpack_require__){
//省略...
})
}]))
在前面有提到window["webpackJsonp"]
的push方法被webpackJsonpCallback
函數(shù)攔截了,如果Home.js加載成功會自動執(zhí)行掩缓,隨后會執(zhí)行webpackJsonpCallback
函數(shù)雪情,其中有installedChunks[chunkId] = 0;
會把installedChunks['Home']
的值置為0。
也就是說,如果Home.js加載超時了你辣,就不能執(zhí)行巡通,就不能將installedChunks['Home']
的值置為0,所以此時installedChunks['Home']
的值還是Promise
對象舍哄。那么就會進入以下代碼執(zhí)行宴凉,最后chunk[1](error)
將錯誤拋出去。
var chunk = installedChunks[chunkId];
if(chunk!==0){
if(chunk){
var errorType = event && (event.type === 'load' ? 'missing' : event.type);
var realSrc = event && event.target && event.target.src;
error.message = 'Loading chunk ' + chunkId
+ ' failed.\n(' + errorType + ': ' + realSrc + ')';
error.name = 'ChunkLoadError';
error.type = errorType;
error.request = realSrc;
chunk[1](error);
}
}
chunk[1]
其實就是reject函數(shù)蠢熄,在以下代碼中給它賦值的跪解。
var promise = new Promise(function (resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
chunk加載失敗處理
加載失敗分為兩種情況,一是Home.js資源加載失敗签孔,二是資源加載成功了叉讥,但是執(zhí)行Home.js里面代碼出錯了導致失敗,所以chunk加載失敗處理的代碼要這么寫
script.onerror = script.onload = onScriptComplete;
后面處理的方式和處理加載超時的一樣饥追。
__webpack_require__.e
最后返回是一個Promise
對象图仓。回到webpackAsyncContext
函數(shù)中
return __webpack_require__.e(ids[1]).then(function () {
return __webpack_require__(id);
});
__webpack_require__.e(ids[1])
執(zhí)行成功后但绕,執(zhí)行__webpack_require__(id);
救崔,此時id為bb51惶看。那么又回到__webpack_require__
函數(shù)中了。在前面提過__webpack_require__
函數(shù)的作用就是執(zhí)行module六孵。id為bb51的nodule是在Home.js內(nèi)纬黎,在webpackJsonpCallback
函數(shù)有以下代碼
function webpackJsonpCallback(data) {
var moreModules = data[1];
for (moduleId in moreModules) {
if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}
}
五、分析Home.js
Home.js
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["Home"],{
"bb51":(function(module, __webpack_exports__, __webpack_require__){
//省略...
})
}]))
可以看出moreModules就是{"bb51":(function(module, __webpack_exports__, __webpack_require__){})}
,
循環(huán)moreModules劫窒,把Home.js里面的module緩存到app.js里面的modules中本今。
再看__webpack_require__
函數(shù)中有這段代碼
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
這樣就執(zhí)行了Home.js里面的module,在module里面有渲染頁面的一系列的方法主巍,就把Home.vue這個路由組件頁面渲染出來了冠息。
到這里路由組件懶加載的整個流程就結(jié)束了,也詳細介紹了怎么加載chunk和怎么執(zhí)行module孕索。