[Фронтенд-инжиниринг] Часть 4. Подметание восьми отходов-Webpack (дополнительно)

Webpack
[Фронтенд-инжиниринг] Часть 4. Подметание восьми отходов-Webpack (дополнительно)

Количество слов: 12197, время чтения: 35 минут, кликичитать оригинал

Камни можно разбить, но не твердые; Дан можно отшлифовать, но не докрасна - "Весенний и осенний период Лу, Честность и Честность"

[Внешняя инженерия] серия ссылок на статьи:

Репозиторий примеров кода:GitHub.com/B неправильно/Dev-…

Отказ от ответственности: эта статья основана на веб-пакете версии 4.43.0. Если в соответствии с кодом в статье сообщается об ошибке, проверьте, совпадает ли версия зависимого модуля с версией в репозитории примеров кода.

*Теплое напоминание: *Содержание этой статьи более практично и включает в себя много кода.Если у вас фобия, связанная с интенсивным использованием кода, пожалуйста, покиньте игру как можно скорее.

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

Вишенка на торте — оптимизация

Используйте инструменты анализа

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

инструмент анализа статистики

инструмент анализа статистикиЭто онлайн-инструмент, официально предоставленный webpack.Метод использования следующий:

  1. генерироватьstat.jsonдокумент.
webpack --profile --json > stats.json
  1. Будуstat.jsonзагрузить файл винструмент анализа статистикиНа странице можно получить результаты анализа.

использоватьwebpack-bundle-analyzer

В дополнение к инструментам анализа, предоставляемым на официальном веб-сайте, сообщество также предоставляет более полезный артефакт анализа.webpack-bundle-analyzer, не только интерфейс красивый, но и каждый зависимый пакет и его размер более интуитивно понятны.

npm install webpack-bundle-analyzer -D
// build/webpack.prod.conf.js
const merge = require('webpack-merge');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const baseConfig = require('./webpack.base.conf.js');

 module.exports = merge(baseConfig, {
   	mode: 'production',
    plugins: [
        new BundleAnalyzerPlugin(),
   		// ...
    ],
   // ...
 });

На рисунке мы можем просмотреть результаты Gzip и оригинальной упаковки, а также увидеть, какие пакеты содержит каждый фрагмент, а также их размер и другую информацию.

анализ скорости

Вышеупомянутые два инструмента ориентированы на анализ результатов упаковки с упором на оптимизацию качества упаковки. и черезspeed-measure-webpack-pluginЭтот плагин также может анализировать процесс сборки, чтобы оптимизировать эффективность сборки и улучшить качество упаковки.

npm i -D speed-measure-webpack-plugin
// build/webpack.prod.conf.js
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
const webpackConfig = {
    // ...
}
module.exports = smp.wrap(webpackConfig) // 需要包裹原来的配置

Теперь вы можете видеть время, потраченное каждым загрузчиком и плагином, чтобы оптимизировать их.

image-20201009111235625

Оптимизация качества упаковки

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

Tree shaking

Термин «тряска дерева» произошел отrollup, встроенный в официальную версию webpack 2.0, может обнаруживать неиспользуемые модули и удалять их. Это зависит от модульной системы ESModule (другие модульные системы не поддерживаются)статические структурные свойства, вы можете удалить неиспользуемый код в контексте JavaScript, удалить неиспользуемый код, что может эффективно уменьшить размер файла кода JS.

Название Tree Shake очень образно, Tree Shake — стряхивание с дерева лишнего.

// src/math.js
export function square(x) {
  return x * x;
}

export function cube(x) {
  return x * x * x;
}

// src/index.js
import { cube } from './math.js' // 仅引用了 cube 这个方法

console.log(cube(3))

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

В режиме разработки мы можем добавить следующую конфигурацию:

// build/webpack.dev.conf.js
module.exports = {
  mode: 'development',
  optimization: {
    usedExports: true, // 不导出模块中未使用的代码
  },
}

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

Если разработчику необходимо указать, какие модули не имеют побочных эффектов, можно использовать другой способ (побочный эффект), который можно настроить вpackage.jsonСвойство "sideEffects" (в основном для некоторых библиотек) также можно найти вmodule.rulesпараметры конфигурацииустановить в"sideEffects". Возможны следующие ситуации:

  • Если весь код не содержит кода побочных эффектов, просто установите для этого свойства значение false, чтобы сообщить веб-пакету, что он может безопасно удалять неиспользуемые экспорты.
//package.json
{
  "name": "your-project",
  "sideEffects": false
}
  • Если тег файла имеет побочные эффекты, нужны маркеры
//package.json
{
  "name": "your-project",
  "sideEffects": [
    "./src/some-side-effectful-file.js",
    "*.css" // 一般css都是具有副作用的,需要在此声明
  ]
}

«Побочный эффект» определяется как код, который выполняет особое поведение при импорте, а не просто предоставляет экспорт или экспорт. Например, полифиллы, которые влияют на глобальную область видимости и, как правило, не обеспечивают экспорт.

Соответствующие инструменты также предоставляются для css.PurgeCSSдля удаления неиспользуемого кода, что похоже на встряхивание дерева.

разделение кода

В эпоху одностраничных приложений (сокращенно называемых SPA) мы не можем упаковать весь код в файл js, и невозможно загрузить все файлы js одновременно при запуске приложения, в противном случае это займет много времени Белый экран сведет пользователей с ума и сильно повлияет на пользовательский опыт. В это время код можно разделить, чтобы его можно было загружать параллельно, или необходимый код можно загрузить на разных этапах (например, по разным маршрутам), что является разделением кода и загрузкой по требованию.

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

Используемый разделительный код:

  • Разбиение фрагментов записи: используйте запись для настройки нескольких фрагментов и ручного разделения кода.
  • Извлечь общий код:SplitChunksPluginДедупликация и разделение чанков.
  • Динамический импорт и загрузка по требованию: в настоящее время рекомендуется отдельный код с помощью встроенных вызовов функций в модулях.import()грамматика.

Сегментация входящего чанка

Самый простой способ разделения — вручную разделить код на разные куски в точке входа.

