01.03 一起來擼個簡易的小程序框架

對於小程序框架實現原理,在支付寶小程序官方文檔上有這樣一段描述:

與傳統的 H5 應用不同,小程序運行架構分為 webview 和 worker 兩個部分。webview 負責渲染,worker 則負責存儲數據和執行業務邏輯。 1.webview 和 worker 之間的通信是異步的。這意味著當我們調用 setData 時,我們的數據並不會立即渲染,而是需要從 worker 異步傳輸到 webview。 2.數據傳輸時需要序列化為字符串,然後通過 evaluateJavascript 方式傳輸,數據大小會影響性能。

概括一下,大致意思是小程序框架核心是通過2個線程來完成的,主線程負責webView的渲染工作,worker線程負責js執行。說到這裡,你是不是會產生一個疑問:為什麼多線程通信損耗性能還要搞多線程呢? 可能大多數人都知道因為Web技術實在是太開放了,開發者可以為所欲為。這種情況在小程序中是不允許的,不允許使用<iframe>、不允許 /<iframe>

但是卻始終無法解決一個問題:如何防止開發者做一些我們想禁用的功能。因為是一個網頁,開發者可以執行JS,可以操作DOM,可以操作BOM,可以做一切事情。so,我們需要一個沙箱環境,來運行我們的js,這個沙箱環境需要可以屏蔽掉所有的危險動作。說了這麼多,大致想法如下:

一起來擼個簡易的小程序框架

關於UI層的渲染,有很多實現方式,比如通過類似VNode -> diff的自定義渲染方式來實現了一個簡易的小程序框架:


<code>function App (props) {
const {msg} = props;
return () => (

{msg}

)
}

render()
複製代碼/<code>

核心就是通過定義@babel/plugin-transform-react-jsx插件來轉換 jsx,生成Vnode,再交給Worker通過Diff,最後通過worker postmsg 來通知渲染進程更新:

<code>let index = 0;
// 得到diff差異
let diffData = diff(0, index, oldVnode, newVnode);
// 通知渲染進程更新
self.postMessage(JSON.stringify(diffData));
複製代碼/<code>

有點麻煩?能不能繼承現有框架能力?比如Vue、React。當然可以,我們下面就來介紹基於Vue來實現的demo.

實現一個基於Vue的小程序框架

有了上面的知識,我們先不著急寫代碼,先來捋一下我們需要什麼,首先我們需要實現這樣一個能力:渲染層和邏輯層分離,emmm。。。大致我們的小程序是這樣的

<code>// page.js 邏輯層
export default {
data: {
msg: 'hello Vox',
},
create() {
console.log(window);
setTimeout(() => {
this.setData({
msg: 'setData',
})
}, 1000);
},
}
複製代碼/<code>
<code>// page.vxml.js 渲染層
export default () => {
return '
{{msg}}
';
}
複製代碼/<code>

這裡的渲染層為啥不是類似於微信或者支付寶小程序 wxml,axml這樣的呢?當然可以,其實我只是為了方便而已,我們可以手寫一個webpack loader 來處理一下我們自定義的文件。這裡有興趣的小夥伴可以嘗試一下。不是本次介紹的核心。

好了,上面是我們想要的功能,我們核心是框架,框架層要乾的事核心有2個:構造worker初始化引擎;構造渲染引擎。

<code>// index.worker.js 構造worker
const voxWorker = options => {
const {config} = options;
// Vue生命週期收集
const lifeCircleMap = {
'lifeCircle:create': [config.create],
};
// 定義setData方法用於通知UI層渲染更新
self.setData = (data) => {
console.log('setData called');
self.postMessage(
JSON.stringify({
type: 'update',
data,
})
,
null
);
};
// worker構建完成,通知渲染層初始化
self.postMessage(
JSON.stringify({
type: 'init',
data: config.data,
})
,
null
);
// 執行生命週期函數
self.onmessage = e => {
const {type} = JSON.parse(e.data);
lifeCircleMap[type].forEach(lifeCircle => lifeCircle.call(self))
}
}

export default voxWorker;
複製代碼/<code>

上面代碼核心乾的事其實並不複雜,也就是:

  1. 收集需要用到的生命週期
  2. 定義setData函數,提供給用戶層更新UI
  3. 定義監聽函數,處理生命週期函數執行
  4. 通知UI進程開啟渲染。

當我們通知UI進程開始渲染的時候,UI進程也就是需要構造Vue實例,進行頁面render:

<code>worker.onmessage = e => {
const {type, data} = JSON.parse(e.data);
if (type === 'init') {
const mountNode = document.createElement('div');
document.body.appendChild(mountNode);
target = new Vue({
el: mountNode,
data: () => data,
template: template(),
created(){
worker.postMessage(JSON.stringify({
type: 'lifeCircle:create',
})
,
null);
}
});
}
}
複製代碼/<code>

可以看到UI線程在初始化的時候,一併初始化了 worker層傳遞來的data,並對生命週期進行了聲明。當生命週期函數在UI層觸發的時候,會通知 worker。在我們的例子中,create 鉤子通過setData 進行了一個更新data的動作。我們知道 setData 就是拿到數據進行通知更新:

<code>// UI線程接收到通知消息,更新UI
if (type === 'update') {
Object.keys(data).map(key => {
target[key] = data[key];
});
}
複製代碼/<code>

說到這裡,似乎一切都感覺清楚多了,我們用worker執行用戶js邏輯,worker內無法操作dom,無法訪問window。當我們需要更新dom,可以去通知渲染線程做更新。我們也很容易想到不斷地數據傳輸對性能的損耗,所以我們當然可以做進一步的優化,多個setData可以組合一起發送?就是建立一個通信。其次再看一些,我們的worker再通信傳輸數據的過程中不斷通過字符串的parse和stringify


一起來擼個簡易的小程序框架


綠色部分時原生JSON.stringify(), 關於這一塊如何提升性能一方面可以通過減少數據傳輸量,其他的優化也可以參考這裡如何提升JSON.stringify()的性能

最後你可能會問,小程序用法都是 <view>, <text> 之類的標籤,為啥我這裡直接用了

。其實吧, 也就是

的語法糖,寫一個 vue組件,組件名稱叫view是不是就可以了呢?


這裡為大家介紹的也只是小程序的冰山一角,內部的容器開放jsBridge能力,離線機制,跨webview通信機制等等有興趣的可以去探索,當然這裡也只是拋磚引玉。

有興趣的可以查看源碼Vox

結語

引用知乎上的一段話:

其實,大家對小程序的底層實現都是使用雙線程模型,大家對外宣稱都會說是為了:方便多個頁面之間數據共享和交互為native開發者提供更好的編碼體驗為了性能(防止用戶的JS執行卡住UI線程)其他好處但其實真正的原因其實是:“安全”和“管控”,其他原因都是附加上去的。因為Web技術是非常開放的,JavaScript可以做任何事。但在小程序這個場景下,它不會給開發者那麼高的權限:不允許開發者把頁面跳轉到其他在線網頁不允許開發者直接訪問DOM不允許開發者隨意使用window上的某些未知的可能有危險的API,當然,想解決這些問題不一定非要使用雙線程模型,但雙線程模型無疑是最合適的技術方案。

經過上面的介紹,是不是發現小程序其實也就那麼回事,並沒有多麼....這邊文章主要希望能讓你對經常使用的框架有一個原理性的初步認識,至少我們再用的時候可以規避掉一些坑,或者性能問題。

參考文章:

zhuanlan.zhihu.com/p/81775922

雙線程前端框架:Voe.js


作者:muwoo
鏈接:https://juejin.im/post/5e0d74c96fb9a048401cff8e
來源:掘金
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

"
/<text>/<view>


分享到:


相關文章: