Source code learning Vue loader source code

Posted by LDM2009 on Thu, 03 Feb 2022 21:10:58 +0100

See the Vue loader source code step by step according to the execution process

Usually, when configuring webpack, we will configure a loader and a plugin

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

When we run webpack, we will first enter Vue loader / lib / plugin

A hook is mounted in the apply method first,

// 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
        })
      })
	  // ...
  }
}

Then read all the rule configurations in the webpack configuration and use foo The vue file name is used as a test to find the index of the rule that can match the vue file, and take out the corresponding 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]

Find Vue loader in rule Use, then take out the corresponding loader configuration and write the ident attribute,

// 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)
})


// Take out the Vue loader configuration, as shown below
/*
	{
		loader:'vue-loader'	
		options:undefined
	}
*/
const vueLoaderUse = vueUse[vueLoaderUseIndex]
vueLoaderUse.ident = 'vue-loader-options'
vueLoaderUse.options = vueLoaderUse.options || {}

Clone all the rules and add a pitcher loader of vue before all the rules. The resourceQuery of this loader matches the files with vue on the query,

Finally, merge these rewrite 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
]

Finally, you will enter the hook hung at the beginning for compilation hooks. Attach another hook to the 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
        })
      })

Finally, the compilation hooks. Normalmoduleloader hook and set the 'Vue loader' attribute of loaderContext to true

// vue-loader/lib/plugin.js
const NS = 'vue-loader'

// .....
normalModuleLoader.tap(id, loaderContext => {
  loaderContext[NS] = true
})

Continue to execute, enter the vue loader, store this in the loaderContext, extract the internal attributes, and parse the vue single file component content into template, script and style content respectively

// 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) // Extract the query after the question mark 
  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))

  // Parse vue file into
  const descriptor = parse({ 
    source,
    compiler: options.compiler || loadTemplateCompiler(loaderContext),
    filename,
    sourceRoot,
    needMap: sourceMap
  })

After parsing the component content, we will judge whether the query parameter has a type attribute, because if there is a type attribute, it means entering the loader for the second time, which we will talk about later

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

Take out the directory relative path of the entry file and the directory relative path + query parameter, and generate a hash string according to this path

Then it is to judge some features, such as whether scoped is used, whether functional components are used, and so on

// 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

The next step is to generate the template. The original vue component content is changed into a new import statement after the following processing

// 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}`
  }

The generated import statement reference is as follows:

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

Similarly, the script is processed after the template is processed

// 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
    )
  }

Generated import statement

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

Final processing 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?
    )
  }

The generated import statement is as follows:

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

After the three modules are processed, the last thing to do is to combine them to generate the final code and return

// 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

The generated code string reference is as follows

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

That's it, one we usually write After the first processing by vue loader, the vue file generates the above code code, and introduces itself again by generating three new import statements vue file, but with different type parameters, is handed over to webpack. Webpack finds this code after receiving it The vue file used to have import If the other three files are referenced, it will continue to look for these three files, that is, it will pass through the loader, and then the loader can judge by type and return the corresponding content.

OK, let's go on Because webpack finds that there is a new import file, it triggers the pitcher loader added in the plugin before. Remember, his rule is like this

// vue-loader/lib/plugin.js
const pitcher = {
      loader: require.resolve('./loaders/pitcher'),
	  // Matching rules
      resourceQuery: query => {
        const parsed = qs.parse(query.slice(1)) // Match? vue file
        return parsed.vue != null
      },
      options: {
        cacheDirectory: vueLoaderUse.options.cacheDirectory,
        cacheIdentifier: vueLoaderUse.options.cacheIdentifier
      }
    }

So we went inside the pitcher loader,

The internal first takes out the parameters of the loader, cachedirectory and cacheidentifier, which are all passed to it by the plugin.
Parse the query parameter and judge whether the type parameter exists When the vue file is, the eslint loader will be filtered out to avoid repeated triggering

And then the pitcher loader Filter it out by yourself. Then judge whether null loader is used. If it is used, it will exit directly

// 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)) { // Avoid duplicate 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
  }
// ...
}

Next, a genRequest function is defined. The function is to receive an array of loaders, and then generate an inline loader path according to the loaders in the array,

// 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('!'))
  }

Generate style reference:

"-!../../../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&"

Then it is important to use different query Type is processed differently, and stylePostLoader and templateLoader are injected for style and template.

If you miss the style, template and custom module, the rest is script. After the three import references generated above enter here, three more contents carrying the inline loader address are generated. Because we have deleted our own leather loader above, we won't enter here again next time

// 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}`

The work of the pitcher loader is over and we move on

At this time, we return to the normal loader. The steps of this part are exactly the same. The only difference is that the request received this time is the request given to us by the pitcher loader, which carries the inline loader

// 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) // Extract the query after the question mark 
  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))

  // Parse vue files into script style template data
  const descriptor = parse({ 
    source,
    compiler: options.compiler || loadTemplateCompiler(loaderContext),
    filename,
    sourceRoot,
    needMap: sourceMap
  })

Go down. This time we carried query Type will enter the selectBlock method and return the result returned by this method

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

The selectBlock method is also very simple, that is, for different queries Type to return the content on the parsed corresponding descriptor object, and call loadercontext Callback passes in the content and gives it to 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
  }
}

OK, the Vue loader process is over. Let's do it again:

vue loader / lib / plugin injection pitcher loader ➡️ vue loader hit for the first time vue file ➡️ Because there is no query Type generates three new import references and carries query type ➡️ Because the new reference carries query Type hit the pitcher loader ➡️ The pitcher loader deletes itself from the loader during execution, and injects a special loader for style and template to process and generate an inline loader reference ➡️ Give it to vue loader ➡️ vue loader receives the reference processed by pitcher loader and returns different contents according to different types. For example, template is the render function ➡️ Because the pitcher loader constructs inline loaders, the returned contents will be processed one by one by these inline loaders

The first time I wrote a series of source code articles, I didn't write very well, and I'm groping

Topics: Javascript Vue.js Webpack