通过debug搞清楚.vue文件怎么变成.js文件
前言
我们每天写的vue
代码都是写在vue
文件中,但是浏览器却只认识html
、css
、js
等文件类型。所以这个时候就需要一个工具将vue
文件转换为浏览器能够认识的js
文件,想必你第一时间就想到了webpack
或者vite
。但是webpack
和vite
本身是没有能力处理vue
文件的,其实实际背后生效的是vue-loader和@vitejs/plugin-vue。本文以@vitejs/plugin-vue
举例,通过debug
的方式带你一步一步的搞清楚vue
文件是如何编译为js
文件的,看不懂你来打我。
举个例子
这个是我的源代码App.vue
文件:
<template>
<h1 class="msg">{{ msg }}</h1>
</template>
<script setup lang="ts">
import { ref } from "vue";
const msg = ref("hello word");
</script>
<style scoped>
.msg {
color: red;
font-weight: bold;
}
</style>
这个例子很简单,在setup
中定义了msg
变量,然后在template
中将msg
渲染出来。
下面这个是我从network
中找到的编译后的js
文件,已经精简过了:
import {
createElementBlock as _createElementBlock,
defineComponent as _defineComponent,
openBlock as _openBlock,
toDisplayString as _toDisplayString,
ref,
} from "/node_modules/.vite/deps/vue.js?v=23bfe016";
import "/src/App.vue?vue&type=style&index=0&scoped=7a7a37b1&lang.css";
const _sfc_main = _defineComponent({
__name: "App",
setup(__props, { expose: __expose }) {
__expose();
const msg = ref("hello word");
const __returned__ = { msg };
return __returned__;
},
});
const _hoisted_1 = { class: "msg" };
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return (
_openBlock(),
_createElementBlock(
"h1",
_hoisted_1,
_toDisplayString($setup.msg),
1
/* TEXT */
)
);
}
__sfc__.render = render;
export default _sfc_main;
编译后的js
代码中我们可以看到主要有三部分,想必你也猜到了这三部分刚好对应vue
文件的那三块。
_sfc_main
对象的setup
方法对应vue
文件中的<script setup lang="ts">
模块。_sfc_render
函数对应vue
文件中的<template>
模块。import "/src/App.vue?vue&type=style&index=0&scoped=7a7a37b1&lang.css";
对应vue
文件中的<style scoped>
模块。
debug搞清楚如何将vue
文件编译为js
文件
大家应该都知道,前端代码运行环境主要有两个,node
端和浏览器端,分别对应我们熟悉的编译时和运行时。浏览器明显是不认识vue
文件的,所以vue
文件编译成js
这一过程肯定不是在运行时的浏览器端。很明显这一过程是在编译时的node
端。
要在node
端打断点,我们需要启动一个debug 终端。这里以vscode
举例,首先我们需要打开终端,然后点击终端中的+
号旁边的下拉箭头,在下拉中点击Javascript Debug Terminal
就可以启动一个debug
终端。
假如vue
文件编译为js
文件是一个毛线团,那么他的线头一定是vite.config.ts
文件中使用@vitejs/plugin-vue
的地方。通过这个线头开始debug
我们就能够梳理清楚完整的工作流程。
vuePlugin函数
我们给上方图片的vue
函数打了一个断点,然后在debug
终端上面执行yarn dev
,我们看到断点已经停留在了vue
函数这里。然后点击step into
,断点走到了@vitejs/plugin-vue
库中的一个vuePlugin
函数中。我们看到vuePlugin
函数中的内容代码大概是这样的:
function vuePlugin(rawOptions = {}) {
const options = shallowRef({
compiler: null,
// 省略...
});
return {
name: "vite:vue",
handleHotUpdate(ctx) {
// ...
},
config(config) {
// ..
},
configResolved(config) {
// ..
},
configureServer(server) {
// ..
},
buildStart() {
// ..
},
async resolveId(id) {
// ..
},
load(id, opt) {
// ..
},
transform(code, id, opt) {
// ..
}
};
}
@vitejs/plugin-vue
是作为一个plugins
插件在vite
中使用,vuePlugin
函数返回的对象中的buildStart
、transform
方法就是对应的插件钩子函数。vite
会在对应的时候调用这些插件的钩子函数,比如当vite
服务器启动时就会调用插件里面的buildStart
等函数,当vite
解析每个模块时就会调用transform
等函数。更多vite
钩子相关内容查看官网。
我们这里主要看buildStart
和transform
两个钩子函数,分别是服务器启动时调用和解析每个模块时调用。给这两个钩子函数打上断点。
然后点击Continue(F5),vite
服务启动后就会走到buildStart
钩子函数中打的断点。我们可以看到buildStart
钩子函数的代码是这样的:
buildStart() {
const compiler = options.value.compiler = options.value.compiler || resolveCompiler(options.value.root);
}
将鼠标放到options.value.compiler
上面我们看到此时options.value.compiler
的值为null
,所以代码会走到resolveCompiler
函数中,点击Step Into(F11)走到resolveCompiler
函数中。看到resolveCompiler
函数代码如下:
function resolveCompiler(root) {
const compiler = tryResolveCompiler(root) || tryResolveCompiler();
return compiler;
}
function tryResolveCompiler(root) {
const vueMeta = tryRequire("vue/package.json", root);
if (vueMeta && vueMeta.version.split(".")[0] >= 3) {
return tryRequire("vue/compiler-sfc", root);
}
}
在resolveCompiler
函数中调用了tryResolveCompiler
函数,在tryResolveCompiler
函数中判断当前项目是否是vue3.x
版本,然后将vue/compiler-sfc
包返回。所以经过初始化后options.value.compiler
的值就是vue
的底层库vue/compiler-sfc
,记住这个后面会用。
然后点击Continue(F5)放掉断点,在浏览器中打开对应的页面,比如:http://localhost:5173/ 。此时vite
将会编译这个页面要用到的所有文件,就会走到transform
钩子函数断点中了。由于解析每个文件都会走到transform
钩子函数中,但是我们只关注App.vue
文件是如何解析的,所以为了方便我们直接在transform
函数中添加了下面这段代码,并且删掉了原来在transform
钩子函数中打的断点,这样就只有解析到App.vue
文件的时候才会走到断点中去。
经过debug我们发现解析App.vue
文件时transform
函数实际就是执行了transformMain
函数,至于transformStyle
函数后面讲解析style
的时候会讲:
transform(code, id, opt) {
const { filename, query } = parseVueRequest(id);
if (!query.vue) {
return transformMain(
code,
filename,
options.value,
this,
ssr,
customElementFilter.value(filename)
);
} else {
const descriptor = query.src ? getSrcDescriptor(filename, query) || getTempSrcDescriptor(filename, query) : getDescriptor(filename, options.value);
if (query.type === "style") {
return transformStyle(
code,
descriptor,
Number(query.index || 0),
options.value,
this,
filename
);
}
}
}
transformMain
函数
继续debug断点走进transformMain
函数,发现transformMain
函数中代码逻辑很清晰。按照顺序分别是:
- 根据源代码code字符串调用
createDescriptor
函数生成一个descriptor
对象。 - 调用
genScriptCode
函数传入第一步生成的descriptor
对象将<script setup>
模块编译为浏览器可执行的js
代码。 - 调用
genTemplateCode
函数传入第一步生成的descriptor
对象将<template>
模块编译为render
函数。 - 调用
genStyleCode
函数传入第一步生成的descriptor
对象将<style scoped>
模块编译为类似这样的import
语句,import "/src/App.vue?vue&type=style&index=0&scoped=7a7a37b1&lang.css";
。
createDescriptor
函数
我们先来看看createDescriptor
函数,将断点走到createDescriptor(filename, code, options)
这一行代码,可以看到传入的filename
就是App.vue
的文件路径,code
就是App.vue
中我们写的源代码。
debug
走进createDescriptor
函数,看到createDescriptor
函数的代码如下:
function createDescriptor(filename, source, { root, isProduction, sourceMap, compiler, template }, hmr = false) {
const { descriptor, errors } = compiler.parse(source, {
filename,
sourceMap,
templateParseOptions: template?.compilerOptions
});
const normalizedPath = slash(path.normalize(path.relative(root, filename)));
descriptor.id = getHash(normalizedPath + (isProduction ? source : ""));
return { descriptor, errors };
}
这个compiler
是不是觉得有点熟悉?compiler
是调用createDescriptor
函数时传入的第三个参数解构而来,而第三个参数就是options
。还记得我们之前在vite
启动时调用了buildStart
钩子函数,然后将vue
底层包vue/compiler-sfc
赋值给options
的compiler
属性。那这里的compiler.parse
其实就是调用的vue/compiler-sfc
包暴露出来的parse
函数,这是一个vue
暴露出来的底层的API
,这篇文章我们不会对底层API进行源码解析,通过查看parse
函数的输入和输出基本就可以搞清楚parse
函数的作用。下面这个是parse
函数的类型定义:
export function parse(
source: string,
options: SFCParseOptions = {},
): SFCParseResult {}
从上面我们可以看到parse
函数接收两个参数,第一个参数为vue
文件的源代码,在我们这里就是App.vue
中的code
字符串,第二个参数是一些options
选项。
我们再来看看parse
函数的返回值SFCParseResult
,主要有类型为SFCDescriptor
的descriptor
属性需要关注。
export interface SFCParseResult {
descriptor: SFCDescriptor
errors: (CompilerError | SyntaxError)[]
}
export interface SFCDescriptor {
filename: string
source: string
template: SFCTemplateBlock | null
script: SFCScriptBlock | null
scriptSetup: SFCScriptBlock | null
styles: SFCStyleBlock[]
customBlocks: SFCBlock[]
cssVars: string[]
slotted: boolean
shouldForceReload: (prevImports: Record<string, ImportBinding>) => boolean
}
仔细看看SFCDescriptor
类型,其中的template
属性就是App.vue
文件对应的template
标签中的内容,里面包含了由App.vue
文件中的template
模块编译成的AST抽象语法树
和原始的template
中的代码。
我们再来看script
和scriptSetup
属性,由于vue
文件中可以写多个script
标签,scriptSetup
对应的就是有setup
的script
标签,script
对应的就是没有setup
对应的script
标签。我们这个场景中只有scriptSetup
属性,里面同样包含了App.vue
中的script
模块中的内容。
我们再来看看styles
属性,这里的styles
属性是一个数组,是因为我们可以在vue
文件中写多个style
模块,里面同样包含了App.vue
中的style
模块中的内容。
所以这一步执行createDescriptor
函数生成的descriptor
对象中主要有三个属性,template
属性包含了App.vue
文件中的template
模块code
字符串和AST抽象语法树
,scriptSetup
属性包含了App.vue
文件中的<script setup>
模块的code
字符串,styles
属性包含了App.vue
文件中<style>
模块中的code
字符串。createDescriptor
函数的执行流程图如下:
genScriptCode
函数
我们再来看genScriptCode
函数是如何将<script setup>
模块编译成可执行的js
代码,同样将断点走到调用genScriptCode
函数的地方,genScriptCode
函数主要接收我们上一步生成的descriptor
对象,调用genScriptCode
函数后会将编译后的script
模块代码赋值给scriptCode
变量。
const { code: scriptCode, map: scriptMap } = await genScriptCode(
descriptor,
options,
pluginContext,
ssr,
customElement
);
将断点走到genScriptCode
函数内部,在genScriptCode
函数中主要就是这行代码: const script = resolveScript(descriptor, options, ssr, customElement);
。将第一步生成的descriptor
对象作为参数传给resolveScript
函数,返回值就是编译后的js
代码,genScriptCode
函数的代码简化后如下:
async function genScriptCode(descriptor, options, pluginContext, ssr, customElement) {
let scriptCode = `const ${scriptIdentifier} = {}`;
const script = resolveScript(descriptor, options, ssr, customElement);
if (script) {
scriptCode = script.content;
map = script.map;
}
return {
code: scriptCode,
map
};
}
我们继续将断点走到resolveScript
函数内部,发现resolveScript
中的代码其实也很简单,简化后的代码如下:
function resolveScript(descriptor, options, ssr, customElement) {
let resolved = null;
resolved = options.compiler.compileScript(descriptor, {
...options.script,
id: descriptor.id,
isProd: options.isProduction,
inlineTemplate: isUseInlineTemplate(descriptor, !options.devServer),
templateOptions: resolveTemplateCompilerOptions(descriptor, options, ssr),
sourceMap: options.sourceMap,
genDefaultAs: canInlineMain(descriptor, options) ? scriptIdentifier : void 0,
customElement
});
return resolved;
}
这里的options.compiler
我们前面第一步的时候已经解释过了,options.compiler
对象实际就是vue
底层包vue/compiler-sfc
暴露的对象,这里的options.compiler.compileScript()
其实就是调用的vue/compiler-sfc
包暴露出来的compileScript
函数,同样也是一个vue
暴露出来的底层的API
,后面我们的分析defineOptions
等文章时会去深入分析compileScript
函数,这篇文章我们不会去读compileScript
函数的源码。通过查看compileScript
函数的输入和输出基本就可以搞清楚compileScript
函数的作用。下面这个是compileScript
函数的类型定义:
export function compileScript(
sfc: SFCDescriptor,
options: SFCScriptCompileOptions,
): SFCScriptBlock{}
这个函数的入参是一个SFCDescriptor
对象,就是我们第一步调用生成createDescriptor
函数生成的descriptor
对象,第二个参数是一些options
选项。我们再来看返回值SFCScriptBlock
类型:
export interface SFCScriptBlock extends SFCBlock {
type: 'script'
setup?: string | boolean
bindings?: BindingMetadata
imports?: Record<string, ImportBinding>
scriptAst?: import('@babel/types').Statement[]
scriptSetupAst?: import('@babel/types').Statement[]
warnings?: string[]
/**
* Fully resolved dependency file paths (unix slashes) with imported types
* used in macros, used for HMR cache busting in @vitejs/plugin-vue and
* vue-loader.
*/
deps?: string[]
}
export interface SFCBlock {
type: string
content: string
attrs: Record<string, string | true>
loc: SourceLocation
map?: RawSourceMap
lang?: string
src?: string
}
返回值类型中主要有scriptAst
、scriptSetupAst
、content
这三个属性,scriptAst
为编译不带setup
属性的script
标签生成的AST抽象语法树。scriptSetupAst
为编译带setup
属性的script
标签生成的AST抽象语法树,content
为vue
文件中的script
模块编译后生成的浏览器可执行的js
代码。下面这个是执行vue/compiler-sfc
的compileScript
函数返回结果:
继续将断点走回genScriptCode
函数,现在逻辑就很清晰了。这里的script
对象就是调用vue/compiler-sfc
的compileScript
函数返回对象,scriptCode
就是script
对象的content
属性 ,也就是将vue
文件中的script
模块经过编译后生成浏览器可直接执行的js
代码code
字符串。
async function genScriptCode(descriptor, options, pluginContext, ssr, customElement) {
let scriptCode = `const ${scriptIdentifier} = {}`;
const script = resolveScript(descriptor, options, ssr, customElement);
if (script) {
scriptCode = script.content;
map = script.map;
}
return {
code: scriptCode,
map
};
}
genScriptCode
函数的执行流程图如下:
genTemplateCode
函数
我们再来看genTemplateCode
函数是如何将template
模块编译成render
函数的,同样将断点走到调用genTemplateCode
函数的地方,genTemplateCode
函数主要接收我们上一步生成的descriptor
对象,调用genTemplateCode
函数后会将编译后的template
模块代码赋值给templateCode
变量。
({ code: templateCode, map: templateMap } = await genTemplateCode(
descriptor,
options,
pluginContext,
ssr,
customElement
))
同样将断点走到genTemplateCode
函数内部,在genTemplateCode
函数中主要就是返回transformTemplateInMain
函数的返回值,genTemplateCode
函数的代码简化后如下:
async function genTemplateCode(descriptor, options, pluginContext, ssr, customElement) {
const template = descriptor.template;
return transformTemplateInMain(
template.content,
descriptor,
options,
pluginContext,
ssr,
customElement
);
}
我们继续将断点走进transformTemplateInMain
函数,发现这里也主要是调用compile
函数,代码如下:
function transformTemplateInMain(code, descriptor, options, pluginContext, ssr, customElement) {
const result = compile(
code,
descriptor,
options,
pluginContext,
ssr,
customElement
);
return {
...result,
code: result.code.replace(
/\nexport (function|const) (render|ssrRender)/,
"\n$1 _sfc_$2"
)
};
}
同理将断点走进到compile
函数内部,我们看到compile
函数的代码是下面这样的:
function compile(code, descriptor, options, pluginContext, ssr, customElement) {
const result = options.compiler.compileTemplate({
...resolveTemplateCompilerOptions(descriptor, options, ssr),
source: code
});
return result;
}
同样这里也用到了options.compiler
,调用options.compiler.compileTemplate()
其实就是调用的vue/compiler-sfc
包暴露出来的compileTemplate
函数,这也是一个vue
暴露出来的底层的API
。不过这里和前面不同的是compileTemplate
接收的不是descriptor
对象,而是一个SFCTemplateCompileOptions
类型的对象,所以这里需要调用resolveTemplateCompilerOptions
函数将参数转换成SFCTemplateCompileOptions
类型的对象。这篇文章我们不会对底层API进行解析。通过查看compileTemplate
函数的输入和输出基本就可以搞清楚compileTemplate
函数的作用。下面这个是compileTemplate
函数的类型定义:
export function compileTemplate(
options: SFCTemplateCompileOptions,
): SFCTemplateCompileResults {}
入参options
主要就是需要编译的template
中的源代码和对应的AST抽象语法树
。我们来看看返回值SFCTemplateCompileResults
,这里面的code
就是编译后的render
函数字符串。
export interface SFCTemplateCompileResults {
code: string
ast?: RootNode
preamble?: string
source: string
tips: string[]
errors: (string | CompilerError)[]
map?: RawSourceMap
}
genTemplateCode
函数的执行流程图如下:
genStyleCode
函数
我们再来看最后一个genStyleCode
函数,同样将断点走到调用genStyleCode
的地方。一样的接收descriptor
对象。代码如下:
const stylesCode = await genStyleCode(
descriptor,
pluginContext,
customElement,
attachedProps
);
我们将断点走进genStyleCode
函数内部,发现和前面genScriptCode
和genTemplateCode
函数有点不一样,下面这个是我简化后的genStyleCode
函数代码:
async function genStyleCode(descriptor, pluginContext, customElement, attachedProps) {
let stylesCode = ``;
if (descriptor.styles.length) {
for (let i = 0; i < descriptor.styles.length; i++) {
const style = descriptor.styles[i];
const src = style.src || descriptor.filename;
const attrsQuery = attrsToQuery(style.attrs, "css");
const srcQuery = style.src ? style.scoped ? `&src=${descriptor.id}` : "&src=true" : "";
const directQuery = customElement ? `&inline` : ``;
const scopedQuery = style.scoped ? `&scoped=${descriptor.id}` : ``;
const query = `?vue&type=style&index=${i}${srcQuery}${directQuery}${scopedQuery}`;
const styleRequest = src + query + attrsQuery;
stylesCode += `
import ${JSON.stringify(styleRequest)}`;
}
}
return stylesCode;
}
我们前面讲过因为vue
文件中可能会有多个style
标签,所以descriptor
对象的styles
属性是一个数组。遍历descriptor.styles
数组,我们发现for
循环内全部都是一堆赋值操作,没有调用vue/compiler-sfc
包暴露出来的任何API
。将断点走到 return stylesCode;
,看看stylesCode
到底是什么东西?
通过打印我们发现stylesCode
竟然变成了一条import
语句,并且import
的还是当前App.vue
文件,只是多了几个query
分别是:vue
、type
、index
、scoped
、lang
。再来回忆一下前面讲的@vitejs/plugin-vue
的transform
钩子函数,当vite
解析每个模块时就会调用transform
等函数。所以当代码运行到这行import
语句的时候会再次走到transform
钩子函数中。我们再来看看transform
钩子函数的代码:
transform(code, id, opt) {
const { filename, query } = parseVueRequest(id);
if (!query.vue) {
// 省略
} else {
const descriptor = query.src ? getSrcDescriptor(filename, query) || getTempSrcDescriptor(filename, query) : getDescriptor(filename, options.value);
if (query.type === "style") {
return transformStyle(
code,
descriptor,
Number(query.index || 0),
options.value,
this,
filename
);
}
}
}
当query
中有vue
字段,并且query
中type
字段值为style
时就会执行transformStyle
函数,我们给transformStyle
函数打个断点。当执行上面那条import
语句时就会走到断点中,我们进到transformStyle
中看看。
async function transformStyle(code, descriptor, index, options, pluginContext, filename) {
const block = descriptor.styles[index];
const result = await options.compiler.compileStyleAsync({
...options.style,
filename: descriptor.filename,
id: `data-v-${descriptor.id}`,
isProd: options.isProduction,
source: code,
scoped: block.scoped,
...options.cssDevSourcemap ? {
postcssOptions: {
map: {
from: filename,
inline: false,
annotation: false
}
}
} : {}
});
return {
code: result.code,
map
};
}
transformStyle
函数的实现我们看着就很熟悉了,和前面处理template
和script
一样都是调用的vue/compiler-sfc
包暴露出来的compileStyleAsync
函数,这也是一个vue
暴露出来的底层的API
。同样我们不会对底层API进行解析。通过查看compileStyleAsync
函数的输入和输出基本就可以搞清楚compileStyleAsync
函数的作用。
export function compileStyleAsync(
options: SFCAsyncStyleCompileOptions,
): Promise<SFCStyleCompileResults> {}
我们先来看看SFCAsyncStyleCompileOptions
入参:
interface SFCAsyncStyleCompileOptions extends SFCStyleCompileOptions {
isAsync?: boolean
modules?: boolean
modulesOptions?: CSSModulesOptions
}
interface SFCStyleCompileOptions {
source: string
filename: string
id: string
scoped?: boolean
trim?: boolean
isProd?: boolean
inMap?: RawSourceMap
preprocessLang?: PreprocessLang
preprocessOptions?: any
preprocessCustomRequire?: (id: string) => any
postcssOptions?: any
postcssPlugins?: any[]
map?: RawSourceMap
}
入参主要关注几个字段,source
字段为style
标签中的css
原始代码。scoped
字段为style
标签中是否有scoped
attribute。id
字段为我们在观察 DOM 结构时看到的 data-v-xxxxx
。这个是debug
时入参截图:
再来看看返回值SFCStyleCompileResults
对象,主要就是code
属性,这个是经过编译后的css
字符串,已经加上了data-v-xxxxx
。
interface SFCStyleCompileResults {
code: string
map: RawSourceMap | undefined
rawResult: Result | LazyResult | undefined
errors: Error[]
modules?: Record<string, string>
dependencies: Set<string>
}
这个是debug
时compileStyleAsync
函数返回值的截图:
genStyleCode
函数的执行流程图如下:
transformMain
函数简化后的代码
现在我们可以来看transformMain
函数简化后的代码:
async function transformMain(code, filename, options, pluginContext, ssr, customElement) {
const { descriptor, errors } = createDescriptor(filename, code, options);
const { code: scriptCode, map: scriptMap } = await genScriptCode(
descriptor,
options,
pluginContext,
ssr,
customElement
);
let templateCode = "";
({ code: templateCode, map: templateMap } = await genTemplateCode(
descriptor,
options,
pluginContext,
ssr,
customElement
));
const stylesCode = await genStyleCode(
descriptor,
pluginContext,
customElement,
attachedProps
);
const output = [
scriptCode,
templateCode,
stylesCode
];
let resolvedCode = output.join("\n");
return {
code: resolvedCode,
map: resolvedMap || {
mappings: ""
},
meta: {
vite: {
lang: descriptor.script?.lang || descriptor.scriptSetup?.lang || "js"
}
}
};
}
transformMain
函数中的代码执行主流程,其实就是对应了一个vue
文件编译成js
文件的流程。
首先调用createDescriptor
函数将一个vue
文件解析为一个descriptor
对象。
然后以descriptor
对象为参数调用genScriptCode
函数,将vue
文件中的<script>
模块代码编译成浏览器可执行的js
代码code
字符串,赋值给scriptCode
变量。
接着以descriptor
对象为参数调用genTemplateCode
函数,将vue
文件中的<template>
模块代码编译成render
函数code
字符串,赋值给templateCode
变量。
然后以descriptor
对象为参数调用genStyleCode
函数,将vue
文件中的<style>
模块代码编译成了import
语句code
字符串,比如:import "/src/App.vue?vue&type=style&index=0&scoped=7a7a37b1&lang.css";
,赋值给stylesCode
变量。
然后将scriptCode
、templateCode
、stylesCode
使用换行符\n
拼接起来得到resolvedCode
,这个resolvedCode
就是一个vue
文件编译成js
文件的代码code
字符串。这个是debug
时resolvedCode
变量值的截图:
总结
这篇文章通过debug
的方式一步一步的带你了解vue
文件编译成js
文件的完整流程,下面是一个完整的流程图。如果文字太小看不清,可以将图片保存下来或者放大看:
@vitejs/plugin-vue-jsx
库中有个叫transform
的钩子函数,每当vite
加载模块的时候就会触发这个钩子函数。所以当import
一个vue
文件的时候,就会走到@vitejs/plugin-vue-jsx
中的transform
钩子函数中,在transform
钩子函数中主要调用了transformMain
函数。
第一次解析这个vue
文件时,在transform
钩子函数中主要调用了transformMain
函数。在transformMain
函数中主要调用了4个函数,分别是:createDescriptor
、genScriptCode
、genTemplateCode
、genStyleCode
。
createDescriptor
接收的参数为当前vue
文件代码code
字符串,返回值为一个descriptor
对象。对象中主要有四个属性template
、scriptSetup
、script
、styles
。
descriptor.template.ast
就是由vue
文件中的template
模块生成的AST抽象语法树
。descriptor.template.content
就是vue
文件中的template
模块的代码字符串。scriptSetup
和script
的区别是分别对应的是vue
文件中有setup
属性的<script>
模块和无setup
属性的<script>
模块。descriptor.scriptSetup.content
就是vue
文件中的<script setup>
模块的代码字符串。
genScriptCode
函数为底层调用vue/compiler-sfc
的compileScript
函数,根据第一步的descriptor
对象将vue
文件的<script setup>
模块转换为浏览器可直接执行的js
代码。
genTemplateCode
函数为底层调用vue/compiler-sfc
的compileTemplate
函数,根据第一步的descriptor
对象将vue
文件的<template>
模块转换为render
函数。
genStyleCode
函数为将vue
文件的style
模块转换为import "/src/App.vue?vue&type=style&index=0&scoped=7a7a37b1&lang.css";
样子的import
语句。
然后使用换行符\n
将genScriptCode
函数、genTemplateCode
函数、genStyleCode
函数的返回值拼接起来赋值给变量resolvedCode
,这个resolvedCode
就是vue
文件编译成js
文件的code
字符串。
当浏览器执行到import "/src/App.vue?vue&type=style&index=0&scoped=7a7a37b1&lang.css";
语句时,触发了加载模块操作,再次触发了@vitejs/plugin-vue-jsx
中的transform
钩子函数。此时由于有了type=style
的query
,所以在transform
函数中会执行transformStyle
函数,在transformStyle
函数中同样也是调用vue/compiler-sfc
的compileStyleAsync
函数,根据第一步的descriptor
对象将vue
文件的<style>
模块转换为编译后的css
代码code
字符串,至此编译style
部分也讲完了。
关注公众号:前端欧阳
,解锁我更多vue
干货文章,并且可以免费向我咨询vue
相关问题。
通过debug搞清楚.vue文件怎么变成.js文件的更多相关文章
- Vue在单独引入js文件中使用ElementUI的组件
Vue在单独引入js文件中使用ElementUI的组件 问题场景: 我想在vue中的js文件中使用elementUI中的组件,因为我在main.js中引入了element包和它的css,并挂载到了全局 ...
- Vue中引入静态JS文件(爬坑)
前言(背景介绍) 开发的项目需要与Threejs的3D项目结合在一起,需要静态引入,jquery.js,stats.js,three.js,ThreeBSP.js等静态文件.开发环境是iview-ad ...
- html文件引用本地js文件出现跨域问题的解决方案
在本地做个小demo,很简单,一个html文件,一个js文件,在html文件中通过<script>标签引入js,但是出现了一个意想不到的问题:浏览器报错—— 一番折腾后,终于弄明白了:加载 ...
- 在被vue组件引用的 js 文件里获取组件实例this
思路: 通过调用函数 把 组件实例this 传递 到 被应用的 js文件里 实例: 文件结构 在SendThis.vue 文件中引用 了modalConfig.js import modalConf ...
- vue页面引入外部js文件遇到的问题
问题一:vue文件中引入外部js文件的方法 //在vue文件中 <script> import * as funApi from '../../../publicJavaScript/pu ...
- Vue 加载外部js文件
Vue.js 加载外部js文件 在项目内新建一个config.js //变量的定义 export var config = { baseurl:'http://172.16.114.5:8088/M ...
- vue引入第三方的js文件
在最近开发中,遇到了vue框架配合openlayers做gis功能的一个模块.过程中要求引用第三方的单独js文件.嗯,解决如下: 把整体js文件用函数abc()封装起来,在需要用到的模块用var ob ...
- vue如何导入外部js文件(es6)
也许大家都是使用习惯了es5的写法喜欢直接用<Script>标签倒入js文件,但是很是可惜,这写法.在es6,或则说vue环境下不支持 真的写法是怎样? 首先.我们要改造我们要映入的外部j ...
- vue中配置axios.js文件,发送请求
为了统一管理请求,每个项目都会去配置axios:而不是在vue中直接使用,那样不好维护等等 下面是我配置的最基础的axios文件 第一步:首先新建一个axios文件,我是放在router文件下的 im ...
- 解决:Angular-cli:执行ng-build --prod后,dist文件里无js文件、文件未压缩等问题
Angular2.0于2016年9月上线,我于9月入坑. 入坑以来,一直让我很困惑的问题 1.angular-cli是个什么鬼东西? 2.为什么我们自己的资源文件还没写什么,就有起码50多个js文件加 ...
随机推荐
- Docker 安装 Nacos 注册中心
废话不多说直接上安装脚本: 在运行安装脚本之前,首先,我们查看一下 Nacos 的版本分别有哪些使用 docker search nacos: 然后在执行: docker pull nacos/nac ...
- 【4】python读写文件操作---详细讲解!
相关文章: 全网最详细超长python学习笔记.14章节知识点很全面十分详细,快速入门,只用看这一篇你就学会了! [1]windows系统如何安装后缀是whl的python库 [2]超级详细Pytho ...
- 20.8 OpenSSL 套接字SSL传输文件
有了上面的基础那么传输文件的实现就变得简单了,在传输时通常我们需要打开文件,并每次读入1024个字节的数据包,通过SSL加密传输即可,此处的文件传输功能在原生套接字章节中也进行过详细讲解,此处我们还是 ...
- 1.5 为x64dbg编写插件
任何一个成熟的软件都会具有可扩展性,可扩展性是现代软件的一个重要特征,因为它使软件更易于维护和适应变化的需求,x64dbg也不例外其可通过开发插件的方式扩展其自身功能,x64dbg提供了多种插件接口, ...
- 从嘉手札<2023-11-20>
写给十年如一日的偶像--Faker "我看了一下,觉得视频还不够清晰,等我换一个清晰点的摄像头再回来直播,不要走开~" 繁星满天,流光飞逝. 世界是一场盛大的表演, 舞台上熙熙攘攘 ...
- ChatGPT 对接微信公众号技术方案实现!
作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 9天假期写了8天代码和10篇文章,这个5.1过的很爽! 如假期前小傅哥的计划一样,这个假期开启 ...
- 推荐一款接口自动化测试数据提取神器 JSonPath
在之前分享中,给大家介绍过一篇:如何快速审核接口返回值全部字段解决方案,详见原文:接口自动化测试,一键快速校验接口返回值全部字段 .当时,提到解决这类问题,市面上常见的解决方案有两种: 根据业务校验需 ...
- Linux虚拟机追加扩展磁盘
一.使用VMware给虚拟机追加磁盘 使用VMware打开虚拟机设置对话框,选择硬盘,点击右侧的扩展按钮,输入扩展后的磁盘容量. 点击扩展按钮.提示磁盘已成功扩展. 二.对闲置的空间进行分区 上面扩展 ...
- NOIp 2023 游记
咕了正好一周的 NOIp 游记,是我第一篇游记,也是一张寄往四年后不得不退役的.即将画上青春句号的自己的,包含了自己的青涩.期待与成长的信笺. Day \((-\infty, -7)\) CSP-S ...
- [Java][Spring]spring profile与maven profile多环境管理
spring profile 与 maven profile 多环境管理 spring profile Spring profile是Spring提供的多环境管理方案. 如下图: 每种环境都对应一个y ...