按执行流程一步步看vue-loader 源码

通常配置webpack 时,咱们会配置一个 loader 和 一个 plugin

// webpack.config.jsconst VueLoaderPlugin = require('vue-loader/lib/plugin')// ...{    test: /\.vue$/,    loader: 'vue-loader'},// ...plugins: [    new VueLoaderPlugin(),]

当咱们运行 webpack 时, 首先会进入 vue-loader/lib/plugin

在apply办法内先挂载了一个钩子,

// vue-loader/lib/plugin.js class VueLoaderPlugin {  apply (compiler) {    compiler.hooks.compilation.tap(id, compilation => {        let normalModuleLoader        if (Object.isFrozen(compilation.hooks)) {          // webpack 5          normalModuleLoader = require('webpack/lib/NormalModule').getCompilationHooks(compilation).loader        } else {          normalModuleLoader = compilation.hooks.normalModuleLoader        }        normalModuleLoader.tap(id, loaderContext => {          loaderContext[NS] = true        })      })      // ...  }}

而后读取webpack配置内的所有rule 配置, 并应用 foo.vue 文件名作为测试,查找出能匹配 vue 文件的Rule所在索引 , 并取出相应 rule

// vue-loader/lib/plugin.jsconst rawRules = compiler.options.module.rulesconst { rules } = new RuleSet(rawRules)// find the rule that applies to vue fileslet vueRuleIndex = rawRules.findIndex(createMatcher(`foo.vue`))if (vueRuleIndex < 0) {  vueRuleIndex = rawRules.findIndex(createMatcher(`foo.vue.html`))}const vueRule = rules[vueRuleIndex]

找到 找到 vue-loader 在 rule.use 内的索引, 而后取出相应的loader 配置, 并写入 ident 属性,

// vue-loader/lib/plugin.jsconst vueUse = vueRule.use// get vue-loader optionsconst vueLoaderUseIndex = vueUse.findIndex(u => {  return /^vue-loader|(\/|\\|@)vue-loader/.test(u.loader)})// 取出 vue-loader 配置, 参考如下/*    {        loader:'vue-loader'            options:undefined    }*/const vueLoaderUse = vueUse[vueLoaderUseIndex]vueLoaderUse.ident = 'vue-loader-options'vueLoaderUse.options = vueLoaderUse.options || {}

克隆出所有的 rule , 在所有规定之前退出一个 vue的 pitcher loader,这个loader 的 resourceQuery 匹配 query 上有 vue的文件,

最初合并这些重写rules

// vue-loader/lib/plugin.jsconst clonedRules = rules  .filter(r => r !== vueRule)  .map(cloneRule)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  }}// replace original rulescompiler.options.module.rules = [  pitcher,  ...clonedRules,  ...rules]

最初会进入一开始挂上的钩子, 针对 compilation.hooks.normalModuleLoader 再挂上一个钩子

// vue-loader/lib/plugin.jscompiler.hooks.compilation.tap(id, compilation => {        let normalModuleLoader        if (Object.isFrozen(compilation.hooks)) {          // webpack 5          normalModuleLoader = require('webpack/lib/NormalModule').getCompilationHooks(compilation).loader        } else {          normalModuleLoader = compilation.hooks.normalModuleLoader        }        normalModuleLoader.tap(id, loaderContext => {          loaderContext[NS] = true        })      })

最初触发 compilation.hooks.normalModuleLoader 钩子, 并对 loaderContext 的 'vue-loader' 属性为 true

// vue-loader/lib/plugin.jsconst NS = 'vue-loader'// .....normalModuleLoader.tap(id, loaderContext => {  loaderContext[NS] = true})

继续执行,进入了vue-loader 外部, 将this存储在 loaderContext , 并提取出外部属性, 并将vue单文件组件内容别离解析成 template 、 script 、 style 内容

// vue-loader/lib/index.js   const loaderContext = this  const stringifyRequest = r => loaderUtils.stringifyRequest(loaderContext, r)  const {    target,    request,    minimize,    sourceMap,    rootContext,    resourcePath,    resourceQuery  } = loaderContext  const rawQuery = resourceQuery.slice(1) // 提取 问号前面的query   const inheritQuery = `&${rawQuery}`  const incomingQuery = qs.parse(rawQuery)  const options = loaderUtils.getOptions(loaderContext) || {}  const isServer = target === 'node'  const isShadow = !!options.shadowMode  const isProduction = options.productionMode || minimize || process.env.NODE_ENV === 'production'  const filename = path.basename(resourcePath)  const context = rootContext || process.cwd()  const sourceRoot = path.dirname(path.relative(context, resourcePath))  // 将 vue 但文件解析成  const descriptor = parse({     source,    compiler: options.compiler || loadTemplateCompiler(loaderContext),    filename,    sourceRoot,    needMap: sourceMap  })

解析出组件内容后, 会判断 query 参数是否有type 属性, 因为有type 属性的话就示意是第二次进入这个loader, 这个咱们前面再说

