遠程組件實踐
本文作者爲 360 奇舞團前端開發工程師
一、什麼是遠程組件
這裏是指在生產環境中,從服務端遠程下載一個 JS 文件並註冊成組件,使其在生產環境中能夠使用。
二、背景
1. 項目背景
我們的項目是個低代碼平臺,它內置了一些常用組件,可供用戶使用。但內置組件不能夠完全滿足用戶的需求,我們希望能夠提供一個入口,用戶自己上傳自定義組件。這樣可以極大的增加項目的可拓展性。
這也是遠程組件的一個典型場景。
2. 技術背景
項目使用的技術棧爲 vue2。我們限定自定義組件開發的技術棧也是 vue2。
三、技術實現
1. 流程步驟
幾個關鍵步驟
-
用戶按照 UMD 模塊規範開發組件
-
註冊組件
-
獲取到組件模塊
-
渲染組件
-
響應用戶的操作
2. 什麼是 UMD 模塊規範呢?
所謂 UMD[1] (Universal Module Definition),就是一種 javascript 通用模塊定義規範,讓你的模塊能在 javascript 所有運行環境中發揮作用。
簡言之就是能兼容主流 javascript 模塊的規範,如 CommonJS, AMD, CMD 等。
下面是規範的代碼,以及對應的說明:
(function(root, factory) {
if (typeof module === 'object' && typeof module.exports === 'object') {
// 這是 commonjs 模塊規範,nodejs 環境
var depModule = require('./umd-module-depended')
module.exports = factory(depModule);
} else if (typeof define === 'function' && define.amd) {
// 這是 AMD 模塊規範,如 require.js
define(['depModule'], factory)
} else if (typeof define === 'function' && define.cmd) {
// 這是 CMD 模塊規範,如sea.js
define(function(require, exports, module) {
var depModule = require('depModule')
module.exports = factory(depModule)
})
} else {
// 沒有模塊環境,直接掛載在全局對象上
root.umdModule = factory(root.depModule);
}
}(this, function(depModule) {
// depModule 是依賴模塊
return {
name: '我自己是一個umd模塊'
}
}))
如果在 html 中直接使用 script 標籤引用 umd 格式的 js 文件。就會走到第四個條件分支,即 直接掛載在全局對象上
。這個全局對象指的就是 window。
在我們的項目中,是直接使用 script 標籤引用的。但沒有走第四個條件分支。之後會說明原因及做法。
3. 如何打包 UMD 規範的組件文件
以 Vue CLI 爲例。當運行 vue-cli-service build 時,可以通過 --target 選項指定構建目標爲 庫
。
上圖中 庫
的名字爲 myLib
。[entry]
爲需要構建的入口文件。構建一個庫會輸出一些文件,需要我們關注的是下面兩個:
-
dist/myLib.umd.js:一個直接給瀏覽器或 AMD loader 使用的 UMD 包
-
dist/myLib.umd.min.js:壓縮後的 UMD 構建版本
可見,使用 Vue CLI 打包 UMD 規範文件是十分方便的。
在我們的項目中,打包命令爲:
vue-cli-service build --target lib --name Demo ./index.js
我們的 庫
名是 Demo
。
./index.js 文件的內容爲:
import Demo from './packages/demo/index.vue'
export default {
version: '1.0.0',
Demo
}
./packages/demo/index.vue 文件的內容爲:
<template>
<div :style="`width: ${config.width}px;`"></div>
</template>
<script>
export default {
name: "demo",
title: "demo演示組件",
props: {
config: {
type: Object,
}
},
watch: {
config: {
handler: function (_, newConfig) {
this.width = newConfig.width
},
deep: true
}
},
mounted() {
this.width = this.config.width
},
getDefaultConfig() {
return {
defaultProperties: [
{
title: "邊長",
name: "width",
type: "SingleInput",
value: 200
}
]
}
}
}
</script>
4. 組件的註冊
當使用 Vue CLI 的庫模式打包時,我們暴露出的 Demo 是個 *.vue
文件。這裏是註冊的關鍵。Vue CLI 將會把這個組件自動包裹並註冊爲 Web Components 組件,無需在 main.js 裏自行註冊。需要注意的是,這個包依賴了在頁面上全局可用的 Vue。
在 Vue CLI 的庫模式中,Vue 是外置的。這意味着包中不會有 Vue。輸出代碼裏會使用一個全局的 Vue 對象。主項目無論使用什麼輸出格式,都需要將自己系統內的 Vue 對象暴露到 window 上。
所以,在項目中我們需要將 Vue 暴露到 window 上。需要在 main.js
文件添加代碼:
window.Vue = Vue
當這個腳本被引入網頁時,你的組件就可以以普通 DOM 元素的方式被使用了。
<script src="demo.umd.js"></script>
<!-- 可在普通 HTML 中或者其它任何框架中使用 -->
<demo></demo>
5. 獲取組件模塊
那麼,想要使用自定義組件的話,必須要知道 Vue CLI 打包後自動註冊的標籤名。
事實上,標籤名就是 ./packages/demo/index.vue 文件的 name 值,即 demo
。
在 3. 如何打包 UMD 規範的組件文件
中,我們的打包入口是 ./index.js 。它暴露出了 './packages/demo/index.vue' 。那麼拿到 ./index.js 就拿到了標籤名。
當你使用一個 .js 文件作爲入口時,它可能會包含具名導出,所以庫會暴露爲一個模塊。也就是說你的庫必須在 UMD 構建中通過 window.yourLib.default 訪問。
也就是我們可以通過 window.Demo.default 拿到 ./index.js 。
又有問題了,這裏我們又必須知道它的庫名 Demo
纔行。
怎麼辦呢?
我們回到上文中的 UMD 模塊規範的代碼觀察。commonjs 模塊規範使用了 module.exports
,它是可以將模塊直接暴露出來的,而不是掛載在 window 上。
那我們就模擬下 node 環境,這樣不需要知道 庫
名,就能拿到模塊。
// 模擬 node 環境
window.module = {}
window.exports = {}
// 模擬 node 環境獲取模塊
const module = window.module.exports
這樣就不必像官網那樣具名訪問模塊了。
// 官網獲取掛載的模塊
const module = window.Demo.default
6. 渲染組件
拿到了組件模塊,下一步就是將它渲染出來。項目裏我們使用 動態組件 + 異步組件 + 渲染函數
的組合來完成。下面分別回顧一下這幾個知識點,然後將他們相結合。
6.1 動態組件
主要用於將已知的組件進行切換。
不適用未知的組件。典型場景是在不同組件之間進行動態切換,比如在一個多標籤的界面裏:
<template>
<component v-bind:is="currentTabComponent"></component>
</template>
<script>
import Home from '../components/Home'
import Posts from '../components/Posts'
import Archive from '../components/Archive'
export default {
components: {
Home,
Posts,
Archive
},
data () {
return {
currentTabComponent
}
}
}
</script>
6.2 異步組件
vue2 官網是這樣描述的:
Vue 2.3.0+ 新增如下書寫方式:
const AsyncComponent = () => ({
// 需要加載的組件 (應該是一個 `Promise` 對象)
component: import('./MyComponent.vue'),
// 異步組件加載時使用的組件
loading: LoadingComponent,
// 加載失敗時使用的組件
error: ErrorComponent,
// 展示加載時組件的延時時間。默認值是 200 (毫秒)
delay: 200,
// 如果提供了超時時間且組件加載也超時了,
// 則使用加載失敗時使用的組件。默認值是:`Infinity`
timeout: 3000
})
我們在項目中使用的是新增的這種方式。
6.3 動態組件 + 異步組件
下面是項目中將兩種組件結合的代碼:
<template>
<component v-bind:is="componentFile" :model="model"></component>
</template>
<script>
export default defineComponent({
name: 'AsyncComponent',
props: {
model: {
type: Object,
default: () => {}
}
},
setup() {
const AsyncComponent = () => ({
component: import('./anonymous.vue'),
delay: 200,
timeout: 3000
})
return {
componentFile: AsyncComponent,
}
},
})
</script>
-
model
是 umd 方式獲取到的組件模塊,裏面包括:組件的標籤、組件的可配置數據等。 -
componentFile
是需要異步加載的組件。
6.4 渲染函數
Vue 推薦在絕大多數情況下使用模板來創建你的 HTML。然而在一些場景中,你真的需要 JavaScript 的完全編程的能力。這時你可以用渲染函數,它比模板更接近編譯器。
項目中的 anonymous.vue
文件就非使用 渲染函數
不可。畢竟,我們都不知道標籤的名字是什麼。
它的代碼如下:
export default {
name: 'Anonymous',
props: {
model: {
type: Object,
default: () => {}
}
},
render(h) {
const tagName = this.model.tagName
const param = {
"props": {
config: this.model.config
}
}
return h(tagName, param, [])
}
}
以上就是渲染遠程組件的具體步驟。下面簡單梳理一下遠程組件的數據是如何響應的。
7. 遠程組件數據的響應
想要數據獲得響應,需要給組件開發者和接入者約定好規範。
7.1 組件開發者規範
在用 Vue CLI 打包的入口文件 ./index.js 中暴露的 Demo (./packages/demo/index.vue)組件中,我們將組件需要響應的屬性以及默認值以 getDefaultConfig()
的形式導出。
getDefaultConfig() {
return {
defaultProperties: [
{
title: "邊長",
name: "width",
type: "SingleInput",
value: 200
}
]
}
}
同時,我們還需要監聽這個傳進來的屬性值,以便在圖表上做出相應的變化。
props: {
config: {
type: Object,
}
}
watch: {
config: {
handler: function (_, newConfig) {
this.width = newConfig.width
},
deep: true
}
}
7.2 組件接入者規範
在 5. 獲取組件模塊
的時候,我們可以通過 module.getDefaultConfig() 獲取到需要響應的屬性以及默認值,通過 name
獲取到標籤名。
在 6.4 渲染函數
步驟中,將屬性的默認值、標籤名、props
(也就是 config) 傳給渲染函數。就可以完成數據的響應 了。
render(h) {
// 這裏獲得了標籤名
const tagName = this.model.tagName
const param = {
// 這裏傳入屬性
"props": {
config: this.model.config
}
}
return h(tagName, param, [])
}
四、待改進的地方
-
js、css 不隔離,沒有沙箱能力
-
限定技術棧(項目限定 vue2 ), 對開發者不友好
-
如果組件標籤相同,會被覆蓋
五、對未來優化方向的調研
方案一:微前端
微前端 [2] 借鑑了微服務的架構理念,核心在於將一個龐大的前端應用拆分成多個獨立靈活的小型應用
,每個應用都可以獨立開發、獨立運行、獨立部署,再將這些小型應用融合爲一個完整的應用。
主流框架有:single-spa 和 qiankun
主要應用場景
1、跨技術棧重構項目時。
2、跨團隊或跨部門協作開發項目時。
微前端拆分的顆粒度爲應用。
結合項目的場景,這個方案不是很吻合。既不能很好的解決問題,也沒有發揮微前端的真正能力。
方案二:微組件
這裏我們把一些基於 Web Components 的輕量級的微前端框架,稱爲微組件。
框架有:micro-app、magic-microservices 等。
這種解決方案更適合當前的場景。它可以解決 js、css 不隔離的問題,並且不再限定組件開發者的技術棧。
對於組件數據的響應與通訊,則需要進一步的調研和實踐。
這裏有一個微組件實踐 [3] 可供參考。
方案三:引入 IDE
以上兩個方案可以解決前兩個問題,但如果要解決三個問題的話就需要引入 IDE 。
這個可能是終極方案,可以極大優化開發者體驗,對應的成本也是最高的。這裏就不做贅述了。
謝謝您的閱讀。
參考資料
[1]
可能是最詳細的 UMD 模塊入門指南: https://juejin.cn/post/6844903927104667662
[2]
引用自 MicroApp 對於微前端的說明: https://zeroing.jd.com/docs.html#/
[3]
微組件實踐: https://juejin.cn/post/7086790887111393293
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/qTR1XcJE6QH058C-inlZFg