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

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

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

GitHub: github.com/CommanderXL

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

Здесь мы смотрим наvue-loader(v15)Что касается внутреннего связанного контента, здесь будет объяснен общий процесс обработки vue-loader, и не будет вдаваться в подробности.

git clone git@github.com:vuejs/vue-loader.git

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

Прежде всего, мы все знаем, что vue-loader взаимодействует с webpack, чтобы предоставить нам большое удобство для разработки приложений Vue, позволяя нам писать наш шаблон/скрипт/стиль в SFC (однофайловый компонент). версия vue-loader также позволяет разработчикам писать пользовательские блоки в SFC. Наконец, посредством обработки vue-loader Vue SFC разберет шаблон/скрипт/стиль/пользовательский блок на независимые блоки, и каждый блок может быть передан соответствующему загрузчику для дальнейшей обработки.Например, ваш шаблон If он написан на мопсе, затем сначала используйте vue-loader для получения содержимого внутреннего шаблона мопса SFC, а затем передайте его загрузчику, связанному с мопсом, для обработки.Можно сказать, что vue-loader — это входной процессор для Вью SFC.

В реальном процессе приложения давайте сначала посмотрим на конфигурацию веб-пакета Vue:

const VueloaderPlugin = require('vue-loader/lib/plugin')

module.exports = {
  ...
  module: {
    rules: [
      ...
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      }
    ]
  }

  plugins: [
    new VueloaderPlugin()
  ]
  ...
}

Одним из них является конфигурация, связанная с module.rules, если путь обрабатываемого модуля.vueЕсли форма заканчивается, она будет передана в vue-loader для обработки, при этом в версии v15долженЧтобы использовать плагин, предоставленный внутри vue-loader, его обязанностью является копирование и применение других правил, которые вы определили для.vueБлоки для соответствующего языка в файле. Например, если у вас есть совпадение/\.js$/правило, то оно будет применяться к.vueв файле<script>Блок, здесь мы сначала посмотрим, какая работа выполняется в этом плагине.

VueLoaderPlugin

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

// vue-loader/lib/plugin.js

class VueLoaderPlugin {
  apply() {
    ...
    // use webpack's RuleSet utility to normalize user rules
    const rawRules = compiler.options.module.rules
    const { rules } = new RuleSet(rawRules)

    // find the rule that applies to vue files
    // 判断是否有给`.vue`或`.vue.html`进行 module.rule 的配置
    let vueRuleIndex = rawRules.findIndex(createMatcher(`foo.vue`))
    if (vueRuleIndex < 0) {
      vueRuleIndex = rawRules.findIndex(createMatcher(`foo.vue.html`))
    }
    const vueRule = rules[vueRuleIndex]

    ...

    // 判断对于`.vue`或`.vue.html`配置的 module.rule 是否有 vue-loader
    // get the normlized "use" for vue files
    const vueUse = vueRule.use
    // get vue-loader options
    const vueLoaderUseIndex = vueUse.findIndex(u => {
      return /^vue-loader|(\/|\\|@)vue-loader/.test(u.loader)
    })
    ...

    // 创建 pitcher loader 的配置
    const pitcher = {
      loader: require.resolve('./loaders/pitcher'),
      resourceQuery: query => {
        const parsed = qs.parse(query.slice(1))
        return parsed.vue != null
      },
      options: {
        cacheDirectory: vueLoaderUse.options.cacheDirectory,
        cacheIdentifier: vueLoaderUse.options.cacheIdentifier
      }
    }

    // 拓展开发者的 module.rule 配置,加入 vue-loader 内部提供的 pitcher loader
    // replace original rules
    compiler.options.module.rules = [
      pitcher,
      ...clonedRules,
      ...rules
    ]
  }
}

Этот плагин в основном завершает следующие три части:

  1. определить, давать ли.vueили.vue.htmlНастроить модуль.правило;
  2. судить за.vueили.vue.htmlИмеет ли настроенный module.rule vue-loader;
  3. Разверните конфигурацию module.rule разработчика и добавьте загрузчик кувшина, предоставленный внутри vue-loader.

Мы видим, что условие соответствия правил о загрузчике кувшина выполнено.resourceQueryметод, чтобы судить, то есть судить, имеет ли параметр запроса на пути к модулю vue, например:

// 这种类型的 module path 就会匹配上
'./source.vue?vue&type=template&id=27e4e96e&scoped=true&lang=pug&'

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

Step 1

Далее давайте взглянем на внутреннюю реализацию vue-loader. Сначала посмотрите на соответствующее содержимое входного файла:

// vue-loader/lib/index.js
...
const { parse } = require('@vue/component-compiler-utils')

function loadTemplateCompiler () {
  try {
    return require('vue-template-compiler')
  } catch (e) {
    throw new Error(
      `[vue-loader] vue-template-compiler must be installed as a peer dependency, ` +
      `or a compatible compiler implementation must be passed via options.`
    )
  }
}

module.exports = function(source) {
  const loaderContext = this // 获取 loaderContext 对象

  // 从 loaderContext 获取相关参数
  const {
    target, // webpack 构建目标,默认为 web
    request, // module request 路径(由 path 和 query 组成)
    minimize, // 构建模式
    sourceMap, // 是否开启 sourceMap
    rootContext, // 项目的根路径
    resourcePath, // module 的 path 路径
    resourceQuery // module 的 query 参数
  } = loaderContext

  // 接下来就是一系列对于参数和路径的处理
  const rawQuery = resourceQuery.slice(1)
  const inheritQuery = `&${rawQuery}`
  const incomingQuery = qs.parse(rawQuery)
  const options = loaderUtils.getOptions(loaderContext) || {}

  ...
  

  // 开始解析 sfc,根据不同的 block 来拆解对应的内容
  const descriptor = parse({
    source,
    compiler: options.compiler || loadTemplateCompiler(),
    filename,
    sourceRoot,
    needMap: sourceMap
  })

  // 如果 query 参数上带了 block 的 type 类型,那么会直接返回对应 block 的内容
  // 例如: foo.vue?vue&type=template,那么会直接返回 template 的文本内容
  if (incomingQuery.type) {
    return selectBlock(
      descriptor,
      loaderContext,
      incomingQuery,
      !!options.appendExtension
    )
  }

  ...

  // template
  let templateImport = `var render, staticRenderFns`
  let templateRequest
  if (descriptor.template) {
    const src = descriptor.template.src || resourcePath
    const idQuery = `&id=${id}`
    const scopedQuery = hasScoped ? `&scoped=true` : ``
    const attrsQuery = attrsToQuery(descriptor.template.attrs)
    const query = `?vue&type=template${idQuery}${scopedQuery}${attrsQuery}${inheritQuery}`
    const request = templateRequest = stringifyRequest(src + query)
    templateImport = `import { render, staticRenderFns } from ${request}`
  }

  // script
  let scriptImport = `var script = {}`
  if (descriptor.script) {
    const src = descriptor.script.src || resourcePath
    const attrsQuery = attrsToQuery(descriptor.script.attrs, 'js')
    const query = `?vue&type=script${attrsQuery}${inheritQuery}`
    const request = stringifyRequest(src + query)
    scriptImport = (
      `import script from ${request}\n` +
      `export * from ${request}` // support named exports
    )
  }

  // styles
  let stylesCode = ``
  if (descriptor.styles.length) {
    stylesCode = genStylesCode(
      loaderContext,
      descriptor.styles,
      id,
      resourcePath,
      stringifyRequest,
      needsHotReload,
      isServer || isShadow // needs explicit injection?
    )
  }

  let code = `
${templateImport}
${scriptImport}
${stylesCode}

/* normalize component */
import normalizer from ${stringifyRequest(`!${componentNormalizerPath}`)}
var component = normalizer(
  script,
  render,
  staticRenderFns,
  ${hasFunctional ? `true` : `false`},
  ${/injectStyles/.test(stylesCode) ? `injectStyles` : `null`},
  ${hasScoped ? JSON.stringify(id) : `null`},
  ${isServer ? JSON.stringify(hash(request)) : `null`}
  ${isShadow ? `,true` : ``}
)
  `.trim() + `\n`

  if (descriptor.customBlocks && descriptor.customBlocks.length) {
    code += genCustomBlocksCode(
      descriptor.customBlocks,
      resourcePath,
      resourceQuery,
      stringifyRequest
    )
  }

  ...

  // Expose filename. This is used by the devtools and Vue runtime warnings.
  code += `\ncomponent.options.__file = ${
    isProduction
      // For security reasons, only expose the file's basename in production.
      ? JSON.stringify(filename)
      // Expose the file's full path in development, so that it can be opened
      // from the devtools.
      : JSON.stringify(rawShortFilePath.replace(/\\/g, '/'))
  }`

  code += `\nexport default component.exports`
  return code
}

Выше приведена основная работа входного файла (index.js) vue-loader: анализировать Vue SFC без типа по запросу, получать соответствующее содержимое каждого блока и преобразовывать Vue SFC различных типов блочных компонентов в js, конкретное содержимое выглядит следующим образом:

import { render, staticRenderFns } from "./source.vue?vue&type=template&id=27e4e96e&scoped=true&lang=pug&"
import script from "./source.vue?vue&type=script&lang=js&"
export * from "./source.vue?vue&type=script&lang=js&"
import style0 from "./source.vue?vue&type=style&index=0&id=27e4e96e&scoped=true&lang=css&"

/* normalize component */
import normalizer from "!../lib/runtime/componentNormalizer.js"
var component = normalizer(
  script,
  render,
  staticRenderFns,
  false,
  null,
  "27e4e96e",
  null
)

/* custom blocks */
import block0 from "./source.vue?vue&type=custom&index=0&blockType=foo"
if (typeof block0 === 'function') block0(component)

// 省略了有关 hotReload 的代码

component.options.__file = "example/source.vue"
export default component.exports

Из сгенерированной строки JS-модуля: Функция рендеринга / staticRenderfns, JS-скрипт, стиль Style будут предоставлены Source.Vue, отформатированы Normalizer и, наконец, экспортированы в Component.exports.

Step 2

Таким образом, первый этап обработки vue-loader завершен.После того, как vue-loader преобразует Vue SFC в js-модуль на этом этапе, он переходит ко второму этапу и добавляет только что сгенерированный js-модуль в ссылку компиляции веб-пакета. , AST-анализ этого js-модуля и процесс сбора связанных зависимостей, здесь я использую каждый запрос для маркировки каждого собранного модуля (здесь описано только содержимое модуля, относящееся к Vue SFC):

[
 './source.vue?vue&type=template&id=27e4e96e&scoped=true&lang=pug&',
 './source.vue?vue&type=script&lang=js&',
 './source.vue?vue&type=style&index=0&id=27e4e96e&scoped=true&lang=css&',
 './source.vue?vue&type=custom&index=0&blockType=foo'
]

Мы видим, что все параметры запроса на пути к модулю, обрабатываемые vue-loader, имеют поля vue. Это включает в себя загрузчик кувшина, добавленный VueLoaderPlugin, о котором мы упоминали в начале статьи. Если встречается путь к модулю с полем vue в параметре запроса, загрузчик кувшина будет добавлен в массив загрузчиков, обрабатывающих этот модуль. Следовательно, этот модуль в конечном итоге будет обработан загрузчиком кувшина. Кроме того, в порядке конфигурации загрузчика загрузчик кувшина является первым, поэтому при обработке модуля Vue SFC он также первым обрабатывается загрузчиком кувшина.

Фактически, второй этап обработки Vue SFC только что упоминался, Vue SFC будет проходить через загрузчик кувшина для дальнейшей обработки. Итак, давайте посмотрим, что в основном делает загрузчик кувшина, предоставленный внутри vue-loader:

  1. Удалить загрузчик eslint;
  2. снять сам загрузчик кувшина;
  3. Выполнить обработку перехвата в соответствии с параметрами запроса различного типа, вернуть соответствующий контент, пропустить последующий этап выполнения загрузчика и перейти к этапу разбора модуля.
// vue-loader/lib/loaders/pitcher.js

module.export = code => code

module.pitch = function () {
  ...
  const query = qs.parse(this.resourceQuery.slice(1))
  let loaders = this.loaders

  // 剔除 eslint loader
  // if this is a language block request, eslint-loader may get matched
  // multiple times
  if (query.type) {
    // if this is an inline block, since the whole file itself is being linted,
    // remove eslint-loader to avoid duplicate linting.
    if (/\.vue$/.test(this.resourcePath)) {
      loaders = loaders.filter(l => !isESLintLoader(l))
    } else {
      // This is a src import. Just make sure there's not more than 1 instance
      // of eslint present.
      loaders = dedupeESLintLoader(loaders)
    }
  }

  // 剔除 pitcher loader 自身
  // remove self
  loaders = loaders.filter(isPitcher)

  if (query.type === 'style') {
    const cssLoaderIndex = loaders.findIndex(isCSSLoader)
    if (cssLoaderIndex > -1) {
      const afterLoaders = loaders.slice(0, cssLoaderIndex + 1)
      const beforeLoaders = loaders.slice(cssLoaderIndex + 1)
      const request = genRequest([
        ...afterLoaders,
        stylePostLoaderPath,
        ...beforeLoaders
      ])
      return `import mod from ${request}; export default mod; export * from ${request}`
    }
  }

  if (query.type === 'template') {
    const path = require('path')
    const cacheLoader = cacheDirectory && cacheIdentifier
      ? [`cache-loader?${JSON.stringify({
        // For some reason, webpack fails to generate consistent hash if we
        // use absolute paths here, even though the path is only used in a
        // comment. For now we have to ensure cacheDirectory is a relative path.
        cacheDirectory: path.isAbsolute(cacheDirectory)
          ? path.relative(process.cwd(), cacheDirectory)
          : cacheDirectory,
        cacheIdentifier: hash(cacheIdentifier) + '-vue-loader-template'
      })}`]
      : []
    const request = genRequest([
      ...cacheLoader,
      templateLoaderPath + `??vue-loader-options`,
      ...loaders
    ])
    // the template compiler uses esm exports
    return `export * from ${request}`
  }

  // if a custom block has no other matching loader other than vue-loader itself,
  // we should ignore it
  if (query.type === `custom` &&
      loaders.length === 1 &&
      loaders[0].path === selfPath) {
    return ``
  }

  // When the user defines a rule that has only resourceQuery but no test,
  // both that rule and the cloned rule will match, resulting in duplicated
  // loaders. Therefore it is necessary to perform a dedupe here.
  const request = genRequest(loaders)
  return `import mod from ${request}; export default mod; export * from ${request}`
}

Для обработки блока стилей сначала определите, есть ли css-загрузчик, и если да, сгенерируйте новый запрос.Этот запрос содержит stylePostLoader, предоставленный внутри vue-загрузчика, и возвращает модуль js.Согласно правилам Pitch, загрузчики после загрузчика кувшина будут пропущены, а возвращаемый js-модуль будет скомпилирован в это время. Соответствующее содержание:

import mod from "-!../node_modules/vue-style-loader/index.js!../node_modules/css-loader/index.js!../lib/loaders/stylePostLoader.js!../lib/index.js??vue-loader-options!./source.vue?vue&type=style&index=0&id=27e4e96e&scoped=true&lang=css&"
export default mod
export * from "-!../node_modules/vue-style-loader/index.js!../node_modules/css-loader/index.js!../lib/loaders/stylePostLoader.js!../lib/index.js??vue-loader-options!./source.vue?vue&type=style&index=0&id=27e4e96e&scoped=true&lang=css&"  

Поток обработки для блока шаблона аналогичен: генерируется новый запрос, который содержит templateLoader, предоставленный vue-loader, возвращает модуль js, пропускает последующий загрузчик, а затем начинает компилировать возвращенный модуль js. Соответствующее содержание:

export * from "-!../lib/loaders/templateLoader.js??vue-loader-options!../node_modules/pug-plain-loader/index.js!../lib/index.js??vue-loader-options!./source.vue?vue&type=template&id=27e4e96e&scoped=true&lang=pug&"

Таким образом, второй этап обработки Vue SFC завершен, кувшин-загрузчик используется для перехвата различных типов блоков, и возвращается новый js-модуль, чтобы пропустить выполнение последующего загрузчика. загрузчик будет удален внутренне, так что при переходе к следующему этапу обработки загрузчик кувшина не входит в область действия используемого загрузчика, поэтому следующий этап Vue SFC не будет обрабатываться загрузчиком кувшина.

Step 3

Затем перейдите к третьему этапу, скомпилируйте возвращенный новый js-модуль, завершите синтаксический анализ AST и сбор зависимостей и начните процесс компиляции и преобразования различных типов блоков. Возьмем в качестве примера блок стиля/шаблона в Vue SFC,

