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

幾個(gè)關(guān)鍵步驟
用戶按照 UMD 模塊規(guī)范開(kāi)發(fā)組件
注冊(cè)組件
獲取到組件模塊
渲染組件
響應(yīng)用戶的操作
2. 什么是 UMD 模塊規(guī)范呢?
所謂 UMD (Universal Module Definition),就是一種 javascript 通用模塊定義規(guī)范,讓你的模塊能在 javascript 所有運(yùn)行環(huán)境中發(fā)揮作用。
簡(jiǎn)言之就是能兼容主流 javascript 模塊的規(guī)范,如 CommonJS, AMD, CMD 等。
下面是規(guī)范的代碼,以及對(duì)應(yīng)的說(shuō)明:
(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 {
// 沒(méi)有模塊環(huán)境,直接掛載在全局對(duì)象上
root.umdModule = factory(root.depModule);
}
}(this, function(depModule) {
// depModule 是依賴模塊
return {
name: '我自己是一個(gè)umd模塊'
}
}))
如果在 html 中直接使用 script 標(biāo)簽引用 umd 格式的 js 文件。就會(huì)走到第四個(gè)條件分支,即 直接掛載在全局對(duì)象上 。這個(gè)全局對(duì)象指的就是 window。
在我們的項(xiàng)目中,是直接使用 script 標(biāo)簽引用的。但沒(méi)有走第四個(gè)條件分支。之后會(huì)說(shuō)明原因及做法。
3. 如何打包 UMD 規(guī)范的組件文件
以 Vue CLI 為例。當(dāng)運(yùn)行 vue-cli-service build 時(shí),可以通過(guò) --target 選項(xiàng)指定構(gòu)建目標(biāo)為 庫(kù) 。

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

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

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