A, start

The last article looked at Webpack packaging, where loaders are used to transform non-JS files, and this time we’ll look at a specific and common loader, vue-Loader.

This article will start from the source code, detailed analysis of vue-loader’s working principle, and explain the implementation principle of Scoped CSS, CSS Modules.

Here’s a flow chart:

Second, pre-knowledge

1. Pitching Loader

Loader is used to convert the source code of a module. Since WebPack only recognizes JS files, loader generally converts other files to JS files.

The loader is called from right to left and from bottom to top. However, before the actual execution of loader, the pitch method of Loader will be lowered from left to right and from top to bottom.

Here is an example from the official website.

The Webpack configuration is as follows:

module.exports = {
  // ...
  module: {
    rules: [{use: ['a-loader'.'b-loader'.'c-loader'],}]}}Copy the code

The steps that will take place are:

|- a-loader `pitch` |- b-loader `pitch` |- c-loader `pitch` |- requested module is picked up as a dependency |- c-loader  normal execution |- b-loader normal execution |- a-loader normal executionCopy the code

A feature of the pitching loader is the ability to share data. The third argument to pitch, data, is exposed to loader’s this.data, as in the following example:

module.exports = function (content) {
  return someSyncOperation(content, this.data.value);
};

module.exports.pitch = function (remainingRequest, precedingRequest, data) {
  data.value = 42;
};
Copy the code

Another function is that if a loader’s pitch method returns non-undefined, it will interrupt the execution of subsequent loaders, such as the b-loader in the above example:

module.exports = function (content) {
  return someSyncOperation(content)
}

module.exports.pitch = function () {
  return 'export {}'
}
Copy the code

The execution sequence of loader is reduced to:

|- a-loader `pitch`
  |- b-loader `pitch` returns a module
|- a-loader normal execution
Copy the code

2. Inline loader

Loader can be used in two modes: configuration mode and inline mode.

  • Configuration mode is the way we usually use it, which is inwebpack.config.jsThe use ofloader.
  • The inline mode is inimport/requireSpecified in the statementloader.

The following is an example of using loader in inline mode:

import Styles from 'style-loader! css-loader? modules! ./styles.css';
Copy the code

! /style. CSS files are processed by csS-loader and style-loader respectively, and css-loader passes modules.

The webpack configuration equivalent to the above statement is as follows:

module.exports = {
  module: {
    rules: [{test: /\.css$/,
        use: [
          { loader: 'style-loader' },
          {
            loader: 'css-loader'.options: {
              modules: true}}, {loader: 'sass-loader'}]}};Copy the code

Inline import statements can be prefixed to override the loader, preLoader, and postLoader in the configuration.

  • use!Prefix, will disable allnormal loader(ordinary loader)import Styles from '! style-loader! css-loader? modules! ./styles.css';
  • use!!!!!Prefix, will disable all configuredloader(preLoader,loader,postLoader) import Styles from '!! style-loader! css-loader? modules! ./styles.css';
  • use-!Prefix, will disable all configuredpreLoaderandloaderBut I can’t help itpostLoader import Styles from '-! style-loader! css-loader? modules! ./styles.css';

3. resourceQuery

When configuring loader, the test field is configured to match files:

{
  test: /\.vue$/,
  loader: 'vue-loader'
}

// When importing a vue file, the file content is transferred to vue-loader for processing
import Foo from './source.vue'
Copy the code

ResourceQuery can match files according to the reference path parameters of files. If the imported file path with query parameter matches, the loader is also loaded

{
  resourceQuery: /vue=true/,
  loader: path.resolve(__dirname, './test-loader.js')}// The following two files will be processed by test-loader
import './test.js? vue=true'
import Foo from './source.vue? vue=true'
Copy the code

Iii. Vue-loader principle

The version for this analysis is V15.9.8.

The VueLoaderPlugin can be used as follows.

// webpack.config.js
const VueLoaderPlugin = require('vue-loader/lib/plugin')

module.exports = {
  module: {
    rules: [
      // ... 
      {
        test: /\.vue$/,
        loader: 'vue-loader'}},plugins: [
    new VueLoaderPlugin()
  ]
}
Copy the code

1. VueLoaderPlugin

Let’s take a look at what the VueLoaderPlugin does:

// plugin-webpack4.js
const RuleSet = require('webpack/lib/RuleSet')

class VueLoaderPlugin = {
  apply(compiler) {

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

    // for each user rule (except the vue rule), create a cloned rule
    // that targets the corresponding language blocks in *.vue files.
    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))
        returnparsed.vue ! =null
      },
      options: {
        cacheDirectory: vueLoaderUse.options.cacheDirectory,
        cacheIdentifier: vueLoaderUse.options.cacheIdentifier
      }
    }
    
    // replace original rules
    compiler.options.module.rules = [
      pitcher,
      ...clonedRules,
      ...rules
    ]
  }
}
module.exports = VueLoaderPlugin
Copy the code

The VueLoaderPlugin first gets the original rules of Webpack and then creates the pitcher rule, which uses the loader in./loaders/pitcher for the file containing vue in query, i.e. PitcherLoader.

And for carrying? Vue&lang =xx files with query parameters create the same rules as.xx files. For example, the query carries? Vue&lang =ts, copy and apply the rules defined by the user for. Ts, such as TS-loader. These duplicated rules are called clonedRules.

Then we have [pitcher,…clonedRules,…rules] as the new rules.

2. Stage 1

The entry file for vue-loader is lib/index.js, which exports a method. This method is called twice as WebPack processes the Vue file.

The first was to parse the. Vue file into blocks according to the teplate/script/style types using the parse method.

const { parse } = require('@vue/component-compiler-utils')

function loadTemplateCompiler() {
  return require('vue-template-compiler')}module.exports = function (source) {
  const {
    resourceQuery = ' ',
    resourcePath
  } = loaderContext = this;

  const descriptor = parse({
    source,
    compiler: loadTemplateCompiler(loaderContext),
  })

  // 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,
    )
  }
   let code = `
${templateImport}
${scriptImport}
${stylesCode}

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

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

Parse @vue/component-compiler-utils as follows:

function parse(options) {
  const {
    source,
    filename = ' ',
    compiler,
    compilerParseOptions = { pad: 'line' },
    sourceRoot = ' ',
    needMap = true
  } = options
  const cacheKey = hash(
    filename + source + JSON.stringify(compilerParseOptions)
  )
  let output = cache.get(cacheKey)
  if (output) return output
  output = compiler.parseComponent(source, compilerParseOptions)
  
  if (needMap) {
    if(output.script && ! output.script.src) { output.script.map = generateSourceMap() }if (output.styles) {
      output.styles.forEach(style= > {
        if(! style.src) { style.map = generateSourceMap() } }) } } cache.set(cacheKey, output)return output
}
Copy the code

ParseComponent (source) returns the output of the cache. If the output is not cached, the parse method calls Compiler.parsecomponent (source) to fetch the output. Assigns the map property to output.script and output.styles and returns output.

Compiler here is the compiler method derived from vue-template-compiler.

@vue/component-compiler-utils The name of this library is just a tool. The core logic is still in other libraries, such as vue-template-compiler here.

ParseComponent is in vue/ SRC/SFC /parser.js, which returns an object of the following structure:

{
  template: null.script: null.styles: [].customBlocks: [].errors: []}Copy the code

Vue-loader determines the template/script/style of descriptor and concatenates the new reference path of each descriptor. The exported content is as follows:

import { render, staticRenderFns } from "./empty-state.vue? vue&type=template&id=619de588&scoped=true&"
import script from "./empty-state.vue? vue&type=script&lang=js&"
export * from "./empty-state.vue? vue&type=script&lang=js&"
import style0 from "./empty-state.vue? vue&type=style&index=0&id=619de588&scoped=true&lang=scss&"

/* normalize component */
import normalizer from ! "" . /.. /.. /.. /.. /.. /node_modules/vue-loader/lib/runtime/componentNormalizer.js"
var component = normalizer(
  script,
  render,
  staticRenderFns,
  false.null."619de588".null
  
)

export default component.exports
Copy the code

3. Stage 2

In the VueLoaderPlugin mentioned above, pitcherLoader is applied if the file query contains vue.

The first phase exports with vUE parameters that trigger the pitcherLoader call. pitcherLoader

const templateLoaderPath = require.resolve('./templateLoader')
const isPitcher = l= >l.path ! == __filenameconst isPreLoader = l= >! l.pitchExecutedconst isPostLoader = l= > l.pitchExecuted

module.exports = code= > code

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

  // remove self
  loaders = loaders.filter(isPitcher)

  const genRequest = loaders= > {
    const seen = new Map(a)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)
        loaderStrings.push(request)
      }
    })

    return loaderUtils.stringifyRequest(this.'-! ' + [
      ...loaderStrings,
      this.resourcePath + this.resourceQuery
    ].join('! '))}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 query.module
        ? `export { default } from  ${request}; export * from ${request}`
        : `export * from ${request}`}}if (query.type === `template`) {
    const preLoaders = loaders.filter(isPreLoader)
    const postLoaders = loaders.filter(isPostLoader)

    const request = genRequest([
      ...cacheLoader,
      ...postLoaders,
      templateLoaderPath + `?? vue-loader-options`. preLoaders ])return `export * from ${request}`
  }
  
  const request = genRequest(loaders)
  return `import mod from ${request}; export default mod; export * from ${request}`
}
Copy the code

The Noraml loader part of pitcherLoader does nothing and directly returns the previous code, so the core is on the pitch method.

Since the pitch method returns non-undefined and it is the first loader, subsequent loaders will be skipped.

The pitcherLoader replaces the query of the previous file with the corresponding query with the loader, as mentioned above.

For a reference to a type=template file, such as:

import { render, staticRenderFns } from "./create-team-dialog.vue? vue&type=template&id=127d8294&scoped=true&"
Copy the code

The pitcherLoader will replace the reference to this file with:

export * from "-! . /.. /.. /.. /.. /.. /node_modules/cache-loader/dist/cjs.js? {\"cacheDirectory\":\"node_modules/.cache/vue-loader\",\"cacheIdentifier\":\"2f391a00-vue-loader-template\"}! . /.. /.. /.. /.. /.. /node_modules/vue-loader/lib/loaders/templateLoader.js?? vue-loader-options! . /.. /.. /.. /.. /.. /node_modules/cache-loader/dist/cjs.js?? ref--0-0! . /.. /.. /.. /.. /.. /node_modules/vue-loader/lib/index.js?? vue-loader-options! ./create-team-dialog.vue? vue&type=template&id=127d8294&scoped=true&"
Copy the code

The intra-line loaders are executed from right to left, that is, vue-loader, cache-loader, templateLoader, and cache-loader are executed in sequence.

The following is the result of converting a file whose type is style:

import mod from "-! . /.. /.. /.. /.. /node_modules/vue-style-loader/index.js?? ref--6-oneOf-1-0! . /.. /.. /.. /.. /node_modules/css-loader/dist/cjs.js?? ref--6-oneOf-1-1! . /.. /.. /.. /.. /node_modules/vue-loader/lib/loaders/stylePostLoader.js! . /.. /.. /.. /.. /node_modules/postcss-loader/src/index.js?? ref--6-oneOf-1-2! . /.. /.. /.. /.. /node_modules/postcss-loader/src/index.js?? ref--6-oneOf-1-3! . /.. /.. /.. /.. /node_modules/cache-loader/dist/cjs.js?? ref--0-0! . /.. /.. /.. /.. /node_modules/vue-loader/lib/index.js?? vue-loader-options! ./gp.vue? vue&type=style&index=0&id=6e34f811&scoped=true&lang=css&";
export default mod;
export * from "/* reference address as above */"
Copy the code

For the preceding style files, the loader execution sequence is vue-loader, cache-loader, postCSS-loader * 2, vue-loader/stylePostLoader, CSS-loader, vue-style-loader.

For files of type=script, the last logic is used to convert the original loaders to the request parameter via genRequest, overwriting the previous file reference.

const request = genRequest(loaders)
return `import mod from ${request}; export default mod; export * from ${request}`
Copy the code

PitcherLoader replaces the file containing vUE in the query argument with the above reference, and then calls the inline Loader on the Query in turn. The first loader to be inlined is vue-loader, so vue-loader is called again.

Stage 3

This time vue-loader will simply execute the next loader according to Query. type.

module.exports = function (source) {
  const incomingQuery = qs.parse(resourceQuery.slice(1))
  if(! incomingQuery.type) {// Enter vue-loader for the second time
    return selectBlock(
      descriptor,
      loaderContext,
      incomingQuery,
    )
  }
}

function selectBlock() {
  if (query.type === `template`) {
    if (appendExtension) {
      loaderContext.resourcePath += '. ' + (descriptor.template.lang || 'html')
    }
    loaderContext.callback(
      null,
      descriptor.template.content,
      descriptor.template.map
    )
    return
  }
  if (query.type === `script`) {}if (query.type === `style`) {}if (query.type === `custom`) {}/ /...
}
Copy the code

For some type = template, insert a templateLoader pitcherLoader, the loader in the lib/loaders/templateLoader in js:

const { compileTemplate } = require('@vue/component-compiler-utils')

module.exports = function (source) {
  // ...
  const compiled = compileTemplate(finalOptions)
  const { code } = compiled
  return code + `\nexport { render, staticRenderFns }`
}
Copy the code

TemplateLoader calls the @vue/component-compiler-utils method compileTemplate and returns an object containing the render method, which is the core functionality of vue-Loader. Turn the component into a render function.

Here’s an example:

var render = function() {
	var _vm = this;
	var _h = _vm.$createElement;
	var _c = _vm._self._c || _h;
	return _c('div', {
		staticClass: "wrap"
	},
	[_c('a', {
		staticClass: "tip-toc-commbtn tip-btn-primary".on: {
			"click": function($event) {
				$event.stopPropagation();
				return _vm.enterGame($event)
			}
		}
	},
	[_vm._v("Enter the game")]])}var staticRenderFns = []
export { render, staticRenderFns }
Copy the code

For some type = style, insert the stylePostLoader pitcherLoader, the loader in the lib/loaders/stylePostLoader in js:

const { compileStyle } = require('@vue/component-compiler-utils')

module.exports = function (source, inMap) {
  const query = qs.parse(this.resourceQuery.slice(1))
  const { code, map, errors } = compileStyle({
    source,
    filename: this.resourcePath,
    id: `data-v-${query.id}`.map: inMap,
    scoped:!!!!! query.scoped,trim: true
  })

  if (errors.length) {
    this.callback(errors[0])}else {
    this.callback(null, code, map)
  }
}
Copy the code

StylePostLoader calls compileStyle, which preends [data-v-hash] to the property selector containing the scoped style property.

The style section is later added to the head via style-loader, or extracted into a public CSS file via the miniCssExtractPlugin.

Get script, Render and staticRenderFns, call normalizeComponent at run time and return component, which includes options and exports.

Four,

The workflow of VUe-Loader is summarized as follows:

  1. will.vueFile split intotemplate/script/stylesThree parts
  2. templatePart of the afterpitcherLoader,templateLoader“Will eventually passcompilegeneraterenderandstaticRenderFns
  3. To obtainscriptPart, namedscriptIn the backnormalizeComponentWill be used in, and exportedscript.
  4. stylesPart of the afterpitcherLoader,stylePostLoader“Will eventually passcss-loader,vue-style-loaderAdded to theheadMedium, or passcss-loader,miniCssExtractPluginExtract to a publiccssFile.
  5. usevue-loaderthenormalizeComponentMethod, mergescript,Render and staticRenderFnsTo return tocomponent, which includes aoptionsandexports.

Check out the Scoped CSS workflow:

  1. vue-loaderIn dealing with.vueOf the filetemplateIs generated based on the file path and file contenthashValue.
  2. if.vueIn the filescopedthestyleTag, generates onescopedIdlikedata-v-hashHere,hashThat’s up therehashValue.
  3. forvueIn thestylePart,vue-loaderWill be incss-loaderBefore adding their ownstylePostLoader.stylePostLoaderAttributes are added to each selector[data-v-hash]And then throughstyle-loaderthecssAdded to theheadMedium, or passminiCssExtractPluginthecssExtract into separate files.
  4. vue-loaderthenormalizeComponentMethod, determine ifvueIn the filescopedthestyle, then it returnsoptions._scopeIdFor the abovescopedId.
  5. The above_scopedIdinvnodeWhen the DOM is generated for renderingdomIncrement and increment on the element scopedIdThat is, increase data-v-hash.

Through the above process, CSS modules are privatized.

In addition, a brief introduction to CSS Modules principle:

  1. vue-loaderIn dealing with.vueFile when encountered containsmodulethestyleTag, will be generatedcodeIn the injectioninjectStylesMethod, which executesthis["a"] = (style0.locals || style0)orthis["$style"] = (style1.locals || style1)So that it can be used in vue filesthis.$style.class0The introduction of modular classes andid.
  2. css-loaderrightvueIn the filestylePartial analysis, exportlocalsProperty, and the original class nameidConvert to a unique value.
  3. normalizeComponentDetermine if it containsinjectStyles, it will berenderMethod is packaged as containinginjectStylestherenderWithStyleInjectionMethods.vueWhen instantiated, it is executed firstinjectStylesMethod, and then execute the originalrenderMethods. suchvueYou can get it on the example$styleThe name of the class andidIn this way, the modularity of CSS is realized.

V. Relevant materials

  1. Vue-loader in-depth learning
  2. Read the vue-loader principle
  3. Deeper into vue-loader principle
  4. Analyze the implementation of CSS Scoped from vue-loader source code
  5. Css-loader style-Loader Mechanism
  6. Implementation principles of less-loader, CSS-loader, and style-loader
  7. Webpack several common loader source analysis