原来 vue3 文件编译是这样工作的!看完后更懂vue3了

这篇具有很好参考价值的文章主要介绍了原来 vue3 文件编译是这样工作的!看完后更懂vue3了。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言

我们每天写的vue代码都是写在vue文件中,但是浏览器却只认识htmlcssjs等文件类型。所以这个时候就需要一个工具将vue文件转换为浏览器能够认识的js文件,想必你第一时间就想到了webpack或者vite。但是webpackvite本身是没有能力处理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终端。
原来 vue3 文件编译是这样工作的!看完后更懂vue3了

假如vue文件编译为js文件是一个毛线团,那么他的线头一定是vite.config.ts文件中使用@vitejs/plugin-vue的地方。通过这个线头开始debug我们就能够梳理清楚完整的工作流程。
原来 vue3 文件编译是这样工作的!看完后更懂vue3了

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函数返回的对象中的buildStarttransform方法就是对应的插件钩子函数。vite会在对应的时候调用这些插件的钩子函数,比如当vite服务器启动时就会调用插件里面的buildStart等函数,当vite解析每个模块时就会调用transform等函数。更多vite钩子相关内容查看官网。

我们这里主要看buildStarttransform两个钩子函数,分别是服务器启动时调用和解析每个模块时调用。给这两个钩子函数打上断点。
原来 vue3 文件编译是这样工作的!看完后更懂vue3了

然后点击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文件的时候才会走到断点中去。
原来 vue3 文件编译是这样工作的!看完后更懂vue3了

经过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中我们写的源代码。
原来 vue3 文件编译是这样工作的!看完后更懂vue3了

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赋值给optionscompiler属性。那这里的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,主要有类型为SFCDescriptordescriptor属性需要关注。

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中的代码。
原来 vue3 文件编译是这样工作的!看完后更懂vue3了

我们再来看scriptscriptSetup属性,由于vue文件中可以写多个script标签,scriptSetup对应的就是有setupscript标签,script对应的就是没有setup对应的script标签。我们这个场景中只有scriptSetup属性,里面同样包含了App.vue中的script模块中的内容。
原来 vue3 文件编译是这样工作的!看完后更懂vue3了

我们再来看看styles属性,这里的styles属性是一个数组,是因为我们可以在vue文件中写多个style模块,里面同样包含了App.vue中的style模块中的内容。
原来 vue3 文件编译是这样工作的!看完后更懂vue3了

所以这一步执行createDescriptor函数生成的descriptor对象中主要有三个属性,template属性包含了App.vue文件中的template模块code字符串和AST抽象语法树scriptSetup属性包含了App.vue文件中的<script setup>模块的code字符串,styles属性包含了App.vue文件中<style>模块中的code字符串。createDescriptor函数的执行流程图如下:
原来 vue3 文件编译是这样工作的!看完后更懂vue3了

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
}

返回值类型中主要有scriptAstscriptSetupAstcontent这三个属性,scriptAst为编译不带setup属性的script标签生成的AST抽象语法树。scriptSetupAst为编译带setup属性的script标签生成的AST抽象语法树,contentvue文件中的script模块编译后生成的浏览器可执行的js代码。下面这个是执行vue/compiler-sfccompileScript函数返回结果:
原来 vue3 文件编译是这样工作的!看完后更懂vue3了

继续将断点走回genScriptCode函数,现在逻辑就很清晰了。这里的script对象就是调用vue/compiler-sfccompileScript函数返回对象,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函数的执行流程图如下:
原来 vue3 文件编译是这样工作的!看完后更懂vue3了

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
}

原来 vue3 文件编译是这样工作的!看完后更懂vue3了

genTemplateCode函数的执行流程图如下:
原来 vue3 文件编译是这样工作的!看完后更懂vue3了

genStyleCode函数

我们再来看最后一个genStyleCode函数,同样将断点走到调用genStyleCode的地方。一样的接收descriptor对象。代码如下:

const stylesCode = await genStyleCode(
  descriptor,
  pluginContext,
  customElement,
  attachedProps
);

我们将断点走进genStyleCode函数内部,发现和前面genScriptCodegenTemplateCode函数有点不一样,下面这个是我简化后的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到底是什么东西?
原来 vue3 文件编译是这样工作的!看完后更懂vue3了

