Недавно я узнал об использовании webpack4 и попытался обновить и оптимизировать веб-пакет проекта, и записал некоторые практические процессы этого обновления.
Болезненный опыт разработки
долгое ожидание
Проект представил webpack в качестве инструмента для упаковки в 2016 году и использовал vue-cli для создания кода, связанного со сборкой, и с тех пор не было никаких серьезных обновлений. С итерацией проекта до сих пор объем кода больше не составляет несколько тысяч строк в год, а локальная среда разработки стартапов увеличилась с более чем дюжины секунд в прошлом до более чем 200 секунд сейчас. или ребилд сопровождается долгим ожиданием с мутными глазами.
Царство хаоса
В течение более чем двух лет код сборки проекта неоднократно изменялся бесчисленным количеством людей, полный избыточности, беспорядка и логики, которую может понять только Бог. Когда мой коллега работал над функцией темы, ему пришлось начинать заново, он открыл небольшой проект, собранный с помощью webpack4, и спрятал его в угол под папку css, ожидая, когда проект снова появится в будущем с унификацией веб-пакет4. Сосуществование webpack2 и webpack4 вынуждает каждого маленького партнера в команде открывать два терминала, один для запуска проекта, а другой для запуска стиля.
В двух словах:Сборка слишком медленная, код сборки хаотичен, эффективность разработки друзей низкая
Как с этим бороться? Хватит нести чушь, быстро обновите webpack4
Повышение эффективности сборки
Улучшение эффективности упаковки можно описать двумя способами:
- Улучшить скорость упаковки в единицу времени
- Очистите ненужные упакованные файлы
Многосторонний подход: happyPack
Как и название этого плагина, он действительно может сделать людей счастливыми после его использования.Скорость упаковки не улучшается ни на одну звезду.Принцип заключается в параллельном открытии нескольких подпроцессов узла и использовании различных загрузчиков для обработки исходных файлов. быть упакованным, другими словамиУлучшить скорость упаковки в единицу времени
Цитируя официальное заявление happyPack:
HappyPack sits between webpack and your primary source files (like JS sources) where the bulk of loader transformations happen. Every time webpack resolves a module, HappyPack will take it and all its dependencies and distributes those files to multiple worker "threads".
Those threads are actually simple node processes that invoke your transformer. When the compiled version is retrieved, HappyPack serves it to its loader and eventually your chunk.
Производительность моего собственного ноутбука лучше, чем у компьютера компании.Ноутбук компании работает более 200 с по конфигурации webpack2.После перезагрузки компьютера первая сборка даже заняла 5 минут.
- Требуется время, чтобы запустить проект локально с помощью webpack2
- Использование webpack4 для локального запуска требует времени
- WebPack4 + Happypack (Babel-loader) требует времени, чтобы начать локально
- webpack4 + happyPack (babel-loader + eslint-loader) требует времени для локального запуска
Из экспериментальных результатов видно, что скорость компиляции очень очевидна после использования happyPack.Сокращение времени почти на 55 %, эффект оптимизации значителен
happyPack поддерживает множество часто используемых загрузчиков (список совместимости happyPack), вы можете использовать несколько экземпляров happyPack в конфигурации веб-пакета и обрабатывать их отдельно с помощью разных загрузчиков, таких как eslint-loader и babel-loader для файлов .js последовательно, и вы можете создать ThreadPool через happyPack, чтобы эти экземпляры happyPack совместно использовали поток пул, улучшить использование ресурсов.
Что касается настройки и использования happyPack, официальная документация очень ясна, Baidu также имеет большое количество обучающих статей для справки, которые не будут здесь подробно описываться.
Удалите стороннюю библиотеку с помощью dll
Неизбежно, что в проекте будут использоваться какие-то сторонние библиотеки, пока версия не будет обновлена, в целом код этих библиотек сильно не изменится, а значит эти библиотеки не должны участвовать в процессе сборки и каждый раз переделывать.. Если эту часть кода можно извлечь и собрать заранее, то стороннюю библиотеку можно пропустить непосредственно при сборке проекта для дальнейшего повышения эффективности, другими словамиОчистите ненужные упакованные файлы.
dllPlugin+dllReferencePlugin
DLL — это способ для Microsoft реализовать концепцию библиотеки общих функций (как говорится в энциклопедии Baidu), которая не может выполняться сама по себе и может вызываться другими программами. Опираясь на идею dll здесь, webpack предоставляет встроенные плагины dllPlugin+dllReferencePlugin, которые можно легко сделать, просто сделайте эти вещи хорошо:
- Самостоятельно создайте набор конфигурации веб-пакета webpack.dll.conf, используйте dllPlugin для определения файла dll для упаковки.
- Запустите webpack.dll.conf для создания
xxx.dll.js
и соответствующий файл манифестаmanifest-xxx.json
, и введите каждый элемент в шаблон проекта index.htmlxxx.dll.js
- В конфигурации веб-пакета проекта через dllReferencePlugin и
manifest-xxx.json
Сообщите webpack, какие пакеты были собраны заранее, не нужно повторять сборку.
webpack4 + happyPack (xxx-loader) + локальный запуск dll занимает много времени
С 71-го по 45-й это еще одно большое улучшение,Время было дополнительно сокращено почти на 40%, по сравнению с первоначальным временем компиляции webpack2,Эффективность увеличилась на 71%, даже если использовать книгу компании, эффективность может быть увеличена как минимум на 50%. Увидев такой результат, первой реакцией автора было: БЛЯДЬ! ! ! Что ж, в этом вздохе больше удивления, чем удивления по поводу результатов, но больше ужасающей неэффективности предыдущих сборок.
Немного оптимизировать производительность
Помимо сокращения времени упаковки кода, использование библиотек DLL также помогает оптимизировать производительность веб-страницы. Обычно мы извлекаем стороннюю библиотеку в блок кода с именем vendors.Преимущество этого состоит в том, чтобы предотвратить повторную упаковку общедоступных зависимостей, и в то же время частота ее изменений низкая, и она имеет относительно стабильное значение хеш-функции в производственной среде. В полной мере используйте стратегию кэширования браузера, чтобы уменьшить количество запросов на файлы поставщиков. Однако это может привести к тому, что размер одного js-файла будет слишком большим, что приведет к очевидной блокировке при повторном запросе ресурсов. После использования dll, поскольку большое количество сторонних библиотек извлекается заранее, размер вендоров соответственно уменьшается, и соответственно уменьшаются сетевые накладные расходы на запрос файлов вендоров
Не используйте dll, объем поставщиков
Объем вендоров после использования dllУ некоторых студентов могут возникнуть сомнения.Хотя объем продавцов уменьшается, уменьшенная часть просто перемещается в другое место и извлекается вxxx.dll.js
Он только в файле, запрос по-прежнему запрашивается, а общий оверхед не уменьшился. На самом деле, саму dll можно продолжать разбивать, настроив несколько записей, и дополнительно оптимизировать производительность запрошенного dll-файла за счет одновременных запросов браузера.
{
entry: {
vue: ['vue', 'vuex', 'vue-router'], // vue全家桶dll: vue.dll.js
ec: ['echarts', 'echarts-wordcloud'], // echarts相关dll: ec.dll.js
commons: [
// 其他第三方库: commons.dll.js
]
}
}
Конечно, даже в среде разработки пакет вендора 3.88M все равно очень большой.Здесь просто показать эффект от зачистки сторонних библиотек через dll.Сегментация кода и сопутствующие оптимизации здесь подробно не обсуждаются.
некоторые ямы
Что касается настройки и использования плагинов, следует отметить, что в webpack.dll.conf имя библиотеки, отображаемое в выводе, должно совпадать с именем DllPlugin, что также подчеркивается в официальной документации.
{
output: {
filename: '[name].dll.js',
path: path.resolve(__dirname, '..', 'lib/dll'),
library: '[name]_[hash]'
// vendor.dll.js中暴露出的全局变量名,DllPlugin中会使用此名称作为manifest的name,
// 故这里需要和webpack.DllPlugin中的 name: '[name]_[hash]' 保持一致。
},
plugins: [
new webpack.DllPlugin({
path: utils.resolve('lib/dll/manifest-[name].json'),
name: "[name]_[hash]" // 和library保持一致
})
]
}
Кроме того, Vue по умолчанию использует пакет времени выполнения.В среде разработки, если вам нужно, например, скомпилировать шаблон с помощью Vue, используйте:
new Vue({
template: '<div>{{ hi }}</div>'
})
Затем вы должны импортировать полную версию пакета vue, который необходимо прописать в конфигурации псевдонима веб-пакета (Обратитесь к документации vue.):
module.exports = {
// ...
resolve: {
alias: {
'vue$': 'vue/dist/vue.esm.js' // 用 webpack 1 时需用 'vue/dist/vue.common.js'
}
}
}
Это также означает, что ссылка на vue в webpack.dll.conf должна соответствовать проекту.В противном случае упаковка vue не будет пропущена при сборке проекта.
оdllPlugin
а такжеdllReferencePlugin
Для конкретной настройки и использования этих двух плагинов официальная документация дает примеры использования Baidu также имеет большое количество обучающих статей для справки, которые здесь не будут подробно представлены.
dll ускорение и исследование манифеста
如果只是想了解如何提升构建效率,那么这部分可以直接跳过了
После того, как автор завершает настройку, она сразу не достигает уровня 45s.Эффект не очень хороший при первом запуске, и нет явного улучшения эффективности.Что вы делаете после долгого времени ? После добавления dll время упаковки существенно не сокращается, что указывает на наличие сторонних библиотек, которые вошли в процесс упаковки. В вебпаке есть понятие манифеста, я знаю только, что он связан с маппингом и загрузкой модулей, а конкретное наполнение мне не известно, поэтому я просто догадался, что это связано с этим, и продолжил расследование по ходу дела. эта дорога. Конечно, при использовании dllReferencePlugin несколько файлов manifest.json были опущены, это чисто из-за того, что автор был небрежен и невнимательно прочитал документ (поэтому очень важно прочитать документ), но я также воспользовался этой возможностью, чтобы бегло изучите манифест.Что за хрень и почему использование dll может ускорить.
Что такое упакованная dll
Посмотрите, какие файлы генерируются после запуска webpack.dll.conf
В случае нескольких записей каждый файл записи будет генерировать файл dll и файл json.Взяв vue в качестве примера, посмотрите, что находится в двух файлах, vue.dll.js и manifest-vue.json.
vue.dll.js:
var vue_01cf92ee1ec06f1bc497 =
(function(modules) { // webpackBootstrap
var installedModules = {};
function __webpack_require__(moduleId) {
// __webpack_require__ source code
}
return __webpack_require__(__webpack_require__.s = 0)
})
({
"./node_modules/vue/dist/vue.esm.js":
(function (module, __webpack_exports__, __webpack_require__)) {
"use strict";
eval("xxx"); // webpack require vue
}),
// 其他模块...
// ...
0: (function (module, exports, __webpack_require__) {
eval("module.exports = __webpack_require__;\n\n//# sourceURL=webpack://%5Bname%5D_%5Bhash%5D/dll_vue?");
})
})
Вышеупомянутая функция немедленного выполнения выглядит немного трудоемко, давайте напишем ее по-другому и сохраним некоторые детали.
var requireModules = function(modules) { // webpackBootstrap
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__的属性和方法
// __webpack_require__.xxx = xxx
// ...
return __webpack_require__(__webpack_require__.s = 0); // 执行modules[0],暴露出vue.dll.js内部模块的加载器
}
var modules = {
"./node_modules/vue/dist/vue.esm.js": // 模块id
function (module, __webpack_exports__, __webpack_require__)) { // 模块加载函数
eval("xxx"); // webpack加载vue
},
// 其他模块
// ...
// 暴露加载器
0: function (module, exports, __webpack_require__) { // 整个vue.dll.js模块
// 暴露vue.dll.js的内部模块加载器,供外部调用并加载vue相关的模块
eval("module.exports = __webpack_require__;\n\n//# sourceURL=webpack://%5Bname%5D_%5Bhash%5D/dll_vue?");
}
}
var vue_01cf92ee1ec06f1bc497 = requireModules(modules);
Файл dll выполняет следующие действия:
- Таблица сопоставления, которая определяет функцию загрузки каждого подмодуля, то есть буквальный объект
modules
- Внутренний загрузчик определен
__webpack_require__
и кеш модуляinstalledModules
- пройти через
requireModule
Функция предоставляет внутреннему загрузчику глобальную переменнуюvue_01cf92ee1ec06f1bc497
, который вызывается при внешней загрузке модуля
Когда вводится index.htmlvue.dll.js
После этого загрузчик внутреннего модуля dll выставляется под глобальным, и его можно вызывать напрямую, когда вебпак загружает модульvue_01cf92ee1ec06f1bc497
, окончательный результат эквивалентен:
var vue_01cf92ee1ec06f1bc497 = __webpack_require__; // 被闭包在内部的加载器
такvue.dll.js
Он не может выполнить код внутреннего модуля сам, а только предоставляет его наружу для вызова, что является определением dll-файла
Одной dll недостаточно. Веб-пакет проекта должен знать, что dll предоставляетvue_01cf92ee1ec06f1bc497
загрузчик, и какие модули включены в этот загрузчик, и файл манифеста содержит эту информацию.
манифест-vue.json:
{
"name": "vue_01cf92ee1ec06f1bc497",
"content": {
"./node_modules/vue/dist/vue.esm.js": {
"id": "./node_modules/vue/dist/vue.esm.js",
"buildMeta": {
"exportsType": "namespace",
"providedExports": ["default"]
}
}
}
}
Манифест сохраняет сведения об источнике модулей в качестве идентификатора для извлечения модуля, а также указывает, какой модуль требуется для загрузки этих модулей.__webpack_require__
загрузчик, когда программа запущена__webpack_require__
Соответствующий модуль можно загрузить через идентификатор модуля, см. официальное объяснение веб-пакета:
As the compiler enters, resolves, and maps out your application, it keeps detailed notes on all your modules. This collection of data is called the "Manifest" and it's what the runtime will use to resolve and load modules once they've been bundled and shipped to the browser. No matter which module syntax you have chosen, those import or require statements have now become webpack_require methods that point to module identifiers. Using the data in the manifest, the runtime will be able to find out where to retrieve the modules behind the identifiers.
Подскажите проекту где dll и что в ней
С помощью манифеста, как я могу сказать проекту, что у меня есть dll и его не нужно переупаковывать? DLLReferencePlugin передает файл манифеста веб-пакету проекта, сообщая ему, на какие модули можно ссылаться напрямую, а процесс упаковки можно пропустить.DllReferencePlugin.js
Файл манифеста считывается, и загрузчик, предоставляемый dll, монтируется в фабрику модулей веб-пакета в виде внешних зависимостей.
Прочитайте манифест:
compiler.hooks.beforeCompile.tapAsync( // webpack创建compilation前的钩子,读取dll中的模块信息(manifest)
"DllReferencePlugin",
(params, callback) => {
if ("manifest" in this.options) {
const manifest = this.options.manifest;
if (typeof manifest === "string") {
params.compilationDependencies.add(manifest);
compiler.inputFileSystem.readFile(manifest, (err, result) => { // 读取manifest文件
params["dll reference " + manifest] = parseJson(result.toString("utf-8"));
return callback();
});
return;
}
}
return callback();
}
);
Создайте внешние зависимости:
// webpack创建compilation后的钩子,告诉webpack我有个dll以及dll里都有哪些模块
compiler.hooks.compile.tap("DllReferencePlugin", params => {
// 读取manifest中的配置
let manifest = this.options.manifest;
if (typeof manifest === 'string') {
manifest = params["dll reference " + manifest];
}
let name = this.options.name || manifest.name;
let sourceType = this.options.sourceType || manifest.sourceType;
let content = this.options.content || manifest.content;
// 创建外部依赖
const externals = {};
const source = "dll-reference " + name; // 告诉webpack暴露出的全局变量,并以dll-reference作为前缀表示这是一个dll资源
externals[source] = name; // 资源名称:vue_01cf92ee1ec06f1bc497
const normalModuleFactory = params.normalModuleFactory;
// 引入外部模块工厂插件,以外部依赖的方式挂载dll
new ExternalModuleFactoryPlugin(sourceType || "var", externals).apply(
normalModuleFactory
);
// 引入代理模块工厂插件,为dll中的每个模块创建代理
new DelegatedModuleFactoryPlugin({
source: source,
type: this.options.type,
scope: this.options.scope,
context: this.options.context || compiler.options.context,
content,
extensions: this.options.extensions
}).apply(normalModuleFactory);
});
Вы можете видеть, что веб-пакет проходит черезmanifest.name
для сопоставления ресурсов dll, поэтому в webpack.dll.conf атрибут имени DllPlugin должен быть согласован с атрибутом библиотеки вывода
Процесс создания веб-пакета завершен. Модуль normalModuleFactory, который содержит ряд встроенных функций для разбора модуля, добавлена логика обработки. Здесь представлены две ключевые вилкиExternalModuleFactoryPlugin
а такжеDelegatedModuleFactoryPlugin
, что они делают в хуковой функции normalModuleFactory?
Создавайте dll-модули для ускорения упаковки
Перед тем, как действительно начнется компиляция веб-пакета проекта, вся информация о dll будет получена, а остальная часть предоставлена normalModuleFactory веб-пакета для самостоятельной обработки.ExternalModuleFactoryPlugin
а такжеDelegatedModuleFactoryPlugin
Эти два плагина добавляют свои собственные функции обратного вызова к фабричному хуку (построить фабрику модулей) и хуку модуля (создать модуль) соответственно, так что веб-пакет сначала будет искать внешние зависимости при разборе модуля и, если они будут найдены, создаст их напрямую. Прокси-объект модуля.На этапе сборки загрузчик больше не используется для обработки модуля.В противном случае создается общий объект модуля, и загрузчик используется для загрузки ресурсов на этапе сборки.
В сочетании с DllReferencePlugin общий процесс выглядит следующим образом:
После входа в процесс normalModuleFactory сначала получите фабричную функцию для создания внешних модулей в фабричном хуке,ExternalModuleFactoryPlugin
Плагин определяет фабричную функцию в фабричном хуке:
// ExternalModuleFactoryPlugin.js
normalModuleFactory.hooks.factory.tap( // factory钩子
"ExternalModuleFactoryPlugin",
factory => (data, callback) => { // 返回一个创建外部模块的工厂函数
const context = data.context;
const dependency = data.dependencies[0];
const handleExternal = (value, type, callback) => {
// 输入参数的整理
// ...
callback(
null,
new ExternalModule(value, type || globalType, dependency.request) // 为dll创建一个外部模块
);
return true;
};
const handleExternals = (externals, callback) => {
// 对Array、Object等不同类型externals的处理
// ...
if (
typeof externals === "object" &&
Object.prototype.hasOwnProperty.call(externals, dependency.request)
) {
return handleExternal(externals[dependency.request], callback); // 如果请求的资源是外部资源,则创建外部模块对象
}
callback();
};
handleExternals(this.externals, (err, module) => {
if (err) return callback(err);
if (!module) return handleExternal(false, callback);
return callback(null, module); // 通过传入的callback,将刚刚创建的外部模块传回到webpack的模块构建流程中
});
}
);
Фабричный хук возвращает эту фабричную функцию, которая будет немедленно вызвана normalModuleFactory,vue_01cf92ee1ec06f1bc497
монтируется как внешний модуль в normalModuleFactory
После того, как фабрика будет установлена, normalModuleFactory войдет в процесс парсинга модуля (resolver).После парсинга по умолчанию создается объект NormalModule для результата парсинга, который передается в качестве параметра функции перехвата модуля. В хуке модуля,DelegatedModuleFactoryPlugin
Он будет судить, существует ли входящий NormalModule в dll, если да, создайте прокси-объект и верните его, в противном случае верните NormalModule напрямую.
normalModuleFactory.hooks.module.tap(
"DelegatedModuleFactoryPlugin",
module => {
if (module.libIdent) {
const request = module.libIdent(this.options);
if (request && request in this.options.content) { // option.content就是manifest中的content
const resolved = this.options.content[request];
return new DelegatedModule( // 为dll中的模块创建代理
this.options.source, // vue_01cf92ee1ec06f1bc497
resolved,
this.options.type,
request,
module
);
}
}
return module;
}
);
Глядя на определение класса DelegatedModule, можно увидеть, что метод needRebuild напрямую возвращает false, а метод сборки напрямую помечает модуль какbuilt
, и добавить связанные зависимости, загрузчик не выполняется, поэтому модули в dll пропускаются при сборке кода и не будут участвовать в процессе упаковки
class DelegatedModule extends Module {
constructor(request, type, userRequest) {}
needRebuild(fileTimestamps, contextTimestamps) {
return false; // 跳过rebuild过程
}
build(options, compilation, resolver, fs, callback) {
this.built = true; // 标记模块为“已构建”状态
this.buildMeta = Object.assign({}, this.delegateData.buildMeta);
this.buildInfo = {};
this.delegatedSourceDependency = new DelegatedSourceDependency(
this.sourceRequest
);
this.addDependency(this.delegatedSourceDependency); // 加入代理的相关依赖
this.addDependency(
new DelegatedExportsDependency(this, this.delegateData.exports || true)
);
callback();
}
// 其他方法
// ...
}
Напротив, обычные модули будут участвовать в процессе упаковки и сборки.
class NormalModule extends Module {
constructor(request, type, userRequest) {}
needRebuild(fileTimestamps, contextTimestamps) {
// rebuild判定代码
// ...
}
build(options, compilation, resolver, fs, callback) {
return this.doBuild();
}
doBuild(options, compilation, resolver, fs, callback) {
runLoaders(); // 运行loaders,构建模块
}
// 其他方法
// ...
}
На данный момент манифест завершил свою миссию, и dll спокойно ожидает вызова среды выполнения.
Эпилог
Благодаря этому обновлению веб-пакета была завершена унификация проекта веб-пакет4, что решило различные головные боли мелких партнеров и получило положительные отзывы от мелких партнеров.Процесс строительства намного чище, чем раньше, и эффективность строительства также значительно улучшилась. Кстати, в процессе обновления я также узнал о рабочем процессе dll и получил много знаний. Вот краткое изложение процесса этого объединения.