Когда файл на странице слишком велик и не обязательно используется, мы хотим начать загрузку, когда он используется, то есть загрузку по требованию. Чтобы добиться загрузки по запросу, мы обычно думаем о методе: динамически создаем тег script и указываем атрибут src на соответствующий путь к файлу. Но в процессе реализации возникают следующие проблемы:
- Как сделать так, чтобы один и тот же файл загружался только один раз?
- Как я могу узнать, загружен ли файл?
- После того, как файл загружен, как уведомить все места, куда файл импортирован?
Загрузка веб-пакета по требованию прекрасно решила вышеуказанные проблемы.С целью обучения я решил глубоко изучить принцип реализации загрузки веб-пакета по требованию.
Когда дело доходит до динамического разделения кода, webpack предлагает два похожих метода. Для динамического импорта первым и предпочтительным способом является использованиеПредложение ECMAScriptизimport()
грамматика. Во-вторых, использовать специфичные для веб-пакетаrequire.ensure
. Эта статья основана на официальной рекомендацииimport()
грамматика
Начнем с самого простого примера
пример
Есть два файла.В входном файле index.js файл a.js импортируется асинхронно с помощью метода import().
основная среда
webpack 4.43.0
веб-пакет настроен как:
const path = require('path');
module.exports = {
mode: 'development',
entry: './index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
код
index.js
import('./a').then((data) => {
console.log(data);
});
a.js
const a = 'a模块';
export default a;
После того, как пакет станет кодом веб-пакета для двух файлов:
bundle.js
// bundle.js 把index.js中的import()语句变成了这个样子
__webpack_require__
.e(/*! import() */ 0)
.then(__webpack_require__.bind(null, /*! ./a */ './a.js'))
.then((data) => {
console.log(data);
});
анализировать
Мы видим, что код, упакованный webpack, заменяет оператор import() на код, настроенный webpack.webpack_require.eфункция, начнем с этой функции:
webpack_require.e
// 定义installedChunks,用来存储加载过的js信息
/******/ var installedChunks = {
/******/ "main": 0
/******/ };
/******/ __webpack_require__.e = function requireEnsure(chunkId) {
/******/ // 定义一个存储promise的数组
/******/ var promises = [];
/******/
/******/ // JSONP chunk loading for javascript
/******/ // installedChunks为一个对象,用来存储加载过的js信息
/******/ var installedChunkData = installedChunks[chunkId];
/******/ if(installedChunkData !== 0) { // 0代表已经加载过了
/******/
/******/ // 如果已经存在不为0,则代表正在加载
/******/ if(installedChunkData) {
// installedChunkData[2]存储的是正在加载中的promise
/******/ promises.push(installedChunkData[2]);
/******/ } else {
/******/ // 定义一个promise
/******/ var promise = new Promise(function(resolve, reject) {
/******/ installedChunkData = installedChunks[chunkId] = [resolve, reject];
/******/ });
// 存储promise
/******/ promises.push(installedChunkData[2] = promise);
/******/
/******/ // 创建script标签,开始加载js
/******/ var script = document.createElement('script');
/******/ var onScriptComplete;
/******/
/******/ script.charset = 'utf-8';
// 设置一个超时时间
/******/ script.timeout = 120;
/******/ if (__webpack_require__.nc) {
/******/ script.setAttribute("nonce", __webpack_require__.nc);
/******/ }
// 获取src,并赋值
/******/ script.src = jsonpScriptSrc(chunkId);
/******/
/******/ // 创建一个error,在加载出错后返回
/******/ var error = new Error();
// 定义加载完成后的时间
/******/ onScriptComplete = function (event) {
/******/ // avoid mem leaks in 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);
// 加载成功和失败都走onScriptComplete,具体原因看下文
/******/ script.onerror = script.onload = onScriptComplete;
/******/ document.head.appendChild(script);
/******/ }
/******/ }
// 返回promise
/******/ return Promise.all(promises);
/******/ };
Всего этот код делает следующее:
- определитьмассив обещаний, используемый для хранения промисов.
- Определите, был ли он загружен, если он загружен, верните пустой массив promise.all().
- Если загружается, вернутьсяСохраненное обещание, соответствующее этому файлу.
- Если не загружен, сначалаОпределите обещание, затем создайте тег script, загрузите этот js и определите обратные вызовы успеха и отказа.
- вернуть обещание
Просто взглянув на эту функцию, у нас могут возникнуть некоторые вопросы:
- Судить о том, был ли он загружен или нет, можно с помощью суждения
installedChunks[chunkId]
имеет значение 0, но вscript.onerror/script.onload
Функция обратного вызова не ставитinstalledChunks[chunkId]
значение установлено на 0 promise
Пучокresolve
а такжеreject
все депонированоinstalledChunks
, не удается получить асинхронные чанкиonload
выполнить в обратном вызовеresolve
,Так,resolve
Когда оно было исполнено?
Для этих двух проблем нам нужно посмотреть на другой упакованный файл:
0.bundle.js
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{
/***/ "./a.js":
/*!**************!*\
!*** ./a.js ***!
\**************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\nconst a = 'a模块';\r\n/* harmony default export */ __webpack_exports__[\"default\"] = (a);\n\n//# sourceURL=webpack:///./a.js?");
/***/ })
}]);
Мы видим, что в этом файле выполнениеwindow["webpackJsonp"].push()
метод, то есть каждый раз, когда файл загружается, глобальныйwebpackJsonp
массивpush
этот метод push является ключевым:
bundle.js
/******/ // 定义全局数组window["webpackJsonp"],并重写window["webpackJsonp"]的push方法
/******/ var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
/******/ var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
/******/ jsonpArray.push = webpackJsonpCallback;
// 重写window["webpackJsonp"]的push方法
/******/ function webpackJsonpCallback(data) {
/******/ var chunkIds = data[0];
/******/ var moreModules = data[1];
/******/
/******/
/******/ // add "moreModules" to the modules object,
/******/ // then flag all "chunkIds" as loaded and fire callback
/******/ var moduleId, chunkId, i = 0, resolves = [];
/******/ for(;i < chunkIds.length; i++) {
/******/ chunkId = chunkIds[i];
/******/ if(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
// 获取此js文件对应的promise中的resolve方法数组
/******/ resolves.push(installedChunks[chunkId][0]);
/******/ }
// 把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);
/******/
// 执行此js文件对应的promise中的resolve方法
/******/ while(resolves.length) {
/******/ resolve.shift()();
/******/ }
/******/
/******/ };
bundle.js的这几段代码干了这几件事:
- определить глобальный массив
window["webpackJsonp"]
, и переписатьwindow["webpackJsonp"]
изpush
метод - В новом методе push поместите
installedChunks[chunkId]
Установите в 0, что означает, что он был загружен и обещание, соответствующее js, выполнено.resolve
метод
Наконец, давайте реорганизуем реализацию загрузки веб-пакета по требованию:
Суммировать
Наконец, давайте ответим на три вопроса в начале статьи:
1. Как сделать так, чтобы один и тот же файл загружался только один раз?
А: ОпределениеinstalledChunks
Объект, который хранит обратный вызов обещания асинхронного js или возвращает пустой массив, если он был загруженpromise.all([])
, если он находится в процессе загрузки, верните обещание, соответствующее этому сохраненному файлу.
2. Как определить, что загрузка файла завершена?
Ответ: 1. Определить глобальный массив в основном файле, переписать его метод push и выполнить метод push этого глобального массива в асинхронном файле.
2. Выполните обратный вызов разрешения обещания в переопределенном методе.
3. После того, как файл загружен, как оповестить все места, куда файл импортирован?
Ответ: То же, что и 2