Рукописный загрузчик и плагин Webpack

Webpack
Рукописный загрузчик и плагин Webpack

Мы представили различные загрузчики и плагины, а также назначение каждого из них в основах Webpack; так как же они оба работают внутри Webpack? Давайте напишем загрузчик и плагин, чтобы увидеть его внутренние принципы, чтобы углубить наше понимание веб-пакета.

рукописный загрузчик

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

  1. Единый принцип: каждый загрузчик выполняет только одну задачу, прост в использовании и обслуживании;
  2. Цепные вызовы: Webpack будет последовательно вызывать каждый загрузчик;
  3. Объединяющий принцип: СледуйтеWebpackУстановленные правила проектирования и структура, вход и выход - строки, каждыйLoaderПолностью независимый, подключи и играй;
  4. Принцип отсутствия статуса: при конвертации разных модулей не должен резервироваться в загрузчике;

Итак, давайте попробуем написатьless-loaderа такжеstyle-loader,Будуless文件После обработки он отображается на странице через тег стиля.

синхронный загрузчик

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

module.exports = function(source, map){
    return source
}

экспортируетсяloader函数Стрелочные функции использовать нельзя, многие свойства и методы внутри загрузчика нужно пропускатьthisпозвонить, напримерthis.cacheable()кешировать,this.sourceMapОпределите, нужно ли вам генерировать sourceMap и т. д.

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

//loader/style-loader.js
function loader(source, map) {
  let style = `
    let style = document.createElement('style');
    style.innerHTML = ${JSON.stringify(source)};
    document.head.appendChild(style)
  `;
  return style;
}
module.exports = loader;

здесьsourceЕго можно рассматривать как обработанную строку css-файла, и мы вставляем его в голову в виде тега стиля, при этом также обнаруживаем, что конечный результат — это строка JS-кода, а webpack окончательно упакует возвращаемая строка в модуль.

асинхронный загрузчик

надstyle-loaderЭто все синхронные операции. Когда мы обрабатываем исходный код, мы иногда выполняем асинхронные операции. Один метод — выполнить блокирующую операцию через async/await, другой — использовать функцию обратного вызова, предоставленную самим загрузчиком.callback.

//loader/less-loader
const less = require("less");
function loader(source) {
  const callback = this.async();
  less.render(source, function (err, res) {
    let { css } = res;
    callback(null, css);
  });
}
module.exports = loader;

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

callback({
    //当无法转换原内容时,给 Webpack 返回一个 Error
    error: Error | Null,
    //转换后的内容
    content: String | Buffer,
    //转换后的内容得出原内容的Source Map(可选)
    sourceMap?: SourceMap,
    //原内容生成 AST语法树(可选)
    abstractSyntaxTree?: AST 
})

Иногда, помимо конвертации и возврата исходного контента, также необходимо вернуть Source Map, соответствующую исходному контенту, например, мы конвертируем less и код scss, а babel-loader конвертирует код ES6, чтобы облегчить отладку. , вместе с ним нам нужно добавить исходную карту.

//loader/less-loader
const less = require("less");
function loader(source) {
  const callback = this.async();
  less.render(source,{sourceMap: {}}, function (err, res) {
    let { css, map } = res;
    callback(null, css, map);
  });
}
module.exports = loader;

Таким образом, мы можем получить исходную карту, возвращенную less-loader, в следующем загрузчике, но следует отметить, что:

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

Загрузить локальный загрузчик

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

module.exports = {
  module: {
    rules: [{
      test: /\.less/,
      use: [
        {
          loader: './loader/style-loader.js',
        },
        {
          loader: path.resolve(__dirname, "loader", "less-loader"),
        },
      ],
    }]
  }
}

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

module.exports = {
  module: {
    rules: [{
      test: /\.less/,
      use: [
        {
          loader: 'style-loader',
        },
        {
          loader: 'less-loader',
        },
      ],
    }]
  },
  resolveLoader:{
    modules: [path.resolve(__dirname, 'loader'), 'node_modules']
  }
}

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

параметры обработки

Когда мы настраиваем загрузчик, мы часто передаем параметры загрузчику для настройки, обычно через атрибут options, а также есть такие параметры, какurl-loaderПередать параметры по строке:

{
  test: /\.(jpg|png|gif|bmp|jpeg)$/,
  use: 'url-loader?limt=1024&name=[hash:8].[ext]'
}

веб-пакет также предоставляетquery属性чтобы получить параметры; ноquery属性Это очень нестабильно, если параметр передается через строку, как указано выше, запрос вернет формат строки, а метод options вернет формат объекта, что не способствует нашей обработке. Поэтому мы используем официальный пакетloader-utilsЧтобы помочь справиться с этим, он также предоставляет множество полезных инструментов.

const { 
  getOptions,
  parseQuery,
  stringifyRequest,
} = require("loader-utils");

module.exports = function (source, map) {
  //获取options参数
  const options = getOptions(this);
  //解析字符串为对象
  parseQuery("?param1=foo")
  //将绝对路由转换成相对路径
  //以便能在require或者import中使用以避免绝对路径
  stringifyRequest(this, "test/lib/index.js")
}

Обычно используетсяgetOptionsВозвращает обработанные параметры, и его внутренняя логика реализации также очень проста.query属性процесс, если это строка, вызовитеparseQueryМетод разобран, и исходный код выглядит следующим образом:

//loader-utils/lib/getOptions.js
'use strict';
const parseQuery = require('./parseQuery');
function getOptions(loaderContext) {
  const query = loaderContext.query;
  if (typeof query === 'string' && query !== '') {
    return parseQuery(loaderContext.query);
  }
  if (!query || typeof query !== 'object') {
    return {};
  }
  return query;
}
module.exports = getOptions;

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

const { getOptions } = require("loader-utils");
const { validate } = require("schema-utils");
const schema = require("./schema.json");
module.exports = function (source, map) {
  const options = getOptions(this);
  const configuration = { name: "Loader Name"};
  validate(schema, options, configuration);
  //省略其他代码
}

validateФункция не возвращает значение, а возвращаемое значение выводится и оказывается неопределенным.,因为如果参数不通过的话直接会抛出ValidationError异常,直接进程中断;这里引入了一个schema.json,就是我们对Соответствующая таблица в формате json для проверки параметров в опциях``:

{
    "type": "object",
    "properties": {
        "source": {
            "type": "boolean"
        },
        "name": {
            "type": "string"
        },
    },
    "additionalProperties": false
}

propertiesИмя здоровья в том, что нам нужно проверитьoptionsимена полей в ,additionalPropertiesпредставляет, разрешить лиoptionsЕсть и другие дополнительные свойства.

анализ исходного кода с меньшей загрузкой

После написания собственного простогоless-loader, посмотрим на официальнуюless-loaderЧто такое исходный код, вот некоторый исходный код:

import less from 'less';
import { getOptions } from 'loader-utils';
import { validate } from 'schema-utils';
import schema from './options.json';
async function lessLoader(source) {
  const options = getOptions(this);
  //校验参数
  validate(schema, options, {
    name: 'Less Loader',
    baseDataPath: 'options',
  });
  const callback = this.async();
  //对options进一步处理,生成less渲染的参数
  const lessOptions = getLessOptions(this, options);
  //是否使用sourceMap,默认取options中的参数
  const useSourceMap =
    typeof options.sourceMap === 'boolean' 
    ? options.sourceMap : this.sourceMap;
  //如果使用sourceMap,就在渲染参数加入
  if (useSourceMap) {
    lessOptions.sourceMap = {
      outputSourceFiles: true,
    };
  }
  let data = source;
  let result;
  try {
    result = await less.render(data, lessOptions);
  } catch (error) {
  }
  const { css, imports } = result;
  //有sourceMap就进行处理
  let map =
    typeof result.map === 'string' 
    ? JSON.parse(result.map) : result.map;
  
  callback(null, css, map);
}
export default lessLoader;

Видно, что официальный less-loader и написанный нами простой загрузчик по сути вызываютless.renderФункция для обработки строки файлового ресурса, а затем вернуть обработанную строку и SOURCEMAP через обратный вызов.

зависимости загрузчика

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

Давайте попробуем написатьbanner-loader, добавьте наш пользовательский контент комментария после каждого ресурса файла js; если переданоfilename, просто получите предустановленное содержимое баннера из файла, сначала мы предустановим txt двух баннеров:

//loader/banner1.txt
/* build from banner1 */

//loader/banner2.txt
/* build from banner2 */

Затем в нашем баннере-загрузчике судить по параметрам:

//loader/banner-loader
const fs = require("fs");
const path = require("path");
const { getOptions } = require("loader-utils");

module.exports = function (source) {
  const options = getOptions(this);
  if (options.filename) {
    let txt = "";
    if (options.filename == "banner1") {
      this.addDependency(path.resolve(__dirname, "./banner1.txt"));
      txt = fs.readFileSync(path.resolve(__dirname, "./banner1.txt"));
    } else if (options.filename == "banner2") {
      this.addDependency(path.resolve(__dirname, "./banner1.txt"));
      txt = fs.readFileSync(path.resolve(__dirname, "./banner1.txt"));
    }
    return source + txt;
  } else if (options.text) {
    return source + `/* ${options.text} */`;
  } else {
    return source;
  }
};

используется здесьthis.addDependencyAPI добавляет текущий обрабатываемый файл в зависимости от файла (не package.json проекта). Если зависимый текстовый файл изменяется в режиме наблюдения, содержимое упакованного файла также изменяется.

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

ускорение кеша

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

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

module.exports = function(source) {
  // 强制不缓存
  this.cacheable(false);
  return source;
};

Весь код рукописного загрузчика находится вwebpackdemo19

Рукописный плагин

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

//plugins/MyPlugin.js
class MyPlugin {
  constructor() {
    console.log("Plugin被创建了");
  }
  apply (compiler) {}
}
module.exports = MyPlugin;

Суть плагина — это класс; когда мы определяем плагин, мы фактически определяем класс; после определения плагина мы можем использовать плагин в конфигурации веб-пакета:

//webpack.config.js
const MyPlugin = require('./plugins/MyPlugin')
module.exports = {
  plugins: [
    new MyPlugin()
  ],
}

plugin.png

Таким образом, наш плагин вступит в силу в веб-пакете; некоторые детские кроссовки могут помнить, что в это время мы используемHtmlWebpackPluginилиCleanWebpackPluginКогда некоторые официальные плагины, вы можете использовать плагин для передачи параметров, тогда мы можем также отправить наш плагин через этот путь?

//plugins/MyPlugin.js
class MyPlugin {
  constructor(options) {
    console.log("Plugin被创建了");
    console.log(options);
    this.options = options;
  }
  apply (compiler) {}
}
//webpack.config.js
module.exports = {
  plugins: [
    new MyPlugin({ title: 'MyPlugin' })
  ],
}

plugin1.png

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

  1. запускается webpack, выполняет новый myPlugin(options), инициализирует плагин и получает экземпляр
  2. Инициализируйте объект компилятора и вызовите myPlugin.apply(complier), чтобы передать объект компилятора плагину.
  3. Экземпляр подключаемого модуля получает компилятор, отслеживает события, транслируемые веб-пакетом через компилятор, и управляет веб-пакетом через объект компилятора.

мы можем пройтиapplyвнедряется в функциюcompilerОбъект для регистрации событий:

class MyPlugin {
  apply(compiler) {
    //不推荐使用,plugin函数被废弃了
    // compiler.plugin("compile", (compilation) => {
    //   console.log("compile");
    // });
    //注册完成的钩子
    compiler.hooks.done.tap("MyPlugin", (compilation) => {
      console.log("compilation done");
    });
  }
}

Компилятор имеет не только синхронные хуки, которые регистрируются через функцию tap, но и асинхронные хуки, которые регистрируются через функцию tap.tapAsyncа такжеtapPromiseЗарегистрироваться:

class MyPlugin {
  apply(compiler) {
    compiler.hooks.run.tapAsync("MyPlugin", (compilation, callback) => {
      setTimeout(()=>{
        console.log("compilation run");
        callback()
      }, 1000)
    });
    compiler.hooks.emit.tapPromise("MyPlugin", (compilation) => {
      return new Promise((resolve, reject) => {
        setTimeout(()=>{
          console.log("compilation emit");
          resolve();
        }, 1000)
      });
    });
  }
}

Вот еще одинcompilationобъект, это и вышеупомянутоеcompilerОбъекты — это мост между плагином и веб-пакетом:

  • compilerОбъект содержит всю информацию о конфигурации для среды Webpack. Этот объект создается один раз при запуске webpack и настраивает все рабочие параметры, включая опции, загрузчики и плагины. При применении плагина в среде веб-пакета плагин получит ссылку на этот объект компилятора. Вы можете использовать его для доступа к основной среде веб-пакета.
  • compilationОбъект содержит текущие ресурсы модуля, скомпилированные ресурсы, измененные файлы и т. д. При запуске промежуточного программного обеспечения среды разработки webpack всякий раз, когда обнаруживается изменение файла, создается новая компиляция, в результате чего создается новый набор скомпилированных ресурсов. Объект компиляции также предоставляет ряд критических обратных вызовов времени, которые подключаемый модуль может выбрать для использования при выполнении пользовательской обработки.

Разница между компилятором и компиляцией заключается в следующем:

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

Рукописный FileListPlugin

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

class FileListPlugin {
    apply(compiler){
        compiler.hooks.emit.tapAsync('FileListPlugin', (compilation, callback)=>{
            var filelist = 'In this build:\n\n';
            // 遍历所有编译过的资源文件,
            // 对于每个文件名称,都添加一行内容。
            for (var filename in compilation.assets) {
                filelist += '- ' + filename + '\n';
            }
            // 将这个列表作为一个新的文件资源,插入到 webpack 构建中:
            compilation.assets['filelist.md'] = {
                source: function() {
                    return filelist;
                },
                size: function() {
                    return filelist.length;
                }
            };
            callback();
        })
    }
}
module.exports = FileListPlugin

мы использовали здесьassetsОбъект, который является выходным объектом для всех файлов сборки, который выглядит так, когда распечатывается:

{
  'main.bundle.js': { source: [Function: source], size: [Function: size] },
  'index.html': { source: [Function: source], size: [Function: size] }
}

Мы вручную добавляемfilelist.mdВывод файла, после упаковки мы найдем еще этот файл в папке dist:

In this build:

- main.bundle.js
- index.html

Этот плагин выполнил нашу ожидаемую задачу.

Ссылаться на

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

Автор рекомендует:

Полный анализ конфигурации Webpack (оптимизация)

Полный анализ конфигурации Webpack (базовый)

Для получения дополнительной информации о внешнем интерфейсе, пожалуйста, обратите внимание на общедоступный номер【前端壹读】.

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