亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

通過debug搞清楚.vue文件如何變成.js文件(案例詳解)

 更新時間:2024年07月09日 11:13:40   作者:諸葛亮的芭蕉扇  
這篇文章主要介紹了通過debug搞清楚.vue文件如何變成.js文件,本文以@vitejs/plugin-vue舉例,通過debug的方式帶你一步一步的搞清楚vue文件是如何編譯為js文件的,需要的朋友可以參考下

我們每天寫的vue代碼都是寫在vue文件中,但是瀏覽器卻只認識htmlcssjs等文件類型。所以這個時候就需要一個工具將vue文件轉(zhuǎn)換為瀏覽器能夠認識的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文件,已經(jīng)精簡過了:

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代碼中我們可以看到主要有三部分,想必你也猜到了這三部分剛好對應(yīng)vue文件的那三塊。

  • _sfc_main對象的setup方法對應(yīng)vue文件中的 <script setup lang="ts"> 模塊。
  • _sfc_render函數(shù)對應(yīng)vue文件中的<template>模塊。
  • import "/src/App.vue?vue&type=style&index=0&scoped=7a7a37b1&lang.css";對應(yīng)vue文件中的 <style scoped> 模塊。

debug搞清楚如何將vue文件編譯為js文件

大家應(yīng)該都知道,前端代碼運行環(huán)境主要有兩個,node端和瀏覽器端,分別對應(yīng)我們熟悉的編譯時和運行時。瀏覽器明顯是不認識vue文件的,所以vue文件編譯成js這一過程肯定不是在運行時的瀏覽器端。很明顯這一過程是在編譯時的node端。

要在node端打斷點,我們需要啟動一個debug 終端。這里以vscode舉例,首先我們需要打開終端,然后點擊終端中的+號旁邊的下拉箭頭,在下拉中點擊Javascript Debug Terminal就可以啟動一個debug終端。

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

vuePlugin函數(shù)

我們給上方圖片的vue函數(shù)打了一個斷點,然后在debug終端上面執(zhí)行yarn dev,我們看到斷點已經(jīng)停留在了vue函數(shù)這里。然后點擊step into,斷點走到了@vitejs/plugin-vue庫中的一個vuePlugin函數(shù)中。我們看到vuePlugin函數(shù)中的內(nèi)容代碼大概是這樣的:

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函數(shù)返回的對象中的buildStart、transform方法就是對應(yīng)的插件鉤子函數(shù)。vite會在對應(yīng)的時候調(diào)用這些插件的鉤子函數(shù),比如當(dāng)vite服務(wù)器啟動時就會調(diào)用插件里面的buildStart等函數(shù),當(dāng)vite解析每個模塊時就會調(diào)用transform等函數(shù)。更多vite鉤子相關(guān)內(nèi)容查看官網(wǎng)。

我們這里主要看buildStarttransform兩個鉤子函數(shù),分別是服務(wù)器啟動時調(diào)用和解析每個模塊時調(diào)用。給這兩個鉤子函數(shù)打上斷點。

然后點擊Continue(F5),vite服務(wù)啟動后就會走到buildStart鉤子函數(shù)中打的斷點。我們可以看到buildStart鉤子函數(shù)的代碼是這樣的:

buildStart() {
  const compiler = options.value.compiler = options.value.compiler || resolveCompiler(options.value.root);
}

將鼠標(biāo)放到options.value.compiler上面我們看到此時options.value.compiler的值為null,所以代碼會走到resolveCompiler函數(shù)中,點擊Step Into(F11)走到resolveCompiler函數(shù)中??吹?code>resolveCompiler函數(shù)代碼如下:

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函數(shù)中調(diào)用了tryResolveCompiler函數(shù),在tryResolveCompiler函數(shù)中判斷當(dāng)前項目是否是vue3.x版本,然后將vue/compiler-sfc包返回。所以經(jīng)過初始化后options.value.compiler的值就是vue的底層庫vue/compiler-sfc,記住這個后面會用