通过打印我们发现stylesCode竟然变成了一条import语句,并且import的还是当前App.vue文件,只是多了几个query分别是:vuetypeindexscopedlang。再来回忆一下前面讲的@vitejs/plugin-vuetransform钩子函数,当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字段,并且querytype字段值为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函数的实现我们看着就很熟悉了,和前面处理templatescript一样都是调用的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时入参截图:
原来 vue3 文件编译是这样工作的!看完后更懂vue3了

再来看看返回值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>
}

这个是debugcompileStyleAsync函数返回值的截图:
原来 vue3 文件编译是这样工作的!看完后更懂vue3了

genStyleCode函数的执行流程图如下:
原来 vue3 文件编译是这样工作的!看完后更懂vue3了

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变量。

然后将scriptCodetemplateCodestylesCode使用换行符\n拼接起来得到resolvedCode,这个resolvedCode就是一个vue文件编译成js文件的代码code字符串。这个是debugresolvedCode变量值的截图:
原来 vue3 文件编译是这样工作的!看完后更懂vue3了

总结

这篇文章通过debug的方式一步一步的带你了解vue文件编译成js文件的完整流程,下面是一个完整的流程图。如果文字太小看不清,可以将图片保存下来或者放大看:
原来 vue3 文件编译是这样工作的!看完后更懂vue3了

@vitejs/plugin-vue-jsx库中有个叫transform的钩子函数,每当vite加载模块的时候就会触发这个钩子函数。所以当import一个vue文件的时候,就会走到@vitejs/plugin-vue-jsx中的transform钩子函数中,在transform钩子函数中主要调用了transformMain函数。

第一次解析这个vue文件时,在transform钩子函数中主要调用了transformMain函数。在transformMain函数中主要调用了4个函数,分别是:createDescriptorgenScriptCodegenTemplateCodegenStyleCode

createDescriptor接收的参数为当前vue文件代码code字符串,返回值为一个descriptor对象。对象中主要有四个属性templatescriptSetupscriptstyles

  • descriptor.template.ast就是由vue文件中的template模块生成的AST抽象语法树
  • descriptor.template.content就是vue文件中的template模块的代码字符串。
  • scriptSetupscript的区别是分别对应的是vue文件中有setup属性的<script>模块和无setup属性的<script>模块。descriptor.scriptSetup.content就是vue文件中的<script setup>模块的代码字符串。

genScriptCode函数为底层调用vue/compiler-sfccompileScript函数,根据第一步的descriptor对象将vue文件的<script setup>模块转换为浏览器可直接执行的js代码。

genTemplateCode函数为底层调用vue/compiler-sfccompileTemplate函数,根据第一步的descriptor对象将vue文件的<template>模块转换为render函数。

genStyleCode函数为将vue文件的style模块转换为import "/src/App.vue?vue&type=style&index=0&scoped=7a7a37b1&lang.css";样子的import语句。

然后使用换行符\ngenScriptCode函数、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=stylequery,所以在transform函数中会执行transformStyle函数,在transformStyle函数中同样也是调用vue/compiler-sfccompileStyleAsync函数,根据第一步的descriptor对象将vue文件的<style>模块转换为编译后的css代码code字符串,至此编译style部分也讲完了。

关注公众号:前端欧阳,解锁我更多vue干货文章,并且可以免费向我咨询vue相关问题。
原来 vue3 文件编译是这样工作的!看完后更懂vue3了文章来源地址https://www.toymoban.com/news/detail-837821.html

