Анализ всего процесса основного принципа упаковки Webapck5

внешний интерфейс Webpack
Анализ всего процесса основного принципа упаковки Webapck5

"Это 4-й день моего участия в ноябрьском испытании обновлений, ознакомьтесь с подробностями события:Вызов последнего обновления 2021 г.".

написать впереди

WebpackЕго можно рассматривать как опору в инструментах построения интерфейса, ежедневном развитии бизнеса, инструментах инфраструктуры интерфейса, расширенных интервью перед интерфейсом... Он появится в любой сцене.

Вы можете быть сбиты с толку внутренним механизмом его реализации.В повседневной работе на основеWebpack Plugin/Loaderпроверить и т.д.APIЕще не понял смысла и применения различных параметров.

На самом деле все эти причины в основном основаны наWebpackОтсутствие четкого понимания рабочего процесса приводит к так называемому «лицу».APIНет возможности начать" разработку.

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

Здесь мы говорим только о «галантерее»., познакомить вас с самым простым для понимания кодомwebpackрабочий процесс.

Предварительные знания, которые, я надеюсь, вы сможете освоить

TapableПакеты — это, по сути, библиотеки для создания пользовательских событий и запуска пользовательских событий для нас, подобныхNodejsсерединаEventEmitter Api.

WebpackМеханизм подключаемых модулей основан на реализации Tapable и разделении процесса упаковки. Все формы подключаемых модулей основаны наTapableвыполнить.

В целях обучения мы сосредоточимся наWebpack Node ApiПроцесс объясняется, на самом деле мы используем его ежедневно в интерфейсе.npm run buildКоманды также вызываются через переменные средыbinскрипт для вызоваNode ApiВыполнить компиляцию и упаковку.

WebpackВнутреннийASTАнализ также зависит отBabelпроцесс, если выBabelне очень знаком. Я предлагаю вам сначала прочитать эти две статьи«Фронтальная инфраструктура» перенесет вас в мир Вавилона,# От Tree Shaking до мира разработчиков плагинов Babel.

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

Сортировка процессов

Прежде чем мы начнем, давайте разберемся во всем процессе упаковки.

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

image.png

В целом, мы будем анализировать из вышеуказанных 5 аспектовWebpackПроцесс упаковки:

  1. Стадия инициализации параметров.

    Этот шаг начнется с нашей конфигурацииwebpack.config.jsСоответствующие параметры конфигурации иshellПараметры, переданные в команду, объединяются для получения окончательных параметров конфигурации упаковки.

  2. Начать этап подготовки к компиляции

    На этом шаге мы будем вызыватьwebpack()метод возвращаетcompilerметод, чтобы создать нашcompilerобъект и зарегистрировать каждыйWebpack Plugin. Найдите запись конфигурации вentryкод, звонитеcompiler.run()метод для компиляции.

  3. этап компиляции модуля

    Проанализируйте из модуля ввода, вызовите соответствующий файлloadersОбработайте файл. При этом анализируются модули, от которых зависят модули, и рекурсивно компилируются модули.

  4. Завершение этапа компиляции

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

  5. этап выходного файла

    Приведите в порядок зависимости модуля при выводе обработанных файлов вouputв каталоге диска.

Давайте подробно рассмотрим, что именно происходит на каждом шаге.

Создать каталог

Если рабочий хочет хорошо работать, он должен сначала заточить свои инструменты. Сначала давайте создадим хороший каталог для управления тем, что нам нужно реализовать.Packing toolБар!

Создадим такой каталог:

image.png

  • webpack/coreхранить то, чего мы сами добьемсяwebpackосновной код.
  • webpack/exampleСодержит пример проекта, который мы будем использовать для упаковки.
    • webpack/example/webpak.config.jsконфигурационный файл.
    • webpack/example/src/entry1первый входной файл
    • webpack/example/src/entry1второй входной файл
    • webpack/example/src/index.jsфайл модуля
  • webpack/loadersсохранить наш обычайloader.
  • webpack/pluginsсохранить наш обычайplugin.

стадия параметра инициализации

Часто у нас есть два способа датьwebpackПередавая упакованные параметры, давайте сначала посмотрим, как передать параметры:

Cliпараметры командной строки

Обычно мы используем вызовwebpackПри выполнении команды иногда передаются некоторые параметры командной строки, например:

webpack --mode=production
# 调用webpack命令执行打包 同时传入mode为production

webpack.config.jsпередать параметры

Другой способ, на мой взгляд, более банален.

Мы используем его в корневом каталоге проектаwebpack.config.jsэкспортировать объектwebpackКонфигурация:

const path = require('path')

// 引入loader和plugin ...
module.exports = {
  mode: 'development',
  entry: {
    main: path.resolve(__dirname, './src/entry1.js'),
    second: path.resolve(__dirname, './src/entry2.js'),
  },
  devtool: false,
  // 基础目录,绝对路径,用于从配置中解析入口点(entry point)和 加载器(loader)。
  // 换而言之entry和loader的所有相对路径都是相对于这个路径而言的
  context: process.cwd(),
  output: {
    path: path.resolve(__dirname, './build'),
    filename: '[name].js',
  },
  plugins: [new PluginA(), new PluginB()],
  resolve: {
    extensions: ['.js', '.ts'],
  },
  module: {
    rules: [
      {
        test: /\.js/,
        use: [
          // 使用自己loader有三种方式 这里仅仅是一种
          path.resolve(__dirname, '../loaders/loader-1.js'),
          path.resolve(__dirname, '../loaders/loader-2.js'),
        ],
      },
    ],
  },
};

В то же время этот файл конфигурации также является тем, что нам нужно в качестве примера проекта.exampleКонфигурация экземпляра ниже, давайте изменимexample/webpack.config.jsСодержимое приведенной выше панели конфигурации.

Конечно здесьloaderа такжеpluginВам пока не обязательно это понимать, мы будем постепенно реализовывать эти вещи и добавлять их в наш процесс упаковки.

Реализовать этап параметра слияния

Этот шаг, давайте фактически начнем реализацию нашегоwebpackБар!

Сначала давайтеwebpack/coreсоздать новыйindex.jsфайл в качестве файла основной записи.

При этом создатьwebpack/coreсоздать новыйwebpack.jsфайл какwebpack()Файл реализации метода.

Во-первых, мы знаем, что вNodeJs Apiчерезwebpack()способ получитьcompilerобъект.

image.png

Теперь давайте следовать оригиналуwebpackФормат интерфейса для добавленияindex.jsЛогика в:

  • нам нужноwebpackметод для выполнения вызывающей команды.
  • При этом мы вводимwebpack.config.jsПриходит файл конфигурацииwebpackметод.
// index.js
const webpack = require('./webpack');
const config = require('../example/webpack.config');
// 步骤1: 初始化参数 根据配置文件和shell参数合成参数
const compiler = webpack(config);

Хм, выглядит неплохо. Далее реализуемwebpack.js:

function webpack(options) {
  // 合并参数 得到合并后的参数 mergeOptions
  const mergeOptions = _mergeOptions(options);
}

// 合并参数
function _mergeOptions(options) {
  const shellOptions = process.argv.slice(2).reduce((option, argv) => {
    // argv -> --mode=production
    const [key, value] = argv.split('=');
    if (key && value) {
      const parseKey = key.slice(2);
      option[parseKey] = value;
    }
    return option;
  }, {});
  return { ...options, ...shellOptions };
}

module.exports = webpack;

Здесь нам нужно добавить, что

webpackФайл должен экспортировать файл с именемwebpackметод, принимая внешние входящие объекты конфигурации. Это то, что мы описали выше.

Конечно, что касается логики наших параметров слияния, этоВнешние входящие объекты и исполнениеshellВходящие параметры при окончательном слиянии.

существуетNode Jsмы можем пройтиprocess.argv.slice(2)получитьshellПараметры, передаваемые в команде, такие как:

image.png

Конечно_mergeOptionsЭтот метод представляет собой простой метод объединения параметров конфигурации, я считаю, что это легко для всех.

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

этап компиляции

После получения окончательных параметров конфигурации нам необходимоwebpack()Функция выполняет следующие действия:

  • Создать с параметрамиcompilerобъект. Мы видим в официальном случае, позвонивwebpack(options)метод возвращаетcompilerобъект. и звонить заодноcompiler.run()Код, запускаемый методом, упаковывается.

  • зарегистрируйте наше определениеwebpack pluginплагин.

  • Найдите соответствующий файл записи пакета в соответствии с входящим объектом конфигурации.

Создайтеcompilerобъект

давай закончим первымindex.jsЛогическое завершение кода в:

// index.js
const webpack = require('./webpack');
const config = require('../example/webpack.config');
// 步骤1: 初始化参数 根据配置文件和shell参数合成参数
// 步骤2: 调用Webpack(options) 初始化compiler对象  
// webpack()方法会返回一个compiler对象

const compiler = webpack(config);

// 调用run方法进行打包
compiler.run((err, stats) => {
  if (err) {
    console.log(err, 'err');
  }
  // ...
});

Как видите, основная реализация компиляции лежит вwebpack()метод возвращенcompiler.run()метод.

Шаг за шагом давайте усовершенствуем этоwebpack()метод:

// webpack.js
function webpack(options) {
  // 合并参数 得到合并后的参数 mergeOptions
  const mergeOptions = _mergeOptions(options);
  // 创建compiler对象
  const compiler = new Compiler(mergeOptions)
  
  return compiler
}

// ...

впусти насwebpack/coreСоздайте новый в каталогеcompiler.jsфайл, какcompilerОсновной файл реализации:

// compiler.js
// Compiler类进行核心编译实现
class Compiler {
  constructor(options) {
    this.options = options;
  }

  // run方法启动编译 
  // 同时run方法接受外部传递的callback
  run(callback) {
  }
}

module.exports = Compiler

В это время нашCompilerСначала класс строит базовый каркасный код.

В настоящее время у нас есть:

  • webpack/core/index.jsВ качестве входного файла команды упаковки этот файл ссылается на нашу собственную реализацию.webpackтакже ссылается на внешниеwebpack.config.js(options). передачаwebpack(options).run()Начать компиляцию.

  • webpack/core/webpack.jsВ настоящее время этот файл обрабатывает объединение параметров и передачу объединенных параметров.new Compiler(mergeOptions), при возврате созданногоCompilerобъект силы.

  • webpack/core/compiler, в это время нашcompilerТак же, как основной скелет, естьrun()метод запуска.

записыватьPlugin

помнишь, мы былиwebpack.config.jsиспользовал дваplugin---pluginA,pluginBплагин? Реализуем их по очереди:

в реализацииPluginПрежде нам нужно улучшитьcompilerметод:

const { SyncHook } = require('tapable');

class Compiler {
  constructor(options) {
    this.options = options;
    // 创建plugin hooks
    this.hooks = {
      // 开始编译时的钩子
      run: new SyncHook(),
      // 输出 asset 到 output 目录之前执行 (写入文件之前)
      emit: new SyncHook(),
      // 在 compilation 完成时执行 全部完成编译执行
      done: new SyncHook(),
    };
  }

  // run方法启动编译
  // 同时run方法接受外部传递的callback
  run(callback) {}
}

module.exports = Compiler;

Мы здесьCompilerСвойство создается в конструкторе этого классаhooks, значениями которого являются три свойстваrun,emit,done.

Значения этих трех атрибутов являются предварительными знаниями, о которых мы упоминали выше.tapableизSyncHookметод, по сути, вы можете просто преобразоватьSyncHook()понимание метода, называемоеEmitter EventДобрый.

когда мы проходимnew SyncHook()После возврата экземпляра объекта мы можем передатьthis.hook.run.tap('name',callback)метод для добавления прослушивателей событий к этому объекту, а затем передатьthis.hook.run.call()выполнить всеtapзарегистрированное событие.

КонечноwebpackВ реальном исходном коде многоhook. И есть синхронные/асинхронные хуки соответственно.Здесь мы более четко объясняем процесс, поэтому перечислены только три общих и простых синхронных хука.

В этот момент нам нужно понять, что мы можемCompilerНа объекте экземпляра, возвращаемом классомcompiler.hooks.run.tapРегистрация хука.

Давайте сократимwebpack.js, давайте заполним логику регистрации плагина:

const Compiler = require('./compiler');

function webpack(options) {
  // 合并参数
  const mergeOptions = _mergeOptions(options);
  // 创建compiler对象
  const compiler = new Compiler(mergeOptions);
  // 加载插件
  _loadPlugin(options.plugins, compiler);
  return compiler;
}

// 合并参数
function _mergeOptions(options) {
  const shellOptions = process.argv.slice(2).reduce((option, argv) => {
    // argv -> --mode=production
    const [key, value] = argv.split('=');
    if (key && value) {
      const parseKey = key.slice(2);
      option[parseKey] = value;
    }
    return option;
  }, {});
  return { ...options, ...shellOptions };
}

// 加载插件函数
function _loadPlugin(plugins, compiler) {
  if (plugins && Array.isArray(plugins)) {
    plugins.forEach((plugin) => {
      plugin.apply(compiler);
    });
  }
}

module.exports = webpack;

Здесь мы создаемcompilerобъект, звонок_loadPluginметодЗарегистрировать плагин.

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

этоapplyметод будет приниматьcompilerобъект. Что мы делали выше, так это вызывали входящийpluginизapplyметод и пройти в нашcompilerобъект.

Здесь прошу запомнить описанный выше процесс, пишем ежедневноwebpack pluginпо сути это операцияcompilerТаким образом, объект влияет на результаты упаковки.

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

Далее давайте напишем эти плагины:

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

Сначала давайте сначала создадим файл:

image.png

// plugin-a.js
// 插件A
class PluginA {
  apply(compiler) {
    // 注册同步钩子
    // 这里的compiler对象就是我们new Compiler()创建的实例哦
    compiler.hooks.run.tap('Plugin A', () => {
      // 调用
      console.log('PluginA');
    });
  }
}

module.exports = PluginA;
// plugin-b.js
class PluginB {
  apply(compiler) {
    compiler.hooks.done.tap('Plugin B', () => {
      console.log('PluginB');
    });
  }
}

module.exports = PluginB;

Увидев это, я думаю, что большинство студентов уже отреагировали.compiler.hooks.done.tapЭто не тот проход, о котором мы говорили вышеtapableСоздаватьSyncHookэкземпляр, а затем передатьtapспособ зарегистрировать события?

Вот так! Действительно, оwebpackплагинПо сути, через модель публикации-подписки, черезcompilerследить за событиями. Затем события, запускающие прослушивание в процессе упаковки и компиляции, добавляют определенную логику, влияющую на результаты упаковки..

в каждом плагинеapplyметод черезtapНа этапе подготовки к компиляции (т. е. при вызовеwebpack()функция), чтобы подписаться на соответствующее событие, и когда наша компиляция выполняется до определенного этапа, соответствующее событие публикуется, чтобы сообщить подписчику о выполнении отслеживаемого события, чтобы инициировать соответствующее событие в разных жизненных циклах этапа компиляции. .plugin.

Итак, здесь вы должны быть ясны, мы собираемсяwebpackПри разработке плагинаcompilerОбъект хранит все связанные свойства этой упаковки, такие какoptionsУпакованная конфигурация и различные свойства, которые мы рассмотрим позже.

ПоискentryВход

После этого подавляющее большинство нашего контента будет размещено вcompiler.jsреализоватьCompilerЭтот класс реализует основной процесс упаковки.

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

// compiler.js
const { SyncHook } = require('tapable');
const { toUnixPath } = require('./utils');

class Compiler {
  constructor(options) {
    this.options = options;
    // 相对路径跟路径 Context参数
    this.rootPath = this.options.context || toUnixPath(process.cwd());
    // 创建plugin hooks
    this.hooks = {
      // 开始编译时的钩子
      run: new SyncHook(),
      // 输出 asset 到 output 目录之前执行 (写入文件之前)
      emit: new SyncHook(),
      // 在 compilation 完成时执行 全部完成编译执行
      done: new SyncHook(),
    };
  }

  // run方法启动编译
  // 同时run方法接受外部传递的callback
  run(callback) {
    // 当调用run方式时 触发开始编译的plugin
    this.hooks.run.call();
    // 获取入口配置对象
    const entry = this.getEntry();
  }

  // 获取入口文件路径
  getEntry() {
    let entry = Object.create(null);
    const { entry: optionsEntry } = this.options;
    if (typeof optionsEntry === 'string') {
      entry['main'] = optionsEntry;
    } else {
      entry = optionsEntry;
    }
    // 将entry变成绝对路径
    Object.keys(entry).forEach((key) => {
      const value = entry[key];
      if (!path.isAbsolute(value)) {
        // 转化为绝对路径的同时统一路径分隔符为 /
        entry[key] = toUnixPath(path.join(this.rootPath, value));
      }
    });
    return entry;
  }
}

module.exports = Compiler;
// utils/index.js
/**
 *
 * 统一路径分隔符 主要是为了后续生成模块ID方便
 * @param {*} path
 * @returns
 */
function toUnixPath(path) {
  return path.replace(/\\/g, '/');
}

На этом этапе мы проходимoptions.entryОбрабатывает получение абсолютного пути к файлу записи.

Вот несколько небольших моментов, на которые стоит обратить внимание:

  • this.hooks.run.call()

в нашем_loadePluginsВ функции для каждого входящего плагинаcompilerОбъект экземпляра подписывается, затем, когда мы вызываемrunметод, это эквивалентно фактическому запуску компиляции. этот этапЭквивалентно подписке, нам нужно сообщить подписчику, что публикация начинает выполняться.. В этот момент мы проходимthis.hooks.run.call()казнить оrunвсеtapМетод прослушивателя, который запускает соответствующийpluginлогика.

  • this.rootPath:

вне вышеперечисленногоwebpack.config.jsВ мы настраиваем context: process.cwd(), на самом деле правдаwebpackв этомcontextЗначение по умолчанию такжеprocess.cwd().

Подробное объяснение об этом вы можете увидеть здесьContext.

Короче говоря, этот путь — это путь к каталогу, в котором начинается наш проект, любойentryа такжеloaderОтносительные пути вcontextОтносительный путь этого параметра.

Здесь мы используемthis.rootPathВ конструкторе сохранить эту переменную.

  • toUnixPathМетод инструмента:

Потому что под разными операционными системами путь разделения файлов разный. Здесь мы используем унифицированный\заменить путь в//чтобы заменить путь к модулю. Позже мыИспользование модулей относительноrootPathпуть как уникальный идентификатор для каждого файла, поэтому разделитель путей здесь обрабатывается единообразно.

  • entryМетод обработки:

оentryнастроить,webpackНа самом деле видов много. Здесь мы рассмотрели два распространенных метода настройки:

entry:'entry1.js'

// 本质上这段代码在webpack中会被转化为
entry: {
    main:'entry1.js
}
entry: {
   'entry1':'./entry1.js',
   'entry2':'/user/wepback/example/src/entry2.js'
}

В любом случае пройдетgetEntryОкончательное преобразование метода называется{ [模块名]:[模块绝对路径]... }в видеgeEntry()Метод на самом деле очень простой, и я не буду слишком громоздким, чтобы реализовать этот метод здесь.

На этом шаге мы проходимgetEntryметод получаетkeyдляentryName,valueдляentryAbsolutePathТеперь начнем процесс компиляции из входного файла.

этап компиляции модуля

Выше мы описали подготовку к этапу компиляции:

  • Добавлена ​​логика базы каталогов/файлов.
  • пройти черезhooks.tapрегистрwebpackплагин.
  • getEntryМетод получает объект каждой записи.

Продолжаем улучшатьcompiler.js.

События, которые нам нужно сделать на этапе компиляции модуля:

  • Проанализируйте файл записи в соответствии с путем к файлу записи и сопоставьте файл записи с соответствующимloaderОбработайте входной файл.
  • БудуloaderИспользуется обработанный входной файлwebpackкомпилировать.
  • Проанализируйте зависимости файла записи и повторите два вышеуказанных шага, чтобы скомпилировать соответствующие зависимости.
  • Если во вложенных файлах есть зависимые файлы, зависимые модули вызываются рекурсивно для компиляции.
  • После завершения рекурсивной компиляции соберите один за другим, содержащий несколько модулей.chunk

Во-первых, давайтеcompiler.jsДобавьте соответствующую логику в конструктор:

class Compiler {
  constructor(options) {
    this.options = options;
    // 创建plugin hooks
    this.hooks = {
      // 开始编译时的钩子
      run: new SyncHook(),
      // 输出 asset 到 output 目录之前执行 (写入文件之前)
      emit: new SyncHook(),
      // 在 compilation 完成时执行 全部完成编译执行
      done: new SyncHook(),
    };
    // 保存所有入口模块对象
    this.entries = new Set();
    // 保存所有依赖模块对象
    this.modules = new Set();
    // 所有的代码块对象
    this.chunks = new Set();
    // 存放本次产出的文件对象
    this.assets = new Set();
    // 存放本次编译所有产出的文件名
    this.files = new Set();
  }
  // ...
 }

