用RegularJS開發小程序 — mpregular解析

本文來自網易雲社區html

 

Mpregular 是基於 RegularJS(簡稱 Regular) 的小程序開發框架。開發者能夠將直接用 RegularJS 開發小程序,或者將現有的 RegularJS 應用經過較少修改移植到小程序上。Mpregular 爲 RegularJS 開發者提供了一套跨 h5 和小程序的前端應用解決方案,讓開發者能在不一樣平臺有一致的開發體驗和開發效。前端

 

0 序

如下是使用 mpregular 先後的效果對比vue

舊版(原生小程序)git

新版(mpregular)github

1 爲什麼而生

1.1 原生小程序開發

小程序自己提供的特性相對簡單,在開發複雜應用的時候,用原生小程序進行開發就會顯得比較吃力。爲了更好支持複雜應用,小程序也推出自定義組件、wxs 等新特性,但這些新特性無形中又會給開發者帶來必定的學習成本。另外,小程序的開發規範和一般的 web 應用的開發規範有着較大差別,若是須要同時在兩端上開發一樣功能的應用,則要求投入雙倍的人力,無疑大大增長了開發和維護的成本。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 的 onLoadonReady 已經經過 mpregular 與 Regular 的實例生命週期綁定在一塊兒了。頁面 url 的 querystring 也能夠經過 this.$mp.options 獲取。 onShowonPageScroll 等小程序特有的生命週期鉤子都一樣綁定到 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 創建聯繫,實現數據更新和事件監聽。因爲小程序中沒法使用 evalnew 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 入口組件進行實例化,並將 PageVMRegularVM 綁定在一塊兒。因爲每一個頁面只有一個 PageVM,因此 PageVM 會與 RegularVM.$root 進行綁定,以後 Regular 的邏輯會利用 RegularVM.$root 所綁定的 PageVM 與小程序進行通訊。當頁面初次渲染完成後,會觸發 onReady 鉤子,對應於 Regular 的 init。當頁面的其餘鉤子函數觸發時,如 onShowonHidePageVM 會經過 callHook 方法調用 RegularVM 上定義的同名方法。在頁面退出銷燬時,onUnload 中則會觸發 RegularVMdestroy 方法,將頁面綁定對應的 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 作了特別的優化,核心方向有兩個:

  1. 下降頻率
  2. 減小數據量

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

參考

 

本文來自網易雲社區,經做者網易考拉前端團隊受權發佈。

原文:用 RegularJS 開發小程序 —— mpregular 解析

相關文章
相關標籤/搜索