通過debug搞清楚.vue文件怎麼變成.js文件

前言

我們每天寫的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終端。
debug-terminal

假如vue文件編譯爲js文件是一個毛線團,那麼他的線頭一定是vite.config.ts文件中使用@vitejs/plugin-vue的地方。通過這個線頭開始debug我們就能夠梳理清楚完整的工作流程。
vite-config

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兩個鉤子函數,分別是服務器啓動時調用和解析每個模塊時調用。給這兩個鉤子函數打上斷點。
vue

然後點擊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文件的時候纔會走到斷點中去。
transform

經過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中我們寫的源代碼。
createDescriptor

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中的代碼。
template

我們再來看scriptscriptSetup屬性,由於vue文件中可以寫多個script標籤,scriptSetup對應的就是有setupscript標籤,script對應的就是沒有setup對應的script標籤。我們這個場景中只有scriptSetup屬性,裏面同樣包含了App.vue中的script模塊中的內容。
script

我們再來看看styles屬性,這裏的styles屬性是一個數組,是因爲我們可以在vue文件中寫多個style模塊,裏面同樣包含了App.vue中的style模塊中的內容。
style

所以這一步執行createDescriptor函數生成的descriptor對象中主要有三個屬性,template屬性包含了App.vue文件中的template模塊code字符串和AST抽象語法樹scriptSetup屬性包含了App.vue文件中的<script setup>模塊的code字符串,styles屬性包含了App.vue文件中<style>模塊中的code字符串。createDescriptor函數的執行流程圖如下:
progress-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
}

返回值類型中主要有scriptAstscriptSetupAstcontent這三個屬性,scriptAst爲編譯不帶setup屬性的script標籤生成的AST抽象語法樹。scriptSetupAst爲編譯帶setup屬性的script標籤生成的AST抽象語法樹,contentvue文件中的script模塊編譯後生成的瀏覽器可執行的js代碼。下面這個是執行vue/compiler-sfccompileScript函數返回結果:
resolved

繼續將斷點走回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函數的執行流程圖如下:
progress-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
}

render

genTemplateCode函數的執行流程圖如下:
progress-genTemplateCode

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到底是什麼東西?
styleCode

通過打印我們發現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時入參截圖:
transformStyle-arg

再來看看返回值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函數返回值的截圖:
transformStyle-res

genStyleCode函數的執行流程圖如下:
progress-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變量。

然後將scriptCodetemplateCodestylesCode使用換行符\n拼接起來得到resolvedCode,這個resolvedCode就是一個vue文件編譯成js文件的代碼code字符串。這個是debugresolvedCode變量值的截圖:
resolvedCode

總結

這篇文章通過debug的方式一步一步的帶你瞭解vue文件編譯成js文件的完整流程,下面是一個完整的流程圖。如果文字太小看不清,可以將圖片保存下來或者放大看:
progress-full

@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相關問題。
qrcode

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章