前言
使用 UniApp 開發(fā) Android / iOS App 時(shí),經(jīng)常會(huì)有地圖、可視化圖表、攝像頭、視頻播放、人臉采集、富文本編輯器等需求。而 UniApp 提供的相關(guān)組件或多或少存在各種影響使用的問題:如地圖層級(jí)過高難以覆蓋,UniApp 插件市場中的可視化圖表功能缺失、攝像頭相關(guān) API 接口過于簡陋等,使得開發(fā)工作難以進(jìn)行。
RenderJS
在這種情況下,RenderJS 的出現(xiàn)極大程度上緩解了這些難題。 RenderJS 是內(nèi)置在 UniApp 框架中的一個(gè)組件,它運(yùn)行在視圖層,可以在 App 端中對 DOM 直接進(jìn)行操作,并運(yùn)行 for web 的 JavaScript 庫,這意味著在 Web 支持的一切功能都可以在 App 端實(shí)現(xiàn)。
與此同時(shí), RenderJS 也存在著一定的限制:如不支持 Vue3 的 Setup 寫法,不能直接和邏輯層進(jìn)行通信,需要借助一定的技巧才能實(shí)現(xiàn)雙向通信等。
示例代碼
下面是 RenderJS 實(shí)際使用的一點(diǎn)例子,這些代碼想說明的問題只有一個(gè):視圖層和邏輯層如何互相調(diào)用方法和傳遞數(shù)據(jù)。示例比較長,可以翻到下面跟著說明來閱讀代碼。
<template>
<view id="container" style="padding-top: 20vh;" :viewLayerCallJson="viewLayerCallJson"
:change:viewLayerCallJson="viewLayer.handleLogicLayerCall" :viewLayerData="viewLayerData" :change:viewLayerData="viewLayer.handleLogicLayerData">
<button @click="handleTest">調(diào)用邏輯層方法</button>
<button @click="viewLayer.handleViewLayerTest">調(diào)用視圖層方法</button>
<button @click="handleTest2">從邏輯層調(diào)用視圖層方法</button>
<button @click="viewLayer.handleViewLayerTest2">從視圖層調(diào)用邏輯層方法</button>
<button @click="handleSendDataToViewLayer">視圖層傳遞數(shù)據(jù)給視圖層</button>
<button @click="handleTest4">動(dòng)態(tài)創(chuàng)建 VIDEO 元素</button>
</view>
</template>
<script>
export default {
data() {
return {
viewLayerCallJson: null,
viewLayerData: null
};
},
methods: {
handleViewLayerCall({ method, params }) {
this[method]?.(params);
},
handleCallViewLayerFunc(method, params) {
const randomNethodPrefix = Math.random().toString(36).substring(2, 15);
this.viewLayerCallJson = JSON.stringify({
method: `${randomNethodPrefix}-${method}`,
params
});
},
handleTest2() {
this.handleCallViewLayerFunc("createEl", {
tag: "input",
value: "hahahaha"
});
},
handleTest3({ content }) {
uni.showModal({
title: "提示",
content,
success: (res) => {
console.log(res);
}
});
},
handleTest4() {
this.handleCallViewLayerFunc("createEl", {
tag: "video"
});
},
handleTest() {
uni.showModal({
title: "提示",
content: "邏輯層方法被調(diào)用啦",
success: (res) => {
console.log(res);
}
});
},
handleSendDataToViewLayer() {
this.viewLayerData = Date.now();
}
}
}
</script>
<script lang="renderjs" module="viewLayer">
export default {
data() {
return {
_data: {}
};
},
methods: {
handleViewLayerTest() {
alert("視圖層方法被調(diào)用啦")
},
handleViewLayerTest2() {
this.callLogicLayerFunc("handleTest3", {
content: "從視圖層調(diào)用邏輯層方法"
});
},
handleLogicLayerCall(json) {
if (!json) return;
const { method: randomMethod, params } = JSON.parse(json);
const method = randomMethod.split("-")[1];
this[method]?.(params);
},
createEl({ value, tag }) {
const el = document.createElement(tag);
if (value) {
el.value = value;
}
document.querySelector("#container").append(el);
},
callLogicLayerFunc(method, params) {
this.$ownerInstance.callMethod("handleViewLayerCall", {
method,
params
});
},
handleLogicLayerData(newval, oldval, owner, instance) {
console.log(newval, oldval, owner, instance);
}
}
}
</script>
<style>
button + button {
margin-top: 20px;
}
</style>
編寫 RenderJS 代碼
寫在正常 script 標(biāo)簽中的代碼運(yùn)行在邏輯層,而 RenderJS 運(yùn)行在視圖層。 RenderJS 的代碼在編寫時(shí)需要單獨(dú)寫在一個(gè) script 標(biāo)簽中,同時(shí)給 script 標(biāo)簽添加 lang="renderjs" 和 module="xxx" 的屬性,前者是固定值不能修改,后者可以自行定義,用來在模板中引用 RenderJS 中定義的屬性和方法。
邏輯層向視圖層傳遞數(shù)據(jù)
傳遞數(shù)據(jù)需要關(guān)注以下幾點(diǎn):
- 在邏輯層中定義數(shù)據(jù),這個(gè)無需多言,正常定義要傳遞的數(shù)據(jù)即可。
2. 在模板內(nèi)任意標(biāo)簽中綁定要傳遞的數(shù)據(jù),通過 :change:[屬性名] 來監(jiān)聽綁定數(shù)據(jù)的變化,并綁定在視圖層中定義的回調(diào)函數(shù),綁定視圖層中定義的回調(diào)函數(shù)時(shí)需要以視圖層 module 屬性為前綴來調(diào)用。
3. 在視圖層內(nèi)定義屬性變化的回調(diào)函數(shù)
回調(diào)函數(shù)會(huì)傳遞四個(gè)參數(shù),分別是綁定屬性新值,上一次的值,視圖層實(shí)例以及邏輯層實(shí)例,需要注意回調(diào)函數(shù)在組件創(chuàng)建之后無論數(shù)據(jù)是否變化都會(huì)被調(diào)用一次。
視圖層向邏輯層傳遞數(shù)據(jù)
視圖層并不能直接向邏輯層傳遞數(shù)據(jù),只能借助調(diào)用邏輯層方法的形式來傳遞數(shù)據(jù),具體可以參見下一小節(jié)。
視圖層調(diào)用邏輯層方法
視圖層可以通過 $ownerInstance.callMethod 方法來調(diào)用邏輯層內(nèi)定義的方法,該方法接受兩個(gè)參數(shù),邏輯層方法名和邏輯層方法接受的參數(shù)。
邏輯層調(diào)用視圖層的方法
邏輯層并不能直接調(diào)用視圖層的方法,但通過一些技巧可以巧妙的實(shí)現(xiàn)調(diào)用。通過將要調(diào)用的方法和參數(shù)名序列化為 JSON 字符串,并作為數(shù)據(jù)傳遞給視圖層,視圖層在回調(diào)方法內(nèi)反序列化 JSON 數(shù)據(jù),然后執(zhí)行相關(guān)方法。
如果連續(xù)調(diào)用同一個(gè)方法并傳遞相同的參數(shù),由于 JSON 字符串沒有發(fā)生變化,因?yàn)橐晥D層的回調(diào)函數(shù)并不會(huì)被執(zhí)行,因此需要在 JSON 字符串內(nèi)加入一些隨機(jī)因子,使得每次調(diào)用方法生成的 JSON 字符串總是不相同的。
需要注意傳遞的數(shù)據(jù)內(nèi)容必須是兼容 JSON 的基本數(shù)據(jù)類型,如果傳遞 Set、Map、ArrayBuffer 等類型將會(huì)出現(xiàn)不可預(yù)知的錯(cuò)誤,同時(shí)傳遞的數(shù)據(jù)也不宜過大過于頻繁,否則可能會(huì)造成性能問題。
使用場景
基于以上的例子,以往很多不能實(shí)現(xiàn)的操作都可以實(shí)現(xiàn)。如引入 ECharts 使用全功能圖表,通過動(dòng)態(tài)創(chuàng)建 Video 元素來避免使用 App 內(nèi)置的 Video 元素,使用 Web 端的各種地圖組件,不再為 App 端 Map 組件層級(jí)過高無法覆蓋而頭痛,使用 navigator.getUserMedia 操作攝像頭來實(shí)現(xiàn)自定義視頻錄制界面等等,一切在 Web 端可以實(shí)現(xiàn)的功能都可以在 App 中實(shí)現(xiàn),不再受限于 UniApp App 的 API 限制。
注意事項(xiàng)
RenderJS 僅兼容 App 和 Web 兩端,雖然在微信小程序中存在著類 RenderJS 的組件 WXS,但其只是 RenderJS 的剪裁版,因此并不兼容。
RenderJS 是一個(gè)用來在 App 中實(shí)現(xiàn) Web 相關(guān)能力的產(chǎn)物,因此在 Web 中并不需要 RenderJS。 RenderJS 在 Web 端運(yùn)行時(shí),其會(huì)以 mixin 形式混入邏輯層,因此在編寫視圖層代碼時(shí),應(yīng)注意在屬性名和方法名上和邏輯層加以區(qū)分,以免出現(xiàn)在 Web 端運(yùn)行時(shí)被覆蓋的問題。