給我5分鐘,保證教會你在vue3中動態載入遠端元件

前端欧阳發表於2024-08-07

前言

在一些特殊的場景中(比如低程式碼、減少小程式包體積、類似於APP的熱更新),我們需要從服務端動態載入.vue檔案,然後將動態載入的遠端vue元件渲染到我們的專案中。今天這篇文章我將帶你學會,在vue3中如何去動態載入遠端元件。

歐陽寫了一本開源電子書vue3編譯原理揭秘,這本書初中級前端能看懂。完全免費,只求一個star。

defineAsyncComponent非同步元件

想必聰明的你第一時間就想到了defineAsyncComponent方法。我們先來看看官方對defineAsyncComponent方法的解釋:

定義一個非同步元件,它在執行時是懶載入的。引數可以是一個非同步載入函式,或是對載入行為進行更具體定製的一個選項物件。

defineAsyncComponent方法的返回值是一個非同步元件,我們可以像普通元件一樣直接在template中使用。和普通元件的區別是,只有當渲染到非同步元件時才會呼叫載入內部實際元件的函式。

我們先來簡單看看使用defineAsyncComponent方法的例子,程式碼如下:

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() => {
  return new Promise((resolve, reject) => {
    // ...從伺服器獲取元件
    resolve(/* 獲取到的元件 */)
  })
})
// ... 像使用其他一般元件一樣使用 `AsyncComp`

defineAsyncComponent方法接收一個返回 Promise 的回撥函式,在Promise中我們可以從服務端獲取vue元件的code程式碼字串。然後使用resolve(/* 獲取到的元件 */)將拿到的元件傳給defineAsyncComponent方法內部處理,最後和普通元件一樣在template中使用AsyncComp元件。

從服務端獲取遠端元件

有了defineAsyncComponent方法後事情從表面上看著就很簡單了,我們只需要寫個方法從服務端拿到vue檔案的code程式碼字串,然後在defineAsyncComponent方法中使用resolve拿到的vue元件。

第一步就是本地起一個伺服器,使用伺服器返回我們的vue元件。這裡我使用的是http-server,安裝也很簡單:

npm install http-server -g

使用上面的命令就可以全域性安裝一個http伺服器了。

接著我在專案的public目錄下新建一個名為remote-component.vue的檔案,這個vue檔案就是我們想從服務端載入的遠端元件。remote-component.vue檔案中的程式碼如下:

<template>
  <p>我是遠端元件</p>
  <p>
    當前遠端元件count值為:<span class="count">{{ count }}</span>
  </p>
  <button @click="count++">點選增加遠端元件count</button>
</template>

<script setup>
import { ref } from "vue";
const count = ref(0);
</script>

<style>
.count {
  color: red;
}
</style>

從上面的程式碼可以看到遠端vue元件和我們平時寫的vue程式碼沒什麼區別,有templateref響應式變數、style樣式。

接著就是在終端執行http-server ./public --cors命令啟動一個本地伺服器,伺服器預設埠為8080。但是由於我們本地起的vite專案預設埠為5173,所以為了避免跨域這裡需要加--cors ./public的意思是指定當前目錄的public資料夾。

啟動了一個本地伺服器後,我們就可以使用 http://localhost:8080/remote-component.vue連結從服務端訪問遠端元件啦,如下圖:
remote-component

從上圖中可以看到在瀏覽器中訪問這個連結時觸發了下載遠端vue元件的操作。

defineAsyncComponent載入遠端元件

const RemoteChild = defineAsyncComponent(async () => {
  return new Promise(async (resolve) => {
    const res = await fetch("http://localhost:8080/remote-component.vue");
    const code = await res.text();
    console.log("code", code);
    resolve(code);
  });
});

接下來我們就是在defineAsyncComponent方法接收的 Promise 的回撥函式中使用fetch從服務端拿到遠端元件的code程式碼字串應該就行啦,程式碼如下:

同時使用console.log("code", code)打個日誌看一下從服務端過來的vue程式碼。

上面的程式碼看著已經完美實現動態載入遠端元件了,結果不出意外在瀏覽器中執行時報錯了。如下圖:
error

