Автор: Цуй Цзин
Эта статья требует от вас определенного понимания веб-пакета.Если вы более заинтересованы, вы можете обратиться к нашей предыдущей серии анализа исходного кода веб-пакета:серия webpack — обзор.
некоторые концептуальные заметки
При инициализации компиляции инициализируются следующие переменные:
this.mainTemplate = new MainTemplate(...)
this.chunkTemplate = new ChunkTemplate(...)
this.runtimeTemplate = new RuntimeTemplate
this.moduleTemplates = {
javascript: new ModuleTemplate(this.runtimeTemplate, "javascript"),
webassembly: new ModuleTemplate(this.runtimeTemplate, "webassembly")
}
this.hotUpdateChunkTemplate // 暂时不关注
mainTemplate: используется для генерации кода для выполнения основного процесса, который содержит код запуска веб-пакета и т. д. chunkTemplate: окончательный полученный код будет загружен JsonP.
Пример ниже: У нас есть входной файл:
// main.js
import { Vue } from 'vue'
new Vue(...)
Такие файлы упаковываются для создания app.js и chunk-vendor.js.
Структура app.js следующая:
(function(modules) { // webpackBootstrap
// webpack 的启动函数
// webpack 内置的方法
}){{
moduleId: (function(module, exports, __webpack_require__) {
// 我们写的 js 代码都在各个 module 中
},
// ...
})
Структура chunk-vendors.js выглядит следующим образом:
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["chunk-vendors"],{
moduleId: (function(module, exports, __webpack_require__) {
// ...
},
// ...
})
app.js содержит загрузочный код webpack, а общая структура этого кода находится в mainTemplate.
app.js загрузит chunk-vendor.js с помощью jonsP, а каркас этого js-кода будет помещен в chunkTemplate.
Процесс генерации кода для каждого модуля в app.js и chunk-vendors.js находится в ModuleTempalte.
Основной процесс генерации кода
Код фрагмента генерируется на этапе уплотнения. отCompilation.createChunkAssets
начинать.
Основная блок-схема выглядит следующим образом
**Примечание 1:** Функция рендеринга определяется в плагине JavascriptModulePlugin. Эта функция рендеринга будет вызываться позже в createChunkAssets.
**Примечание 2:**Здесь moduleTemplate будет сгенерирован в начале компиляции.
this.moduleTemplates = { javascript: new ModuleTemplate(this.runtimeTemplate, "javascript"), webassembly: new ModuleTemplate(this.runtimeTemplate, "webassembly") };
Поскольку это mainTemplate, в функции, которая получает различную информацию о рендере в начале
renderManifest
для запускаJavascriptModulesPlugin
Функция, зарегистрированная в, и эта определяет шаблон, используемый модулем какmoduleTemplates.javascript
compilation.mainTemplate.hooks.renderManifest.tap( "JavascriptModulesPlugin", (result, options) => { //... result.push({ render: () => compilation.mainTemplate.render( hash, chunk, moduleTemplates.javascript, dependencyTemplates ), //... }); return result; } );
Заметка 3:Процесс модуля-источника см. в приложении в конце.
Сначала определите, использует ли текущая структура mainTemplate или chunkTemplate. У этих двух Tempaltes будет свой собственный процесс рендеринга. Давайте возьмем mainTempalte в качестве примера, чтобы увидеть процесс рендеринга.
Код основной структуры будет сгенерирован в основном процессе рендеринга, который является частью структуры кода, сгенерированной ранее в нашей демонстрации app.js. Затем сгенерируйте код для каждой формы. Этот процесс выполняется функциями в ModuleTemplate.
Когда модуль будет сгенерирован, он будет вызванhook.content, hook.module, hook.render, hook.package
эти крючки. После того, как каждый хук получает результат, он передается следующему хуку.hook.module
После выполнения этого хука будет получен код модуля. затем вhook.render
, оберните этот код в функцию. если мы былиwebpack.config.js
настроен вoutput.pathinfo=true
(Инструкции по настройке), затем вhook.package
Здесь к окончательному сгенерированному коду будут добавлены некоторые комментарии, связанные с путями и встряхиванием деревьев, что может облегчить нам чтение кода.
После того, как вы получите весь код модуля, оберните его в массив или объект.
Изменить код
- Используйте хук, сгенерированный вышеуказанным файлом, чтобы добавить дополнительный контент в модуль.
BannerPlugin предназначен для добавления дополнительного контента в начало файла чанка. Что, если мы просто хотим добавить контент в модуль? Оглядываясь назад на приведенную выше блок-схему генерации кода, можно увидеть несколько ключевых ловушек в генерации кода модуля, например:hook.content,hook.module,hook.render
. Вы можете зарегистрировать функции в этих хуках для модификации. Простая демонстрация выглядит следующим образом
const { ConcatSource } = require("webpack-sources");
class AddExternalPlugin {
constructor(options) {
// plugin 初始化。这里处理一些参数格式化等
this.content = options.content // 获取要添加的内容
}
apply(compiler) {
const content = this.content
compiler.hooks.compilation.tap('AddExternal', compilation => {
compilation.moduleTemplates.javascript.hooks.render.tap('AddExternal', (
moduleSource,
module ) => {
// 这里会传入 module 参数,我们可以配置,指定在某一 module 中执行下面的逻辑
// ConcatSource 意味着最后处理的时候,我们 add 到里面的代码,会直接拼接。
const source = new ConcatSource()
// 在最开始插入我们要添加的内容
source.add(content)
// 插入源码
source.add(moduleSource)
// 返回新的源码
return source
})
})
}
}
- Оберните дополнительный слой логики вне кода выполнения фрагмента.
Мы использовали для настройки режима umd, илиoutput.library
параметр. После настройки этих двух элементов окончательная сгенерированная структура кода отличается от результата в начальной демонстрации app.js. кoutput.library='someLibName'
Например, он станет следующим
var someLibName =
(function(modules){
// webpackBootstrap
})([
//... 各个module
])
Осознание этого вышеhooks.renderWithEntry
Ссылка изменяет код, сгенерированный mainTemplate.
Если мы хотим в некоторых случаях обернуть часть нашей собственной логики. можно разобраться прямо здесь. Дайте простую демонстрацию
const { ConcatSource } = require("webpack-sources");
class MyWrapPlugin {
constructor(options) {
}
apply(compiler) {
const onRenderWithEntry = (source, chunk, hash) => {
const newSource = new ConcatSource()
newSource.add(`var myLib =`)
newSource.add(source)
newSource.add(`\nconsole.log(myLib)`)
return newSource
}
compiler.hooks.compilation.tap('MyWrapPlugin', compilation => {
const { mainTemplate } = compilation
mainTemplate.hooks.renderWithEntry.tap(
"MyWrapPlugin",
onRenderWithEntry
)
// 如果我们支持一些变量的配置化,那么就需要把我们配置的信息写入 hash 中。否则,当我们修改配置的时候,会发现 hash 值不会变化。
// mainTemplate.hooks.hash.tap("SetVarMainTemplatePlugin", hash => {
// hash.update()
// });
})
}
}
module.exports = MyWrapPlugin
Результат после компиляции webpack
var myLib =/******/ (function(modules) {
//... webpack bootstrap 代码
/******/ return __webpack_require__(__webpack_require__.s = 0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, exports) {
// ...
/***/ })
/******/ ])
console.log(myLib);
- BannerPlugin
Аналогично встроенному BannerPlugin. После того, как вышеуказанный файл фрагмента сгенерирован, т.е.createChunkAssets
После завершения выполнения измените содержимое всего файла фрагмента. Например, BannerPlugin находится вoptimizaChunkAssets
на крючке
В этом хуке вы можете получить параметрchunks
Все фрагменты, а затем дополнительный контент могут быть добавлены сюда.
Модификация содержимого файла после chunkAssets
После выполнения createChunkAssets другие хуки могут получить содержимое файла и изменить его.
-
Влияние исходной карты, после хука afterOptimizeChunkAssets, веб-пакет генерирует исходную карту. Если после этого вы измените код, например, optimiseAssets или более поздний хук emit, вы обнаружите, что исходная карта неверна. как в примере ниже
compiler.hooks.compilation.tap('AddExternal', compilation => { compilation.hooks.optimizeAssets.tap('AddExternal', assets => { let main = assets["main.js"] main = main.children.unshift('//test\n//test\n') }) })
-
Влияние на хэш. Когда вышеприведенный фрагмент кода генерируется, хэш фактически генерируется вместе с ним. Модификации кода в хуке после генерации хэша, такие как добавление чего-либо, не повлияют на результат хеширования. Например, приведенный выше пример изменения кода фрагмента. Если наш плагин обновлен, измененный контент изменился, но сгенерированный хеш не изменится вместе с ним. Поэтому необходимо в хеш в хуке, связанном с генерацией хеша, прописать содержимое плагина.
генерация источника модуля
В процессе модуля-исходника будет обработана каждая зависимость, сгенерированная на этапе парсера, и будет реализовано преобразование нашего сокращенного исходного кода в соответствии с dependency.Template. Здесь мы рассматриваем модуль-источник в сочетании с исходным парсером. В качестве примера возьмем следующую демонстрацию:
// main.js
import { test } from './b.js'
function some() {
test()
}
some()
// b.js
export function test() {
console.log('b2')
}
AST преобразован из парсера main.js:
Parser ast в этом процессе будет испытывать
if (this.hooks.program.call(ast, comments) === undefined) {
this.detectMode(ast.body);
this.prewalkStatements(ast.body);
this.blockPrewalkStatements(ast.body);
this.walkStatements(ast.body);
}
-
program
Определение того, используется ли импорт/экспорт, приведет к увеличению HarmonyCompatibilityDependency, HarmonyInitDependency (роль будет представлена позже).
-
detectMode
Проверьте, есть ли начальный
use strict
а такжеuse asm
, чтобы убедиться, что use strict, написанный в начале, после того, как наш код скомпилирован, все еще находится в начале -
prewalkStatements
Пройдите все определения переменных в текущей области. во время этого процесса
import { test } from './b.js'
Тест также находится в текущей области, поэтому импорт будет обработан здесь (см. javascript-parser для процесса). Для этого импорта будет дополнительно добавленоConstDependency
а такжеHarmonyImportSideEffectDependency
-
blockPrewalk
Обработка let/const в текущей области видимости (во время предварительного просмотра обрабатывается только var), имя класса, экспорт и экспорт по умолчанию
-
walkStatements
Начните переходить к каждому узлу для обработки. Здесь вы найдете все варианты использования в коде
test
поместите, а затем добавьтеHarmonyImportSpecifierDependency
После того, как менеджер пройдет их, будет добавлена демонстрация выше.
HarmonyCompatibilityDependency
HarmonyInitDependency
ConstDependency
HarmonyImportSideEffectDependency
HarmonyImportSpecifierDependency
Эти зависимости делятся на две категории:
-
moduleDependency: существует соответствующий denpendencyFactory, который будет обработан в процессе processModuleDependencies для получения соответствующего модуля.
HarmonyImportSideEffectDependency --> NormalModuleFactory
HarmonyImportSpecifierDependency --> NormalModuleFactory
Оба указывают на один и тот же модуль (./b.js), поэтому они будут дедуплицированы. Затем webpack следует за зависимостью, обрабатывая b.js... до тех пор, пока не будет обработана вся moduleDependency.
-
Только когда файл сгенерирован, он используется для генерации кода
module.source
Сначала получите исходный код, а затем обработайте каждую зависимость
-
HarmonyCompatibilityDependency
Вставить в начало
__webpack_require__.r(__webpack_exports__);
, идентифицируя это как esModule -
HarmonyInitDependency
Пройдите все зависимости, ответственные за генерацию
import {test} from './b.js'
Соответствующий код для импорта модуля './b.js'/* harmony import */ var _b_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(0);
-
ConstDependency
На этапе HarmonyInitDependency уже вставлено
import
Содержимое, соответствующее оператору, поэтому в исходном кодеimport {test} from './b.js'
необходимо удалить. Роль ConstDependency — заменить это предложение пустым, то есть удалить -
HarmonyImportSideEffectDependency
Этап действия находится в процессе HarmonyInitDependency.
-
HarmonyImportSpecifierDependency
в коде
test()
сгенерированные зависимости. Эффект заключается в замене кода вtest
-
Получите имя переменной, соответствующее модулю './b.js'
_b_js__WEBPACK_IMPORTED_MODULE_0__
-
Получить имя атрибута в b.js, соответствующее test (поскольку оно скомпилировано webpack, для упрощения кода наш экспортный тест в b.js можно преобразовать в export a = test)
Object(_b_js__WEBPACK_IMPORTED_MODULE_0__[/* test */ "a"])
Если она будет вызвана, то возьмет логику???
if (isCall) { if (callContext === false && asiSafe) { return `(0,${access})`; } else if (callContext === false) { return `Object(${access})`; } }
-
Затем замените тест в коде
-
После прохождения всех зависимостей:
После понимания этого процесса, если нам нужно внести некоторые простые изменения в исходный код, мы можем использовать различные хуки на этапе парсера для достижения этого. Преимущество изменения здесь в том, что вам не нужно беспокоиться о нарушении исходной карты и влиянии на генерацию хэша.
- Демонстрация вставки кода в парсер
Например, когда мы используем плагин, нам нужно написать следующее
import MainFunction from './a.js'
import { test } from './b.js'
MainFunction.use(test)
На практике плагин webpack используется для автоматической вставки теста при его обнаружении.
import MainFunction from './a.js'
MainFunction.use(test)
Ключом к реализации является вышеупомянутоеHarmonyImportSideEffectDependency
, HarmonyImportSpecifierDependency
а такжеConstDependency
код показывает, как показано ниже
const path = require('path')
const ConstDependency = require("webpack/lib/dependencies/ConstDependency");
const HarmonyImportSideEffectDependency = require("webpack/lib/dependencies/HarmonyImportSideEffectDependency")
const HarmonyImportSpecifierDependency = require("webpack/lib/dependencies/HarmonyImportSpecifierDependency")
const NullFactory = require("webpack/lib/NullFactory");
// 要引入的 a.js 的路径。这个路径后面会经过 webpack 的 resolve
const externalJSPath = `${path.join(__dirname, './a.js')}`
class ProvidePlugin {
constructor() {
}
apply(compiler) {
compiler.hooks.compilation.tap(
"InjectPlugin",
(compilation, { normalModuleFactory }) => {
const handler = (parser, parserOptions) => {
// 在 parser 处理 import 语句的时候
parser.hooks.import.tap('InjectPlugin', (statement, source) => {
parser.state.lastHarmonyImportOrder = (parser.state.lastHarmonyImportOrder || 0) + 1;
// 新建一个 './a.js' 的依赖
const sideEffectDep = new HarmonyImportSideEffectDependency(
externalJSPath,
parser.state.module,
parser.state.lastHarmonyImportOrder,
parser.state.harmonyParserScope
);
// 为 dependency 设置一个位置。这里设置为和 import { test } from './b.js' 相同的位置,在代码进行插入的时候会插入到改句所在的地方。
sideEffectDep.loc = {
start: statement.start,
end: statement.end
}
// 设置一下 renames,标识代码中 mainFunction 是从外部引入的
parser.scope.renames.set('mainFunction', "imported var");
// 把这个依赖加入到 module 的依赖中
parser.state.module.addDependency(sideEffectDep);
// -------------处理插入 mainFunction.use(test)------------
if (!parser.state.harmonySpecifier) {
parser.state.harmonySpecifier = new Map()
}
parser.state.harmonySpecifier.set('mainFunction', {
source: externalJSPath,
id: 'default',
sourceOrder: parser.state.lastHarmonyImportOrder
})
// 针对 mainFunction.use 中的 mainFunction
const mainFunction = new HarmonyImportSpecifierDependency(
externalJSPath,
parser.state.module,
-1,
parser.state.harmonyParserScope,
'default',
'mainFunction',
[-1, -1], // 插入到代码最开始
false
)
parser.state.module.addDependency(mainFunction)
// 插入代码片段 .use(
const constDep1 = new ConstDependency(
'.use(',
-1,
true
)
parser.state.module.addDependency(constDep1)
// 插入代码片段 test
const useArgument = new HarmonyImportSpecifierDependency(
source,
parser.state.module,
-1,
parser.state.harmonyParserScope,
'test',
'test',
[-1, -1],
false
)
parser.state.module.addDependency(useArgument)
// 插入代码片段 )
const constDep2 = new ConstDependency(
')\n',
-1,
true
)
parser.state.module.addDependency(constDep2)
});
}
normalModuleFactory.hooks.parser
.for("javascript/auto")
.tap("ProvidePlugin", handler);
normalModuleFactory.hooks.parser
.for("javascript/dynamic")
.tap("ProvidePlugin", handler);
}
);
}
}
module.exports = ProvidePlugin;
Сгенерированный код выглядит следующим образом
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
const mainFunction = function () {
console.log('mainFunction')
}
mainFunction.use = function(name) {
console.log('load something')
}
/* harmony default export */ __webpack_exports__["a"] = (mainFunction);
/***/ }),
/* 2 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _Users_didi_Documents_learn_webpack_4_demo_banner_demo_a_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
/* harmony import */ var _b_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(0);
_Users_didi_Documents_learn_webpack_4_demo_banner_demo_a_js__WEBPACK_IMPORTED_MODULE_0__[/* default */ "a"].use(_b_js__WEBPACK_IMPORTED_MODULE_1__[/* test */ "a"])
Object(_b_js__WEBPACK_IMPORTED_MODULE_1__[/* test */ "a"])()
/***/ })
-
DefinePlugin
Вы можете использовать этот плагин для замены некоторых констант во время компиляции, например:
- Обычно используемый код js в соответствии с
process.env.NODE_ENV
Значение , различает разные среды разработки и рабочие среды. Чтобы достичь разной логики ветвей в разных средах.
new DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) })
-
Настраиваемый URL-адрес API
new DefinePlugin({ API_DOMAIN: process.env.NODE_ENV === 'dev' ? '"//10.96.95.200"' : '"//api.didi.cn"' })
Реализовать переключение доменного имени запроса API на разработку и производство.
Краткое введение в некоторые принципы: простейший пример
new DefinePlugin({ 'TEST': "'test'" })
используется в коде
const a = TEST
, Когда синтаксический анализатор перемещается вправо от знака =, он запускает ловушку синтаксического анализа выражения// key 是 TEST parser.hooks.expression.for(key).tap("DefinePlugin", expr => { const strCode = toCode(code, parser); // 结果为我们设置的 'test' if (/__webpack_require__/.test(strCode)) { // 如果用到了 __webpack_require__ ,生成的 ConstantDependency 中 requireWebpackRequire=true // 在后期生成代码,用 function(module, exports){} 将代码包裹起来的时候,参数里面会有 __webpack_require__,即 function(module, exports, __webpack_require__){} return ParserHelpers.toConstantDependencyWithWebpackRequire( parser, strCode )(expr); } else { // ParserHelpers.toConstantDependency 会生成一个 ConstDependency,并且添加到当前的 module 中 // ConstDependency.expression = "'test'",位置就是我们代码中 TEST 对应的位置 return ParserHelpers.toConstantDependency( parser, strCode )(expr); } });
Как упоминалось ранее, ConstDependency заменит соответствующее содержимое исходного кода. Так что сделайте следующее на этапе генерации кода позже
ConstDependency.Template = class ConstDependencyTemplate { apply(dep, source) { // 如果 range 是一个数字,则为插入;如果是一个区间,则为替换 if (typeof dep.range === "number") { source.insert(dep.range, dep.expression); return; } // 把源码中对应的地方替换成了 dep.expression,即 "test" source.replace(dep.range[0], dep.range[1] - 1, dep.expression); } };
Таким образом осуществляется замена TEST в исходном коде.
- Обычно используемый код js в соответствии с
Суммировать
Я считаю, что благодаря подробному анализу процесса, приведенному выше, и некоторым соответствующим демонстрационным практикам, я уже понял весь процесс создания статических файлов веб-пакетом. Я надеюсь, что в будущем, когда вы столкнетесь с подобными сценариями и существующие экологические плагины не смогут удовлетворить ваши потребности, вы сможете сделать это самостоятельно.
Самая большая мотивация для нас, чтобы получить глубокое понимание деталей, исходит от наших потребностей.mpxСуществует много-много приложений для вышеуказанной генерации статических файлов. Если вы заинтересованы, вы можете понять, использовать и строить вместе.
дополнительный,Номер команды технической команды Didi.Он также был онлайн, и мы также синхронизировали определенное количествоПредложения о работе, мы продолжим добавлять новые позиции, и заинтересованные студенты могут общаться вместе.