Two questions:

  • Vue SFC files contain content in multiple formats: style, script, template, and custom block. How does vue-Loader handle these contents?
  • How does vue-loader reuse other loaders for different content blocks? For example, for style blocks defined for less, how does vue-loader call less-loader to load content?

OK, if you are not sure, then read on, we will break down the vue-Loader code, see how the SFC content is converted, and also learn how to write webPack Loader.

An overview of the

Vue-loader consists of three parts:

  1. Normal loader defined in lib/index.js
  2. Pitcher loader defined by lib/loaders/pitcher.js
  3. The plugin defined in lib/plugin.js

The three work together to process SFC. Users need to register normal Loader and Plugin at the same time. Simple example:

const VueLoaderPlugin = require("vue-loader/lib/plugin");

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

The running process can be roughly summarized as two phases:

  1. Pre-processing stage: dynamically modify webPack configuration in plug-in Apply function, inject vue-loader special rules
  2. Content processing stage: Normal Loader cooperated with pitcher Loader to complete file content conversion

Plug-in preprocessing stage

The vue-loader plugin extends the webpack configuration information in the apply function:

class VueLoaderPlugin {
  apply (compiler) {
    // ...

    const rules = compiler.options.module.rules
    // ...

    const clonedRules = rules
      .filter(r= >r ! == rawVueRules) .map((rawRule) = > cloneRule(rawRule, refs))

    // ...

    // global pitcher (responsible for injecting template compiler loader & CSS
    // post loader)
    const pitcher = {
      loader: require.resolve('./loaders/pitcher'),
      resourceQuery: query= > {
        if(! query) {return false }
        const parsed = qs.parse(query.slice(1))
        returnparsed.vue ! =null
      }
      // ...
    }

    // replace original rules
    compiler.options.module.rules = [
      pitcher,
      ...clonedRules,
      ...rules
    ]
  }
}

function cloneRule (rawRule, refs) {
    // ...
}

module.exports = VueLoaderPlugin
Copy the code

Note that the resourceQuery property of the pitcher object in the code is a key point for subsequent matching, which will be discussed later in this article. Here it is understood as “a function similar to test, used to match a specific path”.

Broken down, the plug-in accomplishes two main tasks:

  1. Initialize the pitcher

As in line 16, define a pitcher object, specify the loader path as require.resolve(‘./loaders/pitcher’), and inject the pitcher at the top of the Rules array.

The benefit of this dynamic injection is that the user doesn’t have to pay attention — he doesn’t know there is a pitcher loader until he looks at the source code, and ensures that Pitcher is executed before any other rule, ensuring that it runs in sequence.

  1. Copy the rules list

Such as code line 8, the plugin in the traversal compiler. The options. The module. The rules array, which is provided by the user webpack configuration of the module. The rules, for each rule execution cloneRule method copy rule object. Then, change the Webpack configuration to [pitcher,…clonedRules,…rules].

To get a feel for this in action, for example for the Rules configuration:

module.exports = {
  module: {
    rules: [{test: /.vue$/i,
        use: [{ loader: "vue-loader"}],}, {test: /.css$/i,
        use: [MiniCssExtractPlugin.loader, "css-loader"],}, {test: /.js$/i,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader".options: {
            presets: [["@babel/preset-env", { targets: "defaults"}]],},},},],},plugins: [
    new VueLoaderPlugin(),
    new MiniCssExtractPlugin({ filename: "[name].css"})]};Copy the code

There are three rules defined for vue, JS, and CSS files respectively. The result of plugin transformation is approximately:

module.exports = {
  module: {
    rules: [{loader: "/node_modules/vue-loader/lib/loaders/pitcher.js".resourceQuery: () = > {},
        options: {},}, {resource: () = > {},
        resourceQuery: () = > {},
        use: [{loader: "/node_modules/mini-css-extract-plugin/dist/loader.js"}, {loader: "css-loader"},],}, {resource: () = > {},
        resourceQuery: () = > {},
        exclude: /node_modules/,
        use: [
          {
            loader: "babel-loader".options: {
              presets: [["@babel/preset-env", { targets: "defaults"}}]],ident: "clonedRuleSet-2[0].rules[0].use",},],}, {test: /.vue$/i,
        use: [
          { loader: "vue-loader".options: {}, ident: "vue-loader-options"},],}, {test: /.css$/i,
        use: [
          {
            loader: "/node_modules/mini-css-extract-plugin/dist/loader.js"}, {loader: "css-loader"},],}, {test: /.vue$/i,
        exclude: /node_modules/,
        use: [
          {
            loader: "babel-loader".options: {
              presets: [["@babel/preset-env", { targets: "defaults"}}]],ident: "clonedRuleSet-2[0].rules[0].use",},],},],},};Copy the code

After the transformation, six rules are generated, in the order defined:

  1. For the rule that xx.vue&vue format path takes effect, only vue-loader pitcher is used as loader
  2. The copied CSS processing rules, the use array is the same as the rules defined by the developer
  3. The copied JS processing rules and the use array are the same as those defined by the developer
  4. The content and configuration of vue-Loader rules originally defined by the developer remain unchanged
  5. Developers originally defined CSS rules, using CSS-loader, mini-CSS-extract-plugin loader
  6. The developer originally defined js rules, using babel-loader

As you can see, items 2 and 3 are copied from the configuration provided by the developer. The content is similar, except that cloneRule redefines resourceQuery functions for these rules during replication:

unction cloneRule (rawRule, refs) {
    const rules = ruleSetCompiler.compileRules(`clonedRuleSet-${++uid}`[{rules: [rawRule]
    }], refs)
  
    const conditions = rules[0].rules
      .map(rule= > rule.conditions)
      // shallow flat
      .reduce((prev, next) = > prev.concat(next), [])

    // ...
  
    const res = Object.assign({}, rawRule, {
      resource: resources= > {
        currentResource = resources
        return true
      },
      resourceQuery: query= > {
        if(! query) {return false }
        const parsed = qs.parse(query.slice(1))
        if (parsed.vue == null) {
          return false
        }
        if(! conditions) {return false
        }
        // Use the lang parameter of the import path to test whether it applies to the current rule
        const fakeResourcePath = `${currentResource}.${parsed.lang}`
        for (const condition of conditions) {
          // add support for resourceQuery
          const request = condition.property === 'resourceQuery' ? query : fakeResourcePath
          if(condition && ! condition.fn(request)) {return false}}return true}})// ...
  
    return res
  }
Copy the code

. CloneRule resourceQuery function corresponding to the module of the definition of internal rules. ResourceQuery configuration items, the same way as we often use the test, are used to judge whether the resource path for this rule. /.js$/ I; /.js$/ I; /.js$/ I; /.js$/ I;

import script from "./index.vue? vue&type=script&lang=js&"
Copy the code

Vue-loader matches and reuses user-provided rule Settings for different content blocks (CSS/JS/Template) based on this rule.

SFC content processing phase

An overview of the

After the configuration is processed by the plug-in and Webpack is running, the Vue SFC file will be passed into different Loaders for several times, and the final JS result will be produced after several intermediate form transformations, which can be roughly divided into the following steps:

  1. The path matches the /. Vue $/ I rule, and vue-loader is called to generate intermediate result A
  2. Result A hits xx.vue? Vue rule, calling vue-loader pitcher produces an intermediate result B
  3. Result B directly invokes loader to process the match

The process is roughly as follows:

Here’s an example of a transformation process:

// The original code
import xx from './index.vue';
// First step, hit vue-loader, convert to:
import { render, staticRenderFns } from "./index.vue? vue&type=template&id=2964abc9&scoped=true&"
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&id=2964abc9&scoped=true&lang=css&"

// The second step is to hit the pitcher.
export * from "-! . /.. /node_modules/vue-loader/lib/loaders/templateLoader.js?? vue-loader-options! . /.. /node_modules/vue-loader/lib/index.js?? vue-loader-options! ./index.vue? vue&type=template&id=2964abc9&scoped=true&"
import mod from "-! . /.. /node_modules/babel-loader/lib/index.js?? clonedRuleSet-2[0].rules[0].use! . /.. /node_modules/vue-loader/lib/index.js?? vue-loader-options! ./index.vue? vue&type=script&lang=js&"; 
export default mod; export * from "-! . /.. /node_modules/babel-loader/lib/index.js?? clonedRuleSet-2[0].rules[0].use! . /.. /node_modules/vue-loader/lib/index.js?? vue-loader-options! ./index.vue? vue&type=script&lang=js&"
export * from "-! . /.. /node_modules/mini-css-extract-plugin/dist/loader.js! . /.. /node_modules/css-loader/dist/cjs.js! . /.. /node_modules/vue-loader/lib/loaders/stylePostLoader.js! . /.. /node_modules/vue-loader/lib/index.js?? vue-loader-options! ./index.vue? vue&type=style&index=0&id=2964abc9&scoped=true&lang=css&"

// Step 3, invoke loader in sequence according to the inline path rules
Copy the code

Read on for details of each step.

  1. The vue-loader is executed for the first time

At run time, according to configuration rules, WebPack first passes the raw SFC content to vue-Loader, for example for the following code:

// main.js
import xx from 'index.vue';

/ / index. Vue code
<template>
  <div class="root">hello world</div>
</template>

<script>
export default {
  data() {},
  mounted() {
    console.log("hello world"); }};</script>

<style scoped>
.root {
  font-size: 12px;
}
</style>
Copy the code

When vue-loader is executed for the first time, run the following logic:

  1. The parse function of the @vue/ component-Compiler-utils package is called to parse the SFC text into AST objects
  2. Iterate over the AST object properties, converting them to special reference paths
  3. Return the conversion result

For the above index.vue content, the result of the conversion is:

import { render, staticRenderFns } from "./index.vue? vue&type=template&id=2964abc9&scoped=true&"
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&id=2964abc9&scoped=true&lang=css&"


/* normalize component */
import normalizer from ! "" . /.. /node_modules/vue-loader/lib/runtime/componentNormalizer.js"
var component = normalizer(
  script,
  render,
  staticRenderFns,
  false.null."2964abc9".null)...export default component.exports
Copy the code

Note that we don’t actually deal with the contents of the block, but simply generate import statements for different types of content blocks:

  • Script: “. / index. Vue? vue&type=script&lang=js&”
  • Template: “./index.vue? vue&type=template&id=2964abc9&scoped=true&”
  • Style: “./index.vue? vue&type=style&index=0&id=2964abc9&scoped=true&lang=css&”

These paths correspond to the original. Vue path with vue identifier and parameters such as type and lang added.

  1. Executive pitcher

As mentioned earlier, the vue-loader plug-in inserts a pitcher object with the resourceQuery function during the pre-processing phase:

const pitcher = {
  loader: require.resolve('./loaders/pitcher'),
  resourceQuery: query= > {
    if(! query) {return false }
    const parsed = qs.parse(query.slice(1))
    returnparsed.vue ! =null}}Copy the code

Where resourceQuery matches xx.vue? The path in VUE format, that is to say, the import path after the above vuE-Loader conversion will be hit by pitcher for further processing. The logic for pitcher is simple, and all it does is convert the import path:

const qs = require('querystring')...const dedupeESLintLoader = loaders= >{... }const shouldIgnoreCustomBlock = loaders= >{... }// In normal Loader phase, the result is returned directly
module.exports = code= > code

module.exports.pitch = function (remainingRequest) {
  const options = loaderUtils.getOptions(this)
  const { cacheDirectory, cacheIdentifier } = options
  // Concern 1: Obtain loader parameters by resolving resourceQuery
  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)) {
      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
  }

  const genRequest = loaders= >{... }// Inject style-post-loader before css-loader for scoped CSS and trimming
  if (query.type === `style`) {
    const cssLoaderIndex = loaders.findIndex(isCSSLoader)
    if (cssLoaderIndex > -1) {...return query.module
        ? `export { default } from  ${request}; export * from ${request}`
        : `export * from ${request}`}}// for templates: inject the template compiler & optional cache
  if (query.type === `template`) {...// 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 ` `
  }

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

The core function is to iterate through a user-defined array of rules, concatenating the entire inline reference path, for example:

// Code:
import xx from 'index.vue'
// The first step is to convert the vue-loader to a path with parameters
import script from "./index.vue? vue&type=script&lang=js&"
// Step 2, read the loader array configuration in pitcher and convert the path to the full inline path format
import mod from "-! . /.. /node_modules/babel-loader/lib/index.js?? clonedRuleSet-2[0].rules[0].use! . /.. /node_modules/vue-loader/lib/index.js?? vue-loader-options! ./index.vue? vue&type=script&lang=js&";
Copy the code
  1. Execute vue-loader for the second time

After the vue-loader -> pitcher treatment above, a new inline path is obtained, for example:

import mod from "-! . /.. /node_modules/babel-loader/lib/index.js?? clonedRuleSet-2[0].rules[0].use! . /.. /node_modules/vue-loader/lib/index.js?? vue-loader-options! ./index.vue? vue&type=script&lang=js&";
Copy the code

Using this import statement as an example, Webpack then runs with the following logic:

  • Call vue-loader to process the index.js file
  • Call babel-Loader to process the content returned from the previous step

This gives vue-loader a second chance to execute. Back to the vue-loader code:

module.exports = function (source) {
  // ...

  const {
    target,
    request,
    minimize,
    sourceMap,
    rootContext,
    resourcePath,
    resourceQuery = "",
  } = loaderContext;
  // ...

  const descriptor = parse({
    source,
    compiler: options.compiler || loadTemplateCompiler(loaderContext),
    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) {
    returnselectBlock( descriptor, loaderContext, incomingQuery, !! options.appendExtension ); }/ /...
  return code;
};

module.exports.VueLoaderPlugin = plugin;
Copy the code

The second run, since the path has taken the type argument, hits the statement in line 26 above and enters the selectBlock function, which has very simple logic:

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}}Copy the code

It simply returns the unusable content based on the type argument.

conclusion

OK, here we can answer the question from the beginning of the article:

  1. Vue SFC files contain content in multiple formats: style, script, template, and custom block. How does vue-Loader handle these contents?

In vue-loader, to add different parameters to the original file path, subsequent with resourceQuery function can be separated to process these contents, such implementation compared to one-time processing, logic is clearer and concise, easier to understand

  1. How does vue-loader reuse other loaders for different content blocks? For example, for style blocks defined for less, how does vue-loader call less-loader to load content?

After two phases: normal loader and pitcher loader, the SFC content is transformed to import XXX from ‘! -babel-loader! vue-loader? Reference path in XXX ‘format to reuse user configuration.

In addition, from vue-loader can learn some webpack plug-in, loader routine:

  • Webpack configuration information can be dynamically modified in the plug-in
  • Loaders do not always have to actually process the contents of a file, but can also return some more specific, directional new paths to reuse other modules of WebPack
  • Flexible use of resourceQuery enables loader to match specific path formats more accurately

Hard AD

Now hiring! We are the front-end team of Bytedance games. The team’s current business includes several game-centric platforms with tens of millions of DAU, covering multiple bytedance hosts such as Toutiao, Douyin and Watermelon video. There is also a creator service platform with monthly flow of tens of millions, which is the most important game short video distribution platform and talent realization channel for Douyin officials. The team is responsible for these project products, as well as the related operation background, advertiser service platform and the front-end research and development of efficiency tools. Business technologies include small program, H5, Node and other directions. In addition, with the rapid development of business, technical support scenes continue to be enriched.

Welcome to the portal,

Or direct email: [email protected].