Статья синхронизирована сGithub blog
Возможно, вы научились использовать Webpack и примерно знаете, как он работает, но задумывались ли вы когда-нибудь, как выглядит пакет bundle.js, выводимый Webpack? Почему исходные файлы модуля были объединены в один файл? Почему bundle.js запускается прямо в браузере?
Простая упаковка проекта
Давайте создадим проект, написанный в модуляризации CommonJS через Webpack.У проекта есть веб-страница, которая отображает Hello, Webpack на веб-странице через JavaScript.
Перед запуском сборки сначала создайте самые основные файлы JavaScript и HTML для завершения функции.Необходимы следующие файлы:
Файл входа страницы index.html
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<div id="app"></div>
<!--导入 Webpack 输出的 JavaScript 文件-->
<script src="./dist/bundle.js"></script>
</body>
</html>
Файл функции инструмента JS show.js
// 操作 DOM 元素,把 content 显示到网页上
function show(content) {
window.document.getElementById('app').innerText = 'Hello,' + content;
}
// 通过 CommonJS 规范导出 show 函数
module.exports = show;
Файл входа выполнения JS main.js
// 通过 CommonJS 规范导入 show 函数
const show = require('./show.js');
// 执行 show 函数
show('Webpack');
Webpack будет читать конфигурацию из файла webpack.config.js в корневом каталоге проекта по умолчанию при выполнении сборки, поэтому вам также необходимо создать новый со следующим содержимым:
const path = require('path');
module.exports = {
// JavaScript 执行入口文件
entry: './main.js',
output: {
// 把所有依赖的模块合并输出到一个 bundle.js 文件
filename: 'bundle.js',
// 输出文件都放到 dist 目录下
path: path.resolve(__dirname, './dist'),
}
};
Все файлы готовы, выполните команду webpack в корневом каталоге проекта для запуска сборки webpack, в каталоге вы найдете каталог dist, в нем есть файл bundle.js, файл bundle.js является исполняемым файлом JavaScript , который содержит страницу Два зависимых модуля main.js и show.js и встроенныйwebpackBootstrap
Стартовая функция. Затем вы открываете страницу index.html в своем браузере и видите Hello, Webpack.
Webpack – это инструмент для упаковки модульного JavaScript. Он начинается с main.js, идентифицирует оператор модульного импорта в исходном коде, рекурсивно находит все зависимости входного и выходного файлов и упаковывает вход и все его зависимости в один файл. . . . Начиная с Webpack2, была встроена поддержка модульных операторов ES6, CommonJS, AMD.
Анализ выходного кода
Давайте сначала посмотрим на содержимое файла bundle.js, собранного из простейшего проекта, код выглядит следующим образом:
(
// webpackBootstrap 启动函数
// modules 即为存放所有模块的数组,数组中的每一个元素都是一个函数
function (modules) {
// 安装过的模块都存放在这里面
// 作用是把已经加载过的模块缓存在内存中,提升性能
var installedModules = {};
// 去数组中加载一个模块,moduleId 为要加载模块在数组中的 index
// 作用和 Node.js 中 require 语句相似
function __webpack_require__(moduleId) {
// 如果需要加载的模块已经被加载过,就直接从内存缓存中返回
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 如果缓存中不存在需要加载的模块,就新建一个模块,并把它存在缓存中
var module = installedModules[moduleId] = {
// 模块在数组中的 index
i: moduleId,
// 该模块是否已经加载完毕
l: false,
// 该模块的导出值
exports: {}
};
// 从 modules 中获取 index 为 moduleId 的模块对应的函数
// 再调用这个函数,同时把函数需要的参数传入
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// 把这个模块标记为已加载
module.l = true;
// 返回这个模块的导出值
return module.exports;
}
// Webpack 配置中的 publicPath,用于加载被分割出去的异步代码
__webpack_require__.p = "";
// 使用 __webpack_require__ 去加载 index 为 0 的模块,并且返回该模块导出的内容
// index 为 0 的模块就是 main.js 对应的文件,也就是执行入口模块
// __webpack_require__.s 的含义是启动模块对应的 index
return __webpack_require__(__webpack_require__.s = 0);
})(
// 所有的模块都存放在了一个数组里,根据每个模块在数组的 index 来区分和定位模块
[
/* 0 */
(function (module, exports, __webpack_require__) {
// 通过 __webpack_require__ 规范导入 show 函数,show.js 对应的模块 index 为 1
const show = __webpack_require__(1);
// 执行 show 函数
show('Webpack');
}),
/* 1 */
(function (module, exports) {
function show(content) {
window.document.getElementById('app').innerText = 'Hello,' + content;
}
// 通过 CommonJS 规范导出 show 函数
module.exports = show;
})
]
);
Кажущийся сложным код выше на самом деле является функцией немедленного выполнения, которую можно сократить следующим образом:
(function(modules) {
// 模拟 require 语句
function __webpack_require__() {
}
// 执行存放所有模块数组中的第0个模块
__webpack_require__(0);
})([/*存放所有模块的数组*/])
Причина, по которой bundle.js может работать непосредственно в браузере, по которому файл выводится__webpack_require__
Функция определяет функцию загрузки, которую можно выполнить в браузере для имитации оператора require в Node.js.
Оказалось, что отдельные файлы модулей были объединены в единый bundle.js. Причина в том, что браузер не может загружать файлы модулей локально так же быстро, как Node.js, а должен загружать файлы, которые еще не были получены через сетевые запросы. Если количество модулей велико, время загрузки будет очень долгим, поэтому все модули хранятся в массиве и выполняется сетевая загрузка.
Если внимательно проанализировать__webpack_require__
В реализации функции вы также обнаружили, что Webpack сделал оптимизацию кеширования: загруженный модуль не будет выполняться второй раз, результат выполнения будет закэширован в памяти, а при повторном обращении к модулю он перейти непосредственно в память. Прочитать кэшированное возвращаемое значение.
нагрузка по требованию
При оптимизации загрузки по требованию для одностраничных приложений обычно используются следующие принципы:
- Разделите весь веб-сайт на небольшие функции, а затем сгруппируйте их по категориям в зависимости от того, насколько актуальна каждая функция.
- Объедините каждый класс в фрагмент и загрузите соответствующий фрагмент по требованию.
- Для функций, соответствующих экранам, которые пользователи должны видеть при первом открытии вашего веб-сайта, не загружайте их по запросу, а поместите их в блок, где находится запись выполнения, чтобы сократить время загрузки страницы, которое пользователи могут понимать.
- Отдельные функциональные точки, использующие большой объем кода, например использующие Chart.js для рисования диаграмм и flv.js для воспроизведения видео, можно загружать по требованию.
Для загрузки разделенного кода требуется определенное время, то есть, когда пользователь выполняет или собирается использовать соответствующую функцию, соответствующий код загружается. Время загрузки отдельного кода должно быть измерено и определено разработчиком в соответствии с потребностями веб-страницы.
Поскольку код, который разделен для загрузки по запросу, также требует времени для загрузки, вы можете предсказать, что пользователь может делать дальше, и загрузить соответствующий код заранее, чтобы пользователь не воспринимал время загрузки сети.
Использование Webpack для загрузки по запросу Webpack имеет встроенную мощную функцию разделения кода для загрузки по запросу, которую очень просто реализовать.
Например, сейчас нужно сделать такую страницу, оптимизированную под загрузку по требованию:
Когда веб-страница загружается в первый раз, загружается только файл main.js, и на веб-странице отображается кнопка.Файл main.js содержит только код для отслеживания событий кнопки и загрузки загрузки по требованию.
При нажатии кнопки загружается разделенный файл show.js, и функции в show.js выполняются после успешной загрузки.
Содержимое файла main.js выглядит следующим образом:
window.document.getElementById('btn').addEventListener('click', function () {
// 当按钮被点击后才去加载 show.js 文件,文件加载成功后执行文件导出的函数
import(/* webpackChunkName: "show" */ './show').then((show) => {
show('Webpack');
})
});
Содержимое файла show.js следующее:
module.exports = function (content) {
window.alert('Hello ' + content);
};
Самое критичное предложение в кодеimport(/* webpackChunkName: "show" */ './show')
, Webpack имеет встроенную поддержкуimport(*)
Поддержка операторов, когда Webpack встречает подобный оператор, он обрабатывает его следующим образом:
- Создайте новый чанк с ./show.js в качестве записи;
- Когда код выполняется для оператора импорта, файл, соответствующий фрагменту, будет загружен.
- import возвращает обещание, когда файл успешно загружен, содержимое, экспортированное show.js, может быть получено в методе then обещания.
После разделения кода с помощью import() ваш браузер должен поддерживать Promise API, чтобы код работал правильно, потому что import() возвращает Promise, который зависит от Promise. Для браузеров, которые изначально не поддерживают промисы, вы можете внедрить полифилл промисов.
/* webpackChunkName: "show" */
Смысл в том, чтобы дать имя динамически сгенерированному фрагменту, чтобы облегчить нам трассировку и отладку кода. Если вы не укажете имя динамически сгенерированного чанка, именем по умолчанию будет [id].js./* webpackChunkName: "show" */
Это новая функция, представленная в Webpack 3. До Webpack 3 было невозможно давать имена динамически генерируемому чанку.
Анализ выходного кода загрузки по требованию
Выходные файлы Webpack изменятся, когда будет принят метод оптимизации загрузки по запросу.
Например, измените main.js в исходном коде на следующее:
// 异步加载 show.js
import('./show').then((show) => {
// 执行 show 函数
show('Webpack');
});
После перестроения будут выведены два файла, а именно файл записи выполнения bundle.js и файл асинхронной загрузки 0.bundle.js.
Содержимое 0.bundle.js следующее:
// 加载在本文件(0.bundle.js)中包含的模块
webpackJsonp(
// 在其它文件中存放着的模块的 ID
[0],
// 本文件所包含的模块
[
// show.js 所对应的模块
(function (module, exports) {
function show(content) {
window.document.getElementById('app').innerText = 'Hello,' + content;
}
module.exports = show;
})
]
);
Содержимое bundle.js следующее:
(function (modules) {
/***
* webpackJsonp 用于从异步加载的文件中安装模块。
* 把 webpackJsonp 挂载到全局是为了方便在其它文件中调用。
*
* @param chunkIds 异步加载的文件中存放的需要安装的模块对应的 Chunk ID
* @param moreModules 异步加载的文件中存放的需要安装的模块列表
* @param executeModules 在异步加载的文件中存放的需要安装的模块都安装成功后,需要执行的模块对应的 index
*/
window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
// 把 moreModules 添加到 modules 对象中
// 把所有 chunkIds 对应的模块都标记成已经加载成功
var moduleId, chunkId, i = 0, resolves = [], result;
for (; i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if (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];
}
}
while (resolves.length) {
resolves.shift()();
}
};
// 缓存已经安装的模块
var installedModules = {};
// 存储每个 Chunk 的加载状态;
// 键为 Chunk 的 ID,值为0代表已经加载成功
var installedChunks = {
1: 0
};
// 模拟 require 语句,和上面介绍的一致
function __webpack_require__(moduleId) {
// ... 省略和上面一样的内容
}
/**
* 用于加载被分割出去的,需要异步加载的 Chunk 对应的文件
* @param chunkId 需要异步加载的 Chunk 对应的 ID
* @returns {Promise}
*/
__webpack_require__.e = function requireEnsure(chunkId) {
// 从上面定义的 installedChunks 中获取 chunkId 对应的 Chunk 的加载状态
var installedChunkData = installedChunks[chunkId];
// 如果加载状态为0表示该 Chunk 已经加载成功了,直接返回 resolve Promise
if (installedChunkData === 0) {
return new Promise(function (resolve) {
resolve();
});
}
// installedChunkData 不为空且不为0表示该 Chunk 正在网络加载中
if (installedChunkData) {
// 返回存放在 installedChunkData 数组中的 Promise 对象
return installedChunkData[2];
}
// installedChunkData 为空,表示该 Chunk 还没有加载过,去加载该 Chunk 对应的文件
var promise = new Promise(function (resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
installedChunkData[2] = promise;
// 通过 DOM 操作,往 HTML head 中插入一个 script 标签去异步加载 Chunk 对应的 JavaScript 文件
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.charset = 'utf-8';
script.async = true;
script.timeout = 120000;
// 文件的路径为配置的 publicPath、chunkId 拼接而成
script.src = __webpack_require__.p + "" + chunkId + ".bundle.js";
// 设置异步加载的最长超时时间
var timeout = setTimeout(onScriptComplete, 120000);
script.onerror = script.onload = onScriptComplete;
// 在 script 加载和执行完成时回调
function onScriptComplete() {
// 防止内存泄露
script.onerror = script.onload = null;
clearTimeout(timeout);
// 去检查 chunkId 对应的 Chunk 是否安装成功,安装成功时才会存在于 installedChunks 中
var chunk = installedChunks[chunkId];
if (chunk !== 0) {
if (chunk) {
chunk[1](new Error('Loading chunk ' + chunkId + ' failed.'));
}
installedChunks[chunkId] = undefined;
}
};
head.appendChild(script);
return promise;
};
// 加载并执行入口模块,和上面介绍的一致
return __webpack_require__(__webpack_require__.s = 0);
})
(
// 存放所有没有经过异步加载的,随着执行入口文件加载的模块
[
// main.js 对应的模块
(function (module, exports, __webpack_require__) {
// 通过 __webpack_require__.e 去异步加载 show.js 对应的 Chunk
__webpack_require__.e(0).then(__webpack_require__.bind(null, 1)).then((show) => {
// 执行 show 函数
show('Webpack');
});
})
]
);
Здесь bundle.js очень похож на упомянутый выше bundle.js, разница в следующем:
Еще один __webpack_require __. E используется для загрузки файлов, соответствующих кускам, которые разделены и должны быть загружены асинхронно;
Еще одна функция webpackJsonp для установки модулей из асинхронно загружаемых файлов.
При использовании CommonsChunkPlugin для извлечения общего кода выходной файл совпадает с выходным файлом при использовании асинхронной загрузки, будут __webpack_require__.e и webpackJsonp. Причина в том, что извлечение общего кода и асинхронная загрузка по сути являются разделением кода.