"Серия Webpack" - Принцип маршрутизации отложенной загрузки

Webpack
"Серия Webpack" - Принцип маршрутизации отложенной загрузки

предисловие

Говоря о роутинге ленивой загрузки, все быстро знают, как его реализовать, но когда меня спрашивают о принципе роутинга ленивой загрузки, я боюсь, что некоторые из моих друзей в недоумении. Давайте познакомим всех с принципом ленивой маршрутизации.

Отложенную загрузку маршрутизации также можно назвать отложенной загрузкой компонентов маршрутизации, чаще всего используется черезimport()чтобы достичь этого.

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

Затем, после компиляции и упаковки через Webpack, код каждого компонента роутинга будет разбит на js-файлы один за другим, эти js-файлы не будут загружаться при инициализации, а соответствующие js-файлы будут загружаться только при активации компонента роутинга. .

Здесь неважно, как Webpack разделяет код по компонентам роутинга, только как загружать соответствующие js-файлы компонента роутинга по запросу после компиляции Webpack.

1. Подготовка

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, чтобы имя скомпилированного и упакованного файла js могло соответствовать компоненту маршрутизации один за другим, и модифицировать функцию загрузки.

function load(component) {
    return () => import(/* webpackChunkName: "[request]" */ `views/${component}`)
}

3. Удалите сжатие кода и путаницу

Устраните путаницу со сжатием кода, чтобы мы могли читать скомпилированный и упакованный код. Настроить в vue.config.js

module.exports={
    chainWebpack:config => {
        config.optimization.minimize(false);
    },
}

4. npm запустить сборку

Выполнение заказаnpm run build, скомпилированная и упакованная файловая структура dist выглядит следующим образом

Среди них Home.67f3cd34.js — это соответствующий файл js после компиляции и упаковки компонента маршрутизации Home.vue.

2. Анализ index.html

Как видно из вышеизложенного, сначала используйте ссылку, чтобы определить отношения между Home.js, app.js, chunk-vendors.js и веб-клиентом.

  • ref=preload: Скажите браузеру, что этот ресурс должен быть загружен рано для меня.
  • rel=prefetch: указать браузеру загружать этот ресурс для меня, когда он бездействует.
  • as=script: Сообщите браузеру, что этот ресурс является скриптом, и увеличьте приоритет загрузки.

Затем в тело загружаются два ресурса js, chunk-vendors.js и app.js. Видно, что два ресурса js загружаются при инициализации веб-клиента.

3. Проанализируйте chunk-vendors.js

chunk-vendors.js можно назвать набором публичных модулей проекта, код упрощен следующим образом,

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["chunk-vendors"],{
    "01f9":(function(module,exports,__webpack_require__){
        ...//省略
    })
    ...//省略
}])

Как видно из кода, выполняем chunk-vendors.js, просто ставим следующий массивpushприбытьwindow["webpackJsonp"], а второй элемент массива — это объект, и каждое значение объекта — это функциональное выражение, которое выполняться не будет. Это конец, конечно, нет, мы приносим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.
  • ПучокjsonpArrayизpushметод, назначенныйoldJsonpFunction.
  • использоватьwebpackJsonpCallbackперехват функцииjsopArrayизpushметод, то есть вызовwindow["webpackJsonp"]изpushметод будет выполнятьсяwebpackJsonpCallbackфункция.
  • будетjsonpArrayНеглубокое копирование и назначениеjsonpArray.
  • Поскольку выполнение chunk-vendors.js вwindow["webpackJsonp"].pushВремяpushметод не былwebpackJsonpCallbackПерехват функции, поэтому циклjsonpArray, передавая каждый элемент в качестве параметраwebpackJsonpCallbackфункция и вызов.
  • будетjsonpArrayизpushметод переназначается на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Какова функция функции, мы должны сначала понятьmodules,installedChunks,deferredModulesроль этих трех переменных.

  • Модуль — это произвольный блок кода, а чанк — это набор модулей, сгруппированных во время обработки веб-пакета.
  • modulesКэшировать все модули (кодовые блоки), вызыватьmodulesМодуль может выполнять код внутри.
  • installedChunksКэшировать статус загрузки всех чанков, еслиinstalledChunks[chunk]Если он равен 0, это означает, что чанк был загружен.
  • deferredModulesКаждый элемент также является массивом, например.[module,chunk1,chunk2,chunk3], Его функция заключается в том, что если вы хотите выполнить модуль, он должен быть выполнен после загрузки чанк1, чанк2 и чанк3.

