Vue 文件解析、编译流程

文章转自:Vue 文件解析、编译流程 - 博客 - 编程圈

本文将以目前(2020/10/26)最新的 vue-cli 版本 @vue/cli-service 4.5.8 (后文以 CLI4 代指)以脉络,详细分享 .vue 文件解析和编译的过程。解析指 .vue 文件被解析为 template|script|style 三份源码,编译指 template 源码被编译为渲染函数。

写在前面的一些说明:

  1. 本文并不涉及过多编译细节,主要目的是帮助大家熟悉编译流程,为解决问题提供编译方向上的思路。
  2. 本文使用 Vue 2.6.11 ,并不涉及 Vue3 相关内容。
  3. 阅读本文需要对 Webpack 和 Vue 有一定了解。

1. CLI4 配置处理规则

CLI4 生成的项目模板基于 Webpack ,我们都知道 Webpack 处理 .vue 文件是需要 loader 的,但 CLI4 封装很彻底,我们无法轻易在项目目录找到 Webpack 的配置文件,那么第一步就让我们找到 loader 吧。

1-1. package.json

  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },

yarn build 执行的实际命令为 vue-cli-service build,显然 vue-cli-service 是 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

node_modules 提供的所有命令都能在 node_modules/.bin 目录下找到,于是我们发现 vue-cli-service build 被进一步解释为 node node_modules/@vue/cli-service/bin/vue-cli-service.js build ,这已经是一个 node 能识别的指令了。

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

// ...
const Service = require('../lib/Service')
// 创建 Service 实例
const service = new Service(process.env.VUE_CLI_CONTEXT || process.cwd())
// 获取命令携带参数
const rawArgv = process.argv.slice(2)
// ...
service.run(command, args, rawArgv).catch(err => {
  error(err)
  process.exit(1)
})
  1. 创建 Service 实例。
  2. 获取命令 vue-cli-service 携带的参数 build
  3. 调用 service.run 方法。

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

class Service {
  constructor (context, { plugins, pkg, inlineOptions, useBuiltIn } = {}) {
	...
    // 成员变量 plugins 赋值
    this.plugins = this.resolvePlugins(plugins, useBuiltIn)
     ...
}

在 Service 的构造函数中,成员变量 plugins 调用 resolvePlugins 方法进行初始化。

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

  let plugins

  // 预设的 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 ${typeof files}.`)
    }
    plugins = plugins.concat(files.map(file => ({
      id: `local:${file}`,
      apply: loadModule(`./${file}`, this.pkgContext)
    })))
  }

  return plugins
}

resolvePlugins 函数会把四个类型的插件整合为一个数组并返回:

  1. 初始化 Service 类时传入的 plugins
  2. CLI4 预设的 plugins
  3. devDependencies 中的 plugins
  4. vuePlugins 依赖中的 plugins

数组每个元素都存在一个 apply 方法用于加载相应 plugin 。

async run (name, args = {}, rawArgv = []) {
  // ...
  // load env variables, load user config, apply plugins
  this.init(mode)
  // ...
  // 从 commands 中取出 build 的处理函数(name = 'build')
  let command = this.commands[name]
  // ...
  const { fn } = command
  return fn(args, rawArgv)
}

run 方法调用 init 方法把 build 相应的处理函数挂到成员变量 commands 中,并从 commands 中取出 build 相应的处理函数,然后执行。

init (mode = process.env.VUE_CLI_MODE) {
  // ...
  // 加载用户 webpack 配置
  const userOptions = this.loadUserOptions()
  // 和默认配置合并
  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)
  })
  // ...
}

init 加载用户配置,然后循环调用 plugins 的 apply 方法,传入配置作为参数。