Блок стилей пройдет следующий процесс:

source.vue?vue&type=style -> vue-loader (извлечение блока стиля) -> stylePostLoader (обработка css с заданной областью) -> css-loader (обработка связанных путей импорта ресурсов) -> vue-style-loader (динамически созданная вставка тега стиля css)

vue-loader-style-block

Блок шаблона пройдет следующий процесс:

source.vue?vue&type=template -> vue-loader (извлечь блок шаблона) -> pug-plain-loader (преобразовать модуль pug в строку html) -> templateLoader (скомпилировать строку шаблона html, сгенерировать функцию render/staticRenderFns и выставить ее)

vue-loader-template-block

Мы видим, что при обработке vue-loader различные типы блоков в SFC будут извлечены в соответствии с типом различных путей к модулям (поле типа в параметре запроса). Это также соответствующее правило, определенное внутри vue-loader:

// vue-loader/lib/index.js

const qs = require('querystring')
const selectBlock = require('./select')
...

module.exports = function (source) {
  ...
  const rawQuery = resourceQuery.slice(1)
  const inheritQuery = `&${rawQuery}`
  const incomingQuery = qs.parse(rawQuery)

  ...
  const descriptor = parse({
    source,
    compiler: options.compiler || loadTemplateCompiler(),
    filename,
    sourceRoot,
    needMap: sourceMap
  })

  // if the query has a type field, this is a language block request
  // e.g. foo.vue?type=template&id=xxxxx
  // and we will return early
  if (incomingQuery.type) {
    return selectBlock(
      descriptor,
      loaderContext,
      incomingQuery,
      !!options.appendExtension
    )
  }
  ...
}

Когда параметр запроса на пути к модулю имеет поле типа, метод selectBlock будет вызываться напрямую для получения содержимого блока типа, соответствующего типу, пропуская поток обработки после vue-loader (это также первый раз, когда vue- загрузчик обрабатывает этот модуль Процесс отличается), и входите в процесс обработки следующего загрузчика Метод selectBlock в основном получает содержимое содержимого соответствующего типа на дескрипторе в соответствии с различными типами (шаблон/скрипт/стиль/пользовательский) и передает это в. К следующей обработке загрузчика:

module.exports = function selectBlock (
  descriptor,
  loaderContext,
  query,
  appendExtension
) {
  // template
  if (query.type === `template`) {
    if (appendExtension) {
      loaderContext.resourcePath += '.' + (descriptor.template.lang || 'html')
    }
    loaderContext.callback(
      null,
      descriptor.template.content,
      descriptor.template.map
    )
    return
  }

  // script
  if (query.type === `script`) {
    if (appendExtension) {
      loaderContext.resourcePath += '.' + (descriptor.script.lang || 'js')
    }
    loaderContext.callback(
      null,
      descriptor.script.content,
      descriptor.script.map
    )
    return
  }

  // styles
  if (query.type === `style` && query.index != null) {
    const style = descriptor.styles[query.index]
    if (appendExtension) {
      loaderContext.resourcePath += '.' + (style.lang || 'css')
    }
    loaderContext.callback(
      null,
      style.content,
      style.map
    )
    return
  }

  // custom
  if (query.type === 'custom' && query.index != null) {
    const block = descriptor.customBlocks[query.index]
    loaderContext.callback(
      null,
      block.content,
      block.map
    )
    return
  }
}

Суммировать

Через исходный код vue-loader мы можем видеть, как Vue SFC шаг за шагом обрабатывается во всем процессе компиляции и сборки. Это также связано с тем, что веб-пакет предоставляет такой механизм загрузчика для разработки, позволяя разработчикам проходить такой способ сделать соответствующую работу по преобразованию исходного кода проекта для удовлетворения соответствующих потребностей разработки. Объединив предыдущие 2 статьи (Подробное объяснение загрузчика webpack 1а такжеПодробное объяснение загрузчика webpack 2) Что касается анализа исходного кода загрузчика webpack, вы должны иметь более глубокое понимание загрузчика и надеяться, что сможете изучить и использовать его, а также использовать механизм загрузчика для выполнения дополнительных работ по разработке, которые соответствуют вашим реальным потребностям.