用RegularJS開發小程序 — mpregular解析
來自專欄網易雲社區7 人贊了文章
本文來自網易雲社區。
Mpregular 是基於 RegularJS(簡稱 Regular) 的小程序開發框架。開發者可以將直接用 RegularJS 開發小程序,或者將現有的 RegularJS 應用通過較少修改移植到小程序上。Mpregular 為 RegularJS 開發者提供了一套跨 h5 和小程序的前端應用解決方案,讓開發者能在不同平台有一致的開發體驗和開發效。
0 序
以下是使用 mpregular 前後的效果對比
舊版(原生小程序)
新版(mpregular)
1 為何而生
1.1 原生小程序開發
小程序本身提供的特性相對簡單,在開發複雜應用的時候,用原生小程序進行開發就會顯得比較吃力。為了更好支持複雜應用,小程序也推出自定義組件、wxs 等新特性,但這些新特性無形中又會給開發者帶來一定的學習成本。另外,小程序的開發規範和通常的 web 應用的開發規範有著較大差異,如果需要同時在兩端上開發同樣功能的應用,則要求投入雙倍的人力,無疑大大增加了開發和維護的成本。
1.2 考拉前端業務現狀
目前網易考拉的 wap 前台頁面大部分都是採用 RegularJS 開發的,包括 wap 首頁、詳情頁,因此考拉的前端們都擁有豐富的 RegularJS 開發經驗,RegularJS 可謂是我們最熟悉的前端開發框架之一。相比之下,熟悉小程序的開發就比較少了。微信是一個龐大的流量入口,最近小程序又掀起了一波熱潮,伴隨而來的就是小程序相關業務的增加。我們不僅需要把現有前台頁面遷移到小程序,還需要開發和維護跨小程序和 wap 兩端的業務。因此,我們迫切需要一個能夠支撐當前業務的解決方案,保證我們的開發效率,降低開發和維護成本。
1.3 業界的解決方案
業界關於小程序也有許多解決方案。
小程序官方很早就推出了一個組件化解決方案 —— Wepy,它有自己的一套語法規範,構建時將 Wepy 代碼編譯轉換為小程序代碼。它強依賴小程序自身的特性,因此受小程序自身特性所限,開發規範與考拉當前的前端技術棧差異較大,並不適用。
京東的凹凸實驗室推出了新的跨端開發框架 Taro,它是一個 React-like 的開發框架,有完善的配套設施,支持大部分 React 特性。但 Taro 是在 mpregular 開發完成後才出來,而且不符合我們當前的技術棧。
美團今年早些時候推出 mpvue,一個基於 Vue 實現的小程序開發框架,Vue 開發者看到這個框架以後歡天喜地,Github 上 star 數迅速攀升。對此,我們也做了一些調研,它不僅支持了大部分 Vue 的特性,而且有完善的文檔教程、配套設施,可以說是一個非常完善的解決方案。但我們當前存在的大量 RegularJS 頁面到小程序的遷移需求,在這一場景面前,mpvue 顯得無能為力。
縱觀業界的解決方案,都很難滿足我們當前的需求。我們受到了 mpvue 的啟發,並借鑒它的基本設計思想(包括名稱...),決定對 RegularJS 進行了改造,開發 mpregular 這一個基於 RegularJS 的小程序開發框架。
2 框架特性
既然是基於 RegularJS 實現的框架,語法規範必然是與 RegularJS 基本一致。在開發的的時候,基本上只要遵循 RegularJS 的開發規範進行即可,大大降低了 RegularJS 開發者的學習成本。
2.1 生命周期
小程序有 App 和 Page 兩個重要的概念,但通常業務代碼是寫在 Page 里的,這裡就以小程序頁面為例。開發者在開發小程序頁面的時候,基本只需要了解 Regular 實例的生命周期。小程序 Page 的 onLoad
、onReady
已經通過 mpregular 與 Regular 的實例生命周期綁定在一起了。頁面 url 的 querystring 也可以通過 this.$mp.options
獲取。 onShow
、onPageScroll
等小程序特有的生命周期鉤子都同樣綁定到 Regular 實例上。
<template> <div> <ComponentA /> </div></template><script> import ComponentA from ./component-a.rgl; export default { mpType: page, config() { // this.$mp.options 與 onLoad 中的 options 相同 // 用於獲取 options.query console.log(config, this.$mp.options); }, init() { console.log(init); }, onShow() { console.log(onShow); } }</script>
2.2 語法和特性
mpregular 支持 RegularJS 的語法和大部分特性。例如:
<template> <div> <input r-model="{ input }" on-confirm="{ this.onConfirm($event) }"> <div> {#list toDoList as item} <div class="item { item.checked ? z-checked : }"> <span>{ item.name }</span> <span>{ item.date | dateFormat: yyyy-MM-dd }</span> </div> {/list} </div> </div></template>
上述模版中的語法可以直接在小程序上執行。除此以外,mpregular 還支持 r-html、r-hide、{#include this.$body }、filter 等特性。這些特性在現有業務代碼中被大量使用,因此在遷移現有代碼時,幾乎可以原封不動地拷貝過來(除非原有代碼中包含大量 DOM 操作...)。
mpreguar 支持的特性:
- RegularJS 基本語法,包括 {#list},{#if}, {#include this.$body }
- filter
- r-model
- r-hide
- r-html
- r-class
- r-style
相比於原生小程序和業界其他框架而言,mpregular 給 RegularJS 開發者提供了他們更熟悉的開發模式,支持更多的特性,對模版的處理能力進一步增強,更適應於我們當前複雜應用的業務場景。
3 基本原理
小程序在結構上主要有 Service(JavascriptCore) 和 View(WebView) 兩部分組成,分別運行在獨立的環境上,之間不具備共享數據通道,二者的通信方式是將數據封裝在 js 腳本後傳遞。Page 實例就在 Service 中,通過 setData 方法將數據傳遞到 View。View 則通過事件綁定將視圖層觸發的事件傳遞給 Service。
Regular 是基於 Living Template 實現的,它使用一個內建 DSL 將模版字元串解析成 AST,然後在編譯階段結合數據模型將 AST 進行遞歸遍歷,並在這個遍歷過程中生成 DOM 節點,同時完成插值、指令等的綁定,實現 DOM 與數據的鏈接。
Mpregular 要做的就是將 Regular 的視圖層從 DOM 替換成小程序的 View。在小程序中不能直接操作 View 中的 DOM 節點,而是需要通過小程序的 Service 層 setData
方法去更新 View 的數據。
構建時,mpregular 會將 Regular 的模版字元串預先編譯成小程序的模版 .wxml,通過小程序的 Service 與小程序的 View 建立聯繫,實現數據更新和事件監聽。由於小程序中無法使用 eval
和 new Function
等操作,所以 mpregular 會在構建階段預先生成 AST ,運行時從源碼中讀取 AST。在執行 this.$update
時把更新數據通知 Service,調用 setData
完成視圖更新。View 觸發的事件會被代理到 Service 的 proxyEvent
方法,這個方法會在 RegularVM 中找到對應的事件處理函數並執行。
Mpregular 要做的,就是在 Regular 實例和小程序 Service 之間建立聯繫,完成生命周期綁定、數據更新、事件代理等工作。
3.1 生命周期
小程序中通過調用 Page 方法註冊頁面,而頁面載入時創建的頁面實例 PageVM
就是 mpregular 與小程序建立連接的通道。
Mpregular 在定義頁面入口的 Regular 組件時去調用 Page 方法註冊頁面,並將 Page 的生命周期鉤子與 Regular 的生命周期進行綁定。
page.init = function(config) { Page({ onLoad(options) { this.rootVM = initRootVM(this, config); callHook(this.rootVM,onLoad); }, onReady() { callHook(this.rootVM,onReady, options); initDataToMP(this.rootVM); } })}
在 Page 實例化(頁面載入)時,會觸發 onLoad
鉤子,此時會對這個頁面對應的 Regular 入口組件進行實例化,並將 PageVM
和 RegularVM
綁定在一起。由於每個頁面只有一個 PageVM
,所以 PageVM
會與 RegularVM.$root
進行綁定,之後 Regular 的邏輯會利用 RegularVM.$root
所綁定的 PageVM
與小程序進行通信。當頁面初次渲染完成後,會觸發 onReady
鉤子,對應於 Regular 的 init
。當頁面的其他鉤子函數觸發時,如 onShow
、onHide
,PageVM
會通過 callHook
方法調用 RegularVM
上定義的同名方法。在頁面退出銷毀時,onUnload
中則會觸發 RegularVM
的 destroy
方法,將頁面綁定對應的 Regular 實例銷毀。
3.2 模版轉換
由於 Regular 的模版語法與小程序模版語法不一樣,所以在構建階段,mpregular-loader 會把 Regular 的模版字元串轉換成小程序的 .wxml,不僅會對標籤進行轉換,還會對模版的語法、子組件模版進行處理。所定義的每個 Regular 組件,包括入口組件,都會被轉換成一個個模版片段,存放到對應的 .wxml 文件中,並用 <template name="${componentName}">
包裹起來,用組件名命名。
<!-- app.rgl --><template> <CustomComponent></CustomComponent> <div> <span>{ title }</span> <input r-model="{ input }" on-confirm="{ this.onConfirm($event) }"> </div></template>
上面這段 Regular 的模版就會被轉換會符合小程序模版語法的模版文件,如:標籤 <div>
、<span>
會被轉換為 <view>
、<label>
,事件監聽的語法則會進行轉換且把所以事件統一代理到 PageVm
上的 evenProxy
方法上。對於外部組件,則會通過 <import>
把組件的模版片段引入。由於所有模版片段都在同一個 Page 的作用域下,即從 PageVm.data
上取數據,因此需要一個規則將Regular 各個組件實例的數據映射到對應的模版片段中。
<!-- app.wxml --><import src="./components/custom-component.wxml"><template name="app"> <template is="./CustomComonent" data={{ customComonentData }}> <view> <label>{{ title }}</label> <input bindinput="proxyEvent" bindconfirm="evenProxy" value="{{ input }}"> </view></template>
3.3 數據和視圖的綁定
小程序對於 mpregular 而言只起到了視圖層的作用,小程序的模版全都會彙集通過 <import>
標籤彙集到頁面的入口 .wxml 中,這些被引入的模版的所有數據都是從 PageVM.data
上獲取的,意味著需要一定的映射規則,才能將 RegularVM 樹上各個子組件的數據綁定到小程序模版對應的節點上。對此,mpregular 借鑒了 mpvue 的數據結構設計,利用子組件在 VM 樹上的路徑生成唯一的 id,將子組件上的數據映射到對應的 View 節點上。
用以下這段簡單的代碼進行說明。<Page>
是整個頁面的入口模版,包含三個組件,分別是 <Header>
、<Counter>
、<Panel>
。
<!-- Counter.rgl --><template> <div> <Panel></Panel> </div></template><!-- page.rgl --><template> <Header></Header> <Counter></Counter></template>
以 <Page>
作為根節點,結構如下圖所示,是一個三層的樹結構。按照組件聲明的順序,每一層級的組件序號從 0 開始遞增。每個組件在樹中的 id 則根據它在樹中的路徑生成,如果 <Header>
則為 0,0
,<Panel>
的 id 為 0,1,0
,利用 ,
進行分隔,根據 id 可以反推出該組件實例在樹中的位置。
根據組件的 id,就可以把每個組件要更新到視圖的數據收集起來,並將收集的數據保存到小程序 PageVM.data.$root
上。
{ $root: { 0: { ... } // Page 0,0: { ... } // Header 0,1: { ... } // Counter 0,1,0: { ... } // Panel }}
利用 id 就可以把各個各個組件的數據映射到模版對應的節點上,轉換出來的模版如下所示(為了方便理解,這裡時簡化的實例代碼,並不是實際轉換結果)。
<!-- counter.wxml --><template> <view> <template is="./Panel" data={{ ...$root[ 0,1,0 ] }}> </view></template><!-- page.wxml --><template> <template is="./Header" data={{ ...$root[ 0,0 ] }}> <template is="./Counter" data={{ ...$root[ 0,1 ] }}> <Panel></Panel></template>
而 Page.data.$root
上的掛載的各個組件實例的數據,與模版的映射關係如下圖所示。
有了這個映射關係之後,通過 PageVM.setData
更新 PageVM.data.$root
上的數據,就完成了數據的更新。
3.4 事件代理
如上所述,所有模版片段的作用域都與該頁面的 PageVM
一致,事件只能由 PageVM
進行代理轉發。構建時,mpregular-loader 會為每個包含事件監聽的元素添加上 eventId 和 compId, 用於標記該元素和所屬組件(如下所示)。在註冊頁面的時候,mpregular 會在 Page 上掛載 proxyEvent
方法,所有事件都將代理到這個方法。
<!-- RegularJS 模版 --><div on-click="{ this.onClick($event) }"></div><!-- 轉換後的小程序 .wxml --><div bindtap="proxyEvent" event-id="0" comp-id="0"></div>
Mpregular 在為各個事件註冊處理方的時候,為每個組件創建一個 eventHandlers
對象,根據事件類型和 eventId
記錄各個事件處理函數。
{ componentId: 0, // ... eventHandlers: { 0: { tap: function() handler{} } }}
當事件觸發時,PageVM.proxyEvent
方法會根據 compId
找到對應的 RegularVM
,再根據事件類型和 eventId
找到對應的 handler
,最後執行對應的處理函數,完成事件代理。
3.5 性能優化
上面所講述的原理,就是讓 RegularJS 在小程序中運行的關鍵,但是僅僅運行起來還是不夠的,在實際業務場景下,還需要進一步優化才能更好地支撐業務,尤其是對於數據更新的優化。小程序官方文檔中特彆強調 setData
在傳遞大數據時會大量佔用 WebView JS 線程。同時我們發現,PageVM
上掛載的數據過大,也會嚴重影響 setData
的性能。為此 mpregular 做了特別的優化,核心方向有兩個:
- 降低頻率
- 減少數據量
3.5.1 緩存數據,定期更新
降低頻率的方法比較簡單,mpregular 會在調用 this.$update
時,先把需要更新的數據會緩存起來,每間隔 50ms 從緩存中取出數據進行批量更新,以減少避免頻繁的 setData
操作。
3.5.2 只更新 View 需要的數據
通常,在進行原生小程序的開發時,需要通過 setData
把數據更新到 PageVM.data
和 View 上,這也是唯一讓 View 和 Service 線程保持數據一致的方式。但這樣帶來的一個問題,在調用 setData
時,開發者很少會去區分哪些數據真正是 View 需要的,從而使得有大量的視圖無關數據被傳遞到 View,影響數據更新性能。
舉一個例子,視圖層需要從一個大對象上讀取其中一個值,largeData.info.countdown.time
。最簡單直接的做法時直接將模版編譯成下面這樣,把 largeData.info.countdown.time
寫到 .wxml 上,mpregular 在運行時把 largeData
更新到 View,由 View 去解析這個對象,取得所需的值。如果只是一次性傳遞也還好,但如果這個是一個毫秒級的倒計時模版,每次時間更新,就要重新把 largeData
傳給 View,性能變得極為糟糕。當然,開發者可以通過把值提取到 this.data.time
就可以繞過這個問題,但這樣會為開發者帶來許多不便。
<!-- RegularJS template --><span>{ largeData.info.countdown.time }</span><!-- 轉換後的小程序 wxml --><label>{{ largeData.info.countdown.time }}</label>
為此,mpregular 做了深度優化,在構建時 mpregular-loader 會對視圖層用到的插值表達式進行標記,將標識同步到 AST 上,把模版轉換成如下面代碼那樣。mpregular 在運行時,會根據 AST 上的標誌將執行插值表達式的執行結果填入對應的位置上,最後再更新到視圖層。這樣,數據的傳遞由一個大對象變成了一段字元串,大大提升數據更新性能。
<!-- RegularJS template --><span>{ largeData.info.countdown.time }</span><!-- 轉換後的小程序 wxml --><label>{{ __holders[0] }}</label>
有了這一機制,像 filter、r-html 等特性,都可得以實現。在 Regular 裡面,包含 filter 的插值、r-html 指令都會被轉換成插值表達式,用同樣的方法根據插值表達式的標誌將執行結果映射到對應模版節點上,就能夠實現原生小程序不支持的各種特性,極大地強化了模版的能力。
此外,mpregular 對列表渲染也進行了優化。在對 source
進行遍歷時,視圖層是不需要獲取 source
的實際內容的,mpregular 將 source
重新映射成一個具有同等長度的簡單數組,如 [0, 1, ...]
,再傳遞給視圖層去遍歷渲染,而所渲染的列表內容也會採用相同機制,將數據映射到列表中的對應位置。
<!-- RegularJS template -->{#list source as item} <span>{ item.name }</span>{/list}<!-- 轉換後的小程序 wxml --><block wx:for="{{ __holders[ 0 ] }}" wx:for-item="item" wx:for-index="item_index"> <label>{{ __holder[ 1 + - + item_index ]}}</label></block>
4 實踐
Mpregular 初版完成以後,我們立馬把它投入到生產當中。目前,考拉的小程序商品詳情頁已經用 mpregular 重構完成,頁面性能有明顯提升。
舊版商品詳情頁使用原生小程序進行開發,在處理多 sku 商品時,會存在性能問題。如果在處理下圖中包含 140+ 個 sku 數據的的商品時,點擊加入購物車按鈕後,sku 選擇彈層出來有明顯延時,這正是因為在調用 setData
更新大量 sku 數據時引發性能問題。使用 mpregular 重構後,sku 選擇彈層的彈出速度明顯加快。
新版(mpregular)
另外還有一個包含 220+ sku 數據的商品,新版詳情頁性能沒有受到大量 sku 數據的影響,而舊版詳情頁因為單次 setData
數據量超出限制,使頁面無法正常渲染(下方加車欄渲染失敗)。
新版(mpregular)
除了商品詳情頁以外,小程序的售後、拉新等新老業務都陸續開始使用 mpregular 進行開發。
5 總結與展望
Mpregular 為當前考拉跨 wap 和小程序兩端的老新業務開發和維護提供了有效的跨端解決方案,並能解決部分場景的性能問題。我們將長期維護 mpregular,繼續完善文檔和教程,增加單元測試保障代碼質量,繼續對性能、構建打包方式等進行優化,相關的配套設施也在將進一步完善。
Mpregular 驗證了 RegularJS 在小程序相中運行的可行性,相信 RegularJS 也能與 weex 相結合,成為一個跨端開發框架,也希望 RegularJS 生態能夠活躍起來。
github
- mpregular
- mpregular-loader
- mpregular-example
參考
- mpvue
- 一個對前端模板技術的全面總結
- 小程序優化建議
本文來自網易雲社區,經作者網易考拉前端團隊授權發布。
原文:用 RegularJS 開發小程序 —— mpregular 解析
了解 網易雲 :
網易雲官網:https://www.163yun.com
網易雲社區:https://sq.163yun.com/blog
雲產品全面促銷5折起:https://www.163yun.com/activity/promotion
更多網易研發、產品、運營經驗分享請訪問網易雲社區。
推薦閱讀: