Vue 源码-单文件组件的加载

251 阅读1分钟

vue版本:v2.7.10

import Vue from 'vue'
import App from './App.vue'
console.log(App)
new Vue({
  render: (h) => {
    return h(App)
  }
}).$mount('#app')

上面是创建一个Vue应用的代码,App组件作为参数传递到h函数中,实现应用的渲染。

App.vue文件是通过什么方式解析的呢,我们在终端输入vue inspect > output.js,打开生成的output.js文件 可以看到.vue文件是通过vue-loader-v15,即vue-loader v15版本这个loader进行解析的(以下均以vue-loader代替)。

<template>
  <div class="wrapper">
    {{ msg }}
  </div>
</template>

<script lang="ts" setup>
const msg = 'Welcome to Your Vue.js + TypeScript App'
</script>

App组件的代码如上,我们再看一下console.log(App)打印处理的App组件导出结果。

可以看到,组件导出的是一个对象,包含

  • 生命周期beforeCreate/beforeDestroy
  • 渲染函数render
  • 静态渲染函数staticRenderFns
  • 构造函数_Ctor
  • 文件路径__file
  • 组件名称_name
  • 其他

至于APP组件文件怎么导出成对象,对看一下vue-loader-v15的源码了。

vue-loader-v15

从gihub上面把vue-loader-v15的源码clone下来下来,切换到v15的分支,在lib/index.js可以看到vue-loader的入口了。

由于入口文件没有进行过压缩和编译,其实可以直接在项目的node_modules里面查看源码,这样做的好处是可以进行断点调试。

调试

切换到项目工程

  1. 打开命令面板(command+shift+p),搜索‘Toggle Auto Attach’,选中后回车启用
  2. 在菜单中选择Always

image.png

3.在vue-loader入口处打一个断点,终端输入运行命令npm run serve,可以看到进入断点了,之后就可以愉快地进行调试了。

image.png

VueLoaderPlugin

Vue项目除了引入vue-loader,还使用了VueLoaderPlugin。 在生成的的output.js可以看到,项目的webpack配置中引入了VueLoaderPlugin插件。

我们在vue-loader-v15/lib/plugin-webpack5.js查看插件的具体实现。

可以看到添加了一个pitcher-loader,文件位置在vue-loader-v15/lib/loaders/pitcher.js。 pitcher-loader的匹配规则是请求参数包含vue。

vue单文件组件的解析流程

我们重新回到单文件组件的解析,在vue-loader入口文件的最后一行打个断点

执行`npm run serve`,将code的值复制出来如下
import { render, staticRenderFns } from "./App.vue?vue&type=template&id=7ba5bd90&scoped=true&"
import script from "./App.vue?vue&type=script&lang=ts&setup=true&"
export * from "./App.vue?vue&type=script&lang=ts&setup=true&"
import style0 from "./App.vue?vue&type=style&index=0&id=7ba5bd90&scoped=true&lang=css&"
/* normalize component */
import normalizer from "!../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js"
var component = normalizer(script,render,staticRender,false,null,"7ba5bd90",null')
component.options.__file = "src/App.vue"
export default component.exports

可以看出vue-loader将App.vue转化为一个为ES模块文件,即console.log(App)输出的内容。 再看一下这个文件,App.vue被拆分成三部分,路径都是指向App.vue,参数中都带有vue,type类型分别为template/script/style,这三部分导入的模块最终最为入参调用了componentNormalizer函数并导出作为App.vue的导出值。而在componentNormalizer中,render函数被透传出去,也就是App.vue的render函数是通过import { render, staticRenderFns } from "./App.vue?vue&type=template&id=7ba5bd90&scoped=true&"导入得到。我们在断点处继续往下执行,可以发现还是继续跳转到vue-loader入口处,只是由于incomingQuery.type有值,直接跳转到selectBlock函数后返回了。

module.exports = function selectBlock(
  descriptor,
  scopeId,
  options,
  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`) {
    const script = resolveScript(descriptor, scopeId, options, loaderContext)
    if (appendExtension) {
      loaderContext.resourcePath += '.' + (script.lang || 'js')
    }
    loaderContext.callback(null, script.content, 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
  }
}

selectBlock最终调用loaderContext.callback,也就是交给其他loader进行进一步处理,这里着重关注template类型的解析。

回看pitcher loader

还记得我们上面提及的pitcher loader吗,其中有这么两段代码

  const templateLoaderPath = require.resolve('./templateLoader')

  // for templates: inject the template compiler & optional cache
  if (query.type === `template`) {
    const path = require('path')
    const cacheLoader =
      cacheDirectory && cacheIdentifier
        ? [
            `${require.resolve('cache-loader')}?${JSON.stringify({
              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 { is27 } = resolveCompiler(this.rootContext, this)

    const request = genRequest([
      ...cacheLoader,
      ...postLoaders,
      ...(is27 ? [] : [templateLoaderPath + `??vue-loader-options`]),
      ...preLoaders
    ])

    // the template compiler uses esm exports
    return `export * from ${request}`
  }

query.type为template时,使用的是vue-loader-v15/lib/loaders/templateLoader.js进行解析。

总结

到这里可以得出结论了:

1.App.vue的渲染函数render通过解析./App.vue?vue&type=template得到。即:

import { render, staticRenderFns } from "./App.vue?vue&type=template&id=7ba5bd90&scoped=true&" 2.而./App.vue?vue&type=template则是同vue-loader-v15/lib/loaders/templateLoader.js这个loader进行解析

下一篇我们再继续研究templateLoader这loader,看看模版语句是怎么解析出一个render函数的。