在上圖中可以看到從服務端拿到的遠端元件的程式碼和我們的remote-component.vue的原始碼是一樣的,但是為什麼會報錯呢?

這裡的報錯資訊顯示載入非同步元件報錯,還記得我們前面說過的defineAsyncComponent方法是在回撥中resolve(/* 獲取到的元件 */)。而我們這裡拿到的code是一個元件嗎?

我們這裡拿到的code只是元件的原始碼,也就是常見的單檔案元件SFC。而defineAsyncComponent中需要的是由原始碼編譯後拿的的vue元件物件,我們將元件原始碼丟給defineAsyncComponent當然會報錯了。

看到這裡有的小夥伴有疑問了,我們平時在父元件中import子元件不是也一樣在template就直接使用了嗎?

子元件local-child.vue程式碼:

<template>
  <p>我是本地元件</p>
  <p>
    當前本地元件count值為:<span class="count">{{ count }}</span>
  </p>
  <button @click="count++">點選增加本地元件count</button>
</template>

<script setup>
import { ref } from "vue";
const count = ref(0);
</script>

<style>
.count {
  color: red;
}
</style>

父元件程式碼:

<template>
  <LocalChild />
</template>

<script setup lang="ts">
import LocalChild from "./local-child.vue";
console.log("LocalChild", LocalChild);
</script>

上面的import匯入子元件的程式碼寫了這麼多年你不覺得怪怪的嗎?

按照常理來說要import匯入子元件,那麼在子元件裡面肯定要寫export才可以,但是在子元件local-child.vue中我們沒有寫任何關於export的程式碼。

答案是在父元件import匯入子元件觸發了vue-loader或者@vitejs/plugin-vue外掛的鉤子函式,在鉤子函式中會將我們的原始碼單檔案元件SFC編譯成一個普通的js檔案,在js檔案中export default匯出編譯後的vue元件物件。

這裡使用console.log("LocalChild", LocalChild)來看看經過編譯後的vue元件物件是什麼樣的,如下圖:
import-comp

從上圖可以看到經過編譯後的vue元件是一個物件,物件中有rendersetup等方法。defineAsyncComponent方法接收的元件就是這樣的vue元件物件,但是我們前面卻是將vue元件原始碼丟給他,當然會報錯了。

最終解決方案vue3-sfc-loader

從服務端拿到遠端vue元件原始碼後,我們需要一個工具將拿到的vue元件原始碼編譯成vue元件物件。幸運的是優秀的vue不光暴露出一些常見的API,而且還將一些底層API給暴露了出來。比如在@vue/compiler-sfc包中就暴露出來了compileTemplatecompileScriptcompileStyleAsync等方法。

如果你看過我寫的 vue3編譯原理揭秘 開源電子書,你應該對這幾個方法覺得很熟悉。

  • compileTemplate方法:用於處理單檔案元件SFC中的template模組。

  • compileScript方法:用於處理單檔案元件SFC中的script模組。

  • compileStyleAsync方法:用於處理單檔案元件SFC中的style模組。

vue3-sfc-loader包的核心程式碼就是呼叫@vue/compiler-sfc包的這些方法,將我們的vue元件原始碼編譯為想要的vue元件物件。
下面這個是改為使用vue3-sfc-loader包後的程式碼,如下:

import * as Vue from "vue";
import { loadModule } from "vue3-sfc-loader";

const options = {
  moduleCache: {
    vue: Vue,
  },
  async getFile(url) {
    const res = await fetch(url);
    const code = await res.text();
    return code;
  },
  addStyle(textContent) {
    const style = Object.assign(document.createElement("style"), {
      textContent,
    });
    const ref = document.head.getElementsByTagName("style")[0] || null;
    document.head.insertBefore(style, ref);
  },
};

const RemoteChild = defineAsyncComponent(async () => {
  const res = await loadModule(
    "http://localhost:8080/remote-component.vue",
    options
  );
  console.log("res", res);
  return res;
});

loadModule函式接收的第一個引數為遠端元件的URL,第二個引數為options。在options中有個getFile方法,獲取遠端元件的code程式碼字串就是在這裡去實現的。