// build/webpack.base.conf.js
module.exports = {
  entry: {
    app: '../src/index.js',
   	another: '../src/another-module.js'
  },
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, '../dist')
  }
};

Результат сборки:

...
            Asset     Size   Chunks             Chunk Names
another.bundle.js  550 KiB  another  [emitted]  another
  index.bundle.js  550 KiB    index  [emitted]  index
Entrypoint index = index.bundle.js
Entrypoint another = another.bundle.js
...

Хотя этот метод прост, есть некоторые проблемы:

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

дедупликация модуля

вебпак имеет встроенныйSplitChunksPlugin, который может легко извлекать повторяющиеся модули в общедоступные зависимости и уменьшать размер файла. Более того, некоторые сторонние зависимости (как правило, не требующие частого изменения) извлекаются в публичные зависимости, что также удобно кешировать клиенту, при каждом обновлении кода пользователю не нужно заново скачивать эти модули без изменений.

// build/webpack.base.conf.js
module.exports = {
  // ...
  entry: {
    app: '../src/index.js',
   	another: '../src/another-module.js'
  },
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, '../dist')
  },
  optimization: {
     splitChunks: { // 添加此配置
       chunks: 'all'
     }
   }
};

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

...
                          Asset      Size                 Chunks             Chunk Names
              another.bundle.js  5.95 KiB                another  [emitted]  another
                index.bundle.js  5.89 KiB                  index  [emitted]  index
vendors~another~index.bundle.js   547 KiB  vendors~another~index  [emitted]  vendors~another~index
Entrypoint index = vendors~another~index.bundle.js index.bundle.js
Entrypoint another = vendors~another~index.bundle.js another.bundle.js
...

Автоматическое разделение чанков — самое большое улучшение в webpack 4.0.Если одновременно выполняются следующие условия, чанки будут разделены:

  • Новые фрагменты используются повторно или из каталога node_modules.
  • Размер нового фрагмента превышает 30 КБ (до min+gzip).
  • Количество одновременных запросов на загрузку фрагментов по запросу меньше или равно 5.
  • Количество одновременных запросов при первоначальной загрузке страницы меньше или равно 3.

splitChunksОфициальная конфигурация по умолчанию выглядит следующим образом, при необходимости ее можно изменить:

{
  optimization: {

    splitChunks: {
      chunks: "async", // 对哪些模块优化,取值有"initial"(初始化值) | "all"(所有,推荐) | "async" (动态导入,默认) | 函数
      minSize: 30000,              // 最小尺寸,小于此值才会分割
      minChunks: 1,                // 最小 chunk ,包被引用几次以上才会分割
      maxAsyncRequests: 5,         // 按需加载的最大并行请求数, 默认5
      maxInitialRequests: 3,       // 最大的初始化加载次数,默认3
      automaticNameDelimiter: '~', // 打包分隔符
      name: true,       // 拆分出来块的名字,默认由块名和 hash 值自动生成,此选项可接收 function
      cacheGroups: {   // 这里开始设置缓存的 chunks ,缓存组
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        }
      }
    }
  }
}

Динамический импорт и загрузка по требованию

Webpack предоставляет два способа динамического импорта кода.

  • require.ensure: Путь остался с первых дней.
  • import()грамматика: Метод динамического импорта модулей в спецификации ES (рекомендуется).

import()даПредложение ECMAScriptМетод динамического импорта модулей (асинхронно) в .Когда веб-пакет встречает этот синтаксис, он генерирует отдельный фрагмент с импортированным файлом в качестве записи и выводит отдельный пакет. При выполнении кода не все пакеты будут загружены, но пакет будет загружен при выполнении импортированного файла, что является процессом загрузки по требованию.

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

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

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

document.getElementById('my-button').onclick = function() {
    import('./module.js').then(fn => {
        fn.default && fn.default();
    });
}

module.jsи его внутренние зависимости будут генерировать отдельный пакет и загружать его по требованию. Если мы хотим контролировать имя выходного пакета, мы также можем указать имя чанка, что требует использования магических аннотаций веб-пакета.

document.getElementById('my-button').onclick = function() {
    import(/* webpackChunkName: "moduleA" */ './module.js').then(fn => { // 指定chunkName为moduleA
        fn.default && fn.default();
    });
}

намекать:import()вернет обещание, поэтому вы также можете использоватьasync functionОптимизированная цепочка.

document.getElementById('my-button').onclick = async function() {
    const fn = await import(/* webpackChunkName: "moduleA" */ './module.js')
    fn.default && fn.default();
}

**Расширение:** В дополнение к webpackChunkName, webpack также имеет две другие волшебные аннотации для предварительной выборки и предварительной загрузки модулей, вы можете проверить конкретный метод использования.официальная документация.

В реальных проектах сегментация кода также может относиться к следующим принципам разделения:

  • Разделение бизнес-кода и сторонних зависимостей
  • Разделение бизнес-кода, бизнес-общего кода и сторонних зависимостей
  • Разделение кода, загружаемого после первой загрузки и после доступа

Включить сжатие

Если на сервере развертывания включен gzip, его можно использоватьcompression-webpack-pluginСоздавайте пакеты gzip, чтобы сократить время загрузки.

npm i compression-webpack-plugin -D
// build/webpack.prod.conf.js
const CompressionWebpackPlugin = require('compression-webpack-plugin');
const productionGzipExtensions = ['js', 'css', 'json', 'txt', 'html','ico','svg'];
module.exports = {
	plugins:[
		new CompressionWebpackPlugin({
      // 开启gzip压缩
      algorithm: 'gzip', // 压缩算法
      test: new RegExp('\\.(' + productionGzipExtensions.join('|') + ')$'), 
      threshold: 10240, // 仅处理大于此大小的资源(以字节为单位)
      minRatio: 0.8 // 压缩比大于此值才处理
    })
	]
}

Используйте CDN

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

// build/webpack.base.conf.js
module.exports = {
    externals: {
        'vue': 'Vue', // 配置需要排除的包名称
        // ...
    }
}

В JS или используйтеimport Vue from 'vue'Вот и все, но не забудьте импортировать в html.

