Подробное объяснение четвертого загрузчика вебпака серии 1

исходный код Webpack

Автор серии: Сяо Лэй

GitHub: github.com/CommanderXL

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

  1. Базовая конфигурация загрузчика и правила сопоставления
  2. Подробное объяснение разбора и выполнения загрузчика
  3. грузчик на практике

конфигурация загрузчика

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

  1. Форма конфигурации конфигурации веб-пакета выглядит следующим образом:
// webpack.config.js
module.exports = {
  ...
  module: {
    rules: [{
      test: /.vue$/,
      loader: 'vue-loader'
    }, {
      test: /.scss$/,
      use: [
        'vue-style-loader',
        'css-loader',
        {
          loader: 'sass-loader',
          options: {
            data: '$color: red;'
          }
        }
      ]
    }]
  }
  ...
}
  1. встроенная встроенная форма
// module

import a from 'raw-loader!../../utils.js'

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

согласование загрузчика

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

webpack loader

В процессе создания модуля сначала вызовите соответствующий конструктор в соответствии с типом зависимости модуля (например, NormalModuleFactory), чтобы создать соответствующий модуль. При создании модуля (new NormalModuleFactory()), соответствующий экземпляр RuleSet будет создан в соответствии с правилами в файле webpack.config разработчика и встроенными правилами веб-пакета. Этот экземпляр RuleSet очень важен в процессе сопоставления и фильтрации загрузчика. Для конкретного анализа исходного кода , пожалуйста, обратитесь кРазбор правила соответствия набора правил загрузчика Webpack. После создания экземпляра RuleSet также регистрируются 2 функции-ловушки:

class NormalModuleFactory {
  ...
  // 内部嵌套 resolver 的钩子,完成相关的解析后,创建这个 normalModule
  this.hooks.factory.tap('NormalModuleFactory', () => (result, callback) => { ... })

  // 在 hooks.factory 的钩子内部进行调用,实际的作用为解析构建一共 module 所需要的 loaders 及这个 module 的相关构建信息(例如获取 module 的 packge.json等)
  this.hooks.resolver.tap('NormalModuleFactory', () => (result, callback) => { ... })
  ...
}

Когда создается экземпляр NormalModuleFactory и внутри компиляции вызывается метод create этого экземпляра, чтобы фактически начать создание normalModule. первый звонокhooks.factoryПолучите соответствующую функцию ловушки, а затем вызовите ловушку преобразователя (hooks.resolver) вступил в стадию разрешения. Прежде чем запустить загрузчик разрешения, первое, что нужно сделать, — это сопоставить и отфильтровать, чтобы найти все загрузчики, необходимые для сборки этого модуля. Первое, что нужно сделать, это разобраться со встроенными загрузчиками:

// NormalModuleFactory.js

// 是否忽略 preLoader 以及 normalLoader
const noPreAutoLoaders = requestWithoutMatchResource.startsWith("-!");
// 是否忽略 normalLoader
const noAutoLoaders =
  noPreAutoLoaders || requestWithoutMatchResource.startsWith("!");
// 忽略所有的 preLoader / normalLoader / postLoader
const noPrePostAutoLoaders = requestWithoutMatchResource.startsWith("!!");

// 首先解析出所需要的 loader,这种 loader 为内联的 loader
let elements = requestWithoutMatchResource
  .replace(/^-?!+/, "")
  .replace(/!!+/g, "!")
  .split("!");
let resource = elements.pop(); // 获取资源的路径
elements = elements.map(identToLoaderRequest); // 获取每个loader及对应的options配置(将inline loader的写法变更为module.rule的写法)

Первый - в соответствии с правилами пути модуля, например, путь модуля начинается с этих символов! / -! / !!Чтобы судить, использует ли этот модуль только встроенный загрузчик или исключает такие правила, как preLoader, postLoader:

  • !Игнорировать normalLoader, который соответствует правилам в конфигурации webpack.config.
  • -!Игнорировать preLoader/normalLoader, которые соответствуют правилам в конфигурации webpack.config.
  • !!Игнорировать postLoader/preLoader/normalLoader, которые соответствуют правилам в конфигурации webpack.config.

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

Затем преобразуйте все встроенные загрузчики в форму массива, например:

import 'style-loader!css-loader!stylus-loader?a=b!../../common.styl'

Окончательный вывод встроенного загрузчика в унифицированном формате:

[{
  loader: 'style-loader',
  options: undefined
}, {
  loader: 'css-lodaer',
  options: undefined
}, {
  loader: 'stylus-loader',
  options: '?a=b'
}]

Обработка встроенного загрузчика заключается в его непосредственном разрешении для получения соответствующей информации о соответствующем загрузчике:

asyncLib.parallel([
  callback => 
    this.resolveRequestArray(
      contextInfo,
      context,
      elements,
      loaderResolver,
      callback
    ),
  callback => {
    // 对这个 module 进行 resolve
    ...
    callack(null, {
      resouceResolveData, // 模块的基础信息,包含 descriptionFilePath / descriptionFileData 等(即 package.json 等信息)
      resource // 模块的绝对路径
    })
  }
], (err, results) => {
  const loaders = results[0] // 所有内联的 loaders
  const resourceResolveData = results[1].resourceResolveData; // 获取模块的基本信息
  resource = results[1].resource; // 模块的绝对路径
  ...
  
  // 接下来就要开始根据引入模块的路径开始匹配对应的 loaders
  let resourcePath =
    matchResource !== undefined ? matchResource : resource;
  let resourceQuery = "";
  const queryIndex = resourcePath.indexOf("?");
  if (queryIndex >= 0) {
    resourceQuery = resourcePath.substr(queryIndex);
    resourcePath = resourcePath.substr(0, queryIndex);
  }
  // 获取符合条件配置的 loader,具体的 ruleset 是如何匹配的请参见 ruleset 解析(https://github.com/CommanderXL/Biu-blog/issues/30)
  const result = this.ruleSet.exec({
    resource: resourcePath, // module 的绝对路径
    realResource:
      matchResource !== undefined
        ? resource.replace(/\?.*/, "")
        : resourcePath,
    resourceQuery, // module 路径上所带的 query 参数
    issuer: contextInfo.issuer, // 所解析的 module 的发布者
    compiler: contextInfo.compiler 
  });

  // result 为最终根据 module 的路径及相关匹配规则过滤后得到的 loaders,为 webpack.config 进行配置的
  // 输出的数据格式为:

  /* [{
    type: 'use',
    value: {
      loader: 'vue-style-loader',
      options: {}
    },
    enforce: undefined // 可选值还有 pre/post  分别为 pre-loader 和 post-loader
  }, {
    type: 'use',
    value: {
      loader: 'css-loader',
      options: {}
    },
    enforce: undefined
  }, {
    type: 'use',
    value: {
      loader: 'stylus-loader',
      options: {
        data: '$color red'
      }
    },
    enforce: undefined 
  }] */

  const settings = {};
  const useLoadersPost = []; // post loader
  const useLoaders = []; // normal loader
  const useLoadersPre = []; // pre loader
  for (const r of result) {
    if (r.type === "use") {
      // postLoader
      if (r.enforce === "post" && !noPrePostAutoLoaders) {
        useLoadersPost.push(r.value);
      } else if (
        r.enforce === "pre" &&
        !noPreAutoLoaders &&
        !noPrePostAutoLoaders
      ) {
        // preLoader
        useLoadersPre.push(r.value);
      } else if (
        !r.enforce &&
        !noAutoLoaders &&
        !noPrePostAutoLoaders
      ) {
        // normal loader
        useLoaders.push(r.value);
      }
    } else if (
      typeof r.value === "object" &&
      r.value !== null &&
      typeof settings[r.type] === "object" &&
      settings[r.type] !== null
    ) {
      settings[r.type] = cachedMerge(settings[r.type], r.value);
    } else {
      settings[r.type] = r.value;
    }

    // 当获取到 webpack.config 当中配置的 loader 后,再根据 loader 的类型进行分组(enforce 配置类型)
    // postLoader 存储到 useLoaders 内部
    // preLoader 存储到 usePreLoaders 内部
    // normalLoader 存储到 useLoaders 内部
    // 这些分组最终会决定加载一个 module 时不同 loader 之间的调用顺序

    // 当分组过程进行完之后,即开始 loader 模块的 resolve 过程
    asyncLib.parallel([
      [
        // resolve postLoader
        this.resolveRequestArray.bind(
          this,
          contextInfo,
          this.context,
          useLoadersPost,
          loaderResolver
        ),
        // resove normal loaders
        this.resolveRequestArray.bind(
          this,
          contextInfo,
          this.context,
          useLoaders,
          loaderResolver
        ),
        // resolve preLoader
        this.resolveRequestArray.bind(
          this,
          contextInfo,
          this.context,
          useLoadersPre,
          loaderResolver
        )
      ],
      (err, results) => {
        ...
        // results[0]  ->  postLoader
        // results[1]  ->  normalLoader
        // results[2]  ->  preLoader
        // 这里将构建 module 需要的所有类型的 loaders 按照一定顺序组合起来,对应于:
        // [postLoader, inlineLoader, normalLoader, preLoader]
        // 最终 loader 所执行的顺序对应为: preLoader -> normalLoader -> inlineLoader -> postLoader
        // 不同类型 loader 上的 pitch 方法执行的顺序为: postLoader.pitch -> inlineLoader.pitch -> normalLoader.pitch -> preLoader.pitch (具体loader内部执行的机制后文会单独讲解)
        loaders = results[0].concat(loaders, results[1], results[2]);

        process.nextTick(() => {
          ...
          // 执行回调,创建 module
        })
      }
    ])
  }
})

Краткое описание процесса согласования:

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