Vue路由懶加載原理及實現


本文通過搭建一個Vue的簡單項目,引入了Vue-router組件,從頁面的3個文件入手,詳細介紹了怎么加載chunk和怎么執行module,從而介紹整個路由組件懶加載的過程,希望對你有幫助。 原文:https://segmentfault.com/a/1190000022846552

前言

說起路由懶加載,大家很快就知道怎么實現它,但是問到路由懶加載的原理,怕有一部分小伙伴是一頭霧水了吧。下面帶大家一起去理解路由懶加載的原理。 路由懶加載也可以叫做路由組件懶加載,最常用的是通過import()來實現它。

function load(component) { return () => import(`views/${component}`) }

然后通過Webpack編譯打包后,會把每個路由組件的代碼分割成一一個js文件,初始化時不會加載這些js文件,只當激活路由組件才會去加載對應的js文件。 在這里先不管Webpack是怎么按路由組件分割代碼,只管在Webpack編譯后,怎么實現按需加載對應的路由組件js文件。

一、准備工作

1、搭建項目

想要理解路由懶加載的原理,建議從最簡單的項目開始,用Vue Cli3搭建一個項目,其中只包含一個路由組件。在main.js只引入vue-router,其它統統不要。 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文件名字能和路由組件一一對應,修改一下load函數。

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

執行命令npm run build,編譯打包后的dist文件結構如下所示  其中Home.67f3cd34.js就是路由組件Home.vue編譯打包后對應的js文件。

二、分析index.html

 從上面我們可以看到,先用link定義Home.js、app.js、chunk-vendors.js這些資源和web客戶端的關系。

  • ref=preload:告訴瀏覽器這個資源要給我提前加載。
  • rel=prefetch:告訴瀏覽器這個資源空閑的時候給我加載一下。
  • as=script:告訴瀏覽器這個資源是script,提升加載的優先級。

