Автор серии: Сяо Лэй
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
]
}
}
Этот плагин в основном завершает следующие три части:
- определить, давать ли
.vue
или.vue.html
Настроить модуль.правило; - судить за
.vue
или.vue.html
Имеет ли настроенный module.rule vue-loader; - Разверните конфигурацию 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:
- Удалить загрузчик eslint;
- снять сам загрузчик кувшина;
- Выполнить обработку перехвата в соответствии с параметрами запроса различного типа, вернуть соответствующий контент, пропустить последующий этап выполнения загрузчика и перейти к этапу разбора модуля.
// 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)
Блок шаблона пройдет следующий процесс:
source.vue?vue&type=template -> vue-loader (извлечь блок шаблона) -> pug-plain-loader (преобразовать модуль pug в строку html) -> templateLoader (скомпилировать строку шаблона html, сгенерировать функцию render/staticRenderFns и выставить ее)
Мы видим, что при обработке 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, вы должны иметь более глубокое понимание загрузчика и надеяться, что сможете изучить и использовать его, а также использовать механизм загрузчика для выполнения дополнительных работ по разработке, которые соответствуют вашим реальным потребностям.