<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>app</title>
</head>
<body>
    <div id="root">root</div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
</body>
</html>

Повышение эффективности сборки

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

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

оптимизацияresolveПравила парсинга

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

// build/webpack.base.conf.js
resolve: {
  modules: [
    path.resolve(__dirname, 'node_modules'), // 使用绝对路径明确指定 node_modules
  ],
  // 减少后缀自动补全类型,减少自动匹配工作,缩短文件路径查询的时间,其他类型的文件需要在引入时指定后缀名
  extensions: [".js"],
  // 避免使用默认文件,而是必须使用完整的路径
  mainFiles: ['index'],
},

сузить поиск

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

// build/webpack.base.conf.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        include: path.resolve(__dirname, 'src'), // 仅仅搜索src下的文件,一般node_modules中的文件都已编译好,不需再处理,需要排除掉
        loader: 'babel-loader'
      }
    ]
  }
};

рационализироватьloader/plugin

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

Использовать DLL-плагин

DLLPluginЭто плагин, официально предоставленный webpack, использующийDllPluginСоздавайте отдельные результаты компиляции для редко изменяемого кода (например, vue, react и других сторонних модулей) и кэшируйте их, а затем используйте эти файлы непосредственно в последующих сборках, чтобы избежать повторных сборок. Это может значительно повысить скорость компиляции приложения.

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

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

npm install --save-dev autodll-webpack-plugin 
// build/webpack.base.conf.js
plugins: [
  //...
  new AutoDllPlugin({
    inject: true, 
    filename: '[name].js',
    entry: {
      vendor: [
        'vue'
      ]
    }
  })
]

优化前

优化后

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

маленький это быстро

Используйте меньшее количество библиотек меньшего размера, например, замените momentJS на dayJS, чтобы уменьшить общий размер скомпилированных результатов и повысить производительность сборки. Старайтесь, чтобы кусочки были как можно меньше.

  • Используйте меньше/меньше библиотек.
  • Использование в многостраничных приложенияхSplitChunksPlugin.
  • Использование в многостраничных приложенияхSplitChunksPlugin , и включитеasyncмодель.
  • Удалить неиспользуемый код.
  • Компилируйте только тот код, который вы сейчас разрабатываете.

постоянный кеш

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

npm install cache-loader -D
// build/webpack.base.conf.js
module.exports = {
    module: {
        rules: [{
            test: /\.jsx?$/,
            use: ['cache-loader','babel-loader'] // 仅需将cache-loader放在需要缓存的loader前面就行了
        }]
    }
}

**Совет.** Некоторые загрузчики сами поддерживают настройку кеша. Вы можете использовать функцию кеша, поставляемую вместе с загрузчиком. если даноbabel-loadercacheDirectory настраивает путь к кешу (true или no path установлен для использования пути по умолчаниюnode_modules/.cache/babel-loader), чтобы включить кэширование.

В дополнение к использованию загрузчика некоторые плагины также могут предоставлять функции кэширования, такие какHardSourceWebpackPlugin.

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

webpack5.0 имеет встроенныйСхема постоянного кэширования, что может эффективно повысить скорость сборки.

Адаптироваться к местным условиям

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

включить многопроцессорность

thread-loaderМожно управлять несколькими процессами, а ресурсоемкий загрузчик можно назначить рабочему процессу, тем самым снижая нагрузку на производительность основного процесса.

npm install thread-loader -D
// build/webpack.base.conf.js
module.exports = {
    module: {
        rules: [
            {
                test: /\.jsx?$/,
                use: ['thread-loader', 'babel-loader'] // 仅需在对应的loader前面添加thread-loader即可
            }
        ]
    }
}

Не используйте слишком много рабочих процессов, потому что и среда выполнения Node.js, и загрузчики имеют дополнительные затраты на запуск. Свести к минимуму передачу данных между воркером и основным процессом (main process), т.к. межпроцессное взаимодействие (IPC, Inter Process Communication) тоже очень ресурсоемкое.

Кромеthread-loader, HappyPack тоже может открывать мультипроцесс, но конфигурация немного сложнее, т.к. больше не поддерживается, использовать не рекомендуется.

Теперь встроенное JS-сжатие веб-пакета использует TerserWebpackPlugin, который уже открыл многопроцессорность, поэтому его больше не нужно открывать вручную.

В дополнение к вышеперечисленным методам каждая итерация webpack и nodeJS также будет значительно повышать производительность, а своевременное обновление инструментов также является эффективным методом повышения эффективности сборки. Другие методы оптимизации производительности можно просмотретьОфициальная документация — Производительность сборки.

Развитие внутренней силы — принципы

Принцип работы

Запуск процесса

Процесс работы WebPack - это последовательный, следующим образом:

  1. Параметры инициализации: чтение и объединение конфигураций параметров из файлов конфигурации и операторов оболочки для получения окончательной конфигурации параметров;
  2. Начать компиляцию: Инициализируйте объект Compiler с конфигурацией параметров, полученной на предыдущем шаге, загрузите все настроенные плагины и выполните метод запуска объекта, чтобы начать компиляцию;
  3. Определить запись: найти все файлы записи в соответствии с записью в конфигурации;
  4. Скомпилируйте модуль: начиная с входного файла, вызовите все сконфигурированные загрузчики для преобразования модуля, затем найдите модули, от которых зависит модуль, а затем повторите этот шаг. Пока на этом шаге не будут обработаны все файлы, зависящие от записи;
  5. Завершение компиляции модуля: После перевода всех модулей с помощью Loader на шаге 4 получается финальное содержимое каждого модуля после перевода и зависимости между ними;
  6. Выходные ресурсы: в соответствии с зависимостями между записями и модулями соберите фрагменты, содержащие несколько модулей, один за другим, а затем преобразуйте каждый фрагмент в отдельный файл и добавьте его в список выходных данных.Этот шаг — последний шанс изменить выходное содержимое. ;
  7. Вывод завершен: после определения содержимого вывода определите путь вывода и имя файла в соответствии с конфигурацией и запишите содержимое файла в файловую систему.

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

Вроде процессов много, можно упростить:

  1. Инициализация: запустите сборку, прочитайте и объедините параметры конфигурации, загрузите подключаемый модуль и создайте экземпляр компилятора.

  2. Компиляция: отправляется из Entry, последовательно вызывает соответствующий загрузчик для каждого модуля, чтобы перевести содержимое файла, затем находит модуль, от которого зависит модуль, и рекурсивно компилирует его.

  3. Вывод: объедините скомпилированные модули в куски, преобразуйте куски в файлы и выведите их в файловую систему.

Использованная литература:Обзор того, как работает веб-пакет

Реализовать простой веб-пакет

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

Готов к работе

Создайте проект со следующими файлами и папками:

image-20200705215338484

// /src/index.js
import a from './a.js';

console.log(a);
// /src/a.js
import b from './b.js';
const a = `b content: ${b}`;
export default a;
// /src/b.js
const b = 'Im B';
export default b;

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

Реализовать упаковку модулей

Перед вскрытием уточняем цели и процесс упаковки:

  1. Найдите запись проекта (т.е./src/index.js) и прочитать его содержимое;
  2. Анализировать содержимое входного файла, рекурсивно находить его зависимости и генерировать граф зависимостей;
  3. В соответствии с сгенерированным графом зависимостей скомпилируйте и сгенерируйте окончательный выходной код.

/myBundle.jsЭто наш упаковщик, в нем также будет написан весь соответствующий код, приступим!

1. Получите содержимое модуля

Прочитайте содержимое файла ввода, это очень просто, мы создаем методgetModuleInfo,использоватьfsчтобы прочитать содержимое файла:

// myBundle.js
const fs = require('fs')
const getModuleInfo = file => {
    const content = fs.readFileSync(file, 'utf-8')
    console.log(content)
}
getModuleInfo('./src/index.js')

Несомненно, это выведетindex.jsСодержимое файла, но это набор строк, откуда нам знать, от каких модулей он зависит? Есть два способа:

  • Обычный: получить соответствующий путь к файлу, регулярно сопоставляя ключевое слово «импорт», но это слишком хлопотно и ненадежно Что, если в коде есть строка, которая также имеет это содержимое?
  • бабель: может пройти@babel/parserПреобразовать код в AST (Абстрактное синтаксическое дерево, сокращенно AST), а затем проанализировать AST для поиска зависимостей. Это кажется более надежным.

Не сомневайтесь, воспользуйтесь вторым способом.

npm i @babel/parser ## 安装 @babel/parser
// myBundle.js
const fs = require('fs')
const parser = require('@babel/parser')
const getModuleInfo = file => {
    const content = fs.readFileSync(file, 'utf-8')
    const ast = parser.parse(content, {
       sourceType: 'module' // 解析ESModule须配置
    })
    console.log(ast)
    console.log(ast.program.body)
}
getModuleInfo('./src/index.js')

Результат преобразования выглядит следующим образом, вы можете увидеть всего два узла,typeатрибут идентифицирует тип узла,ImportDeclarationчто соответствуетimportпредложение, котороеsource.valueТо есть относительный путь импортируемого модуля. У меня есть все данные, которые я хочу, разве это не здорово!

image-20200705223745187

2. Создайте таблицу зависимостей

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

На самом деле он проходитast.program.body, которые будутImportDeclarationТип узла извлекается и сохраняется в таблице зависимостей.

Здесь нет необходимости вручную реализовывать детали, просто используйте их напрямую.@babel/traverseВот и все.

npm i  @babel/traverse ## 安装@babel/traverse

getModuleInfoМетод модифицируется следующим образом:

// myBundle.js
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const getModuleInfo = (file) => {
	const content = fs.readFileSync(file, 'utf-8');
	const ast = parser.parse(content, {
		sourceType: 'module'
	});
  	const dependencies = {}; // 用于存储依赖
    traverse(ast, {
        ImportDeclaration({ node }) { // 只处理ImportDeclaration类型的节点
            const dirname = path.dirname(file);
            const newFile = '.'+ path.sep + path.join(dirname, node.source.value); // 此处将相对路径转化为绝对路径,
            dependencies[node.source.value] = newFile;
        }
  	});
  	console.log(dependencies);
};
getModuleInfo('./src/index.js')

Результат выглядит следующим образом:

image-20200705230403954

Далее мы можем вернуть полную информацию о модуле.