Здесь мы даемcompilerДобавьте некоторые свойства в конструктор для хранения соответствующих объектов ресурсов/модулей, созданных на этапе компиляции.

оentries\modules\chunks\assets\filesэти несколькоSetОбъекты — это атрибуты, которые проходят через наш основной процесс упаковки.Они используются для хранения различных ресурсов на этапе компиляции и, наконец, для создания скомпилированных файлов с помощью соответствующих атрибутов.

Анализ файла записи на основе пути к файлу записи

Выше было сказано, что мы находимся вrunметод уже может быть переданthis.getEntry();Получить соответствующий объект записи~

Далее, давайте начнем с входного файла, чтобы проанализировать входной файл!

class Compiler {
    // run方法启动编译
  // 同时run方法接受外部传递的callback
  run(callback) {
    // 当调用run方式时 触发开始编译的plugin
    this.hooks.run.call();
    // 获取入口配置对象
    const entry = this.getEntry();
    // 编译入口文件
    this.buildEntryModule(entry);
  }

  buildEntryModule(entry) {
    Object.keys(entry).forEach((entryName) => {
      const entryPath = entry[entryName];
      const entryObj = this.buildModule(entryName, entryPath);
      this.entries.add(entryObj);
    });
  }
  
  
  // 模块编译方法
  buildModule(moduleName,modulePath) {
    // ...
    return {}
  }
}

Здесь мы добавляем файл с именемbuildEntryModuleМетод скомпилирован как метод входного модуля. Переберите объекты входа и получите имя и путь каждого объекта входа.

Например, если мы перейдем в началеentry:{ main:'./src/main.js' }если,buildEntryModuleполученный параметрentryдля{ main: "/src...[你的绝对路径]" }, в это время мыbuildModuleМетод entryName принимаетmain,entryPathвходной файлmainСоответствующий абсолютный путь.

После того, как единая запись составлена, мыbuildModuleМетод возвращает объект. Этот объект является объектом после того, как мы скомпилируем входной файл.

buildModuleМетод компиляции модуля

Перед записью кода давайте отсортироваем этоbuildModuleметод, что ему нужно сделать:

  • buildModuleПринимает два аргумента для компиляции модуля,Первый — это имя входного файла, которому принадлежит модуль., второй — путь к модулю, который необходимо скомпилировать.

  • buildModuleПредпосылкой для метода компиляции кода является передачаfsМодуль считывает исходный код файла в соответствии с путем к файлу записи.

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

  • получатьloaderПосле обработки результатов пройтиbabelанализироватьloaderОбработанный код компилируется. (Этот этап компиляции в основном предназначен дляrequireзаявление, изменить исходный кодrequireпуть оператора).

  • Если входной файл не зависит ни от одного модуля (requireоператор), то возвращается скомпилированный объект модуля.

  • Если входной файл имеет зависимые модули, рекурсивноbuildModuleМетод компиляции модуля.

прочитать содержимое файла

  1. Мы сначала звонимfsМодуль считывает содержимое файла.
const fs = require('fs');
// ...
class Compiler {
      //...
      // 模块编译方法
      buildModule(moduleName, modulePath) {
        // 1. 读取文件原始代码
        const originSourceCode =
          ((this.originSourceCode = fs.readFileSync(modulePath, 'utf-8'));
        // moduleCode为修改后的代码
        this.moduleCode = originSourceCode;
      }
      
      // ...
 }

передачаloaderПроцесс сопоставления файлов суффикса

  1. Далее, после того, как мы получим конкретное содержимое файла, нам нужно сопоставить соответствующийloaderСкомпилировал наш исходный код.

Реализовать простой пользовательский загрузчик

в ходе выполненияloaderПеред компиляцией давайте реализуем настройку, которую мы передали выше.loaderБар.

image.png

webpack/loaderновый каталогloader-1.js,loader-2.js:

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

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

// loader本质上就是一个函数,接受原始内容,返回转换后的内容。
function loader1(sourceCode) {
  console.log('join loader1');
  return sourceCode + `\n const loader1 = 'https://github.com/19Qingfeng'`;
}

module.exports = loader1;
function loader2(sourceCode) {
  console.log('join loader2');
  return sourceCode + `\n const loader2 = '19Qingfeng'`;
}

module.exports = loader2;

Использовать загрузчик для обработки файлов

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

// 模块编译方法
  buildModule(moduleName, modulePath) {
    // 1. 读取文件原始代码
    const originSourceCode =
      ((this.originSourceCode = fs.readFileSync(modulePath)), 'utf-8');
    // moduleCode为修改后的代码
    this.moduleCode = originSourceCode;
    //  2. 调用loader进行处理
    this.handleLoader(modulePath);
  }

  // 匹配loader处理
  handleLoader(modulePath) {
    const matchLoaders = [];
    // 1. 获取所有传入的loader规则
    const rules = this.options.module.rules;
    rules.forEach((loader) => {
      const testRule = loader.test;
      if (testRule.test(modulePath)) {
        if (loader.loader) {
          // 仅考虑loader { test:/\.js$/g, use:['babel-loader'] }, { test:/\.js$/, loader:'babel-loader' }
          matchLoaders.push(loader.loader);
        } else {
          matchLoaders.push(...loader.use);
        }
      }
      // 2. 倒序执行loader传入源代码
      for (let i = matchLoaders.length - 1; i >= 0; i--) {
        // 目前我们外部仅支持传入绝对路径的loader模式
        // require引入对应loader
        const loaderFn = require(matchLoaders[i]);
        // 通过loader同步处理我的每一次编译的moduleCode
        this.moduleCode = loaderFn(this.moduleCode);
      }
    });
  }

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

Наконец, в каждой компиляции модуляthis.moduleCodeпройдет через соответствующийloaderиметь дело с.

webpackэтап компиляции модуля

На предыдущем шаге мы прошлиloaderОбработал код нашего входного файла, получил обработанный код и сохранил его вthis.moduleCodeсередина.

В это время послеloaderПосле обработки мы собираемся ввестиwebpackФаза внутренней компиляции тоже.

Что нам нужно сделать здесь:Компиляция для текущего модуля модуль будет зависеть от текущего модуля (require()) вводит путь относительно конечного пути (this.rootPath) относительный путь.

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

Продолжаем совершенствоватьсяbuildModuleметод:

const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generator = require('@babel/generator').default;
const t = require('@babel/types');
const tryExtensions = require('./utils/index')
// ...
  class Compiler {
     // ...
      
     // 模块编译方法
      buildModule(moduleName, modulePath) {
        // 1. 读取文件原始代码
        const originSourceCode =
          ((this.originSourceCode = fs.readFileSync(modulePath)), 'utf-8');
        // moduleCode为修改后的代码
        this.moduleCode = originSourceCode;
        //  2. 调用loader进行处理
        this.handleLoader(modulePath);
        // 3. 调用webpack 进行模块编译 获得最终的module对象
        const module = this.handleWebpackCompiler(moduleName, modulePath);
        // 4. 返回对应module
        return module
      }

