欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

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

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

我們每天寫的vue代碼都是寫在vue文件中,但是瀏覽器卻只認(rèn)識(shí)html、css、js等文件類型。所以這個(gè)時(shí)候就需要一個(gè)工具將vue文件轉(zhuǎn)換為瀏覽器能夠認(rèn)識(shí)的js文件,想必你第一時(shí)間就想到了webpack或者vite。但是webpackvite本身是沒有能力處理vue文件的,其實(shí)實(shí)際背后生效的是vue-loader和@vitejs/plugin-vue。本文以@vitejs/plugin-vue舉例,通過debug的方式帶你一步一步的搞清楚vue文件是如何編譯為js文件的。

舉個(gè)例子

這個(gè)是我的源代碼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>

這個(gè)例子很簡(jiǎn)單,在setup中定義了msg變量,然后在template中將msg渲染出來。

下面這個(gè)是我從network中找到的編譯后的js文件,已經(jīng)精簡(jiǎn)過了:

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

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

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

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

要在node端打斷點(diǎn),我們需要啟動(dòng)一個(gè)debug 終端。這里以vscode舉例,首先我們需要打開終端,然后點(diǎn)擊終端中的+號(hào)旁邊的下拉箭頭,在下拉中點(diǎn)擊Javascript Debug Terminal就可以啟動(dòng)一個(gè)debug終端。

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

vuePlugin函數(shù)

我們給上方圖片的vue函數(shù)打了一個(gè)斷點(diǎn),然后在debug終端上面執(zhí)行yarn dev,我們看到斷點(diǎn)已經(jīng)停留在了vue函數(shù)這里。然后點(diǎn)擊step into,斷點(diǎn)走到了@vitejs/plugin-vue庫(kù)中的一個(gè)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是作為一個(gè)plugins插件在vite中使用,vuePlugin函數(shù)返回的對(duì)象中的buildStart、transform方法就是對(duì)應(yīng)的插件鉤子函數(shù)。vite會(huì)在對(duì)應(yīng)的時(shí)候調(diào)用這些插件的鉤子函數(shù),比如當(dāng)vite服務(wù)器啟動(dòng)時(shí)就會(huì)調(diào)用插件里面的buildStart等函數(shù),當(dāng)vite解析每個(gè)模塊時(shí)就會(huì)調(diào)用transform等函數(shù)。更多vite鉤子相關(guān)內(nèi)容查看官網(wǎng)。

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

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

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

將鼠標(biāo)放到options.value.compiler上面我們看到此時(shí)options.value.compiler的值為null,所以代碼會(huì)走到resolveCompiler函數(shù)中,點(diǎn)擊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)前項(xiàng)目是否是vue3.x版本,然后將vue/compiler-sfc包返回。所以經(jīng)過初始化后options.value.compiler的值就是vue的底層庫(kù)vue/compiler-sfc,記住這個(gè)后面會(huì)用

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

經(jīng)過debug我們發(fā)現(xiàn)解析App.vue文件時(shí)transform函數(shù)實(shí)際就是執(zhí)行了transformMain函數(shù),至于transformStyle函數(shù)后面講解析style的時(shí)候會(huì)講:

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斷點(diǎn)走進(jìn)transformMain函數(shù),發(fā)現(xiàn)transformMain函數(shù)中代碼邏輯很清晰。按照順序分別是:

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

createDescriptor

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

debug走進(jìn)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 };
}

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

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

從上面我們可以看到parse函數(shù)接收兩個(gè)參數(shù),第一個(gè)參數(shù)為vue文件的源代碼,在我們這里就是App.vue中的code字符串,第二個(gè)參數(shù)是一些options選項(xiàng)。我們?cè)賮砜纯?code>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
}

仔細(xì)看看SFCDescriptor類型,其中的template屬性就是App.vue文件對(duì)應(yīng)的template標(biāo)簽中的內(nèi)容,里面包含了由App.vue文件中的template模塊編譯成的AST抽象語(yǔ)法樹和原始的template中的代碼。

我們?cè)賮砜?code>script和scriptSetup屬性,由于vue文件中可以寫多個(gè)script標(biāo)簽,scriptSetup對(duì)應(yīng)的就是有setupscript標(biāo)簽,script對(duì)應(yīng)的就是沒有setup對(duì)應(yīng)的script標(biāo)簽。我們這個(gè)場(chǎng)景中只有scriptSetup屬性,里面同樣包含了App.vue中的script模塊中的內(nèi)容。

我們?cè)賮砜纯?code>styles屬性,這里的styles屬性是一個(gè)數(shù)組,是因?yàn)槲覀兛梢栽?code>vue文件中寫多個(gè)style模塊,里面同樣包含了App.vue中的style模塊中的內(nèi)容。

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

genScriptCode函數(shù)

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

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

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

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ù)將斷點(diǎn)走到resolveScript函數(shù)內(nèi)部,發(fā)現(xiàn)resolveScript中的代碼其實(shí)也很簡(jiǎn)單,簡(jiǎn)化后的代碼如下:

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

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

這個(gè)函數(shù)的入?yún)⑹且粋€(gè)SFCDescriptor對(duì)象,就是我們第一步調(diào)用生成createDescriptor函數(shù)生成的descriptor對(duì)象,第二個(gè)參數(shù)是一些options選項(xiàng)。我們?cè)賮砜捶祷刂?code>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、scriptSetupAstcontent這三個(gè)屬性,scriptAst為編譯不帶setup屬性的script標(biāo)簽生成的AST抽象語(yǔ)法樹。scriptSetupAst為編譯帶setup屬性的script標(biāo)簽生成的AST抽象語(yǔ)法樹,contentvue文件中的script模塊編譯后生成的瀏覽器可執(zhí)行的js代碼。下面這個(gè)是執(zhí)行vue/compiler-sfccompileScript函數(shù)返回結(jié)果:

