Процесс генерации статических файлов webpack, о котором вы не знаете

внешний интерфейс Webpack

Автор: Цуй Цзин

Эта статья требует от вас определенного понимания веб-пакета.Если вы более заинтересованы, вы можете обратиться к нашей предыдущей серии анализа исходного кода веб-пакета:серия 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начинать.

Основная блок-схема выглядит следующим образом

create-asset.png

**Примечание 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:

ast.png

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

    ast-prewalk.png

  • blockPrewalk

    Обработка let/const в текущей области видимости (во время предварительного просмотра обрабатывается только var), имя класса, экспорт и экспорт по умолчанию

  • walkStatements

    Начните переходить к каждому узлу для обработки. Здесь вы найдете все варианты использования в кодеtestпоместите, а затем добавьтеHarmonyImportSpecifierDependency

    ast-walk.png

После того, как менеджер пройдет их, будет добавлена ​​демонстрация выше.

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})`;
      				}
      			}
      
    • Затем замените тест в коде

После прохождения всех зависимостей:

before-after-code.png

После понимания этого процесса, если нам нужно внести некоторые простые изменения в исходный код, мы можем использовать различные хуки на этапе парсера для достижения этого. Преимущество изменения здесь в том, что вам не нужно беспокоиться о нарушении исходной карты и влиянии на генерацию хэша.

  • Демонстрация вставки кода в парсер

Например, когда мы используем плагин, нам нужно написать следующее

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

    Введение в 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 в исходном коде.

Суммировать

Я считаю, что благодаря подробному анализу процесса, приведенному выше, и некоторым соответствующим демонстрационным практикам, я уже понял весь процесс создания статических файлов веб-пакетом. Я надеюсь, что в будущем, когда вы столкнетесь с подобными сценариями и существующие экологические плагины не смогут удовлетворить ваши потребности, вы сможете сделать это самостоятельно.

Самая большая мотивация для нас, чтобы получить глубокое понимание деталей, исходит от наших потребностей.mpxСуществует много-много приложений для вышеуказанной генерации статических файлов. Если вы заинтересованы, вы можете понять, использовать и строить вместе.

дополнительный,Номер команды технической команды Didi.Он также был онлайн, и мы также синхронизировали определенное количествоПредложения о работе, мы продолжим добавлять новые позиции, и заинтересованные студенты могут общаться вместе.