      // 调用webpack进行模块编译
      handleWebpackCompiler(moduleName, modulePath) {
        // 将当前模块相对于项目启动根目录计算出相对路径 作为模块ID
        const moduleId = './' + path.posix.relative(this.rootPath, modulePath);
        // 创建模块对象
        const module = {
          id: moduleId,
          dependencies: new Set(), // 该模块所依赖模块绝对路径地址
          name: [moduleName], // 该模块所属的入口文件
        };
        // 调用babel分析我们的代码
        const ast = parser.parse(this.moduleCode, {
          sourceType: 'module',
        });
        // 深度优先 遍历语法Tree
        traverse(ast, {
          // 当遇到require语句时
          CallExpression:(nodePath) => {
            const node = nodePath.node;
            if (node.callee.name === 'require') {
              // 获得源代码中引入模块相对路径
              const requirePath = node.arguments[0].value;
              // 寻找模块绝对路径 当前模块路径+require()对应相对路径
              const moduleDirName = path.posix.dirname(modulePath);
              const absolutePath = tryExtensions(
                path.posix.join(moduleDirName, requirePath),
                this.options.resolve.extensions,
                requirePath,
                moduleDirName
              );
              // 生成moduleId - 针对于跟路径的模块ID 添加进入新的依赖模块路径
              const moduleId =
                './' + path.posix.relative(this.rootPath, absolutePath);
              // 通过babel修改源代码中的require变成__webpack_require__语句
              node.callee = t.identifier('__webpack_require__');
              // 修改源代码中require语句引入的模块 全部修改变为相对于跟路径来处理
              node.arguments = [t.stringLiteral(moduleId)];
              // 为当前模块添加require语句造成的依赖(内容为相对于根路径的模块ID)
              module.dependencies.add(moduleId);
            }
          },
        });
        // 遍历结束根据AST生成新的代码
        const { code } = generator(ast);
        // 为当前模块挂载新的生成的代码
        module._source = code;
        // 返回当前模块对象
        return module
      }
  }

На этом шаге мы примерноwebpackЭтап компиляции завершен.

нужно знать, это:

  • Здесь мы используемbabelСвязанныйAPIТаргетинг наrequireведомость составляется, если дляbabelСвязанныйapiДрузья, которые мало что знают, могут просмотреть две мои статьи перед знаниями. Я здесь не громоздкий.

  • В то же время наш код ссылается наtryExtensions()Инструментальный метод, этот метод предназначен для инструментального метода с неполным суффиксом имени, вы можете увидеть конкретное содержание этого метода позже.

  • Для каждой компиляции файла мы возвращаемmoduleОбъект, этот объект имеет наивысший приоритет.

    • idатрибут, указывающий, что текущий модуль нацеленthis.rootPathотносительный каталог.
    • dependenciesсобственность, этоSetИдентификаторы всех модулей, от которых зависит этот модуль, хранятся внутри.
    • nameатрибут, указывающий, к какому входному файлу принадлежит модуль.
    • _sourceатрибут, в котором хранится сам модульbabelСкомпилированный строковый код.

реализация метода tryExtensions

наш вышеwebpack.config.jsЕсть такая конфигурация:

image.png

знакомыйwebpackСтуденты конфигурации могут знать,resolve.extensionsОн направлен на введение зависимостей, без написания файловых суффиксов,webpackОн автоматически поможет нам добавить суффикс к файлу согласно входящим правилам.

После того, как мы поняли принцип, давайте посмотримutils/tryExtensionsРеализация метода:


/**
 *
 *
 * @param {*} modulePath 模块绝对路径
 * @param {*} extensions 扩展名数组
 * @param {*} originModulePath 原始引入模块路径
 * @param {*} moduleContext 模块上下文(当前模块所在目录)
 */
function tryExtensions(
  modulePath,
  extensions,
  originModulePath,
  moduleContext
) {
  // 优先尝试不需要扩展名选项
  extensions.unshift('');
  for (let extension of extensions) {
    if (fs.existsSync(modulePath + extension)) {
      return modulePath + extension;
    }
  }
  // 未匹配对应文件
  throw new Error(
    `No module, Error: Can't resolve ${originModulePath} in  ${moduleContext}`
  );
}

Этот метод очень прост, мы проходимfs.existsSyncПроверить привязки входящих файловextensionsПерейдите по очереди, чтобы найти, существует ли соответствующий совпадающий путь, и вернитесь напрямую, если он найден. Дает дружественную подсказку об ошибке, если не найден.

требует внимания extensions.unshift('');Чтобы пользователь не мог передать суффикс, мы сначала пытаемся найти его напрямую и возвращаемся напрямую, если файл может быть найден. Если он не найден, он будет пытаться один за другим.

Рекурсивная обработка

После предыдущего шага вызываем входной файлbuildModuleТакой возвращаемый объект может быть получен.

Давайте посмотрим на бегwebpack/core/index.jsПолучите обратный результат.

image.png

я здесьbuildEntryModuleПосле того, как обработка завершена,entriesобъект. Вы можете увидеть, как мы и ожидали:

  • idДля каждого модуля относительно модуля с путем.(Здесь мы настраиваемcontext:process.cwd())дляwebpackсодержание.
  • dependenciesЭто модуль, который внутренне зависит от этого модуля, и он еще не добавлен сюда.
  • nameимя файла входа, к которому принадлежит этот модуль.
  • _sourceСкомпилированный исходный код для этого модуля.

В настоящее время_sourceСодержание основано на

Теперь давайте откроемsrcДавайте добавим некоторые зависимости и содержимое к нашим двум файлам записей:

// webpack/example/entry1.js
const depModule = require('./module');

console.log(depModule, 'dep');
console.log('This is entry 1 !');


// webpack/example/entry2.js
const depModule = require('./module');

console.log(depModule, 'dep');
console.log('This is entry 2 !');

// webpack/example/module.js
const name = '19Qingfeng';

module.exports = {
  name,
};

В этот момент давайте перезапустимwebpack/core/index.js:

image.png

Хорошо, пока мы ориентируемсяentryКомпиляция может временно закончиться.

Короче говоря, на этом шаге мы используем метод `` дляentryОбъект получается после анализа и компиляции. добавить этот объект вthis.entriesвходить.

Далее займемся зависимыми модулями.

По сути, те же шаги используются для зависимых модулей:

  • Проверьте входной файл на наличие зависимостей.
  • Если есть зависимость, рекурсивный вызовbuildModuleспособ компиляции модуля. входящийmoduleNameЭто входной файл, которому принадлежит текущий модуль.modulePathДля абсолютного пути к зависимому в данный момент модулю.
  • 同理检查递归检查被依赖的模块内部是否仍然存在依赖,存在的话递归依赖进行模块编译。 Этоглубина перваяпроцесс.
  • Сохраните каждый скомпилированный модуль вthis.modulesвходить.

Далее у нас есть немногоhandleWebpackCompilerНебольшое изменение в методе:

 // 调用webpack进行模块编译
  handleWebpackCompiler(moduleName, modulePath) {
    // 将当前模块相对于项目启动根目录计算出相对路径 作为模块ID
    const moduleId = './' + path.posix.relative(this.rootPath, modulePath);
    // 创建模块对象
    const module = {
      id: moduleId,
      dependencies: new Set(), // 该模块所依赖模块绝对路径地址
      name: [moduleName], // 该模块所属的入口文件
    };
    // 调用babel分析我们的代码
    const ast = parser.parse(this.moduleCode, {
      sourceType: 'module',
    });
    // 深度优先 遍历语法Tree
    traverse(ast, {
      // 当遇到require语句时
      CallExpression: (nodePath) => {
        const node = nodePath.node;
        if (node.callee.name === 'require') {
          // 获得源代码中引入模块相对路径
          const requirePath = node.arguments[0].value;
          // 寻找模块绝对路径 当前模块路径+require()对应相对路径
          const moduleDirName = path.posix.dirname(requirePath);
          const absolutePath = tryExtensions(
            path.posix.join(moduleDirName, requirePath),
            this.options.resolve.extensions,
            moduleName,
            moduleDirName
          );
          // 生成moduleId - 针对于跟路径的模块ID 添加进入新的依赖模块路径
          const moduleId =
            './' + path.posix.relative(this.rootPath, absolutePath);
          // 通过babel修改源代码中的require变成__webpack_require__语句
          node.callee = t.identifier('__webpack_require__');
          // 修改源代码中require语句引入的模块 全部修改变为相对于跟路径来处理
          node.arguments = [t.stringLiteral(moduleId)];
          // 为当前模块添加require语句造成的依赖(内容为相对于根路径的模块ID)
          module.dependencies.add(moduleId);
        }
      },
    });
    // 遍历结束根据AST生成新的代码
    const { code } = generator(ast);
    // 为当前模块挂载新的生成的代码
    module._source = code;
    // 递归依赖深度遍历 存在依赖模块则加入
    module.dependencies.forEach((dependency) => {
      const depModule = this.buildModule(moduleName, dependency);
      // 将编译后的任何依赖模块对象加入到modules对象中去
      this.modules.add(depModule);
    });
    // 返回当前模块对象
    return module;
  }

Здесь мы добавляем этот фрагмент кода:

    // 递归依赖深度遍历 存在依赖模块则加入
    module.dependencies.forEach((dependency) => {
      const depModule = this.buildModule(moduleName, dependency);
      // 将编译后的任何依赖模块对象加入到modules对象中去
      this.modules.add(depModule);
    });

Здесь мы делаем рекурсивные вызовы зависимых модулейbuildModule, объект модуля вывода добавляется вthis.modulesвходить.

В этот момент давайте перезапустимwebpack/core/index.jsдля компиляции, вот яbuildEntryModuleПосле компиляции печатаетassetsа такжеmodules:

image.png

Set {
  {
    id: './example/src/entry1.js',
    dependencies: Set { './example/src/module.js' },
    name: [ 'main' ],
    _source: 'const depModule = __webpack_require__("./example/src/module.js");\n' +
      '\n' +
      "console.log(depModule, 'dep');\n" +
      "console.log('This is entry 1 !');\n" +
      "const loader2 = '19Qingfeng';\n" +
      "const loader1 = 'https://github.com/19Qingfeng';"
  },
  {
    id: './example/src/entry2.js',
    dependencies: Set { './example/src/module.js' },
    name: [ 'second' ],
    _source: 'const depModule = __webpack_require__("./example/src/module.js");\n' +
      '\n' +
      "console.log(depModule, 'dep');\n" +
      "console.log('This is entry 2 !');\n" +
      "const loader2 = '19Qingfeng';\n" +
      "const loader1 = 'https://github.com/19Qingfeng';"
  }
} entries
Set {
  {
    id: './example/src/module.js',
    dependencies: Set {},
    name: [ 'main' ],
    _source: "const name = '19Qingfeng';\n" +
      'module.exports = {\n' +
      '  name\n' +
      '};\n' +
      "const loader2 = '19Qingfeng';\n" +
      "const loader1 = 'https://github.com/19Qingfeng';"
  },
  {
    id: './example/src/module.js',
    dependencies: Set {},
    name: [ 'second' ],
    _source: "const name = '19Qingfeng';\n" +
      'module.exports = {\n' +
      '  name\n' +
      '};\n' +
      "const loader2 = '19Qingfeng';\n" +
      "const loader1 = 'https://github.com/19Qingfeng';"
  }
} modules

Видно, что у насmodule.jsЭта зависимость была добавлена ​​вmodulesв, и он также прошел черезloaderобработка. Но мы обнаружили, что он был добавлен дважды.

Это потому чтоmodule.jsЭтот модуль упоминается дважды, этоentry1а такжеentry2у обоих есть зависимости, мы делаем это дважды при рекурсивной компиляцииbuildModuleтот самый модуль.

Разберемся с этой проблемой:

    handleWebpackCompiler(moduleName, modulePath) {
       ...
        // 通过babel修改源代码中的require变成__webpack_require__语句
          node.callee = t.identifier('__webpack_require__');
          // 修改源代码中require语句引入的模块 全部修改变为相对于跟路径来处理
          node.arguments = [t.stringLiteral(moduleId)];
          // 转化为ids的数组 好处理
          const alreadyModules = Array.from(this.modules).map((i) => i.id);
          if (!alreadyModules.includes(moduleId)) {
            // 为当前模块添加require语句造成的依赖(内容为相对于根路径的模块ID)
            module.dependencies.add(moduleId);
          } else {
            // 已经存在的话 虽然不进行添加进入模块编译 但是仍要更新这个模块依赖的入口
            this.modules.forEach((value) => {
              if (value.id === moduleId) {
                value.name.push(moduleName);
              }
            });
          }
        }
      },
    });
    ...
    }

Здесь в каждом преобразовании зависимостей анализа кода сначала определитеthis.moduleСуществует ли уже объект в текущем модуле (судя по уникальному пути идентификатора модуля).

Если он не существует, добавьте его в зависимость для компиляции.Если модуль уже существует, это доказывает, что модуль был скомпилирован. Так что на данный момент нам не нужно его снова компилировать, нам просто нужно обновить чанк, к которому принадлежит этот модуль, для егоnameсвойство для добавления принадлежащего в настоящее времяchunkимя.

Запустим повторно, посмотрим на распечатку:

Set(2) {
  {
    id: './example/src/entry1.js',
    dependencies: Set(1) { './example/src/module.js' },
    name: [ 'main' ],
    _source: 'const depModule = __webpack_require__("./example/src/module.js");\n' +
      '\n' +
      "console.log(depModule, 'dep');\n" +
      "console.log('This is entry 1 !');\n" +
      "const loader2 = '19Qingfeng';\n" +
      "const loader1 = 'https://github.com/19Qingfeng';"
  },
  {
    id: './example/src/entry2.js',
    dependencies: Set(0) {},
    name: [ 'second' ],
    _source: 'const depModule = __webpack_require__("./example/src/module.js");\n' +
      '\n' +
      "console.log(depModule, 'dep');\n" +
      "console.log('This is entry 2 !');\n" +
      "const loader2 = '19Qingfeng';\n" +
      "const loader1 = 'https://github.com/19Qingfeng';"
  }
} 入口文件
Set(1) {
  {
    id: './example/src/module.js',
    dependencies: Set(0) {},
    name: [ 'main', 'second' ],
    _source: "const name = '19Qingfeng';\n" +
      'module.exports = {\n' +
      '  name\n' +
      '};\n' +
      "const loader2 = '19Qingfeng';\n" +
      "const loader1 = 'https://github.com/19Qingfeng';"
  }
} modules

