Из недавнего сводного отчета об асинхронной загрузке подпакетов webpack для бизнес-проектов
Схема выглядит следующим образом:
- Связанные концепции
- конфигурация подпакета webpack
- Как webpack асинхронно загружает подпакеты?
Связанные концепции
- Концептуальный модуль, кусок, пучок
Во-первых, волна объяснений существительных. Во-первых, картинка в Интернете объясняет:
Понятия этих существительных можно четко различить на схеме:1,module
: каждый файл в нашем исходном каталоге рассматривается какmodule
для обработки (типы файлов, которые изначально не поддерживаются веб-пакетом, реализуются загрузчиком).module
сформированныйchunk
.
2,chunk
.webpack
Продукция в процессе упаковки, в общем случае по умолчанию (без учета субподряда и т. д.), xwebpack
изentry
выведет хbundle
.
3.bundle
.webpack
Окончательный вывод можно запустить прямо в браузере. Как видно из рисунка, в случае извлечения css (конечно, это могут быть и картинки, файлы шрифтов и т.chunk
будет выводить несколькоbundle
, но по умолчанию обычноchunk
будет выводить толькоbundle
-
hash
,chunkhash
,contenthash
Демо-демонстрации здесь нет, в интернете уже есть много подобных демонстраций.
хэш. Все пакеты используют одно и то же значение хеш-функции, связанное с каждым процессом упаковки веб-пакетов.
содержаниехэш. Расчеты относятся к самому содержимому файла.
советы: Следует отметить, что в热更新
режим, приведет кchunkhash
а такжеcontenthash
Ошибка расчета, произошла ошибка (Cannot use [chunkhash] or [contenthash] for chunk in '[name].[chunkhash].js' (use [hash] instead)
). Поэтому его можно использовать только при горячем обновлении.hash
режим или не использоватьhash
. В производственной среде мы обычно используемcontenthash
илиchunkhash
.
Сказав все это, в чем преимущество использования асинхронной загрузки/загрузки подпакетов. Проще говоря, существуют следующие
1. Лучшее использование кеша браузера. Если у нас большой проект и не используются подпакеты, для каждого пакета будет сгенерирован только один js-файл, при условии, что упакованный js имеет 2 МБ. Когда ежедневный код выпущен, мы можем просто изменить одну строку кода, но поскольку содержимое изменилось, хеш-значение упакованного js также изменилось. В это время браузер перезагрузит файл js размером 2 МБ. Если используется субподряд, отделяются несколько чанков, и модифицируется строка кода, то затрагивается только хеш этого чанка (строго говоря, здесь может быть несколько хэшей, которые меняются без извлечения мэйнифеста), остальные хэши не изменяются . Это может воспользоваться кешем кода, который не меняет хэш
2. Более высокая скорость загрузки. Предполагая, что для входа на страницу необходимо загрузить 2 МБ js, после извлечения подпакета при входе на эту страницу может загружаться четыре 500 КБ js. Мы знаем, что максимальное количество одновременных запросов, которые браузер может сделать к одному и тому же доменному имени, равно 6 (поэтому веб-пакетmaxAsyncRequests
Значение по умолчанию — 6), так что четыре js по 500 КБ будут загружены одновременно, что эквивалентно простой загрузке через них ресурса 500 КБ, и скорость будет соответственно улучшена.
3. Если код загружается асинхронно отложенно. Для некоторого кода, который можно использовать только в определенных местах, он загружается только тогда, когда он используется, что также может сэкономить трафик.
конфигурация подпакета webpack
Перед этим давайте подчеркнем концепцию один раз,splitChunk
, дляchunk
, нетmodule
.对于同一个 chunk 中,无论一个代码文件被同 chunk 引用了多少次,它都还是算 1 次。只有一个代码文件被多个 chunk 引用,才算是多次。
Подпакет по умолчанию для веб-пакетанастроитьследующим образом
module.exports = {
optimization: {
splitChunks: {
// **`splitChunks.chunks: 'async'`**。表示哪些类型的chunk会参与split。默认是异步加载的chunk。值还可以是`initial`(表示入口同步chunk)、`all`(相当于`initial`+`async`)。
chunks: "async",
// minSize 表示符合代码分割产生的新生成chunk的最小大小。默认是大于30kb的才会生成新的chunk
minSize: 30000,
// maxSize 表示webpack会尝试将大于maxSize的chunk拆分成更小的chunk,拆解后的值需要大于minSize
maxSize: 0,
// 一个模块被最少多少个chunk共享时参与split
minChunks: 1,
// 最大异步请求数。该值可以理解为一个异步chunk,被抽离出同时加载的chunk数不超过该值。若为1,该异步chunk将不会抽离出任意代码块
maxAsyncRequests: 5,
// 入口chunk最大请求数。在多entry chunk的情况下会用到,表示多entry chunk公共代码抽出的最大同时加载的chunk数
maxInitialRequests: 3,
// 初始chunk最大请求数。
// 多个chunk拆分出小chunk时,这个chunk的名字由多个chunk与连接符组合成
automaticNameDelimiter: "~",
// 表示chunk的名字自动生成(由cacheGroups的key、entry名字)
name: true,
// cacheGroups 表示分包分组规则,每一个分组会继承于default
// priority表示优先级,一个chunk可能被多个分组规则命中时,会使用优先级较高的
// test提供时 表示哪些模块会被抽离
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
// 复用已经生成的chunk
reuseExistingChunk: true
}
}
}
}
};
Еще одна важная конфигурацияoutput.jsonpFunction
(по умолчаниюwebpackJsonp
). Это глобальная переменная, используемая при асинхронной загрузке чанков. Если существует несколько сред веб-пакетов, чтобы предотвратить конфликт имени функции, который может вызвать проблемы, лучше всего установить для него относительно уникальное значение.
Вообще говоря, нет идеальной конфигурации субподрядчиков, только наиболее подходящей конфигурации для текущих требований к сценарию проекта. Много раз на конфигурации по умолчанию достаточно.
Вообще говоря, для обеспечения стабильности хэша рекомендуется:
1,webpack.HashedModuleIdsPlugin
.这个插件会根据模块的相对路径生成一个四位数的 hash 作为模块 id。默认情况下 webpack 是使用模块数字自增 id 来命名,当插入一个模块占用了一个 id(或者一个删去一个模块)时,后续所有的模块 id 都受到影响,导致模块 id 变化引起打包文件的 hash 变化。使用这个插件就能解决这个问题。
2,chunk
id автоматически увеличивается, так же могут возникнуть проблемы с id модуля. Вы можете установитьoptimization.namedChunks
имеет значение true (true в режиме dev по умолчанию, false в режиме prod),chunk
имя с помощьюchunk
.
Эффект после 1 и 2 следующий.
3. Извлеките использование cssmini-css-extract-plugin
. hash 模式使用contenthash
.
Вот пример страницы консоли Tencent Cloud, эффект асинхронной загрузки выглядит следующим образом после использования пути веб-пакета. Как видите, страница открывается впервые. Здесь нужно сначала запросить общую запись js, а затем загрузить код, относящийся к этому маршруту, в соответствии с маршрутом, который мы посещаем (маршрут 1). Здесь вы можете видеть, что количество js, которые мы загружаем асинхронно, равно 5, что эквивалентно элементу конфигурации по умолчанию, упомянутому выше.maxAsyncRequests
,пройти черезwaterfall
Вы можете видеть, что здесь есть параллельные запросы. Если вы введете другой маршрут (маршрут 2), будут загружены js только одного другого маршрута (или js поставщика, которые в данный момент не загружены). Если здесь изменить только собственный бизнес-код маршрута 1, хэш, связанный с поставщиком, и хэш других маршрутов не останутся без изменений, и эти файлы могут эффективно использовать кеш браузера.
Как webpack асинхронно загружает подпакеты?
Мы знаем, что по умолчанию js среды браузера не поддерживается.import
и асинхнimport('xxx').then(...)
из. Итак, как веб-пакет заставляет браузер поддерживать его, следующий код после построения веб-пакета для анализа, чтобы понять принцип, лежащий в его основе.
Структура экспериментального кода выглядит следующим образом
Развернуть для просмотра
// webpack.js const webpack = require("webpack"); const path = require("path"); const CleanWebpackPlugin = require("clean-webpack-plugin").CleanWebpackPlugin; const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = { entry: { a: "./src/a.js", b: "./src/b.js" }, output: { filename: "[name].[chunkhash].js", chunkFilename: "[name].[chunkhash].js", path: **dirname + "/dist", jsonpFunction: "_**jsonp" }, optimization: { splitChunks: { minSize: 0 } // namedChunks: true }, plugins: [ new CleanWebpackPlugin(), new HtmlWebpackPlugin() //new webpack.HashedModuleIdsPlugin() ], devServer: { contentBase: path.join(__dirname, "dist"), compress: true, port: 8000 } };
// src/a.js import { common1 } from "./common1"; import { common2 } from "./common2"; common1(); common2(); import(/_ webpackChunkName: "asyncCommon2" _/ "./asyncCommon2.js").then( ({ asyncCommon2 }) => { asyncCommon2(); console.log("done"); } );
// src/b.js import { common1 } from "./common1"; common1(); import(/_ webpackChunkName: "asyncCommon2" _/ "./asyncCommon2.js").then( ({ asyncCommon2 }) => { asyncCommon2(); console.log("done"); } );
// src/asyncCommon1.js export function asyncCommon1(){ console.log('asyncCommon1') } // src/asyncCommon2.js export function asyncCommon2(){ console.log('asyncCommon2') }
// ./src/common1.js export function common1() { console.log("common11"); } import(/_ webpackChunkName: "asyncCommon1" _/ "./asyncCommon1").then( ({ asyncCommon1 }) => { asyncCommon1(); } );
// src/common2.js export function common2(){ console.log('common2') }
// 入口文件 a.js
(function() {
//.....
function webpackJsonpCallback(data){
//....
}
// 缓存已经加载过的module。无论是同步还是异步加载的模块都会进入该缓存
var installedModules = {};
// 记录chunk的状态位
// 值:0 表示已加载完成。
// undefined : chunk 还没加载
// null :chunk preloaded/prefetched
// Promise : chunk正在加载
var installedChunks = {
a: 0
};
// 用于根据chunkId,拿异步加载的js地址
function jsonpScriptSrc(chunkId){
//...
}
// 同步import
function __webpack_require__(moduleId){
//...
}
// 用于加载异步import的方法
__webpack_require__.e = function requireEnsure(chunkId) {
//...
}
// 加载并执行入口js
return __webpack_require__((__webpack_require__.s = "./src/a.js"));
})({
"./src/a.js": function(module, __webpack_exports__, __webpack_require__) {
eval( ...); // ./src/a.js的文件内容
},
"./src/common1.js": ....,
"./src/common2.js": ...
});
Видно, что файл входа, запакованный webpack, является функцией немедленного выполнения, а параметр функции немедленного выполнения является синхронизацией функции входа.import
Объект модуля кода. Значение ключа — это имя пути, а значение значения — код, выполняющий соответствующий код модуля.eval
функция. В этой входной функции есть несколько важных переменных/функций.
-
webpackJsonpCallback
функция.加载异步模块完成的回调。 -
installedModules
Переменная. Кешировать уже загруженные модули. Модули, загруженные синхронно или асинхронно, попадут в этот кеш.key
идентификатор модуля,value
является объектом{ i: 模块id, l: 布尔值,表示模块是否已经加载过, exports: 该模块的导出值 }
. -
installedChunks
Переменная. Кэшировать состояние чанков, которые уже были загружены. Есть несколько битов состояния.0
Указывает, что он был загружен,undefined
чанк не загружен,null
: кусокpreloaded/prefetched
загружаемые модули,Promise
: чанк загружается -
jsonpScriptSrc
Переменная. Адрес js, используемый для возврата асинхронных фрагментов. если установленоwebpack.publicPath
(обычно доменное имя cdn, оно будет храниться в__webpack_require__.p
В), а также адрес до конечного адреса склейки -
__webpack_require__
функция. Синхронизироватьimport
вызов -
__webpack_require__.e
функция. асинхронныйimport
вызов
После того, как каждый модуль построен, он представляет собой функцию следующего вида, а входной параметр функцииmodule
В соответствии с соответствующим статусом текущего модуля (завершена ли загрузка, значение экспорта, идентификатор и т. д., упомянутые ниже),__webpack_exports__
Это экспорт текущего модуля (уже экспорт),__webpack_require__
Входной кусок__webpack_require__
функция дляimport
другой код
function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval(模块代码...);// (1)
}
eval
Код внутри выглядит следующим образом, сa.js
Например.
// (1)
// 格式化为js后
__webpack_require__.r(__webpack_exports__);
var _common1__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
"./src/common1.js"
);
var _common2__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(
"./src/common2.js"
);
// _common1__WEBPACK_IMPORTED_MODULE_0__是导出对象
// 执行导出的common1方法
// 源码js:
// import { common1 } from "./common1";
// common1();
Object(_common1__WEBPACK_IMPORTED_MODULE_0__["common1"])();
Object(_common2__WEBPACK_IMPORTED_MODULE_1__["common2"])();
__webpack_require__
.e("asyncCommon2")
.then(__webpack_require__.bind(null, "./src/asyncCommon2.js"))
.then(({ asyncCommon2 }) => {
asyncCommon2();
console.log("done");
});
Итак, вы можете знать
- Синхронизировать
import
В конечном итоге преобразуется в__webpack_require__
функция - асинхронный
import
в итоге превратился в__webpack_require__.e
метод
Весь процесс выполняется.
Входной файл сначала проходит через__webpack_require__((__webpack_require__.s = "./src/a.js"))
JS Loading вход (можно наблюдать верхнюю поверхностьinstalledChunked
Начальное значение переменной равно{a:0}
,), и поeval
Выполните код в a.js.
__webpack_require__
Можно сказать, что код появляется больше всего после сборки всего веб-пакета, затем__webpack_require__
что ты сделал.
function __webpack_require__(moduleId) {
// 如果一个模块已经import加载过了,再次import的话就直接返回
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 之前没有加载的话将它挂到installedModules进行缓存
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;
}
Здесь все очень интуитивно понятно, эта функция получаетmoduleId
, соответствующий параметру, переданному в функцию немедленного выполненияkey
стоимость. Если модуль был загружен ранее, значение экспорта модуля возвращается напрямую; если модуль не был загружен, модуль выполняется и кэшируется вinstalledModules
соответствующийmoduleId
в позиции ключа, а затем возвращает экспортированное значение модуля. Итак, в коде упаковки webpackimport
модуль несколько раз, модуль будет выполняться только один раз. Другое дело, что в модуле упаковки webpack по умолчаниюimport
а такжеrequire
то же самое, в конце концов оно превращается в__webpack_require__
.
Возвращаясь к классическому вопросу,webpack
Что произойдет, если в среде возникнет циклическая ссылка?a.js
есть одинimport x from './b.js'
,b.js
есть одинimport x from 'a.js'
. После столкновения выше__webpack_require__
анализ легко понять. Перед выполнением модуляwebpack
уже повесил трубкуinstalledModules
середина. Например, выполнитьa.js
Он вводит этоb.js
,b.js
повторно представлен вa.js
. В настоящее времяb.js
Введениеa
содержание только вa.js
Текущее время выполнения былоexport
из чего-то (потому что уже зависinstalledModules
, поэтому он не будет выполняться сноваa.js
).
После синхронной загрузки блок входа выполняетсяa.js
.
рядом сeval
выполнено в течениеa.js
Фрагмент кода модуля, который асинхронно загружает часть js.
// a.js模块
__webpack_require__
.e("asyncCommon2")
.then(__webpack_require__.bind(null, "./src/asyncCommon1.js")) // (1) 异步的模块文件已经被注入到立即执行函数的入参`modules`变量中了,这个时候和同步执行`import`调用`__webpack_require__`的效果就一样了
.then(({ asyncCommon2 }) => {
//(2) 就能拿到对应的模块,并且执行相关逻辑了(2)。
asyncCommon2();
console.log("done");
});
__webpack_require__.e
Что он делает, согласно входящемуchunkId
, чтобы загрузить этоchunkId
Соответствующий файл асинхронного фрагмента, который возвращаетpromise
. пройти черезjsonp
способ использованияscript
тег для загрузки. Эта функция вызывается несколько раз или инициируется только один запрос js. Если он был загружен, файл асинхронного модуля был введен во входной параметр немедленно выполняемой функции.modules
В переменной на этот раз и синхронное выполнениеimport
передача__webpack_require__
Эффект тот же (эту инъекцию делаетwebpackJsonpCallback
функция завершена). В это время вpromise
Вызывается снова в обратном вызове__webpack_require__.bind(null, "./src/asyncCommon1.js")
(1) Вы можете получить соответствующий модуль и выполнить соответствующую логику (2).
// __webpack_require__.e 异步import调用函数
// 再回顾下上文提到的 chunk 的状态位
// 记录chunk的状态位
// 值:0 表示已加载完成。
// undefined : chunk 还没加载
// null :chunk preloaded/prefetched
// Promise : chunk正在加载
var installedChunks = {
a: 0
};
__webpack_require__.e = function requireEnsure(chunkId) {
//...只保留核心代码
var promises = [];
var installedChunkData = installedChunks[chunkId];
if (installedChunkData !== 0) {
// chunk还没加载完成
if (installedChunkData) {
// chunk正在加载
// 继续等待,因此只会加载一遍
promises.push(installedChunkData[2]);
} else {
// chunk 还没加载
// 使用script标签去加载对应的js
var promise = new Promise(function(resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
promises.push((installedChunkData[2] = promise)); // start chunk loading
//
var script = document.createElement("script");
var onScriptComplete;
script.src = jsonpScriptSrc(chunkId);
document.head.appendChild(script);
//.....
}
// promise的resolve调用是在jsonpFunctionCallback中调用
return Promise.all(promises);
};
Давайте посмотрим на общую структуру кода для асинхронной загрузки чанка asyncCommon1 (то есть асинхронно загружаемого js). Что он делает, просто, просто перейдите кjsonpFunction
Глобальный массивpush
webpackJsonpCallback
функция) массив, состоящий изchunk
имя иchunk
Объекты модуля составляются вместе.
// asyncCommon1 chunk
(window["jsonpFunction"] = window["jsonpFunction"] || []).push([["asyncCommon1"],{
"./src/asyncCommon1.js":
(function(module, __webpack_exports__, __webpack_require__) {
eval(module代码....);
})
}]);
при выполненииwebpackJsonpCallback
время, когда мы проходимscript
Получите асинхронный блудник (да, потому что код запроса возвращается и выполняет код внутри хрупки Asyncpush
метод! ). Объедините код асинхронного фрагмента со следующимwebpackJsonpCallback
Легко узнать,webpackJsonpCallback
В основном сделайте несколько вещей:
1, будет асинхроннымchunk
Бит состояния равен 0, что указывает на то, что чанк был загружен.installedChunks[chunkId] = 0;
2 пары__webpack_require__.e
Соответствующий фрагмент, сгенерированный в обещании загрузки, разрешается
3. Будет асинхроннымchunk
Модуль монтируется на входchunk
Параметр функции немедленного выполненияmodules
середина. доступный__webpack_require__
чтобы получить. В анализе модуля a.js выше этот процесс уже упоминался.
//
function webpackJsonpCallback(data) {
var chunkIds = data[0];
var moreModules = data[1];
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]);
}
// 将当前chunk设置为已加载
installedChunks[chunkId] = 0;
}
for (moduleId in moreModules) {
if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
// 将异步`chunk`的模块 挂载到入口`chunk`的立即执行函数参数`modules`中
modules[moduleId] = moreModules[moduleId];
}
}
// 执行旧的jsonPFunction
// 可以理解为原生的数组Array,但是这里很精髓,可以防止撞包的情况部分模块没加载!
if (parentJsonpFunction) parentJsonpFunction(data);
while (resolves.length) {
// 对__webpack_require__.e 中产生的相应的chunk 加载promise进行resolve
resolves.shift()();
}
}
Краткое содержание:
1. После упаковки webpack файлы модулей в каждом чанке объединяются в такую форму, как
{
[moduleName:string] : function(module, __webpack_exports__, __webpack_require__){
eval('模块文件源码')
}
}
2, такая же страница несколько современных среды WebPack,output.jsonpFunction
Старайтесь не натыкаться на имена. Обычно он не зависает, если вы нажмете на него. Он просто немедленно выполняет входные параметры функцииmodules
Повесьте некоторый код модуля, асинхронно загруженный другими средами веб-пакета. (может быть, причина некоторого увеличения памяти?)
3. Каждая запись фрагмента записи является аналогичной функцией немедленного выполнения.
(function(modules){
//....
})({
[moduleName:string] : function(module, __webpack_exports__, __webpack_require__){
eval('模块文件源码')
}
})
4. За асинхронной загрузкой стоит использованиеscript
тег для загрузки кода
5. Асинхронная загрузка не такая загадочная, и она может иметь лучший эффект, когда проект в определенной степени большой.
(Ограничено, если какая-либо ошибка, пожалуйста, Paizhuan)