前言 在一些特殊的場景中(比如低代碼、減少小程式包體積、類似於APP的熱更新),我們需要從服務端動態載入.vue文件,然後將動態載入的遠程vue組件渲染到我們的項目中。今天這篇文章我將帶你學會,在vue3中如何去動態載入遠程組件。 歐陽寫了一本開源電子書vue3編譯原理揭秘,這本書初中級前端能看懂。 ...
前言
在一些特殊的場景中(比如低代碼、減少小程式包體積、類似於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代碼沒什麼區別,有template
、ref
響應式變數、style
樣式。
接著就是在終端執行http-server ./public --cors
命令啟動一個本地伺服器,伺服器預設埠為8080
。但是由於我們本地起的vite項目預設埠為5173
,所以為了避免跨域這裡需要加--cors
。 ./public
的意思是指定當前目錄的public
文件夾。
啟動了一個本地伺服器後,我們就可以使用 http://localhost:8080/remote-component.vue鏈接從服務端訪問遠程組件啦,如下圖:
從上圖中可以看到在瀏覽器中訪問這個鏈接時觸發了下載遠程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代碼。
上面的代碼看著已經完美實現動態載入遠程組件了,結果不出意外在瀏覽器中運行時報錯了。如下圖:
在上圖中可以看到從服務端拿到的遠程組件的代碼和我們的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組件對象是什麼樣的,如下圖:
從上圖可以看到經過編譯後的vue組件是一個對象,對象中有render
、setup
等方法。defineAsyncComponent方法接收的組件就是這樣的vue組件對象,但是我們前面卻是將vue組件源碼丟給他,當然會報錯了。
最終解決方案vue3-sfc-loader
從服務端拿到遠程vue組件源碼後,我們需要一個工具將拿到的vue組件源碼編譯成vue組件對象。幸運的是優秀的vue不光暴露出一些常見的API,而且還將一些底層API給暴露了出來。比如在@vue/compiler-sfc
包中就暴露出來了compileTemplate
、compileScript
、compileStyleAsync
等方法。
如果你看過我寫的 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組件對象是什麼樣的,如下圖:
從上圖中可以看到經過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
。我們來看看執行效果,如下圖:
從上面的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中如何從服務端載入遠程組件,首先我們需要使用defineAsyncComponent
方法定義一個非同步組件,這個非同步組件是可以直接在template中像普通組件一樣使用。
但是由於defineAsyncComponent
接收的組件必須是編譯後的vue組件對象,而我們從服務端拿到的遠程組件就是一個普通的vue文件,所以這時我們引入了vue3-sfc-loader
包。vue3-sfc-loader
包的作用就是在運行時將一個vue文件編譯成vue組件對象,這樣我們就可以實現從服務端載入遠程組件了。
關註公眾號:【前端歐陽】,給自己一個進階vue的機會
另外歐陽寫了一本開源電子書vue3編譯原理揭秘,這本書初中級前端能看懂。完全免費,只求一個star。