На этом наша «фаза компиляции модуля» в основном завершена.На этом этапе мы анализируем все модули из входного файла.

  • Начиная с записи, прочитать содержимое файла записи и вызвать совпадениеloaderОбработайте входной файл.
  • пройти черезbabelПроанализируйте зависимости и заодно замените все зависимости путями относительно каталога запуска проекта.options.contextмаршрут из.
  • Если в файле входа имеется зависимость, вышеуказанные шаги являются рекурсивными для компиляции зависимого модуля.
  • Добавьте скомпилированный объект каждого зависимого модуляthis.modules.
  • Добавьте скомпилированный объект каждого файла записиthis.entries.

Этап завершения компиляции

На предыдущем шаге мы завершили компиляцию между модулями, иmoduleа такжеentryНаполнен содержанием.

После рекурсивной компиляции всех модулей нам нужноВ соответствии с приведенными выше зависимостями объедините окончательный выводchunkмодуль.

Продолжаем преобразовывать нашуCompilerБар:

class Compiler {

    // ...
    buildEntryModule(entry) {
        Object.keys(entry).forEach((entryName) => {
          const entryPath = entry[entryName];
          // 调用buildModule实现真正的模块编译逻辑
          const entryObj = this.buildModule(entryName, entryPath);
          this.entries.add(entryObj);
          // 根据当前入口文件和模块的相互依赖关系,组装成为一个个包含当前入口所有依赖模块的chunk
          this.buildUpChunk(entryName, entryObj);
        });
        console.log(this.chunks, 'chunks');
    }
    
     // 根据入口文件和依赖模块组装chunks
      buildUpChunk(entryName, entryObj) {
        const chunk = {
          name: entryName, // 每一个入口文件作为一个chunk
          entryModule: entryObj, // entry编译后的对象
          modules: Array.from(this.modules).filter((i) =>
            i.name.includes(entryName)
          ), // 寻找与当前entry有关的所有module
        };
        // 将chunk添加到this.chunks中去
        this.chunks.add(chunk);
      }
      
      // ...
}

Здесь мы передаем каждый модуль (module)изnameСвойство находит все зависимые файлы для соответствующей записи.

Давайте сначала посмотримthis.chunksЧто в итоге будет выведено:

Set {
  {
    name: 'main',
    entryModule: {
      id: './example/src/entry1.js',
      dependencies: [Set],
      name: [Array],
      _source: 'const depModule = __webpack_require__("./example/src/module.js");\n' +
        '\n' +
        "console.log(depModule, 'dep');\n" +
        "console.log('This is entry 1 !');\n" +
        "const loader2 = '19Qingfeng';\n" +
        "const loader1 = 'https://github.com/19Qingfeng';"
    },
    modules: [ [Object] ]
  },
  {
    name: 'second',
    entryModule: {
      id: './example/src/entry2.js',
      dependencies: Set {},
      name: [Array],
      _source: 'const depModule = __webpack_require__("./example/src/module.js");\n' +
        '\n' +
        "console.log(depModule, 'dep');\n" +
        "console.log('This is entry 2 !');\n" +
        "const loader2 = '19Qingfeng';\n" +
        "const loader1 = 'https://github.com/19Qingfeng';"
    },
    modules: []
  }
} 

этот шаг,у нас естьWebpackДва окончательных результата вchunk.

Имеют соответственно:

  • name: Имя текущего файла записи
  • entryModule: скомпилированный объект входного файла.
  • modules: Массив всех объектов модуля, от которых зависит входной файл, формат каждого элемента иentryModuleсогласуется.

На этом компиляция завершена и мы собираемchunkчасть успешно завершена.

этап выходного файла

Давайте сначала поместим все скомпилированные части, собранные на предыдущем шаге.this.chunks.

Анализ необработанных упакованных выходных данных

Вот, я положилwebpack/core/index.jsСделал следующие модификации:

- const webpack = require('./webpack');
+ const webpack = require('webpack')

...

использовать оригиналwebpackвместо нашего собственногоwebpackСначала упакуйте.

бегатьwebpack/core/index.jsпозже мыwebpack/src/buildполучить два файла:main.jsа такжеsecond.js, берем один изmain.jsДавайте посмотрим на его содержимое:

(() => {
  var __webpack_modules__ = {
    './example/src/module.js': (module) => {
      const name = '19Qingfeng';

      module.exports = {
        name,
      };

      const loader2 = '19Qingfeng';
      const loader1 = 'https://github.com/19Qingfeng';
    },
  };
  // The module cache
  var __webpack_module_cache__ = {};

  // The require function
  function __webpack_require__(moduleId) {
    // Check if module is in cache
    var cachedModule = __webpack_module_cache__[moduleId];
    if (cachedModule !== undefined) {
      return cachedModule.exports;
    }
    // Create a new module (and put it into the cache)
    var module = (__webpack_module_cache__[moduleId] = {
      // no module.id needed
      // no module.loaded needed
      exports: {},
    });

    // Execute the module function
    __webpack_modules__[moduleId](module, module.exports, __webpack_require__);

    // Return the exports of the module
    return module.exports;
  }

  var __webpack_exports__ = {};
  // This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
  (() => {
    const depModule = __webpack_require__(
      /*! ./module */ './example/src/module.js'
    );

    console.log(depModule, 'dep');
    console.log('This is entry 1 !');

    const loader2 = '19Qingfeng';
    const loader1 = 'https://github.com/19Qingfeng';
  })();
})();

Здесь я вручную удаляю лишние комментарии после упаковки и генерации и упрощаю код.

Давайте немного проанализируем код, сгенерированный оригинальной упаковкой:

webpackУпакованный код определяет__webpack_require__функционировать вместоNodeJsВнутреннийrequireметод.

При этом дно

image.png

Этот фрагмент кода всем знаком, это код нашего скомпилированного входного файла. При этом код вверху — это объект, определяемый всеми модулями, от которых зависит входной файл:

image.png

здесь определяет__webpack__modulesобъект, **объектkey— относительный путь зависимого модуля относительно замыкающего пути, путь объектаvalueСкомпилированный код зависимого модуля. `

этап выходного файла

После завершения анализаwebpackПосле исходного упакованного кода давайте перейдем к предыдущему шагу. через нашthis.chunksПопробуем вывести конечный эффект.

давай вернемсяCompilerВверхrunВ методе:

   class Compiler {
   
   }
  // run方法启动编译
  // 同时run方法接受外部传递的callback
  run(callback) {
    // 当调用run方式时 触发开始编译的plugin
    this.hooks.run.call();
    // 获取入口配置对象
    const entry = this.getEntry();
    // 编译入口文件
    this.buildEntryModule(entry);
    // 导出列表;之后将每个chunk转化称为单独的文件加入到输出列表assets中
    this.exportFile(callback);
  }

мы вbuildEntryModuleПосле того, как модуль скомпилирован, перейдитеthis.exportFileМетод реализует логику для экспорта файла.

Давайте взглянемthis.exportFileметод:

 // 将chunk加入输出列表中去
  exportFile(callback) {
    const output = this.options.output;
    // 根据chunks生成assets内容
    this.chunks.forEach((chunk) => {
      const parseFileName = output.filename.replace('[name]', chunk.name);
      // assets中 { 'main.js': '生成的字符串代码...' }
      this.assets[parseFileName] = getSourceCode(chunk);
    });
    // 调用Plugin emit钩子
    this.hooks.emit.call();
    // 先判断目录是否存在 存在直接fs.write 不存在则首先创建
    if (!fs.existsSync(output.path)) {
      fs.mkdirSync(output.path);
    }
    // files中保存所有的生成文件名
    this.files = Object.keys(this.assets);
    // 将assets中的内容生成打包文件 写入文件系统中
    Object.keys(this.assets).forEach((fileName) => {
      const filePath = path.join(output.path, fileName);
      fs.writeFileSync(filePath, this.assets[fileName]);
    });
    // 结束之后触发钩子
    this.hooks.done.call();
    callback(null, {
      toJson: () => {
        return {
          entries: this.entries,
          modules: this.modules,
          files: this.files,
          chunks: this.chunks,
          assets: this.assets,
        };
      },
    });
  }

exportFileСделал следующие вещи:

  • Сначала получите выходную конфигурацию параметров конфигурации, повторите нашуthis.chunks,Будуoutput.filenameсередина[name]Замена называется соответствующим именем файла записи. В то же время согласноchunksСодержаниеthis.assetsДобавьте имя файла и содержимое файла, которые необходимо упаковать и сгенерировать.

  • Вызывается перед записью файла на дискpluginизemitфункция крючка.

  • судитьoutput.pathСуществует ли папка, если нет, пройтиfsСоздайте эту папку.

  • Все имена файлов (this.assetsизkeyмассив значений) хранится вfilesвходить.

  • циклthis.assets, запишите файлы на соответствующие диски по очереди.

  • Все процессы упаковки заканчиваются, запускаютсяwebpackплагинdoneкрюк.

  • в то же время дляNodeJs Webpack APiэхо, звонокrunизвне передается в методcallbackПередайте два параметра.

В основном,this.assetsДелается это относительно просто, т. е. путем анализаchunksполучатьassetsЗатем выведите соответствующий код на диск.

Посмотрите внимательно на приведенный выше код, вы найдете.this.assetsэтоMapкаждого элемента вvalueпозвонивgetSourceCode(chunk)метод для генерации кода, соответствующего модулю.

ТакgetSourceCodeКак этот методchunkчтобы сгенерировать наш окончательный скомпилированный код? Давайте посмотрим вместе!

getSourceCodeметод

Во-первых, давайте кратко поясним обязанности этого метода.getSourceCodeМетод принимает входящийchunkобъект. тем самым возвращаяchunkисходный код.

Не много ерунды, на самом деле, я использовал здесь относительно ленивый метод, но он совершенно не мешает вашему пониманиюWebpackПроцесс, мы проанализировали оригиналwebpackупакованный кодТолько входной файл и зависимости модуля каждый раз упаковываются по-разному, примерноrequireметоды те же.

Каждый раз улавливая разницу, давайте сначала посмотрим на его реализацию:

// webpack/utils/index.js

...


/**
 *
 *
 * @param {*} chunk
 * name属性入口文件名称
 * entryModule入口文件module对象
 * modules 依赖模块路径
 */
function getSourceCode(chunk) {
  const { name, entryModule, modules } = chunk;
  return `
  (() => {
    var __webpack_modules__ = {
      ${modules
        .map((module) => {
          return `
          '${module.id}': (module) => {
            ${module._source}
      }
        `;
        })
        .join(',')}
    };
    // The module cache
    var __webpack_module_cache__ = {};

    // The require function
    function __webpack_require__(moduleId) {
      // Check if module is in cache
      var cachedModule = __webpack_module_cache__[moduleId];
      if (cachedModule !== undefined) {
        return cachedModule.exports;
      }
      // Create a new module (and put it into the cache)
      var module = (__webpack_module_cache__[moduleId] = {
        // no module.id needed
        // no module.loaded needed
        exports: {},
      });

      // Execute the module function
      __webpack_modules__[moduleId](module, module.exports, __webpack_require__);

      // Return the exports of the module
      return module.exports;
    }

    var __webpack_exports__ = {};
    // This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
    (() => {
      ${entryModule._source}
    })();
  })();
  `;
}
...

Этот код на самом деле очень, очень прост, гораздо менее сложен, чем вы думаете! Это похоже на возвращение к истокам, не так ли?

существуетgetSourceCodeметод, мы комбинируемchunkПолучите соответствующее:

  • name: Файл ввода соответствует имени выходного файла.
  • entryModule: сохранить скомпилированный объект файла записи.
  • modules: Объекты, в которых хранятся все модули, от которых зависит входной файл.

Мы делаем это путем конкатенации строк__webpack__modulesсвойства на объекте, а также передавая внизу${entryModule._source}Код для склеивания входного и выходного файлов.

Здесь мы упоминали выше, почему модульrequireПуть метода преобразуется относительно конечного пути (context) пути, и, увидев это, я полагаю, у всех есть четкое понимание того, почему это делается. потому что мы, наконец, достигли__webpack_require__Все методы реализованы для относительного пути между модулем и путем.requireметод.

В то же время, если неясноrequireМетод - это то, как преобразование называется__webpack_require__Студенты, изучающие метод, могут вернуться к нашей главе о компиляции, чтобы внимательно просмотреть ее.babelсуществуетASTЭтап преобразования будетrequireВызов метода становится__webpack_require__.

ты закончил

Пока что вернемся кwebpack/core/index.jsвходить. Перезапустите этот файл, и вы найдетеwebpack/exampleВ каталоге будет еще одинbuildсодержание.

image.png

На этом шаге мы прекрасно реализуем наши собственныеwebpack.

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

любой последующийwebpackВ соответствующем базовом развитии действительно достичьcompilerИспользование понятно до груди. К пониманиюcompilerКак различные свойства влияют на результаты компиляции и упаковки.

Давайте используем блок-схему для красивого завершения:

image.png

напиши в конце

Прежде всего, спасибо всем, кто может видеть здесь.

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

Статья для реализации упрощенной версииWebpackЭто подходит к концу со всеми здесь, это на самом деле только самая основная версияwebpackРабочий процесс.

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

Фактически, после понимания основного рабочего процесса, дляloaderа такжеpluginРазработка — удобная часть Введение в разработку этих двух частей в статье относительно поверхностно.loaderа такжеpluginподробный процесс разработки. Заинтересованные студенты могут вовремя обратить внимание 😄.

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

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