然后在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__){ ...//省略 }) ...//省略 }])

從代碼中可以看出,執行chunk-vendors.js,僅僅把下面這個數組pushwindow["webpackJsonp"]中,而數組第二項是個對象,對象的每個value值是一個函數表達式,不會執行。就這樣結束了,當然不是,我們帶着window["webpackJsonp"]去app.js中找找。

四、分析app.js

app.js可以稱為項目的入口文件。 app.js里面是一個自執行函數,通過搜索window["webpackJsonp"]可以找到如下相關代碼。

(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
  • jsonpArraypush方法賦值給oldJsonpFunction
  • webpackJsonpCallback函數攔截jsopArraypush方法,也就是說調用window["webpackJsonp"]push方法都會執行webpackJsonpCallback函數。
  • jsonpArray淺拷貝一下再賦值給jsonpArray
  • 因為執行chunk-vendors.js中的window["webpackJsonp"].pushpush方法還未被webpackJsonpCallback函數攔截,所以要循環jsonpArray,將每項作為參數傳入webpackJsonpCallback函數並調用。
  • jsonpArraypush方法再賦值給parentJsonpFunction

1、webpackJsonpCallback函數

接下來我們看一下webpackJsonpCallback這個函數。

(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函數有什么作用,要先弄明白modulesinstalledChunksdeferredModules這三個變量的作用。

  • module是指任意的代碼塊,chunk是webpack處理過程中被分組的module的合集。
  • modules緩存所有的module(代碼塊),調用modules中的module就可以執行里面的代碼。
  • installedChunks緩存所有chunk的加載狀態,如果installedChunks[chunk]為0,代表chunk已經加載完畢。
  • deferredModules中每項也是一個數組,例如[module,chunk1,chunk2,chunk3],其作用是如果要執行module,必須在chunk1、chunk2、chunk3都加載完畢后才能執行。

if (parentJsonpFunction) parentJsonpFunction(data)這句代碼在多入口項目中才有作用,在前面提到過jsonpArraypush方法被賦值給parentJsonpFunction,調用parentJsonpFunction是真正把chunk中push方法中的參數push到window["webpackJsonp"]這個數組中。 比如說現在項目有兩個入口,app.js和app1.js,app.js中緩存一些module,在app1.js就可以通過window["webpackJsonp"]來調用這些module,調用代碼如下。

for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);

再來理解webpackJsonpCallback函數是不是清楚了很多,接下來看一下checkDeferredModules這個函數。

2、checkDeferredModules函數

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; }
  • 循環deferredModules,創建變量fulfilled表示deferredModule中的chunk加載情況,true表示全部加載完畢,false表示未全部加載完畢。
  • j=1開始循環deferredModule中的chunk,因為deferredModule[0]是module,如果installedChunks[chunk]!==0,則這個chunk未加載完畢,把變量fulfilled設置為false。循環結束后返回result。
  • 經循環deferredModule中的chunk並判斷chunk的加載狀態后,fulfilled還是為true,則調用__webpack_require__函數,將deferredModule[0](module)作為參數傳入執行。
  • deferredModules.splice(i--, 1),刪除滿足條件的deferredModule,並將i減一,其中i--是先使用i,然后在減一。

因為在webpackJsonpCallback函數中deferredModules[],所以回到主體函數繼續往下看。

deferredModules.push([0, "chunk-vendors"]); return checkDeferredModules();

按上面邏輯分析后,會執行__webpack_require__(0),那么來看一下__webpack_require__這個函數。

3、__webpack_require__函數

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__就是一個執行module的方法。

  • installedModules用來緩存module的執行狀態。
  • 通過moduleId在modules(在webpackJsonpCallback函數中緩存所有module的集合)獲取對應的module用call方法執行。
  • 將執行結果賦值到module.exports並返回。

所以執行__webpack_require__(0),其實就是執行下面的代碼。

(function (module, exports, __webpack_require__) { module.exports = __webpack_require__("56d7"); }),

在里面又用__webpack_require__執行id為56d7的module,我們找到對應的module繼續看,看一下里面關鍵的代碼片段。

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還是作為加載路由組件的函數,里面用__webpack_require__("9dac")返回的方法來執行加載路由組件,我們來看一下__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函數

其中的關鍵函數為webpackAsyncContext,調用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'的錯誤。有執行__webpack_require__.e方法,參數為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內存泄漏。 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方法是實現懶加載的核心,在這個方法里面處理了三件事情。

  • 使用JSONP模式加載路由對應的js文件,也可以稱為chunk。
  • 設置chunk加載的三種狀態並緩存在installedChunks中,防止chunk重復加載。
  • 處理chunk加載超時和加載出錯的場景。

chunk加載的三種狀態

  • installedChunks[chunkId]0,代表該chunk已經加載完畢。
  • 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設置個120秒的計時器,在120秒后執行onScriptComplete({ type: 'timeout', target: script })。 在看一下onScriptComplete函數

var onScriptComplete = function (event) { // 避免IE內存泄漏。 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函數攔截了,如果Home.js加載成功會自動執行,隨后會執行webpackJsonpCallback函數,其中有installedChunks[chunkId] = 0;會把installedChunks['Home']的值置為0。 也就是說,如果Home.js加載超時了,就不能執行,就不能將installedChunks['Home']的值置為0,所以此時installedChunks['Home']的值還是Promise對象。那么就會進入以下代碼執行,最后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函數,在以下代碼中給它賦值的。

var promise = new Promise(function (resolve, reject) { installedChunkData = installedChunks[chunkId] = [resolve, reject]; });

chunk加載失敗處理 加載失敗分為兩種情況,一是Home.js資源加載失敗,二是資源加載成功了,但是執行Home.js里面代碼出錯了導致失敗,所以chunk加載失敗處理的代碼要這么寫

script.onerror = script.onload = onScriptComplete;

后面處理的方式和處理加載超時的一樣。 __webpack_require__.e最后返回是一個Promise對象。回到webpackAsyncContext函數中

return __webpack_require__.e(ids[1]).then(function () { return __webpack_require__(id); });

__webpack_require__.e(ids[1])執行成功后,執行 __webpack_require__(id);,此時id為bb51。那么又回到__webpack_require__函數中了。在前面提過__webpack_require__函數的作用就是執行module。id為bb51的nodule是在Home.js內,在webpackJsonpCallback函數有以下代碼

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__){})}, 循環moreModules,把Home.js里面的module緩存到app.js里面的modules中。 再看__webpack_require__函數中有這段代碼

modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

這樣就執行了Home.js里面的module,在module里面有渲染頁面的一系列的方法,就把Home.vue這個路由組件頁面渲染出來了。 到這里路由組件懶加載的整個流程就結束了,也詳細介紹了怎么加載chunk和怎么執行module。

 


文章就分享到這,歡迎關注“前端大神之路


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM