This article will take the latest vue-CLI version @vue/cli-service 4.5.8 (later referred to as CLI4) as the context to share the detailed. Vue file parsing and compilation process. Resolution refers to the vue file is resolved as the template | script | style three copies of the source code, compile refers to the rendering function is the template source code is compiled.

Some notes written up front:

  1. This article does not involve too much compilation details, the main purpose is to help you get familiar with the compilation process, to solve the problem to provide ideas on the direction of compilation.
  2. This article uses Vue2.6.11Is not related to Vue3.
  3. Reading this article requires readingWebpackVueHave some understanding.

1. Configure processing rules in CLI4

The project template generated by CLI4 is based on Webpack, we all know that Webpack processing. Vue file needs loader, but CLI4 packaging is very thorough, we can not easily find the Webpack configuration file in the project directory. So the first step is to find loader.

1-1. package.json

  "scripts": {
    "serve": "vue-cli-service serve"."build": "vue-cli-service build"."lint": "vue-cli-service lint"
  },
Copy the code

The actual command to run yarn build is vue-cli-service build. Vue-cli-service is a command provided by a package in node_modules.

1-2. node_modules/.bin/vue-cli-service

if [ -x "$basedir/node" ]; then
  "$basedir/node"  "$basedir/.. /@vue/cli-service/bin/vue-cli-service.js" "$@"
  ret=$?
else 
  node  "$basedir/.. /@vue/cli-service/bin/vue-cli-service.js" "$@"
  ret=$?
fi
Copy the code

All commands provided by node_modules can be found in node_modules/.bin, Nodenode_modules /@vue/cli-service/bin/vue-cli-service.js build, node_modules/@vue/cli-service/bin This is already an instruction that Node recognizes.

1-3. node_modules/@vue/cli-service/bin/vue-cli-service.js

// ...
const Service = require('.. /lib/Service')
// Create a Service instance
const service = new Service(process.env.VUE_CLI_CONTEXT || process.cwd())
// Get command carrying parameters
const rawArgv = process.argv.slice(2)
// ...
service.run(command, args, rawArgv).catch(err= > {
  error(err)
  process.exit(1)})Copy the code
  1. Create a Service instance.
  2. Get commandvue-cli-serviceCarrying parametersbuild.
  3. callservice.runMethods.

1-4. node_modules/@vue/cli-service/lib/Service.js

class Service {
  constructor (context, { plugins, pkg, inlineOptions, useBuiltIn } = {}) {...// Assign member variables to plugins
    this.plugins = this.resolvePlugins(plugins, useBuiltIn)
     ...
}
Copy the code

In the constructor of the Service, the member variable plugins is initialized by calling the resolvePlugins method.

resolvePlugins (inlinePlugins, useBuiltIn) {
  const idToPlugin = id= > ({
    id: id.replace(/ ^. / / /.'built-in:'),
    apply: require(id)
  })

  let plugins

  // Default plugins
  const builtInPlugins = [
    './commands/serve'.'./commands/build'.'./commands/inspect'.'./commands/help'.// config plugins are order sensitive
    './config/base'.'./config/css'.'./config/prod'.'./config/app'
  ].map(idToPlugin)

  if(inlinePlugins) { plugins = useBuiltIn ! = =false
      ? builtInPlugins.concat(inlinePlugins)
      : inlinePlugins
  } else {
    const projectPlugins = Object.keys(this.pkg.devDependencies || {})
      .concat(Object.keys(this.pkg.dependencies || {}))
      .filter(isPlugin)
      .map(id= > {
        if (
          this.pkg.optionalDependencies &&
          id in this.pkg.optionalDependencies
        ) {
          let apply = () = > {}
          try {
            apply = require(id)
          } catch (e) {
            warn(`Optional dependency ${id} is not installed.`)}return { id, apply }
        } else {
          return idToPlugin(id)
        }
      })
    plugins = builtInPlugins.concat(projectPlugins)
  }

  // Local plugins
  if (this.pkg.vuePlugins && this.pkg.vuePlugins.service) {
    const files = this.pkg.vuePlugins.service
    if (!Array.isArray(files)) {
      throw new Error(`Invalid type for option 'vuePlugins.service', expected 'array' but got The ${typeof files}. `)
    }
    plugins = plugins.concat(files.map(file= > ({
      id: `local:${file}`.apply: loadModule(`. /${file}`.this.pkgContext)
    })))
  }

  return plugins
}
Copy the code

The resolvePlugins function consolidates the four types of plugins into an array and returns:

  1. Initialize theServiceClass to pass in plugins
  2. CLI4 default plugins
  3. The plugins devDependencies
  4. VuePlugins depend on plugins in

Each element of the array has an apply method that loads the corresponding plugin.

async run (name, args = {}, rawArgv = []) {
  // ...
  // load env variables, load user config, apply plugins
  this.init(mode)
  // ...
  // Remove build handler from commands (name = 'build')
  let command = this.commands[name]
  // ...
  const { fn } = command
  return fn(args, rawArgv)
}
Copy the code

The run method calls init to attach the build handler to the member variable commands, fetch the build handler from commands, and execute.

init (mode = process.env.VUE_CLI_MODE) {
  // ...
  // Load the user Webpack configuration
  const userOptions = this.loadUserOptions()
  // Merge with the default configuration
  this.projectOptions = defaultsDeep(userOptions, defaults())
  // ...
  // apply plugins.
  this.plugins.forEach(({ id, apply }) = > {
    if (this.pluginsToSkip.has(id)) return
    apply(new PluginAPI(id, this), this.projectOptions)
  })
  // ...
}
Copy the code

Init loads the user configuration, then loops through the Apply method of plugins, passing in the configuration as a parameter.

These plug-ins have two main behaviors:

  1. callPluginAPIregisterCommandMethod, the corresponding module of the command (@vue/cli-service/lib/commands/*) onServiceMember variable ofcommandsIn the.
  2. callPluginAPIchainWebpackMethod to add the respective Webpack chain configuration (@vue/cli-service/config/*)pushServiceMember variable ofwebpackChainFnsIn the.

Let’s look at the logic that the build command performs.

1-5. node_modules/@vue/cli-service/lib/commands/build/index.js

(api, options) => {
  // Register build command
  api.registerCommand('build', {
    description: 'build for production'.usage: 'vue-cli-service build [options] [entry|pattern]'.options: {
      '--mode': `specify env mode (default: production)`.'--dest': `specify output directory (default: ${options.outputDir}) `.'--modern': `build app targeting modern browsers with auto fallback`.'--no-unsafe-inline': `build app without introducing inline scripts`.'--target': `app | lib | wc | wc-async (default: ${defaults.target}) `.'--inline-vue': 'include the Vue module in the final bundle of library or web component target'.'--formats': `list of output formats for library builds (default: ${defaults.formats}) `.'--name': `name for lib or web-component mode (default: "name" in package.json or entry filename)`.'--filename': `file name for output, only usable for 'lib' target (default: value of --name)`.'--no-clean': `do not remove the dist directory before building the project`.'--report': `generate report.html to help analyze bundle content`.'--report-json': 'generate report.json to help analyze bundle content'.'--skip-plugins': `comma-separated list of plugin names to skip for this run`.'--watch': `watch for changes`.'--stdin': `close when stdin ends`}},async (args, rawArgs) => {
    // Execute the command callback
    // Merge the default parameters into args
    for (const key in defaults) {
      if (args[key] == null) {
        args[key] = defaults[key]
      }
    }
    ...
    await build(args, api, options)
  }
Copy the code

Call PluginAPI. Registered registerCommand method build command and callback, in the callback to add some default option args (such as target: ‘app’), and then execute the file under the build method.

async function build (args, api, options) {...// resolve raw webpack config
  let webpackConfig
  if (args.target === 'lib') {
    webpackConfig = require('./resolveLibConfig')(api, args, options)
  } else if (
    args.target === 'wc' ||
    args.target === 'wc-async'
  ) {
    webpackConfig = require('./resolveWcConfig')(api, args, options)
  } else {
    webpackConfig = require('./resolveAppConfig')(api, args, options)
  }

  ...
  return new Promise((resolve, reject) = > {
    webpack(webpackConfig, (err, stats) = >{... })})}Copy the code

The build method matches the configuration file against the value of args.target and performs the packaging using the Webpack Nodejs Api. / resolveappconfig.js is loaded by default and webpack is called to perform the packing.

1-6. node_modules/@vue/cli-service/lib/commands/build/resolveAppConfig.js

module.exports = (api, args, options) = >{...const config = api.resolveChainableWebpackConfig()
  ...
  return api.resolveWebpackConfig(config)
})
Copy the code

The file called PluginAPI resolveChainableWebpackConfig method to obtain the Webpack chain configuration, Before returning, the PluginAPI resolveWebpackConfig method is called to convert the chained configuration to JSON configuration. Let’s look at the concrete implementation of these two methods.

1-7. node_modules/@vue/cli-service/lib/PluginAPI.js

 resolveWebpackConfig (chainableConfig) {
    return this.service.resolveWebpackConfig(chainableConfig)
 }

  resolveChainableWebpackConfig () {
    return this.service.resolveChainableWebpackConfig()
  }
Copy the code

The two methods in PluginAPI that get the configuration actually call methods of the same name in the Service.

1-8. node_modules/@vue/cli-service/lib/Service.js

  resolveChainableWebpackConfig () {
    const chainableConfig = new Config()
    // apply chains
    this.webpackChainFns.forEach(fn= > fn(chainableConfig))
    return chainableConfig
  }
Copy the code

In 1-4, we mentioned that webpackChainFns stores the chain configuration under node_modules/@vue/cli-service/lib/config/. ResolveChainableWebpackConfig function is constructed a Webpack Config object, and using the object implementation chain configuration, Including node_modules / @ vue/cli – service/lib/config/base on processing in js. Vue file configuration:

webpackConfig.module
  .rule('vue')
    .test(/\.vue$/)
    .use('cache-loader')
      .loader(require.resolve('cache-loader'))
      .options(vueLoaderCacheConfig)
      .end()
    .use('vue-loader')
      .loader(require.resolve('vue-loader'))
      .options(Object.assign({
        compilerOptions: {
          whitespace: 'condense'
        }
      }, vueLoaderCacheConfig))

webpackConfig
  .plugin('vue-loader')
    .use(require('vue-loader').VueLoaderPlugin)
Copy the code

The original CLI4 also uses vue-loader to process.vue files, but it also relies on a plugin for VueLoaderPlugin compared to CLI3.

 resolveWebpackConfig (chainableConfig = this.resolveChainableWebpackConfig()) {
    ...
    let config = chainableConfig.toConfig()
    ...
    return config
 }
Copy the code

The resolveWebpackConfig method is simpler, calling toConfig directly and returning.

VueLoaderPlugin rewrite rule

As mentioned above, CLI4 relies on the VueLoaderPlugin plugin in addition to CLI3, and this plugin is initialized in process 1-8, so let’s first look at what this plugin does.

2-1. node_modules/vue-loader/lib/plugin.js

if (webpack.version && webpack.version[0] > 4) {
  // webpack5 and upper
  VueLoaderPlugin = require('./plugin-webpack5')}else {
  // webpack4 and lower
  VueLoaderPlugin = require('./plugin-webpack4')}Copy the code

This article uses Webpack 4.44.2 to match the plug-in version based on the Webpack version.

2-2. node_modules/vue-loader/lib/plugin-webpack4.js

class VueLoaderPlugin {
  apply (compiler) {
    // ...
    const vueLoaderUse = vueUse[vueLoaderUseIndex]
    vueLoaderUse.ident = 'vue-loader-options'
    vueLoaderUse.options = vueLoaderUse.options || {}
    
    // create a cloned rule
    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
    ]
  }
}
Copy the code

The VueloaderPlugin. apply method overrides the configuration of the loaders for the current instance when the Webpack plug-in executes the Apply method on the plug-in (function) prototype chain at initialization.

  1. To deal with.vueThe loader configuration of the file is separated and stored in variablesvueLoaderUseIn the.
  2. compiler.options.module.rulesThe rest of the rules are copied to variablesclonedRulesIn the.
  3. Based on thevueLoaderUseThe options set by the user in generates a new rulepitcher
  4. rewritecompiler.options.module.rules

The rewritten rules have two rules related to Vue:

  1. vue-loader/lib/loaders/pitcher.js(This article is new).
  2. Vue-loader and cache-loader in the original Webpack configuration.

Can be described as:

{
  test: /\.vue$/,
  use: [
    'vue-loader/lib/loaders/pitcher.js',]}, {test: /\.vue$/,
  use: [
    'vue-loader/lib/index.js'.'cache-loader/dist/cjs.js']}Copy the code

3. Parse the Vue file

The loaders that process files have been clarified above, so let’s follow these loaders to see how they parse and compile.

3-1. node_modules/vue-loader/lib/index.js

const { parse } = require('@vue/component-compiler-utils')...module.exports = function (source) {...const {
    target,
    request,
    minimize,
    sourceMap,
    rootContext,
    resourcePath,
    resourceQuery
  } = loaderContext
  const rawQuery = resourceQuery.slice(1)
  // Obtain the loader parameters
  const incomingQuery = qs.parse(rawQuery)
  ...
  const descriptor = parse({
    source,
    // The user configured compiler is used by default
    compiler: options.compiler || loadTemplateCompiler(loaderContext),
    filename,
    sourceRoot,
    needMap: sourceMap
  })
  
  // If the specified type exists during loader configuration
  if (incomingQuery.type) {
    returnselectBlock( descriptor, loaderContext, incomingQuery, !! options.appendExtension ) } ...// 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}`}...let code = `
    ${templateImport}
    ${scriptImport}
    ${stylesCode}. `. code +=`\nexport default component.exports`
  return code
}
Copy the code

It can be seen that vue-loader mainly has three responsibilities:

  1. call@vue/component-compiler-utilsparsefunction
  2. If yes, loader parameters existtypeProperty is executedselectBlockFunction to select source code (for example, from the template tag in a Vue file, depending on and aboveparseAnalytic result of function)
  3. According to theparseReturns the result by string, and returns

The parse function has a compiler parameter that takes the user-configured compiler by default, and if not loads a default compiler via loadTemplateCompiler.

function loadTemplateCompiler (loaderContext) {
  try {
    return require('vue-template-compiler')}catch (e) {
    if (/version mismatch/.test(e.toString())) {
      loaderContext.emitError(e)
    } else {
      loaderContext.emitError(new Error(
        `[vue-loader] vue-template-compiler must be installed as a peer dependency, ` +
        `or a compatible compiler implementation must be passed via options.`))}}}Copy the code

LoadTemplateCompiler loads the vue-template-Compiler library and gives a hint when loading errors occur.

Parse the returned result is an object, it records the three special label template | script | style content in Vue file location, so that subsequent loader can through location information to choose the right content.

3-2. node_modules/@vue/component-compiler-utils/dist/parse.js

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(filename, source, output.script.content, sourceRoot, compilerParseOptions.pad); } if (output.styles) { output.styles.forEach(style => { if (! style.src) { style.map = generateSourceMap(filename, source, style.content, sourceRoot, compilerParseOptions.pad); }}); } } cache.set(cacheKey, output); return output; }Copy the code

Parse checks the cache first and returns the contents if there is one, or if there is none:

  1. The implementation of VueTemplateCompilerparseComponentFunction to get the parsing result.
  2. Perform sourceMap processing.

3-3. node_modules/vue-template-compiler/build.js

function parseComponent (content, options) {
  if ( options === void 0 ) options = {};
  var sfc = {
    template: null.script: null.styles: [].customBlocks: [].errors: []};var depth = 0;
  var currentBlock = null;
  
  var warn = function(msg) {
    sfc.errors.push(msg);
  }
  
  function start(. args) {
    // Process the start tag, save the label block object
  }
  
  function end(tag, start) {
    // Process the end tag and modify the label block object
  }
  
  function checkAttrs (block, attrs){
    for (var i = 0; i < attrs.length; i++) {
      var attr = attrs[i];
      if (attr.name === 'lang') {
        block.lang = attr.value;
      }
      if (attr.name === 'scoped') {
        block.scoped = true;
      }
      if (attr.name === 'module') {
        block.module = attr.value || true;
      }
      if (attr.name === 'src') { block.src = attr.value; }}}function padContent(block, pad) {
    // Fill empty lines to keep the line numbers of the separated template and script blocks unchanged (easy for sourceMap mapping)
  }
  
  parseHTML(content, {
    warn: warn,
    start: start,
    end: end,
    outputSourceRange: options.outputSourceRange
  });

  return sfc
}
Copy the code

ParseComponent takes two arguments. The first argument, Content, is the source code of the Vue file. The second argument, options, defaults to {pad: ‘line’} and is a user-configurable parsing option. This function creates an SFC object to hold the result of parsing the Vue file. Its structure is described as follows:

interface SFCDescriptor {
  filename: string
  source: string
  template: SFCBlock
  script: SFCBlock
  scriptSetup: SFCBlock
  styles: SFCBlock[]
  customBlocks: SFCBlock[]
}

interface SFCBlock {
  type: 'template' | 'script' | 'style'
  attrs: { lang: string.functional: boolean },
  content: string.// Content, equal to html.slice(start, end)
  start: number.// Start offset
  end: number.// End offset
  lang: string
}
Copy the code

The warn, start, and end functions are also declared and passed in as parameters to parseHTML, so let’s go into parseHTML to see what’s going on.

    function parseHTML (html, options) {
      var stack = [];
      var expectHTML = options.expectHTML;
      var isUnaryTag$$1 = options.isUnaryTag || no;
      var canBeLeftOpenTag$$1 = options.canBeLeftOpenTag || no;
      var index = 0;
      var last, lastTag;
      while (html) {
        last = html;
        // Make sure we're not in a plaintext content element like script/style
        if(! lastTag || ! isPlainTextElement(lastTag)) {var textEnd = html.indexOf('<');
          if (textEnd === 0) {
            // Comment:
            if (comment.test(html)) {
              var commentEnd = html.indexOf('-->');

              if (commentEnd >= 0) {
                if (options.shouldKeepComment) {
                  options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3);
                }
                advance(commentEnd + 3);
                continue}}// http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
            if (conditionalComment.test(html)) {
              var conditionalEnd = html.indexOf('] > ');

              if (conditionalEnd >= 0) {
                advance(conditionalEnd + 2);
                continue}}/ / processing Doctype:.// Handle End tag:.// Handle the Start tag:
            var startTagMatch = parseStartTag();
            if (startTagMatch) {
              handleStartTag(startTagMatch);
              if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
                advance(1);
              }
              continue}}... }else{... parseEndTag(stackedTag, index - endTagLength, index); }// Clean up any remaining tags
      parseEndTag();

      function advance (n) {
        index += n;
        html = html.substring(n);
      }

      function parseStartTag () {... }function handleStartTag (match) {... }function parseEndTag (tagName, start, end) {... }}Copy the code

ParseHTML main duties and responsibilities of the template was isolated from the Vue file | script | style code, in the three tag is to match the start and end tags, And put these information through the incoming start | end function records the SFC in parseComponent variable (of course also record label attribute lang | scoped | module | SRC).

The key is advance, which changes the offset index and removes the processed code from the HTML (which is the content of the Vue file).

See sequence diagram for detailed steps:

After the execution of parseHtml, all the information of the Vue file is recorded in the object SFC of parseComponent, and the result is returned step by step to 3-1. After 3-1 obtains the SFC object, it will use the information for string splicing. Finally, a new module file (code) is generated and handed to the next loader.

At this point, we will have fully parsed the Vue file! The last two sections of this section show how the obtained template source code is compiled by calling the Vue source code (which falls under the Vue source code category).

Changes in file contents before and after parsing:

app.vue

<template>
  <div id="app">
    <h1>Vue Compile Share</h1>
    <Details></Details>
  </div>
</template>

<script lang="ts">
import Vue from "vue";
import Details from "./components/details.vue";

export default Vue.extend({
  name: "app",
  components: {
    Details
  }
});
</script>

<style lang="stylus">
#app {
  font-family Avenir, Helvetica, Arial, sans-serif
  -webkit-font-smoothing antialiased
  -moz-osx-font-smoothing grayscale
  text-align center
  color red
  margin-top 60px
}
</style>
Copy the code

Compiled intermediate code

import { render, staticRenderFns } from "./App.vue? vue&type=template&id=7a0f6a6c&"
import script from "./App.vue? vue&type=script&lang=ts&"
export * from "./App.vue? vue&type=script&lang=ts&"
import style0 from "./App.vue? vue&type=style&index=0&lang=stylus&"

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

As you can see, the import is of type type, so it will be handled by the selectBlock mentioned above, but this is just a string generated. How does it go through the VueLoader again? Let’s move on.

3-4 node_modules/vue-loader/lib/loaders/pitcher.js

module.exports.pitch = function (remainingRequest) {...// Handle tempalte
  if (query.type === `template`) {
    const path = require('path')
    const cacheLoader = cacheDirectory && cacheIdentifier
      ? [`The ${require.resolve('cache-loader')}?The ${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}`}... }Copy the code

Above, we can say the handling rules and a Vue, namely using the Vue – loader/lib/loaders/pitcher. Js finally, Import {render, staticRenderFns} from “./ app.vue? Vue&type = template&ID = 7A0F6a6c& “will enter the flow here, And eventually be replaced by using the vue – loader/lib/index, js and vue – loader/lib/loaders/templateLoader js to deal with.

So the Vue file is processed again by VueLoader. This time it will be parsed again, but as we mentioned above parse is cached, so the second time it will hit the cache and return the result of the first parse. So will be executed selectBlock method and returns the template | script | style of the source code.

3-5. node_modules/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.// styles.// custom. }Copy the code

The template/script/style code separated from the Vue file is passed to the next loader via loaderContext.callback. The vue – loader/lib/loaders/templateLoader js.

3-6. node_modules/vue-loader/lib/loaders/templateLoader.js

module.exports = function (source) {
  // ...
  // allow using custom compiler via options
  const compiler = options.compiler || require('vue-template-compiler')

  const compilerOptions = Object.assign({
    outputSourceRange: true
  }, options.compilerOptions, {
    scopeId: query.scoped ? `data-v-${id}` : null.comments: query.comments
  })
  
  // for vue-component-compiler
  const finalOptions = {
    source,
    filename: this.resourcePath,
    compiler,
    compilerOptions,
    // allow customizing behavior of vue-template-es2015-compiler
    transpileOptions: options.transpileOptions,
    transformAssetUrls: options.transformAssetUrls || true,
    isProduction,
    isFunctional,
    optimizeSSR: isServer && options.optimizeSSR ! = =false.prettify: options.prettify
  }

  const compiled = compileTemplate(finalOptions)
  
  // ...
  const { code } = compiled

  // finish with ESM exports
  return code + `\nexport { render, staticRenderFns }`
}
Copy the code

As you can see, the loader generates a configuration object for compilation, passes it to the @vue/component-compiler-utils library compileTemplate, and returns the result with minor changes.

3-7. node_modules/@vue/component-compiler-utils/dist/compileTemplate.js

function compileTemplate(options) {
    const { preprocessLang } = options;
    const preprocessor = preprocessLang && consolidate[preprocessLang];
    if (preprocessor) {
        return actuallyCompile(Object.assign({}, options, {
            source: preprocess(options, preprocessor)
        }));
    }
    else if (preprocessLang) {
        // Remind specific language of preprocessing
    }
    else {
        returnactuallyCompile(options); }}Copy the code

Check for preprocessor languages:

  1. If a preprocessor is present, it is preprocessed and then compiled.
  2. If there is no preprocessor, an error message is displayed.
  3. If not, compile is performed.
const assetUrl_1 = __importDefault(require("./templateCompilerModules/assetUrl"));
const srcset_1 = __importDefault(require("./templateCompilerModules/srcset"));

function actuallyCompile(options) {
    const { source, compiler, compilerOptions = {}, transpileOptions = {}, transformAssetUrls, transformAssetUrlsOptions, isProduction = process.env.NODE_ENV === 'production', isFunctional = false, optimizeSSR = false, prettify = true } = options;
    const compile = optimizeSSR && compiler.ssrCompile ? compiler.ssrCompile : compiler.compile;
    let finalCompilerOptions = compilerOptions;
    if (transformAssetUrls) {
        const builtInModules = [
            transformAssetUrls === true
                ? assetUrl_1.default(undefined, transformAssetUrlsOptions)
                : assetUrl_1.default(transformAssetUrls, transformAssetUrlsOptions),
            srcset_1.default(transformAssetUrlsOptions)
        ];
        finalCompilerOptions = Object.assign({}, compilerOptions, {
            modules: [...builtInModules, ...(compilerOptions.modules || [])],
            filename: options.filename
        });
    }
    const { ast, render, staticRenderFns, tips, errors } = compile(source, finalCompilerOptions);
    
    // ...
    return {
      ast,
      code,
      source,
      tips,
      errors
   };
}
Copy the code

In the function actuallyCompile, two special processing rules are merged into the finalCompilerOptions object, assetUrl_1 for handling resource paths and SRCset_1 for setting reactive images. We’ll talk about what they do to these values later.

When you get the new compiler.compile configuration item, the function compiler.compile in the configuration is called and the result is returned. If you are careful enough, you will see that compiler configuration items were added to templateloader.js in 3-2, It in the user not to explicitly specify the compiler defaults to using the vue – the template – the compiler (const compiler = options.com piler | | the require (‘ vue – the template – the compiler ‘)).

4. VueTemplateCompiler compiles templates

Analysis to be determined…