繼續(xù)將斷點(diǎn)走回genScriptCode函數(shù),現(xiàn)在邏輯就很清晰了。這里的script對(duì)象就是調(diào)用vue/compiler-sfccompileScript函數(shù)返回對(duì)象,scriptCode就是script對(duì)象的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ù)

我們?cè)賮砜?code>genTemplateCode函數(shù)是如何將template模塊編譯成render函數(shù)的,同樣將斷點(diǎn)走到調(diào)用genTemplateCode函數(shù)的地方,genTemplateCode函數(shù)主要接收我們上一步生成的descriptor對(duì)象,調(diào)用genTemplateCode函數(shù)后會(huì)將編譯后的template模塊代碼賦值給templateCode變量。

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

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

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

我們繼續(xù)將斷點(diǎn)走進(jìn)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"
    )
  };
}

同理將斷點(diǎn)走進(jìn)到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()其實(shí)就是調(diào)用的vue/compiler-sfc包暴露出來的compileTemplate函數(shù),這也是一個(gè)vue暴露出來的底層的API。不過這里和前面不同的是compileTemplate接收的不是descriptor對(duì)象,而是一個(gè)SFCTemplateCompileOptions類型的對(duì)象,所以這里需要調(diào)用resolveTemplateCompilerOptions函數(shù)將參數(shù)轉(zhuǎn)換成SFCTemplateCompileOptions類型的對(duì)象。通過查看compileTemplate函數(shù)的輸入和輸出基本就可以搞清楚compileTemplate函數(shù)的作用。下面這個(gè)是compileTemplate函數(shù)的類型定義:

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

入?yún)?code>options主要就是需要編譯的template中的源代碼和對(duì)應(yīng)的AST抽象語(yǔ)法樹。我們來看看返回值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ù)

我們?cè)賮砜醋詈笠粋€(gè)genStyleCode函數(shù),同樣將斷點(diǎn)走到調(diào)用genStyleCode的地方。一樣的接收descriptor對(duì)象。代碼如下:

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

我們將斷點(diǎn)走進(jìn)genStyleCode函數(shù)內(nèi)部,發(fā)現(xiàn)和前面genScriptCodegenTemplateCode函數(shù)有點(diǎn)不一樣,下面這個(gè)是我簡(jiǎn)化后的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;
}

我們前面講過因?yàn)?code>vue文件中可能會(huì)有多個(gè)style標(biāo)簽,所以descriptor對(duì)象的styles屬性是一個(gè)數(shù)組。遍歷descriptor.styles數(shù)組,我們發(fā)現(xiàn)for循環(huán)內(nèi)全部都是一堆賦值操作,沒有調(diào)用vue/compiler-sfc包暴露出來的任何API。將斷點(diǎn)走到 return stylesCode;,看看stylesCode到底是什么東西?

通過打印我們發(fā)現(xiàn)stylesCode竟然變成了一條import語(yǔ)句,并且import的還是當(dāng)前App.vue文件,只是多了幾個(gè)query分別是:vue、type、index、scoped、lang。再來回憶一下前面講的@vitejs/plugin-vuetransform鉤子函數(shù),當(dāng)vite解析每個(gè)模塊時(shí)就會(huì)調(diào)用transform等函數(shù)。所以當(dāng)代碼運(yùn)行到這行import語(yǔ)句的時(shí)候會(huì)再次走到transform鉤子函數(shù)中。我們?cè)賮砜纯?code>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時(shí)就會(huì)執(zhí)行transformStyle函數(shù),我們給transformStyle函數(shù)打個(gè)斷點(diǎn)。當(dāng)執(zhí)行上面那條import語(yǔ)句時(shí)就會(huì)走到斷點(diǎn)中,我們進(jìn)到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ù)的實(shí)現(xiàn)我們看著就很熟悉了,和前面處理templatescript一樣都是調(diào)用的vue/compiler-sfc包暴露出來的compileStyleAsync函數(shù),這也是一個(gè)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)注幾個(gè)字段,source字段為style標(biāo)簽中的css原始代碼。scoped字段為style標(biāo)簽中是否有scoped attribute。id字段為我們?cè)谟^察 DOM 結(jié)構(gòu)時(shí)看到的 data-v-xxxxx。這個(gè)是debug時(shí)入?yún)⒔貓D:

再來看看返回值SFCStyleCompileResults對(duì)象,主要就是code屬性,這個(gè)是經(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>
}

這個(gè)是debug時(shí)compileStyleAsync函數(shù)返回值的截圖:

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

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

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

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í)行主流程,其實(shí)就是對(duì)應(yīng)了一個(gè)vue文件編譯成js文件的流程。

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

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

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

然后以descriptor對(duì)象為參數(shù)調(diào)用genStyleCode函數(shù),將vue文件中的 <style> 模塊代碼編譯成了import語(yǔ)句code字符串,比如:import "/src/App.vue?vue&type=style&index=0&scoped=7a7a37b1&lang.css";,賦值給stylesCode變量。

然后將scriptCode、templateCodestylesCode使用換行符\n拼接起來得到resolvedCode,這個(gè)resolvedCode就是一個(gè)vue文件編譯成js文件的代碼code字符串。這個(gè)是debug時(shí)resolvedCode變量值的截圖:

總結(jié)

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

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

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

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

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

genTemplateCode函數(shù)為底層調(diào)用vue/compiler-sfccompileTemplate函數(shù),根據(jù)第一步的descriptor對(duì)象將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語(yǔ)句。

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

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

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

相關(guān)文章

最新評(píng)論