到了这里,关于原来 vue3 文件编译是这样工作的!看完后更懂vue3了的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 原来你是这样的SpringBoot--初识SpringBootAdmin

    Spring Boot Admin(SBA)是一个针对spring-boot的actuator接口进行UI美化封装的监控工具。它可以:在列表中浏览所有被监控spring-boot项目的基本信息,详细的Health信息、内存信息、JVM信息、垃圾回收信息,还可以直接修改logger日志的level。 SBA分为server端和client端,下面来看一个简单示

    2024年02月12日
    浏览(76)
  • 今天才知道原来它是这样的HTTP

    HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于从万维网(WWW:World Wide Web )服务器传输超文本到本地浏览器的传送协议。 HTTP是一个基于TCP/IP通信协议来传递数据(HTML 文件, 图片文件, 查询结果等)。 HTTP三点注意事项 客户端发送一个HTTP请求到服务器的请求

    2024年01月16日
    浏览(39)
  • Python实现京东茅台抢购脚本, 原来这样就可以了?

    京东茅台抢购脚本可以分为以下几部分,具体实现步骤如下: 登录京东账号 首先需要登录京东账号。一个简单的方式是使用Python的 selenium 库。在使用 selenium 库前,需要安装 selenium 库和对应的浏览器驱动。 示例代码如下所示: 注意:在使用 selenium 的时候,要配置对应的浏

    2024年02月14日
    浏览(74)
  • 原来你是这样的SpringBoot--Async异步任务

    本节我们一起学习一下SpringBoot中的异步调用,主要用于优化耗时较长的操作,提高系统性能和吞吐量。 首先给启动类增加注解@EnableAsync,支持异步调用 然后定义要执行的Task,分类增加一个同步方法和异步方法,其中异步方法需要增加注解@Async 其实接下来就可以在controller中

    2024年02月11日
    浏览(36)
  • 一文读懂JavaWeb,前后端数据交互原来是这样的

    Request的请求体中负载着前端向后端发送请求的数据; Response的响应体中负载着后端向前端返回响应的数据; 当然,Request的请求行中的URL中也可以负载着前端向后端发送请求的数据,这个后面会详细说明。 Request模型 Request实例 Repsonse模型 Response实例 JSON字符串就是符合JSON语法

    2024年02月08日
    浏览(80)
  • C语言初学者:原来分支和循环语句可以这样简单!

    目录 1.分支语句 1.if 语句 2.else语句 3.嵌套if  4.悬空else的问题 5.Switch语句 2.循环语句  1.while循环 2.for循环 3do-while循环 3.break和continue语句 1.break语句 2.continue语句 4.goto语句  5.小结 6.为你们点赞呢~ 7.完结散花                            不是每个人都能做自己想做的事,成

    2024年04月12日
    浏览(46)
  • 深入理解 python 虚拟机:字节码教程(1)——原来装饰器是这样实现的

    在本篇文章当中主要给大家介绍在 cpython 当中一些比较常见的字节码,从根本上理解 python 程序的执行。在本文当中主要介绍一些 python 基本操作的字节码,并且将从字节码的角度分析函数装饰器的原理! 这个指令用于将一个常量加载到栈中。常量可以是数字、字符串、元组

    2023年04月09日
    浏览(44)
  • Python实现吃鸡游戏子弹and导弹自动追踪!原来是这样的一个原理啊!

    screen.blit(missile, (x1-missile.get_width(), y1-missile.get_height()/2)) dis_angle=d_angle-old_angle #dis_angle就是到下一个位置需要改变的角度 old_angle=d_angle #更新初始角度 pygame.display.update() 如果仅把导弹考虑为一个质点的话,那么以上算法就已经足矣,我没有做导弹的旋转,因为一个质点也不分头

    2024年03月28日
    浏览(83)
  • vue:如果.vue文件内容过多,我们可以这样拆分

    一、标签内容太多,我们可以用组件的方式拆分 二、那如果JS的内容太多呢?因为耦合性太高,拆成组件后父子组件传值不方便,我们可以这样: 子组件: 父组件(也就是vue主页面): 逻辑拆分,相互调用不受组件影响  

    2024年02月02日
    浏览(48)
  • [数据结构 -- C语言] 堆实现Top-K问题,原来王者荣耀的排名是这样实现的,又涨知识了

    目录 1、什么是Top-K问题? 1.1 Top-K基本思路 2、Top-K问题逻辑分析 2.1 建堆,大小为K的小堆 2.2 将剩余的N - K 个元素依次与堆顶元素比较,大于就替换 2.3 打印堆 3、TopK实现代码 4、Top-K问题完整代码 结果展示: TopK问题的引入: 大家在玩王者荣耀的时候都遇到过xxx市第xxx某英雄

    2024年02月09日
    浏览(61)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包