关于vue.js:源码学习-vueloader源码

3次阅读

共计 14077 个字符,预计需要花费 36 分钟才能阅读完成。

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

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

// webpack.config.js
const 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.js
const rawRules = compiler.options.module.rules
const {rules} = new RuleSet(rawRules)

// find the rule that applies to vue files
let 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.js
const vueUse = vueRule.use
// get vue-loader options
const 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.js
const 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 rules
compiler.options.module.rules = [
  pitcher,
  ...clonedRules,
  ...rules
]

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

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

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

// vue-loader/lib/plugin.js
const 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.js
const 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.js
module.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.js
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-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 给挨个解决

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

正文完
 0