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里面查看源码,这样做的好处是可以进行断点调试。
调试
切换到项目工程
- 打开命令面板(command+shift+p),搜索‘Toggle Auto Attach’,选中后回车启用
- 在菜单中选择Always
3.在vue-loader入口处打一个断点,终端输入运行命令npm run serve
,可以看到进入断点了,之后就可以愉快地进行调试了。
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入口文件的最后一行打个断点
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函数的。