// vue-loader/lib/index.js if (incomingQuery.type) {    return selectBlock(      descriptor,      loaderContext,      incomingQuery,      !!options.appendExtension    )}

取出入口文件的目录相对路径, 和目录相对路径 + query 参数, 并依据这个门路生成一个 hash 字符串

而后就是做一些个性的判断,如 是否用了 scoped, 是否应用了 functional 组件等等

// vue-loader/lib/index.js   // module id for scoped CSS & hot-reload  const rawShortFilePath = path    .relative(context, resourcePath)    .replace(/^(\.\.[\/\\])+/, '')  const shortFilePath = rawShortFilePath.replace(/\\/g, '/') + resourceQuery  const id = hash(    isProduction      ? (shortFilePath + '\n' + source)      : shortFilePath  )   const hasScoped = descriptor.styles.some(s => s.scoped)  const hasFunctional = descriptor.template && descriptor.template.attrs.functional

接下来就是生成template模版了, 原本的 vue组件内容, 通过一下的解决后变成引入一个新的 import 语句

// vue-loader/lib/index.js 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}`  }

生成的 import 语句参考如下:

import { render, staticRenderFns } from "./index.vue?vue&type=template&id=3cf90f21&"

同样的,解决完template 完后处理 script

// vue-loader/lib/index.js // 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    )  }

生成的 import 语句

import script from "./index.vue?vue&type=script&lang=js&"export * from "./index.vue?vue&type=script&lang=js&"

最初解决 style

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

生成的 import 语句如下:

import style0 from "./index.vue?vue&type=style&index=0&lang=less&"

三个模块都解决完后, 最初要做的就是将他们合并起来生成最终 code , 并返回

// vue-loader/lib/index.js   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    )  }  code += `\nexport default component.exports`  return code

生成的code 字符串参考如下

import { render, staticRenderFns } from "./index.vue?vue&type=template&id=3cf90f21&"import script from "./index.vue?vue&type=script&lang=js&"export * from "./index.vue?vue&type=script&lang=js&"import style0 from "./index.vue?vue&type=style&index=0&lang=less&"/* normalize component */import normalizer from "!../../../node_modules/.pnpm/vue-loader@15.7.0_css-loader@2.1.1+webpack@4.41.2/node_modules/vue-loader/lib/runtime/componentNormalizer.js"var component = normalizer(  script,  render,  staticRenderFns,  false,  null,  null,  null  )export default component.exports

就这样, 一个咱们平时书写的 .vue文件通过 vue-loader 的第一次解决后 生成了如上的 code 代码, 通过生成了3个新的 import 语句,再次引入本身 .vue文件然而携带不同的 type 参数,交给webpack 。 webpack 接管到这个 code 后,发现 这个.vue文件原来还有 import. 援用了其余三个文件,它会持续查找这个三个文件, 也就是再通过 loader,而后loader 就能够通过 type 进行判断,返回相应的内容。

好,咱们持续往下走. 因为webpack 发现还有新的 import 文件, 这时候就触发了之前在 plugin中增加的 pitcher loader 了, 还记得吗,他的规定是这样的

// vue-loader/lib/plugin.jsconst pitcher = {      loader: require.resolve('./loaders/pitcher'),      // 匹配规定      resourceQuery: query => {        const parsed = qs.parse(query.slice(1)) // 匹配 ?vue 文件        return parsed.vue != null      },      options: {        cacheDirectory: vueLoaderUse.options.cacheDirectory,        cacheIdentifier: vueLoaderUse.options.cacheIdentifier      }    }

于是咱们就进入了 pitcher loader 外部,

外部首先取出loader 的参数, cacheDirectory, cacheIdentifier 都是 plugin 给它传的。
解析出query 参数, 并判断 type 参数是否存在, 验证了.vue 文件时,会将 eslint-loader 给过滤掉, 防止反复触发

并且紧接着会将 pitcher loader. 本身给过滤掉。 再判断是否应用了 null-loader , 应用了的话就间接退出了

// vue-loader/lib/loaders/pitcher.jsmodule.exports.pitch = function (remainingRequest) { const options = loaderUtils.getOptions(this)  const { cacheDirectory, cacheIdentifier } = options  const query = qs.parse(this.resourceQuery.slice(1))  let loaders = this.loaders  // 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)) { // 防止反复linter      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)    }  }  // remove self  loaders = loaders.filter(isPitcher)  // do not inject if user uses null-loader to void the type (#1239)  if (loaders.some(isNullLoader)) {    return  }// ...}

接下来定义了一个 genRequest 的函数, 这个函数的作用呢就是接管一个 loaders 数组,而后依据数组内的loader 它会生成 内联的loader 门路,

// vue-loader/lib/loaders/pitcher.js  const genRequest = loaders => {    const seen = new Map()    const loaderStrings = []    loaders.forEach(loader => {      const identifier = typeof loader === 'string'        ? loader        : (loader.path + loader.query)      const request = typeof loader === 'string' ? loader : loader.request      if (!seen.has(identifier)) {        seen.set(identifier, true)        // loader.request contains both the resolved loader path and its options        // query (e.g. ??ref-0)        loaderStrings.push(request)      }    })    return loaderUtils.stringifyRequest(this, '-!' + [      ...loaderStrings,      this.resourcePath + this.resourceQuery    ].join('!'))  }

生成款式参考:

"-!../../../node_modules/.pnpm/vue-loader@15.7.0_css-loader@2.1.1+webpack@4.41.2/node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!../../../node_modules/.pnpm/vue-loader@15.7.0_css-loader@2.1.1+webpack@4.41.2/node_modules/vue-loader/lib/index.js??vue-loader-options!./index.vue?vue&type=template&id=3cf90f21&"

而后重要的就是依据不同的 query.type 做不同的解决, 并针对 style 和 template 注入了 stylePostLoadertemplateLoader

如果没命中 style , template , 自定义模块,剩下的就是script 了。下面生成的3个import 援用进入到这里后, 又生成了3个携带了内联 loader 地址的内容。因为下面咱们将本身的 picther loader 曾经删除了 ,所以下次就不会再进入这里了

// vue-loader/lib/loaders/pitcher.js    const templateLoaderPath = require.resolve('./templateLoader')const stylePostLoaderPath = require.resolve('./stylePostLoader')  // Inject style-post-loader before css-loader for scoped CSS and trimming  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      ])      // console.log(request)      return `import mod from ${request}; export default mod; export * from ${request}`    }  }  // for templates: inject the template compiler & optional cache  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).replace(/\\/g, '/'),        cacheIdentifier: hash(cacheIdentifier) + '-vue-loader-template'      })}`]      : []    const preLoaders = loaders.filter(isPreLoader)    const postLoaders = loaders.filter(isPostLoader)    const request = genRequest([      ...cacheLoader,      ...postLoaders,      templateLoaderPath + `??vue-loader-options`,      ...preLoaders    ])    // console.log(request)    // the template compiler uses esm exports    return `export * from ${request}`  }  // if a custom block has no other matching loader other than vue-loader itself  // or cache-loader, we should ignore it  if (query.type === `custom` && shouldIgnoreCustomBlock(loaders)) {    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}`

pitcher loader 的工作完结 ,咱们持续往下走

此时又回到了 失常的loader 外部, 这部分通过的步骤都完全相同, 惟一不同的是这次接管到的 request 是pitcher loader 交给咱们的携带了内联 loader 的request

// vue-loader/lib/index.js   const loaderContext = this  const stringifyRequest = r => loaderUtils.stringifyRequest(loaderContext, r)  const {    target,    request,    minimize,    sourceMap,    rootContext,    resourcePath,    resourceQuery  } = loaderContext  const rawQuery = resourceQuery.slice(1) // 提取 问号前面的query   const inheritQuery = `&${rawQuery}`  const incomingQuery = qs.parse(rawQuery)  const options = loaderUtils.getOptions(loaderContext) || {}  const isServer = target === 'node'  const isShadow = !!options.shadowMode  const isProduction = options.productionMode || minimize || process.env.NODE_ENV === 'production'  const filename = path.basename(resourcePath)  const context = rootContext || process.cwd()  const sourceRoot = path.dirname(path.relative(context, resourcePath))  // 将 vue 但文件解析成 script style template 数据  const descriptor = parse({     source,    compiler: options.compiler || loadTemplateCompiler(loaderContext),    filename,    sourceRoot,    needMap: sourceMap  })

往下走,这次咱们因为携带了 query.type 就会进入到 selectBlock 这个办法, 并且将返回这个办法所返回的后果

// vue-loader/lib/index.js  if (incomingQuery.type) {    return selectBlock(      descriptor,      loaderContext,      incomingQuery,      !!options.appendExtension    )  }

selectBlock 这个办法也很简略, 就是针对不同的query.type 来返回已解析好的对应的 descriptor 对象上的内容, 调用 loaderContext.callback 传入内容,交给webpack

// vue-loader/lib/select.jsmodule.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-loader/lib/plugin 注入 pitcher loader ➡️ vue-loader 第一次命中.vue 文件 ➡️ 因为没有 query.type 所以生成了三个新的import 援用并携带了 query.type ➡️ 因为新的援用携带了 query.type 所以命中了 pitcher loader ➡️ pitcher -loader 执行过程中将本人从 loader 中删除, 并针对 style, template 注入了 专门的loader 进行解决 生成内联 loader 援用 ➡️ 交给 vue-loader ➡️ vue-loader接管到pitcher loader 解决后的援用, 依据不同的type 返回了不同内容, 比方 template 是render函数 ➡️ 因为 pitcher loader 结构了内联 loader , 所以返回的内容又会被这些 内联的loader 给挨个解决

第一次写源码系列文章,写的不是很好,摸索中