Vue 文件解析、编译流程
文章转自:Vue 文件解析、编译流程 - 博客 - 编程圈本文将以目前(2020/10/26)最新的 vue-cli 版本 @vue/cli-service4.5.8(后文以 CLI4 代指)以脉络,详细分享 .vue 文件解析和编译的过程。解析指.vue文件被解析为template|script|style三份源码,编译指template源码被编译为渲染函数。写在前面的一些说明:本文并不涉及过多
本文将以目前(2020/10/26)最新的 vue-cli 版本 @vue/cli-service 4.5.8
(后文以 CLI4 代指)以脉络,详细分享 .vue 文件解析和编译的过程。解析指 .vue
文件被解析为 template|script|style
三份源码,编译指 template
源码被编译为渲染函数。
写在前面的一些说明:
- 本文并不涉及过多编译细节,主要目的是帮助大家熟悉编译流程,为解决问题提供编译方向上的思路。
- 本文使用 Vue
2.6.11
,并不涉及 Vue3 相关内容。 - 阅读本文需要对
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)
})
- 创建 Service 实例。
- 获取命令
vue-cli-service
携带的参数build
。 - 调用
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
函数会把四个类型的插件整合为一个数组并返回:
- 初始化
Service
类时传入的 plugins - CLI4 预设的 plugins
- devDependencies 中的 plugins
- 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 方法,传入配置作为参数。
这些插件主要有两个行为:
- 调用
PluginAPI
的registerCommand
方法,把命令对应的模块(@vue/cli-service/lib/commands/*
)挂到Service
的成员变量commands
中。 - 调用
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 的配置。
- 处理
.vue
文件的 loader 配置被分离出来,存放在变量vueLoaderUse
中。 compiler.options.module.rules
中的其余规则复制到变量clonedRules
中。- 基于
vueLoaderUse
中用户设置的 options 生成一个新的规则pitcher
。 - 重写
compiler.options.module.rules
。
重写后的 rules 存在两条和 Vue 相关的规则:
vue-loader/lib/loaders/pitcher.js
(本条是新增的)。- 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 的职责主要是三件:
- 调用
@vue/component-compiler-utils
的parse
函数 - 如果存在 loader 的参数存在
type
属性,则执行selectBlock
函数,用于选取源码(比如从 Vue 文件中选取 template 标签中的源码,依赖与上方parse
函数的解析结果) - 根据
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
会先检查缓存,如果存在则返回缓存内容,如果没有缓存则:
- 执行 VueTemplateCompiler 的
parseComponent
函数,获取解析结果。 - 执行 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);
}
}
检查是否存在预处理语言:
- 如果存在且有预处理器,则先进行预处理,再进行编译。
- 存在但没有预处理器,则报错提示。
- 如果不存在,则执行编译。
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 编译模板
分析待定 ...
分享
相关推荐
javascript – d3js Parallel坐标分类数据
使用Docker部署用于学习的ElasticSearch集群
聊聊rocketmq的ConsumeMode.CONCURRENTLY
关于作者
推荐作者
标签
js java delphi 易语言 html html5 vue.js macos python mysql sqlite swift xml git css javascript
更多推荐
所有评论(0)