这些插件主要有两个行为:

  1. 调用 PluginAPI 的 registerCommand 方法,把命令对应的模块(@vue/cli-service/lib/commands/*)挂到 Service 的成员变量 commands 中。
  2. 调用 PluginAPI 的 chainWebpack 方法,把各自的 Webpack 链式配置(@vue/cli-service/config/*) push 到 Service 的成员变量 webpackChainFns 中。

下面来看看 build 命令要执行的逻辑。

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

(api, options) => {
  // 注册 build 命令
  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) => {
    // 执行命令的回调
    // 把默认参数合并到 args
    for (const key in defaults) {
      if (args[key] == null) {
        args[key] = defaults[key]
      }
    }
    ...
    await build(args, api, options)
  }

调用 PluginAPI.registerCommand 方法注册 build 命令和回调,在回调中会向 args 中添加一些默认选项(比如 target: 'app'),然后执行该文件下的 build 方法。

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) => {
      ...
    })
  })
}

build 方法根据 args.target 的值匹配配置文件,并在使用 Webpack Nodejs Api 执行打包。上面我们刚提到 target 的默认值是 app ,所以默认加载 ./resolveAppConfig.js 这个配置文件,然后调用 webpack 执行打包。

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

该文件调用 PluginAPI 的 resolveChainableWebpackConfig 方法获得 Webpack 的链式配置,并在返回前调用 PluginAPI 的 resolveWebpackConfig 方法把链式配置转换为 JSON 配置,接下来我们看看这两个方法的具体实现。

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

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

  resolveChainableWebpackConfig () {
    return this.service.resolveChainableWebpackConfig()
  }

PluginAPI 中的两个获取配置的方法,其实都调用的 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
  }

在 1-4 我们提到,webpackChainFns 储存着 node_modules/@vue/cli-service/lib/config/ 目录下的链式配置,resolveChainableWebpackConfig 函数则构造了一个 Webpack Config 对象,并使用该对象执行链式配置,其中就包括 node_modules/@vue/cli-service/lib/config/base.js 中的关于处理 .vue 文件的配置:

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)

原来 CLI4 也是使用的是 vue-loader 来处理 .vue 文件,只不过相较于 CLI3 还依赖一个 VueLoaderPlugin 的插件(歪嘴一笑:我早知道了)。

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

resolveWebpackConfig 方法则比较简单,直接调用 toConfig 并返回。

2. VueLoaderPlugin 重写规则

上文提到 CLI4 相较于 CLI3 会额外依赖 VueLoaderPlugin 的插件,并且该插件在 1-8 的流程中进行了初始化,所以让我们先来看看这个插件会做些什么。

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

根据 Webpack 版本匹配插件版本 ,本文使用 Webpack 4.44.2

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))
        return parsed.vue != null
      },
      options: {
        cacheDirectory: vueLoaderUse.options.cacheDirectory,
        cacheIdentifier: vueLoaderUse.options.cacheIdentifier
      }
    }
    
    // replace original rules
    compiler.options.module.rules = [
      pitcher,
      ...clonedRules,
      ...rules
    ]
  }
}

Webpack 插件在初始化时会执行插件(函数)原型链上的 apply 方法,而 VueLoaderPlugin.apply 方法重写了当前实例的 loaders 的配置。

  1. 处理 .vue 文件的 loader 配置被分离出来,存放在变量 vueLoaderUse 中。
  2. compiler.options.module.rules 中的其余规则复制到变量 clonedRules 中。
  3. 基于 vueLoaderUse 中用户设置的 options 生成一个新的规则 pitcher 。
  4. 重写 compiler.options.module.rules 。

重写后的 rules 存在两条和 Vue 相关的规则:

  1. vue-loader/lib/loaders/pitcher.js (本条是新增的)。
  2. Webpack 原始配置中的 vue-loader 和 cache-loader 。

可以描述为:

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

3. 解析 Vue 文件

上面已经理清了处理文件的 loaders ,下面就跟随这些 loaders 来看具体阅读下解析和编译的过程。

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)
  // 获取 loader 传参
  const incomingQuery = qs.parse(rawQuery)
  ...
  const descriptor = parse({
    source,
    // 默认使用用户配置的 compiler
    compiler: options.compiler || loadTemplateCompiler(loaderContext),
    filename,
    sourceRoot,
    needMap: sourceMap
  })
  
  // 如果 loader 配置时有指定 type 存在
  if (incomingQuery.type) {
    return selectBlock(
      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
}

可以看到 vue-loader 的职责主要是三件:

  1. 调用 @vue/component-compiler-utils 的 parse 函数
  2. 如果存在 loader 的参数存在 type 属性,则执行 selectBlock 函数,用于选取源码(比如从 Vue 文件中选取 template 标签中的源码,依赖与上方 parse 函数的解析结果)
  3. 根据 parse 返回结果凭借字符串,并返回

parse 函数存在一个 compiler 参数,默认获取用户配置的编译器,如果未配置则通过 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.`
      ))
    }
  }
}

loadTemplateCompiler 加载 vue-template-compiler 库,并在加载错误时给出一定提示。

parse 返回的结果是一个对象,它记录了三个特殊标签 template|script|style 的内容在 Vue 文件中的位置,以便后续 loader 可以通过位置信息选取正确的内容。

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;
}

parse 会先检查缓存,如果存在则返回缓存内容,如果没有缓存则:

  1. 执行 VueTemplateCompiler 的 parseComponent 函数,获取解析结果。
  2. 执行 sourceMap 相关处理。

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) {
    // 处理开始标签,保存标签 block 对象
  }
  
  function end(tag, start) {
    // 处理结束标签,修改标签 block 对象
  }
  
  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) {
    // 填充空行,保证分离出的 template、script 代码块行号不变(便于 sourceMap 映射)
  }
  
  parseHTML(content, {
    warn: warn,
    start: start,
    end: end,
    outputSourceRange: options.outputSourceRange
  });

  return sfc
}

parseComponent 接收两个参数,第一个参数 content 是 Vue 文件的源码代,第二个参数 options 默认为 { pad: 'line' } ,是可以由用户配置的解析选项。该函数会创建一个 sfc 对象,用于存放对 Vue 文件的解析结果,它的结构描述如下:

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, // 内容,等于 html.slice(start, end)
  start: number, // 开始偏移量
  end: number, // 结束偏移量
  lang: string
}

除此之外还声明了 warn 、start 、end 三个函数,并当做 parseHTML 的参数传入,所以接下来我们进入 parseHTML 一窥究竟。

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

            // 处理 Doctype:
            ...
            // 处理 End tag:
            ...
            // 处理 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) {
        ...
      }
    }

parseHTML 的主要职责是从 Vue 文件中分离出 template|script|style 这三个标签内的代码,方式就是匹配开始和结束标签位置,并把这些信息通过传入的 start|end 函数记录到 parseComponent 中的 sfc 变量中(当然还会记录标签上的属性 lang|scoped|module|src)。

这其中的关键的函数 advance ,它的作用是改变偏移量 index 和从 html 中删除已经处理过的代码( html 是 Vue 文件的内容)。

详细步骤见时序图: 

parseHtml 执行完毕后,Vue 文件的所有信息就都记录在了 parseComponent 的对象 sfc 中,并一步步把结果返回到 3-1 ,3-1 获取到 sfc 对象后会使用其中信息进行字符串拼接,最终生成一个新的模块文件(代码)交给下一个 loader 处理。

此时,我们已经将完全实现了对 Vue 文件的解析! 本节还最后两小节内容,展示获取到的模板源码,是如何调用 VueTemplateCompiler 进行编译的(这属于 Vue 源码范畴)。

解析前后文件内容变化:

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>

编译后的中间代码

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

可以看到,这时的导入是存在 type 类型的,所以会被上文提到的 selectBlock 处理,但这里只是生成了一个字符串,它会通过什么杨的方式再走一次 VueLoader 呢?我们接着往下看。

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

module.exports.pitch = function (remainingRequest) {
  ...
  // 处理 type 为 tempalte 的情况
  if (query.type === `template`) {
    const path = require('path')
    const cacheLoader = cacheDirectory && cacheIdentifier
      ? [`${require.resolve('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}`
  }
  ...
}

上文我们能说了处理 Vue 的规则还有一条,即使用 vue-loader/lib/loaders/pitcher.js 最后,上文经过 parse 解析后存在的 import { render, staticRenderFns } from "./App.vue?vue&type=template&id=7a0f6a6c&" 就会进入到这里的流程中,并最终被替换为使用 vue-loader/lib/index.js 和 vue-loader/lib/loaders/templateLoader.js 来处理。

所以 Vue 文件再一次被 VueLoader 处理 ,这次依然会经过 parse 解析,但我们上文提到过 parse 是存在缓存机制的,所以第二次会直接命中缓存并返回第一次解析的结果,然后判断存在 type,所以就会执行 selectBlock 方法并返回 template|script|style 的源代码。

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

就可以可以看到从 Vue 文件中分离出来的 template/script/style 代码,通过 loaderContext.callback 传给了下一个 loader 处理,即 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 }`
}

可以看到,该 loader 的功能主要是生成一个编译需要的配置对象,然后把这个配置对象传给 @vue/component-compiler-utils 库中的 compileTemplate 函数,并在获取到编译结果后稍作修改便返回。

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) {
        // 提醒特定语言进行预处理
    }
    else {
        return actuallyCompile(options);
    }
}

检查是否存在预处理语言:

  1. 如果存在且有预处理器,则先进行预处理,再进行编译。
  2. 存在但没有预处理器,则报错提示。
  3. 如果不存在,则执行编译。
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
   };
}

在 actuallyCompile 这个函数中,会把两个特殊的处理规则合并入 finalCompilerOptions 对象中,他们分别是用来处理资源路径的 assetUrl_1 和设置响应式图片的 srcset_1,至于它们会对这些属性值做哪些调整,我们之后再讲。

得到新的编译配置项后,就会调用在配置中的 compiler.compile 函数,并返回结果。如果你足够仔细,就会发现 compiler 是在 3-2 templateLoader.js 中被添加编译配置项的,它在用户未进行明确指定编译器时默认使用 vue-template-compiler (const compiler = options.compiler || require('vue-template-compiler') )。

4. VueTemplateCompiler 编译模板

分析待定 ...

收藏 点赞

 分享 

相关推荐

DCache搭建测试

fomo3D游戏系统开发

javascript – d3js Parallel坐标分类数据

使用Docker部署用于学习的ElasticSearch集群

聊聊rocketmq的ConsumeMode.CONCURRENTLY

关于作者

开心茉莉

普通用户

711 篇

0 粉丝

推荐作者

标签

js java delphi 易语言 html html5 vue.js macos python mysql sqlite swift xml git css javascript

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值