Здесь мы проходим мимоbabelинструменты ("@babel/core@babel/preset-env`), чтобы преобразовать код в синтаксис ES5.

npm i @babel/core @babel/preset-env ## 安装@babel/core @babel/preset-env
// myBundle.js
const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const babel = require('@babel/core');

const getModuleInfo = (file) => {
	const content = fs.readFileSync(file, 'utf-8');
	const ast = parser.parse(content, {
		sourceType: 'module'
  });
  const dependencies = {};
	traverse(ast, {
		ImportDeclaration({ node }) {
			const dirname = path.dirname(file);
			const newFile = '.'+ path.sep + path.join(dirname, node.source.value);
			dependencies[node.source.value] = newFile; // 使用文件相对路径为key,绝对路径为value
		}
  });
  const { code } = babel.transformFromAst(ast, null, {
		presets: ['@babel/preset-env']
  });
  const moduleInfo = { file, dependencies, code };
  console.log(moduleInfo);
	return moduleInfo;
};

getModuleInfo('./src/index.js');

Результат выглядит следующим образом:

image-20200705231114976

Теперь код модуля был преобразован в объект, содержащий абсолютный путь к модулю, зависимости и код, преобразованный Babel, но приведенное выше касается толькоindex.jsзависимость,a.jsЗависимости не обрабатываются, поэтому это неполная таблица зависимостей, нам нужно продолжить обработку.

На самом деле тоже очень просто, то есть начиная с записи вызывается каждый модуль и его зависимостиgetModuleInfoМетод анализируется, и, наконец, будет возвращена полная таблица зависимостей (также называемая графом зависимостей, графом зависимостей).

Мы напрямую пишем новый метод для обработки:

// myBundle.js
const generDepsGraph = (entry) => {
	const entryModule = getModuleInfo(entry);
	const graphArray = [ entryModule ];
	for(let i = 0; i < graphArray.length; i++) {
		const item = graphArray[i];
		const { dependencies } = item;
		if(dependencies) {
			for(let j in dependencies) {
				graphArray.push(
					getModuleInfo(dependencies[j])
				);
			}
		}
	}
	const graph = {};
	graphArray.forEach(item => {
		graph[item.file] = {
			dependencies: item.dependencies,
			code: item.code
		};
	});
	return graph;
};

image-20200705232430595

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

3. Сгенерируйте выходной код

Прежде чем генерировать код, давайте посмотрим на приведенный выше код, вы можете найти его внутри.exportа такжеrequireТакойcommonjsСинтаксис, и наша среда выполнения (здесь браузер) не поддерживает этот синтаксис, поэтому вам нужно реализовать эти два метода самостоятельно. Сначала вставьте код, а затем медленно произнесите:

Создайте новый метод сборки для генерации выходного кода.

// myBundle.js
const build = (entry) => {
	const graph = JSON.stringify(generDepsGraph(entry)); 
	return `
		(function(graph){
			function require(module) {				
				var exports = {};				
				return exports;
			};
			require('${entry}')
		})(${graph});
	`;
};

const code = build('./src/index.js');

проиллюстрировать:

  • Третий рядJSON.stringifyсостоит в том, чтобы преобразовать данные в строку, иначе то, что будет получено в немедленно выполняемой ниже функции, будет[object object], так как в строковом шаблоне используется следующее, происходит преобразование типов.
  • Возвращенный код упаковывается в IIFE (немедленно выполняемая функция), чтобы предотвратить загрязнение области между модулями.
  • requireФункция должна быть определена в выходном содержимом, а не в текущей среде выполнения, поскольку она будет выполняться в сгенерированном коде.

Далее нам нужно получить код входного файла и использоватьevalфункция для его выполнения:

// myBundle.js
const build = (entry) => {
	const graph = JSON.stringify(generDepsGraph(entry));
	return `
		(function(graph){
			function require(module) {				
				var exports = {};
				(function(require, exports, code){
					eval(code)
				})(require, exports, graph[module].code);
				return exports;
			};
			require('${entry}')
		})(${graph});
	`;
};

const code = build('./src/index.js');
console.log(code);

проиллюстрировать:

  • Для того, чтобы код в коде не конфликтовал с нашей областью видимости (в возвращаемой строке), мы все же используем пакет IIFE и передаем в него нужные параметры.
  • graph[module].codeКод входа можно получить из приведенной выше таблицы зависимостей.

Результат выглядит следующим образом:

image-20200706091126005

Это результат упаковки, но не радуйтесь, здесь все равно большая дыра.

То, как мы импортируем модули в сгенерированный код, основано на относительном пути 'index.js', если пути модулей, представленные в других модулях, сравниваются сindex.jsКогда он несовместим, соответствующий модуль не будет найден (путь неверен), поэтому мы должны иметь дело с путем модуля. К счастью, фронт зависит от столаdependenciesАбсолютный путь модуля прописан в атрибуте, и его нужно только вынуть и использовать.

добавить одинlocalRequireфункция для использования изdependenciesПолучите абсолютный путь к модулю в формате .

// myBundle.js
const build = (entry) => {
	const graph = JSON.stringify(generDepsGraph(entry));
	return `
		(function(graph){
			function require(module) {
				function localRequire(relativePath) {
					return require(graph[module].dependencies[relativePath]);
				}
				var exports = {};
				(function(require, exports, code){
					eval(code)
				})(localRequire, exports, graph[module].code);
				return exports;
			};
			require('${entry}')
		})(${graph});
	`;
};

Затем запишите выходной код в файл.

// myBundle.js
const code = build('./src/index.js')
fs.mkdirSync('./dist')
fs.writeFileSync('./dist/bundle.js', code)

Наконец, внедрите его в html и проверьте, может ли он нормально работать. Нет сомнений, что он будет работать нормально. :Улыбка улыбка:

image-20200706092931226

Наконец, вставьте полный код: :cow::beers:

// myBundle.js
const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const babel = require('@babel/core');

const getModuleInfo = (file) => {
	const content = fs.readFileSync(file, 'utf-8');
	const ast = parser.parse(content, {
		sourceType: 'module'
  });
  const dependencies = {};
	traverse(ast, {
		ImportDeclaration({ node }) {
			const dirname = path.dirname(file);
			const newFile = '.'+ path.sep + path.join(dirname, node.source.value);
			dependencies[node.source.value] = newFile;
		}
  });
  const { code } = babel.transformFromAst(ast, null, {
		presets: ['@babel/preset-env']
  });
  const moduleInfo = { file, dependencies, code };
	return moduleInfo;
};

const generDepsGraph = (entry) => {
	const entryModule = getModuleInfo(entry);
	const graphArray = [ entryModule ];
	for(let i = 0; i < graphArray.length; i++) {
		const item = graphArray[i];
		const { dependencies } = item;
		if(dependencies) {
			for(let j in dependencies) {
				graphArray.push(
					getModuleInfo(dependencies[j])
				);
			}
		}
	}
	const graph = {};
	graphArray.forEach(item => {
		graph[item.file] = {
			dependencies: item.dependencies,
			code: item.code
		};
	});
	return graph;
};
const build = (entry) => {
	const graph = JSON.stringify(generDepsGraph(entry));
	return `
		(function(graph){
			function require(module) {
				function localRequire(relativePath) {
					return require(graph[module].dependencies[relativePath]);
				}
				var exports = {};
				(function(require, exports, code){
					eval(code)
				})(localRequire, exports, graph[module].code);
				return exports;
			};
			require('${entry}')
		})(${graph});
	`;
};

const code = build('./src/index.js');
fs.mkdirSync('./dist');
fs.writeFileSync('./dist/bundle.js', code);

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

Горячая замена модуля чата

Причина, по которой горячая замена модуля (HMR) указана здесь отдельно, заключается в том, что я всегда думал, что это крутая функция, как черная магия.

В чем преимущество

Горячая замена модуля сокращенно называется HMR (горячая замена модуля), что может значительно улучшить опыт разработки и является очень практичной функцией. До этого чаще использовалиlive-reload, после того как редактор и браузер установит соответствующие плагины, при сохранении редактора браузер обновит страницу, что на самом деле удобнее, чем ручная F5. Однако он может использовать только полное обновление страницы, поэтому есть некоторые недостатки:

  • Неэффективность, полное обновление страницы означает, что все ресурсы будут перезагружены, а скорость можно себе представить.
  • Если состояние на странице непостоянно, оно будет сброшено и потеряно после обновления.

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

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

  • Состояние приложения не теряется при обновлении содержимого страницы (не абсолютное, в зависимости от области действия модуля).
  • Обновляйте только тот контент, который изменился, вместо полной перезагрузки страницы, быстро и эффективно.

Как это работает

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

Manifest

Сначала вам нужно знатьManifest, которая представляет собой таблицу данных, поддерживаемую веб-пакетом для управления всеми модулями и их связями в процессе создания. Она содержит подробную информацию, такую ​​как зависимости между модулями и содержимым модуля. Это важная основа для веб-пакета для анализа и загрузки модулей.

процесс обновления

img

На рисунке выше представлена ​​блок-схема горячего обновления модуля для webpack и webpack-dev-server для разработки приложений, на которой записан весь процесс обновления.

  • Красное поле в нижней части рисунка выше — это сторона сервера, а оранжевое поле вверху — сторона браузера.
  • Зеленое поле — это область, контролируемая кодом веб-пакета. Синее поле — это область, контролируемая кодом webpack-dev-server, пурпурное поле — это файловая система, в которой происходят изменения файлов, а голубое поле — это само приложение.

На рисунке выше показан цикл от изменения кода до завершения горячего обновления модуля (этапы отмечены порядковыми номерами), процесс выглядит следующим образом:

  1. webpack наблюдает за файловой системой, упакованной в память. В режиме просмотра WebPACK файловая система модифицируется в документ, отслеживаются изменения файлов WebPACK, переупаковываются скомпилированные модули в соответствии с файлом конфигурации и упаковывается с помощью простого кода объекта JavaScript, хранящегося в памяти (Файл записывается в память, что быстрее и производительнее, используяmemory-fsинструмент полный).
  2. Взаимодействие интерфейса между webpack-dev-server и webpack. На этом этапе это в основном взаимодействие между промежуточным программным обеспечением dev-сервера webpack-dev-middleware и webpack, webpack-dev-middleware вызывает API, предоставляемый webpack, для отслеживания изменений кода и сообщает webpack упаковать код в середину памяти.
  3. webpack-dev-server Монитор изменений файлов. Этот шаг отличается от первого тем, что он не отслеживает изменения кода для переупаковки. Когда мы настраиваем в файле конфигурацииdevServer.watchContentBaseКогда это правда, сервер будет отслеживать изменения статических файлов в этих папках конфигурации и уведомит браузер о необходимости выполнить перезагрузку приложения в реальном времени после изменения. Обратите внимание, что здесь обновление браузера и HMR — это два разных понятия.
  4. Код webpack-dev-server работает. Этот шаг в основном осуществляется черезsockjs(зависимость от webpack-dev-server) Установить длинное соединение по вебсокету между браузером и сервером, сообщить браузеру информацию о статусе каждого этапа компиляции и упаковки вебпака, а также включить мониторинг статических файлов сервером на третьем шаге. изменение информации. Сторона браузера выполняет различные операции на основе этих сообщений сокета. Разумеется, самой важной информацией, передаваемой сервером, является хеш-значение нового модуля, и последующие шаги будут выполнять горячую замену модуля на основе этого хэш-значения.
  5. Сторона webpack-dev-server/client не может ни запрашивать обновленный код, ни выполнять операции горячего модуля и передавать эти задачи webpack.Работа webpack/hot/dev-server основана на webpack-dev. -server/client и конфигурация dev-server определяют, следует ли обновлять браузер или выполнять горячее обновление модуля. Конечно, если вы просто обновите браузер, дальнейших шагов не будет.
  6. HotModuleReplacement.runtime является хабом HMR клиента, получает хеш-значение нового модуля, переданного ему на предыдущем шаге, отправляет Ajax-запрос на сервер через JsonpMainTemplate.runtime, и сервер возвращает json, содержащий все обновления для обновления.После получения обновленного списка хэш-значения модуля модуль снова запрашивает последний код модуля через jsonp. Это шаги 7, 8 и 9 на изображении выше.
  7. 10-й шаг является ключевым шагом для определения успеха HMR.На этом этапе HotModulePlugin сравнит старый и новый модули, чтобы решить, следует ли обновлять модуль.Приняв решение об обновлении модуля, проверьте зависимости между модулями и обновите module Также обновите зависимости между модулями.
  8. На последнем шаге, когда HMR дает сбой, вернитесь к операции перезагрузки в реальном времени, которая заключается в обновлении браузера для получения последнего упакованного кода.

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

if(module.hot) { // 先判断是否开启HMR
    module.hot.accept('./xxx.js', function() {
        // do something
    })
}

намекать:acceptСодержимое метода, как правило, не нужно реализовывать самостоятельно, многие инструменты (такие как vue-loader, style-loader) предоставляются внутри и могут использоваться напрямую.

Использованная литература:Принципиальный анализ Webpack HMR,Потрясающее учебное пособие по Webpack HMR.

загрузчик разработки

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

Разработать простой загрузчик

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

  • новый/loaders/replaceLoader.js,
// /loaders/replaceLoader.js
module.exports = function (source) {
  source = source.replace(/webpack/gi, 'world'); // 将源文件中的webpack替换成world
  return source;
};
  • новый/src/index.js
// /src/index.js
console.log('hello webpack');
  • настроить веб-пакет

Далее необходимо понять, как настроить вышеуказанный загрузчик. Как правило, сторонний загрузчик устанавливается вnode_modulesДалее webpack будет искать его здесь, но теперь загрузчик находится в/loaders/testLoader.js, поэтому необходимо выполнить некоторую обработку следующими способами:

  1. Непосредственно требуется соответствующий загрузчик (применимо к одному загрузчику)
// webpack.config.js
module: {
  rules: [
    {
      test: /\.js$/,
      exclude: /node_modules/,
      loader: path.resolve('./loaders/replaceLoader.js')
    }
  ]
}
  1. использоватьresolveLoaderЭлемент конфигурации (применимо к нескольким загрузчикам)
// webpack.config.js
module: {
  rules: [
    {
      test: /\.js$/,
      exclude: /node_modules/,
      use:'replaceLoader'
    }
  ]
},
resolveLoader: {
  modules: ['node_modules', path.resolver(__dirname, 'loaders')] // 查找优先级从左到右
}
  1. Если вы собираетесь опубликовать загрузчик в npm, вы также можете использоватьnpm-link.

Здесь, используя второй способ, полная конфигурация выглядит следующим образом:

// webpack.config.js
const path = require('path');
module.exports = {
  mode:'development',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  resolveLoader: {
    modules: ['node_modules', path.resolve(__dirname, 'loaders')]
  },
  module: {
    rules: [{
      test: /\.js$/,
      use:'replaceLoader'
    }]
  }
};

Запустите его, пакетная командаnpx webpack, вы можете видеть, что webpack в выходном файле заменен на world.

image-20200614224707431

Как правило, загрузчик предоставляет элементы конфигурации (параметры), что удобно для пользователей, чтобы сделать персонализированную конфигурацию.this.queryПолучите содержимое конфигурации, но обычно используйте официально рекомендованныеloader-utilsИнструменты удобнее.

// webpack.config.js
const path = require('path');
module.exports = {
  mode:'development',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  resolveLoader: {
    modules: ['node_modules', path.resolve(__dirname, 'loaders')]
  },
  module: {
    rules: [{
      test: /\.js$/,
      use: {
        loader:'replaceLoader',
        options: {
          src: /webpack/ig, // 配置要替换的内容
          to: 'world!' // 配置替换的目标内容
        }
      }
    }]
  }
};

немного отремонтироватьreplaceLoader:

// /loaders/replaceLoader.js
const loaderUtils = require('loader-utils');
module.exports = function (source) {
  const options = loaderUtils.getOptions(this);
  source = source.replace(options.src, options.to);
  return source;
};

**Примечание: **Это используется здесь.Это дает много полезной информации, поэтому стрелочные функции нельзя использовать (что изменит указатель this). Если вам нужно проверить параметры, вы можете использоватьschema-utils.

сложная ситуация

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

Фактически функция загрузчика получит три параметра: контент, карта, мета.

  • content: содержимое модуля, которое может быть строкой или буфером
  • карта: объект исходной карты
  • мета: некоторая информация о метаданных

Если вы просто возвращаете результат обработки, вы можете напрямую вернуть контент. Но если вам нужно сгенерировать объекты исходной карты, метаданные или генерировать исключения, вам нужно заменить return наthis.callback(err, content, map, meta)передавать данные.

this.callback(
    // 当无法转换原内容时,给 Webpack 返回一个 Error
    err: Error | null,
    // 原内容转换后的内容
    content: string | Buffer,
    // 用于把转换后的内容得出原内容的 Source Map,方便调试
    sourceMap?: SourceMap,
    // 如果本次转换为原内容生成了 AST 语法树,可以把这个 AST 返回,
    // 以方便之后需要 AST 的 Loader 复用该 AST,以避免重复生成 AST,提升性能
    abstractSyntaxTree?: AST
);

Кроме того, Loader также поддерживает асинхронные задачи, которые можно использовать.this.async()выполнить.

// /loaders/replaceLoader.js
const loaderUtils = require('loader-utils');
module.exports = function (source) {
  const options = loaderUtils.getOptions(this);
  const asyncfunc = this.async() // 调用异步func
  setTimeout(() => {
  	source = source.replace(options.src, options.to);
    asyncfunc(null, source) // 传递结果
  }, 200)
};

Что касается разработки загрузчика, официально предоставлено множество API, читатели могут обратиться кloader interfaceК пониманию.

Меры предосторожности

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

плагин разработки

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

**Примечание.** Ниже используется версия webpack 4.0, которая отличается от API 3.0.

Разработать простой плагин

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

// webpack.config.js
const path = require('path')
const TestWebpackPlugin = require('./test-webpack-plugin')
module.exports = {
    mode: 'development',
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name]_[hash].js'
    },
    plugins: [
        new TestWebpackPlugin({
            // ...options
        })
    ]
}

Имя плагина для веб-пакета должно следовать插件名字-webpack-pluginформат.

Внутренняя реализация плагина также относительно проста и состоит из следующих частей:

  • Функция класса, которую можно вызвать с помощью new
  • в этой функцииprototypeопределитьapplyметод, параметрыcompiler
  • существуетapplyЗарегистрируйте имя хука, имя плагина и функцию обратного вызова, которые будут отслеживаться в методе.
  • Чтение или управление внутренними данными экземпляра Webpack с помощью введенных параметров в функции обратного вызова.
  • Асинхронный обработчик событий, который необходимо вызывать при обработке плагина.callbackили вернутьсяpromise
// test-webpack-plugin.js
class TestWebpackPlugin {
    constructor (options) {
        // 在这里获取插件配置
    }
    // Webpack 会调用此 apply 方法并传入 compiler 对象
    apply (compiler) {
     		 // 这里可以在compiler的钩子(hook)上注册一些方法,	当webpack执行到特定的钩子时就会执行该阶段注册的方法
        compiler.hooks.done.tap('TestWebpackPlugin', (stats) => {
          console.log('Hello TestWebpackPlugin!');
            // ...
        })
      	// 在emit钩子上注册一个处理函数,因为该钩子为异步的,所以需要使用tapAsync
      	// emit钩子执行时机在资源输出到output之前
        compiler.hooks.emit.tapAsync('TestWebpackPlugin', (compilation, cb) => {
            // 在输出文件中增加一个文件test.js
            compilation.assets['test.js'] = {
              source() { // 文件内容
                return 'console.log("hello world")'
              },
              size() { // 文件的长度
                return 27;
              }
            };
            cb();    // 处理完成调用cb
        })
    }
}
module.exports = TestWebpackPlugin

Процесс реализации

Ядром плагина является применение.При использовании плагина веб-пакет автоматически вызывает метод применения экземпляра плагина и передает компилятору в качестве параметра. существуетapplyВнутри мы можемcompilerРазличные функции слушателя (режим публикации и подписки) регистрируются на определенных хуках в хуках.Когда веб-пакет выполняет эти хуки, он вызывает соответствующие функции слушателя для обработки процесса построения.

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

  • пройти через<instance>.hooks.<hook name>.<tap API>('<plugin name>', callback )способ регистрации событий ловушек.

    • instance:compilerилиcompilationпример
    • hook name: имя целевого хука монтирования
    • Хук обработки может быть синхронным или асинхронным, и вам нужно использовать тот, который вы выбираете в зависимости от различных ситуаций.tap API,tap APIЕсть три вида:
      • tapИспользуется для монтирования синхронного обратного вызова, подходящего для любого типа обработчика событий, и использования обратного вызова для возврата результата.
      • tapAsyncОн используется для монтирования асинхронного обратного вызова, его нельзя использовать для хуков синхронного типа, он будет внедрен в обратный вызов.callbackПараметры для вызова плагина после обработки операции, если он не вызываетсяcallbackВозврат управления процессом и последующие операции будут невозможны.
      • tapPromiseа такжеtapAsyncФункция и ограничение аналогичны, разница в том, что требуется вернутьPromiseэкземпляр, и этоPromiseдолжен быть разрешен (независимо от разрешения или отклонения )