然后點擊Continue(F5)放掉斷點,在瀏覽器中打開對應(yīng)的頁面,比如:http://localhost:5173/ 。此時vite將會編譯這個頁面要用到的所有文件,就會走到transform鉤子函數(shù)斷點中了。由于解析每個文件都會走到transform鉤子函數(shù)中,但是我們只關(guān)注App.vue文件是如何解析的,所以為了方便我們直接在transform函數(shù)中添加了下面這段代碼,并且刪掉了原來在transform鉤子函數(shù)中打的斷點,這樣就只有解析到App.vue文件的時候才會走到斷點中去。

經(jīng)過debug我們發(fā)現(xiàn)解析App.vue文件時transform函數(shù)實際就是執(zhí)行了transformMain函數(shù),至于transformStyle函數(shù)后面講解析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函數(shù)

繼續(xù)debug斷點走進transformMain函數(shù),發(fā)現(xiàn)transformMain函數(shù)中代碼邏輯很清晰。按照順序分別是:

  • 根據(jù)源代碼code字符串調(diào)用createDescriptor函數(shù)生成一個descriptor對象。
  • 調(diào)用genScriptCode函數(shù)傳入第一步生成的descriptor對象將 <script setup> 模塊編譯為瀏覽器可執(zhí)行的js代碼。
  • 調(diào)用genTemplateCode函數(shù)傳入第一步生成的descriptor對象將<template>模塊編譯為render函數(shù)。
  • 調(diào)用genStyleCode函數(shù)傳入第一步生成的descriptor對象將 <style scoped> 模塊編譯為類似這樣的import語句,import "/src/App.vue?vue&type=style&index=0&scoped=7a7a37b1&lang.css";

createDescriptor

我們先來看看createDescriptor函數(shù),將斷點走到createDescriptor(filename, code, options)這一行代碼,可以看到傳入的filename就是App.vue的文件路徑,code就是App.vue中我們寫的源代碼。

debug走進createDescriptor函數(shù),看到createDescriptor函數(shù)的代碼如下:

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是調(diào)用createDescriptor函數(shù)時傳入的第三個參數(shù)解構(gòu)而來,而第三個參數(shù)就是options。還記得我們之前在vite啟動時調(diào)用了buildStart鉤子函數(shù),然后將vue底層包vue/compiler-sfc賦值給optionscompiler屬性。那這里的compiler.parse其實就是調(diào)用的vue/compiler-sfc包暴露出來的parse函數(shù),這是一個vue暴露出來的底層的API,這篇文章我們不會對底層API進行源碼解析,通過查看parse函數(shù)的輸入和輸出基本就可以搞清楚parse函數(shù)的作用。下面這個是parse函數(shù)的類型定義:

export function parse(
source: string,
options: SFCParseOptions = {},
): SFCParseResult {}

從上面我們可以看到parse函數(shù)接收兩個參數(shù),第一個參數(shù)為vue文件的源代碼,在我們這里就是App.vue中的code字符串,第二個參數(shù)是一些options選項。我們再來看看parse函數(shù)的返回值SFCParseResult,主要有類型為SFCDescriptordescriptor屬性需要關(guān)注。

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文件對應(yīng)的template標(biāo)簽中的內(nèi)容,里面包含了由App.vue文件中的template模塊編譯成的AST抽象語法樹和原始的template中的代碼。

我們再來看scriptscriptSetup屬性,由于vue文件中可以寫多個script標(biāo)簽,scriptSetup對應(yīng)的就是有setupscript標(biāo)簽,script對應(yīng)的就是沒有setup對應(yīng)的script標(biāo)簽。我們這個場景中只有scriptSetup屬性,里面同樣包含了App.vue中的script模塊中的內(nèi)容。

我們再來看看styles屬性,這里的styles屬性是一個數(shù)組,是因為我們可以在vue文件中寫多個style模塊,里面同樣包含了App.vue中的style模塊中的內(nèi)容。

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

genScriptCode函數(shù)

我們再來看genScriptCode函數(shù)是如何將 <script setup> 模塊編譯成可執(zhí)行的js代碼,同樣將斷點走到調(diào)用genScriptCode函數(shù)的地方,genScriptCode函數(shù)主要接收我們上一步生成的descriptor對象,調(diào)用genScriptCode函數(shù)后會將編譯后的script模塊代碼賦值給scriptCode變量。

