原來 vue3 文件編譯是這樣工作的!看完後更懂vue3了

来源:https://www.cnblogs.com/heavenYJJ/p/18058142
-Advertisement-
Play Games

前言 我們每天寫的vue代碼都是寫在vue文件中,但是瀏覽器卻只認識html、css、js等文件類型。所以這個時候就需要一個工具將vue文件轉換為瀏覽器能夠認識的js文件,想必你第一時間就想到了webpack或者vite。但是webpack和vite本身是沒有能力處理vue文件的,其實實際背後生效的 ...


前言

我們每天寫的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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 寫在前面 情人節已經接近尾聲了,雖然跟我沒什麼關係,但是我還是很渴望,能遇到一個良人相伴一生。 現在時間: 內心異常平靜,相對吵鬧我更喜歡安靜的晚上,沒人打擾,enjoy自己獨處的時間! 保存內容顯示 1、任務拆解 讀取已保存內容 將讀取內容在富文本里顯示 2、讀取已保存內容 這個很好理解,就是增加 ...
  • Vue進階 一、vue實例 1.一個基本的vue的實例 <head> <meta charset="UTF-8"> <title></title> </head> <body> <div id="app"> <h1> {{title}} </h1> <button id="btn" @click=" ...
  • 前言 Composables 稱之為可組合項,熟悉 react 的同學喜歡稱之為 hooks ,由於可組合項的存在,Vue3 中的組件之間共用狀態比以往任何時候都更容易。這種新範例引入了一種更有組織性和可擴展性的方式來管理整個應用程式的狀態和邏輯。 什麼是Composables 本質上,可組合項是一 ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 一、組件設計 組件就是把圖形、非圖形的各種邏輯均抽象為一個統一的概念(組件)來實現開發的模式 現在有一個場景,點擊新增與編輯都彈框出來進行填寫,功能上大同小異,可能只是標題內容或者是顯示的主體內容稍微不同 這時候就沒必要寫兩個組件,只需要 ...
  • 當我們在引入應該組件的時候 提示找不到這個組件但是項目明明就有這個物理文件 報錯原因:typescript 只能理解 .ts 文件,無法理解 .vue文件 出現這樣的 第一種 方法就是在env.d.ts 裡面添加下麵代碼 1 declare module '*.vue' { 2 import typ ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 一、是什麼 Tree shaking 是一種通過清除多餘代碼方式來優化項目打包體積的技術,專業術語叫 Dead code elimination 簡單來講,就是在保持代碼運行結果不變的前提下,去除無用的代碼 如果把代碼打包比作製作蛋糕,傳 ...
  • 一、問題闡述 有的時候我們需要控制非同步函數的執行順序,比如a方法中如果要用到非同步函數b方法的請求結果,就需要進行順序控制,否則a函數先執行就會導致找不到數據直接報錯。 二、方法 1.非同步控制 1.1.async,await等做非同步控制 1.2修改函數放置位置達到非同步控制效果(我遇到的情況無效,但是確 ...
  • 網站: 即時熱點 - 正在發生的事 (Solo 社區投稿) 簡介: 一個熱門信息聚合站,幫助您輕鬆瞭解正在發生的事。 描述: 即時熱點是一個熱門信息聚合站,彙集來自百度、微博、頭條、知乎、抖音、快手等多個主流平臺的熱門話題,幫助您輕鬆瞭解正在發生的事。無需跳轉多個平臺,即刻瀏覽最新、最熱、最有趣的話 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...