// 同步
compiler.hooks.done.tap('MyPlugin', (stats, callback) => {
  // ...
  callback()
})
// 异步promise
compiler.hooks.emit.tapPromise('MyPlugin', (compilation) => {
  return new Promise((resolve, reject) => {
    // ...
  })
})
// 异步async function
compiler.hooks.emit.tapPromise('MyPlugin', async (compilation) => {
  await new Promise((resolve, reject) => {
    // ...
  })
})
// 异步回调
compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
	// ...
  callback()
})
  • Функция, зарегистрированная на хуке, получает два параметра, первый — это имя плагина, а второй — функция обратного вызова.

    • Эта функция обратного вызова является основным содержанием функции обработки, а полученные параметры определяются хуком, гдеemitХук получает два параметра (соответственно компиляция, cb), компиляция записывает соответствующее содержимое этой упаковки, и вызывается функция обратного вызова cb для возврата управления после завершения обработки.
  • compilation.assetsСохраните всю информацию о файле, упакованную на этот раз, приведенный выше пример будетassetsдобавилtest.jsФайл (при условии, что перед его добавлением в активах нет файла с таким же именем), источник и размер соответственно задают содержимое и длину файла. Если файл обрабатывается, непосредственноassetsВы можете работать с файлами в формате .

  • Хуки также могут быть зарегистрированы при компиляции, т.е.Compiler Hooksа такжеCompilation Hooksдва вида.

compiler.hooks.compilation.tap('MyPlugin', (compilation) => {
  compilation.hooks.optimizeChunkAssets.tapAsync(
      'MyPlugin',
      (chunks, callback) => {
       	// 处理chunks
        // 结束调用callback方法
        callback();
      }
    );
})

hooks

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

entryOption

существуетentryВыполняется после обработки элемента конфигурации, ловушка синхронизации.

compiler.hooks.entryOption.tap('MyPlugin', (context, entry) => {
  // context保存了当前目录信息,entry保存了入口文件信息
});

afterEmit

генерировать ресурсы дляoutputАсинхронный хук после выполнения.

done

Компиляция завершает выполнение, хук синхронизации.

compiler.hooks.done.tap('TestWebpackPlugin', (stats) => {
  // stats保存了生成文件的内容
})

Другие хуки и использование можно посмотретьPlugin-Compiler Hooks.

Реализация этих хуков основана наtapableЭта библиотека, эта библиотека предоставляет массу хуков. Эти крючки можно условно разделить на следующие категории:

  • Параллельно: Имена сparallel, функции этого класса будут вызываться параллельно после регистрации.
  • Последовательно: имена сbail, функции этого класса будут вызываться последовательно после регистрации.
  • Потоковое: Имена сwaterfall, после того как функция этого класса будет зарегистрирована, она будет стримиться при вызове, а возвращаемый результат предыдущей функции будет использоваться как параметр следующей функции.
  • Комбинированные: есть также хуки, в которых объединены три вышеуказанных правила.

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

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

Справочная документация:официальный сайт webpack - китайский,На языке непрофессионала webpack,Глубокое погружение в Webpack

Эпилог

Каждая версия веб-пакета имеет кодовое имя, а версия 4.0 называетсяLegato, что означает «легато», подразумевая, что веб-пакет постоянно развивается. Точки развития веб-пакета определяются участниками и пользователями. В версии 3.0 в weback 4.0 были значительно улучшены пользовательский интерфейс с наибольшим количеством голосов, производительность сборки и т. д.

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

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

Тем не менее, изучение инструментов построения, таких как веб-пакет, не является нашей конечной целью, потому что в конечном счете они всего лишь инструмент, а инструменты будут обновляться и заменяться, а то, как использовать инструменты для высвобождения производительности, должно быть конечной целью внешнего интерфейса. инженерия . Фронтенд-инжиниринг — это скорее идея, а не конкретный инструмент, это просто средство. На работе вы должны осознанно передать какую-то сложную или трудоемкую работу программе, а также можете поделиться используемыми вами инструментами и методами с другими, чтобы вы могли не только повысить эффективность разработки себя и своей команды, но и увеличить свое влияние Сила, повышение по службе и повышение зарплаты, женитьба на Бай Фумэй... Думая об этом, вы чувствуете себя немного осторожным?