const { code: scriptCode, map: scriptMap } = await genScriptCode(
  descriptor,
  options,
  pluginContext,
  ssr,
  customElement
);

將斷點走到genScriptCode函數(shù)內(nèi)部,在genScriptCode函數(shù)中主要就是這行代碼: const script = resolveScript(descriptor, options, ssr, customElement);。將第一步生成的descriptor對象作為參數(shù)傳給resolveScript函數(shù),返回值就是編譯后的js代碼,genScriptCode函數(shù)的代碼簡化后如下:

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
  };
}

我們繼續(xù)將斷點走到resolveScript函數(shù)內(nèi)部,發(fā)現(xiàn)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我們前面第一步的時候已經(jīng)解釋過了,options.compiler對象實際就是vue底層包vue/compiler-sfc暴露的對象,這里的options.compiler.compileScript()其實就是調(diào)用的vue/compiler-sfc包暴露出來的compileScript函數(shù),同樣也是一個vue暴露出來的底層的API。通過查看compileScript函數(shù)的輸入和輸出基本就可以搞清楚compileScript函數(shù)的作用。下面這個是compileScript函數(shù)的類型定義:

export function compileScript(
  sfc: SFCDescriptor,
  options: SFCScriptCompileOptions,
): SFCScriptBlock{}

這個函數(shù)的入?yún)⑹且粋€SFCDescriptor對象,就是我們第一步調(diào)用生成createDescriptor函數(shù)生成的descriptor對象,第二個參數(shù)是一些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
}

返回值類型中主要有scriptAstscriptSetupAst、content這三個屬性,scriptAst為編譯不帶setup屬性的script標(biāo)簽生成的AST抽象語法樹。scriptSetupAst為編譯帶setup屬性的script標(biāo)簽生成的AST抽象語法樹,contentvue文件中的script模塊編譯后生成的瀏覽器可執(zhí)行的js代碼。下面這個是執(zhí)行vue/compiler-sfccompileScript函數(shù)返回結(jié)果:

繼續(xù)將斷點走回genScriptCode函數(shù),現(xiàn)在邏輯就很清晰了。這里的script對象就是調(diào)用vue/compiler-sfccompileScript函數(shù)返回對象,scriptCode就是script對象的content屬性 ,也就是將vue文件中的script模塊經(jīng)過編譯后生成瀏覽器可直接執(zhí)行的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函數(shù)的執(zhí)行流程圖如下:

genTemplateCode函數(shù)

我們再來看genTemplateCode函數(shù)是如何將template模塊編譯成render函數(shù)的,同樣將斷點走到調(diào)用genTemplateCode函數(shù)的地方,genTemplateCode函數(shù)主要接收我們上一步生成的descriptor對象,調(diào)用genTemplateCode函數(shù)后會將編譯后的template模塊代碼賦值給templateCode變量。

({ code: templateCode, map: templateMap } = await genTemplateCode(
  descriptor,
  options,
  pluginContext,
  ssr,
  customElement
))

同樣將斷點走到genTemplateCode函數(shù)內(nèi)部,在genTemplateCode函數(shù)中主要就是返回transformTemplateInMain函數(shù)的返回值,genTemplateCode函數(shù)的代碼簡化后如下:

async function genTemplateCode(descriptor, options, pluginContext, ssr, customElement) {
  const template = descriptor.template;
  return transformTemplateInMain(
    template.content,
    descriptor,
    options,
    pluginContext,
    ssr,
    customElement
  );
}

我們繼續(xù)將斷點走進transformTemplateInMain函數(shù),發(fā)現(xiàn)這里也主要是調(diào)用compile函數(shù),代碼如下:

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函數(shù)內(nèi)部,我們看到compile函數(shù)的代碼是下面這樣的:

function compile(code, descriptor, options, pluginContext, ssr, customElement) {
  const result = options.compiler.compileTemplate({
    ...resolveTemplateCompilerOptions(descriptor, options, ssr),
    source: code
  });
  return result;
}