if (parentJsonpFunction) parentJsonpFunction(data)Этот код работает только в проектах с несколькими входами, как упоминалось ранее.jsonpArrayизpushметод назначен наparentJsonpFunction,перечислитьparentJsonpFunctionОн действительно подталкивает параметры метода push в чанке кwindow["webpackJsonp"]в этом массиве.

Например, в проекте теперь две записи, app.js и app1.js, часть модулей кешируется в app.js, а в app1.js можно передатьwindow["webpackJsonp"]Для вызова этих модулей код вызова выглядит следующим образом.

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Загрузка чанка в ,trueУказывает, что вся загрузка завершена,falseУказывает, что не все загружены.
  • отj=1начать циклdeferredModuleкуски в , потому чтоdeferredModule[0]является модулем, еслиinstalledChunks[chunk]!==0, то чанк не грузится, ставим переменнуюfulfilledУстановить какfalse. Возврат к результату после завершения цикла.
  • зацикленныйdeferredModuleОценив статус загрузки чанка,fulfilledпо-прежнему верно, тогда позвоните__webpack_require__функция, будетdeferredModule[0](модуль) передается в качестве параметра выполнению.
  • 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__Это метод, который выполняет модуль.

  • installedModulesИспользуется для кэширования состояния выполнения модуля.
  • По moduleId в модулях (вwebpackJsonpCallbackКоллекция всех модулей кэшируется в функции), чтобы получить соответствующий модуль и выполнить его с помощью метода вызова.
  • Назначьте результат выполнения module.exports и верните.

так выполнить__webpack_require__(0), по сути, заключается в выполнении следующего кода.

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

используется снова в__webpack_require__Выполняем модуль с id 56d7, находим соответствующий модуль и продолжаем читать, смотрим фрагменты кода ключа внутри.

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Есть ли ключ для./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-файла, соответствующего маршруту, который также можно назвать чанком.
  • Установите три состояния загрузки чанка и кеша вinstalledChunks, чтобы предотвратить повторную загрузку фрагментов.
  • Обработка тайм-аута загрузки чанков и сценариев ошибок загрузки.

Три состояния загрузки чанка

  • installedChunks[chunkId]за0, указывая на то, что фрагмент был загружен.
  • installedChunks[chunkId]заundefined, что означает, что фрагмент не удалось загрузить, истекло время ожидания или он никогда не загружался.
  • installedChunks[chunkId]заPromiseОбъект, представляющий загружаемый фрагмент.

обработка тайм-аута загрузки чанков

script.timeout = 120;
var timeout = setTimeout(function () {
    onScriptComplete({ type: 'timeout', target: script });
}, 120000);

script.timeout = 120Истечет время ожидания, если чанк не был загружен после 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;
    }
};

На данный момент chunkIdHome, загрузка Home.js, код

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["Home"],{
    "bb51":(function(module, __webpack_exports__, __webpack_require__){
        //省略...
    })
}]))

упомянутый ранееwindow["webpackJsonp"]Метод проталкивания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]Фактически это функция отклонения, которой в следующем коде присваивается значение.

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

обработка сбоев при загрузке чанков

Ошибка загрузки делится на два случая, один из которых заключается в том, что ресурс Home.js не загружается, другой заключается в том, что ресурс загружается успешно, но выполнение кода в Home.js вызывает ошибку, поэтому код для загрузки фрагмента неудача должна быть написана так

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);, в настоящее время идентификатор bb51. затем обратно в__webpack_require__функция. упомянутый ранее__webpack_require__Функция функции заключается в выполнении модуля. Узел с id bb51 находится в 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];
        }
    }
}

5. Проанализируйте 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 в модулях в app.js.

посмотри снова__webpack_require__В функции есть этот код

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

Таким образом, модуль в Home.js выполняется, и в модуле есть ряд методов для рендеринга страниц, а также рендерится страница компонента маршрутизации Home.vue.

На этом весь процесс ленивой загрузки компонентов маршрутизации завершен, а также подробно рассказывается, как загружать чанки и как выполнять модули.