我們在終端來看看經過loadModule函式處理後拿到的vue元件物件是什麼樣的,如下圖:
compiler-remote-comp

從上圖中可以看到經過loadModule函式的處理後就拿到來vue元件物件啦,並且這個元件物件上面也有熟悉的render函式和setup函式。其中render函式是由遠端元件的template模組編譯而來的,setup函式是由遠端元件的script模組編譯而來的。

看到這裡你可能有疑問,遠端元件的style模組怎麼沒有在生成的vue元件物件上面有提現呢?

答案是style模組編譯成的css不會塞到vue元件物件上面去,而是單獨透過options上面的addStyle方法傳回給我們了。addStyle方法接收的引數textContent的值就是style模組編譯而來css字串,在addStyle方法中我們是建立了一個style標籤,然後將得到的css字串插入到頁面中。

完整父元件程式碼如下:

<template>
  <LocalChild />
  <div class="divider" />
  <button @click="showRemoteChild = true">載入遠端元件</button>
  <RemoteChild v-if="showRemoteChild" />
</template>

<script setup lang="ts">
import { defineAsyncComponent, ref, onMounted } from "vue";
import * as Vue from "vue";
import { loadModule } from "vue3-sfc-loader";
import LocalChild from "./local-child.vue";

const showRemoteChild = ref(false);

const options = {
  moduleCache: {
    vue: Vue,
  },
  async getFile(url) {
    const res = await fetch(url);
    const code = await res.text();
    return code;
  },
  addStyle(textContent) {
    const style = Object.assign(document.createElement("style"), {
      textContent,
    });
    const ref = document.head.getElementsByTagName("style")[0] || null;
    document.head.insertBefore(style, ref);
  },
};

const RemoteChild = defineAsyncComponent(async () => {
  const res = await loadModule(
    "http://localhost:8080/remote-component.vue",
    options
  );
  console.log("res", res);
  return res;
});
</script>

<style scoped>
.divider {
  background-color: red;
  width: 100vw;
  height: 1px;
  margin: 20px 0;
}
</style>

在上面的完整例子中,首先渲染了本地元件LocalChild。然後當點選“載入遠端元件”按鈕後再去渲染遠端元件RemoteChild。我們來看看執行效果,如下圖:
full

從上面的gif圖中可以看到,當我們點選“載入遠端元件”按鈕後,在network中才去載入了遠端元件remote-component.vue。並且將遠端元件渲染到了頁面上後,透過按鈕的點選事件可以看到遠端元件的響應式依然有效。

vue3-sfc-loader同時也支援在遠端元件中去引用子元件,你只需在options額外配置一個pathResolve就行啦。pathResolve方法配置如下:

const options = {
  pathResolve({ refPath, relPath }, options) {
    if (relPath === ".")
      // self
      return refPath;

    // relPath is a module name ?
    if (relPath[0] !== "." && relPath[0] !== "/") return relPath;

    return String(
      new URL(relPath, refPath === undefined ? window.location : refPath)
    );
  },
  // getFile方法
  // addStyle方法
}

其實vue3-sfc-loader包的核心程式碼就300行左右,主要就是呼叫vue暴露出來的一些底層API。如下圖:
vue3-sfc-loader

總結

這篇文章講了在vue3中如何從服務端載入遠端元件,首先我們需要使用defineAsyncComponent方法定義一個非同步元件,這個非同步元件是可以直接在template中像普通元件一樣使用。

但是由於defineAsyncComponent接收的元件必須是編譯後的vue元件物件,而我們從服務端拿到的遠端元件就是一個普通的vue檔案,所以這時我們引入了vue3-sfc-loader包。vue3-sfc-loader包的作用就是在執行時將一個vue檔案編譯成vue元件物件,這樣我們就可以實現從服務端載入遠端元件了。

關注公眾號:【前端歐陽】,給自己一個進階vue的機會

另外歐陽寫了一本開源電子書vue3編譯原理揭秘,這本書初中級前端能看懂。完全免費,只求一個star。

相關文章