遠程組件實踐
點擊上方 程序員成長指北,關注公眾號
回復1,加入高級Node交流群
前言
從服務端遠程下載一個 JS 文件并注冊成組件。
一、什么是遠程組件
這里是指在生產(chǎn)環(huán)境中,從服務端遠程下載一個 JS 文件并注冊成組件,使其在生產(chǎn)環(huán)境中能夠使用。
二、背景
1. 項目背景
我們的項目是個低代碼平臺,它內(nèi)置了一些常用組件,可供用戶使用。但內(nèi)置組件不能夠完全滿足用戶的需求,我們希望能夠提供一個入口,用戶自己上傳自定義組件。這樣可以極大的增加項目的可拓展性。
低代碼平臺
需求流程
這也是遠程組件的一個典型場景。
【第2837期】低代碼在數(shù)據(jù)分析場景的應用
2. 技術背景
項目使用的技術棧為 vue2。我們限定自定義組件開發(fā)的技術棧也是 vue2。
三、技術實現(xiàn)
1. 流程步驟

幾個關鍵步驟
用戶按照 UMD 模塊規(guī)范開發(fā)組件
注冊組件
獲取到組件模塊
渲染組件
響應用戶的操作
2. 什么是 UMD 模塊規(guī)范呢?
所謂 UMD (Universal Module Definition),就是一種 javascript 通用模塊定義規(guī)范,讓你的模塊能在 javascript 所有運行環(huán)境中發(fā)揮作用。
簡言之就是能兼容主流 javascript 模塊的規(guī)范,如 CommonJS, AMD, CMD 等。
下面是規(guī)范的代碼,以及對應的說明:
(function(root, factory) {
if (typeof module === 'object' && typeof module.exports === 'object') {
// 這是 commonjs 模塊規(guī)范,nodejs 環(huán)境
var depModule = require('./umd-module-depended')
module.exports = factory(depModule);
} else if (typeof define === 'function' && define.amd) {
// 這是 AMD 模塊規(guī)范,如 require.js
define(['depModule'], factory)
} else if (typeof define === 'function' && define.cmd) {
// 這是 CMD 模塊規(guī)范,如sea.js
define(function(require, exports, module) {
var depModule = require('depModule')
module.exports = factory(depModule)
})
} else {
// 沒有模塊環(huán)境,直接掛載在全局對象上
root.umdModule = factory(root.depModule);
}
}(this, function(depModule) {
// depModule 是依賴模塊
return {
name: '我自己是一個umd模塊'
}
}))
如果在 html 中直接使用 script 標簽引用 umd 格式的 js 文件。就會走到第四個條件分支,即 直接掛載在全局對象上 。這個全局對象指的就是 window。
在我們的項目中,是直接使用 script 標簽引用的。但沒有走第四個條件分支。之后會說明原因及做法。
3. 如何打包 UMD 規(guī)范的組件文件
以 Vue CLI 為例。當運行 vue-cli-service build 時,可以通過 --target 選項指定構(gòu)建目標為 庫 。

上圖中 庫 的名字為 myLib 。[entry] 為需要構(gòu)建的入口文件。構(gòu)建一個庫會輸出一些文件,需要我們關注的是下面兩個:
dist/myLib.umd.js:一個直接給瀏覽器或 AMD loader 使用的 UMD 包dist/myLib.umd.min.js:壓縮后的 UMD 構(gòu)建版本
可見,使用 Vue CLI 打包 UMD 規(guī)范文件是十分方便的。
在我們的項目中,打包命令為:
vue-cli-service build --target lib --name Demo ./index.js
我們的 庫 名是 Demo 。
./index.js 文件的內(nèi)容為:
import Demo from './packages/demo/index.vue'
export default {
version: '1.0.0',
Demo
}
./packages/demo/index.vue 文件的內(nèi)容為:
<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 對象。主項目無論使用什么輸出格式,都需要將自己系統(tǒng)內(nèi)的 Vue 對象暴露到 window 上。
所以,在項目中我們需要將 Vue 暴露到 window 上。需要在 main.js 文件添加代碼:
window.Vue = Vue
當這個腳本被引入網(wǎng)頁時,你的組件就可以以普通 DOM 元素的方式被使用了。
<script src="demo.umd.js"></script>
<!-- 可在普通 HTML 中或者其它任何框架中使用 -->
<demo></demo>
5. 獲取組件模塊
那么,想要使用自定義組件的話,必須要知道 Vue CLI 打包后自動注冊的標簽名。
事實上,標簽名就是 ./packages/demo/index.vue 文件的 name 值,即 demo 。
在 3. 如何打包 UMD 規(guī)范的組件文件 中,我們的打包入口是 ./index.js 。它暴露出了 './packages/demo/index.vue' 。那么拿到 ./index.js 就拿到了標簽名。
當你使用一個 .js 文件作為入口時,它可能會包含具名導出,所以庫會暴露為一個模塊。也就是說你的庫必須在 UMD 構(gòu)建中通過
window.yourLib.default訪問。
也就是我們可以通過 window.Demo.default 拿到 ./index.js 。
又有問題了,這里我們又必須知道它的庫名 Demo 才行。
怎么辦呢?
我們回到上文中的 UMD 模塊規(guī)范的代碼觀察。commonjs 模塊規(guī)范使用了 module.exports ,它是可以將模塊直接暴露出來的,而不是掛載在 window 上。
那我們就模擬下 node 環(huán)境,這樣不需要知道 庫 名,就能拿到模塊。
// 模擬 node 環(huán)境
window.module = {}
window.exports = {}
// 模擬 node 環(huán)境獲取模塊
const module = window.module.exports
這樣就不必像官網(wǎng)那樣具名訪問模塊了。
// 官網(wǎng)獲取掛載的模塊
const module = window.Demo.default
6. 渲染組件
拿到了組件模塊,下一步就是將它渲染出來。項目里我們使用 動態(tài)組件 + 異步組件 + 渲染函數(shù) 的組合來完成。下面分別回顧一下這幾個知識點,然后將他們相結(jié)合。
6.1 動態(tài)組件
主要用于將已知的組件進行切換。
不適用未知的組件。典型場景是在不同組件之間進行動態(tài)切換,比如在一個多標簽的界面里:

<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 官網(wǎng)是這樣描述的:

Vue 2.3.0+ 新增如下書寫方式:
const AsyncComponent = () => ({
// 需要加載的組件 (應該是一個 `Promise` 對象)
component: import('./MyComponent.vue'),
// 異步組件加載時使用的組件
loading: LoadingComponent,
// 加載失敗時使用的組件
error: ErrorComponent,
// 展示加載時組件的延時時間。默認值是 200 (毫秒)
delay: 200,
// 如果提供了超時時間且組件加載也超時了,
// 則使用加載失敗時使用的組件。默認值是:`Infinity`
timeout: 3000
})
我們在項目中使用的是新增的這種方式。
6.3 動態(tài)組件 + 異步組件
下面是項目中將兩種組件結(jié)合的代碼:
<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 方式獲取到的組件模塊,里面包括:組件的標簽、組件的可配置數(shù)據(jù)等。
componentFile 是需要異步加載的組件。
6.4 渲染函數(shù)
Vue 推薦在絕大多數(shù)情況下使用模板來創(chuàng)建你的 HTML。然而在一些場景中,你真的需要 JavaScript 的完全編程的能力。這時你可以用渲染函數(shù),它比模板更接近編譯器。
項目中的 anonymous.vue 文件就非使用 渲染函數(shù) 不可。畢竟,我們都不知道標簽的名字是什么。
它的代碼如下:
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, [])
}
}
以上就是渲染遠程組件的具體步驟。下面簡單梳理一下遠程組件的數(shù)據(jù)是如何響應的。
7. 遠程組件數(shù)據(jù)的響應
想要數(shù)據(jù)獲得響應,需要給組件開發(fā)者和接入者約定好規(guī)范。
7.1 組件開發(fā)者規(guī)范
在用 Vue CLI 打包的入口文件 ./index.js 中暴露的 Demo (./packages/demo/index.vue)組件中,我們將組件需要響應的屬性以及默認值以 getDefaultConfig() 的形式導出。
getDefaultConfig() {
return {
defaultProperties: [
{
title: "邊長",
name: "width",
type: "SingleInput",
value: 200
}
]
}
}
同時,我們還需要監(jiān)聽這個傳進來的屬性值,以便在圖表上做出相應的變化。
props: {
config: {
type: Object,
}
}
watch: {
config: {
handler: function (_, newConfig) {
this.width = newConfig.width
},
deep: true
}
}
7.2 組件接入者規(guī)范
在 5. 獲取組件模塊 的時候,我們可以通過 module.getDefaultConfig() 獲取到需要響應的屬性以及默認值,通過 name 獲取到標簽名。
在 6.4 渲染函數(shù) 步驟中,將屬性的默認值、標簽名、props (也就是 config) 傳給渲染函數(shù)。就可以完成數(shù)據(jù)的響應 了。
render(h) {
// 這里獲得了標簽名
const tagName = this.model.tagName
const param = {
// 這里傳入屬性
"props": {
config: this.model.config
}
}
return h(tagName, param, [])
}
四、待改進的地方
js、css 不隔離,沒有沙箱能力
限定技術棧(項目限定 vue2 ), 對開發(fā)者不友好
如果組件標簽相同,會被覆蓋
五、對未來優(yōu)化方向的調(diào)研
方案一:微前端
微前端借鑒了微服務的架構(gòu)理念,核心在于將一個龐大的前端應用拆分成多個獨立靈活的小型應用,每個應用都可以獨立開發(fā)、獨立運行、獨立部署,再將這些小型應用融合為一個完整的應用。

主流框架有:single-spa 和 qiankun
主要應用場景
1、跨技術棧重構(gòu)項目時。
2、跨團隊或跨部門協(xié)作開發(fā)項目時。
微前端拆分的顆粒度為應用。
【第2695期】基于微前端qiankun的多頁簽緩存方案實踐
結(jié)合項目的場景,這個方案不是很吻合。既不能很好的解決問題,也沒有發(fā)揮微前端的真正能力。
方案二:微組件
這里我們把一些基于 Web Components 的輕量級的微前端框架,稱為微組件。
框架有:micro-app、magic-microservices 等。
這種解決方案更適合當前的場景。它可以解決 js、css 不隔離的問題,并且不再限定組件開發(fā)者的技術棧。
對于組件數(shù)據(jù)的響應與通訊,則需要進一步的調(diào)研和實踐。
這里有一個微組件實踐可供參考。
方案三:引入 IDE
以上兩個方案可以解決前兩個問題,但如果要解決三個問題的話就需要引入 IDE 。
這個可能是終極方案,可以極大優(yōu)化開發(fā)者體驗,對應的成本也是最高的。這里就不做贅述了。
Node 社群
我組建了一個氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對Node.js學習感興趣的話(后續(xù)有計劃也可以),我們可以一起進行Node.js相關的交流、學習、共建。下方加 考拉 好友回復「Node」即可。
“分享、點贊、在看” 支持一波