同樣這里也用到了options.compiler,調(diào)用options.compiler.compileTemplate()其實就是調(diào)用的vue/compiler-sfc包暴露出來的compileTemplate函數(shù),這也是一個vue暴露出來的底層的API。不過這里和前面不同的是compileTemplate接收的不是descriptor對象,而是一個SFCTemplateCompileOptions類型的對象,所以這里需要調(diào)用resolveTemplateCompilerOptions函數(shù)將參數(shù)轉(zhuǎn)換成SFCTemplateCompileOptions類型的對象。通過查看compileTemplate函數(shù)的輸入和輸出基本就可以搞清楚compileTemplate函數(shù)的作用。下面這個是compileTemplate函數(shù)的類型定義:

export function compileTemplate(
  options: SFCTemplateCompileOptions,
): SFCTemplateCompileResults {}

入?yún)?code>options主要就是需要編譯的template中的源代碼和對應(yīng)的AST抽象語法樹。我們來看看返回值SFCTemplateCompileResults,這里面的code就是編譯后的render函數(shù)字符串。

export interface SFCTemplateCompileResults {
  code: string
  ast?: RootNode
  preamble?: string
  source: string
  tips: string[]
  errors: (string | CompilerError)[]
  map?: RawSourceMap
}

genTemplateCode函數(shù)的執(zhí)行流程圖如下:

genStyleCode函數(shù)

我們再來看最后一個genStyleCode函數(shù),同樣將斷點走到調(diào)用genStyleCode的地方。一樣的接收descriptor對象。代碼如下:

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

我們將斷點走進genStyleCode函數(shù)內(nèi)部,發(fā)現(xiàn)和前面genScriptCodegenTemplateCode函數(shù)有點不一樣,下面這個是我簡化后的genStyleCode函數(shù)代碼:

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標(biāo)簽,所以descriptor對象的styles屬性是一個數(shù)組。遍歷descriptor.styles數(shù)組,我們發(fā)現(xiàn)for循環(huán)內(nèi)全部都是一堆賦值操作,沒有調(diào)用vue/compiler-sfc包暴露出來的任何API。將斷點走到 return stylesCode;,看看stylesCode到底是什么東西?

通過打印我們發(fā)現(xiàn)stylesCode竟然變成了一條import語句,并且import的還是當(dāng)前App.vue文件,只是多了幾個query分別是:vuetype、index、scoped、lang。再來回憶一下前面講的@vitejs/plugin-vuetransform鉤子函數(shù),當(dāng)vite解析每個模塊時就會調(diào)用transform等函數(shù)。所以當(dāng)代碼運行到這行import語句的時候會再次走到transform鉤子函數(shù)中。我們再來看看transform鉤子函數(shù)的代碼:

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
      );
    }
  }
}

當(dāng)query中有vue字段,并且querytype字段值為style時就會執(zhí)行transformStyle函數(shù),我們給transformStyle函數(shù)打個斷點。當(dāng)執(zhí)行上面那條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函數(shù)的實現(xiàn)我們看著就很熟悉了,和前面處理templatescript一樣都是調(diào)用的vue/compiler-sfc包暴露出來的compileStyleAsync函數(shù),這也是一個vue暴露出來的底層的API。通過查看compileStyleAsync函數(shù)的輸入和輸出基本就可以搞清楚compileStyleAsync函數(shù)的作用。

export function compileStyleAsync(
  options: SFCAsyncStyleCompileOptions,
): Promise<SFCStyleCompileResults> {}

我們先來看看SFCAsyncStyleCompileOptions入?yún)ⅲ?/p>

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
}

入?yún)⒅饕P(guān)注幾個字段,source字段為style標(biāo)簽中的css原始代碼。scoped字段為style標(biāo)簽中是否有scoped attribute。id字段為我們在觀察 DOM 結(jié)構(gòu)時看到的 data-v-xxxxx。這個是debug時入?yún)⒔貓D:

再來看看返回值SFCStyleCompileResults對象,主要就是code屬性,這個是經(jīng)過編譯后的css字符串,已經(jīng)加上了data-v-xxxxx。

interface SFCStyleCompileResults {
  code: string
  map: RawSourceMap | undefined
  rawResult: Result | LazyResult | undefined
  errors: Error[]
  modules?: Record<string, string>
  dependencies: Set<string>
}

這個是debugcompileStyleAsync函數(shù)返回值的截圖:

genStyleCode函數(shù)的執(zhí)行流程圖如下:

transformMain函數(shù)簡化后的代碼

現(xiàn)在我們可以來看transformMain函數(shù)簡化后的代碼:

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函數(shù)中的代碼執(zhí)行主流程,其實就是對應(yīng)了一個vue文件編譯成js文件的流程。

首先調(diào)用createDescriptor函數(shù)將一個vue文件解析為一個descriptor對象。

然后以descriptor對象為參數(shù)調(diào)用genScriptCode函數(shù),將vue文件中的 <script> 模塊代碼編譯成瀏覽器可執(zhí)行的js代碼code字符串,賦值給scriptCode變量。

接著以descriptor對象為參數(shù)調(diào)用genTemplateCode函數(shù),將vue文件中的 <template>模塊代碼編譯成render函數(shù)code字符串,賦值給templateCode變量。

然后以descriptor對象為參數(shù)調(diào)用genStyleCode函數(shù),將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變量值的截圖:

總結(jié)

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

@vitejs/plugin-vue庫中有個叫transform的鉤子函數(shù),每當(dāng)vite加載模塊的時候就會觸發(fā)這個鉤子函數(shù)。所以當(dāng)import一個vue文件的時候,就會走到@vitejs/plugin-vue中的transform鉤子函數(shù)中,在transform鉤子函數(shù)中主要調(diào)用了transformMain函數(shù)。

第一次解析這個vue文件時,在transform鉤子函數(shù)中主要調(diào)用了transformMain函數(shù)。在transformMain函數(shù)中主要調(diào)用了4個函數(shù),分別是:createDescriptorgenScriptCode、genTemplateCode、genStyleCode。

createDescriptor接收的參數(shù)為當(dāng)前vue文件代碼code字符串,返回值為一個descriptor對象。對象中主要有四個屬性template、scriptSetupscript、styles。

  • descriptor.template.ast就是由vue文件中的template模塊生成的AST抽象語法樹。
  • descriptor.template.content就是vue文件中的template模塊的代碼字符串。scriptSetupscript的區(qū)別是分別對應(yīng)的是vue文件中有setup屬性的 <script> 模塊和無setup屬性的 <script>模塊。
  • descriptor.scriptSetup.content就是vue文件中的<script setup>模塊的代碼字符串。
  • genScriptCode函數(shù)為底層調(diào)用vue/compiler-sfccompileScript函數(shù),根據(jù)第一步的descriptor對象將vue文件的 <script setup> 模塊轉(zhuǎn)換為瀏覽器可直接執(zhí)行的js代碼。

genTemplateCode函數(shù)為底層調(diào)用vue/compiler-sfccompileTemplate函數(shù),根據(jù)第一步的descriptor對象將vue文件的<template>模塊轉(zhuǎn)換為render函數(shù)。

genStyleCode函數(shù)為將vue文件的style模塊轉(zhuǎn)換為import "/src/App.vue?vue&type=style&index=0&scoped=7a7a37b1&lang.css";樣子的import語句。

然后使用換行符\ngenScriptCode函數(shù)、genTemplateCode函數(shù)、genStyleCode函數(shù)的返回值拼接起來賦值給變量resolvedCode,這個resolvedCode就是vue文件編譯成js文件的code字符串。

當(dāng)瀏覽器執(zhí)行到import "/src/App.vue?vue&type=style&index=0&scoped=7a7a37b1&lang.css";語句時,觸發(fā)了加載模塊操作,再次觸發(fā)了@vitejs/plugin-vue中的transform鉤子函數(shù)。此時由于有了type=stylequery,所以在transform函數(shù)中會執(zhí)行transformStyle函數(shù),在transformStyle函數(shù)中同樣也是調(diào)用vue/compiler-sfccompileStyleAsync函數(shù),根據(jù)第一步的descriptor對象將vue文件的 <style> 模塊轉(zhuǎn)換為編譯后的css代碼code字符串, 至此編譯style部分也講完了。

到此這篇關(guān)于通過debug搞清楚.vue文件如何變成.js文件的文章就介紹到這了,更多相關(guān).vue文件變成.js文件內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評論