megalo -- 網易考拉小程式解決方案

網易考拉前端團隊發表於2018-10-26

megalo 是基於 Vue 的小程式框架(沒錯,又是基於 Vue 的小程式框架),但是它不僅僅支援微信小程式,還支援支付寶小程式,同時還支援在開發時使用更多 Vue 的特性。

背景

對於使用者而言,小程式能提供更好的體驗,但對於開發者而言,要讓一個應用跑在多個平臺上,則需要寫多套程式碼。如何提高小程式開發效率讓很多開發者都感到頭疼。

業界也有相關的解決方案,如 taro 和 mpvue,二者都是基於 react 和 vue 的開發模式實現,讓開發者能夠以他們熟知的 react 或 vue 模式來開發小程式,提高開發效率。

mpvue 的釋出給了我們很多啟發,更早的時候,我們基於 RegularJS(網易自研的前端框架)開發了一個名為 mpregular 的小程式框架。在 mpregular 的開發和實際使用過程中,我們發現如果小程式框架所支援的特性只是原框架的子集(例如不支援 filter、模版複雜表示式等),開發效率會大打折扣。

所以,我們在方案上做了很多嘗試,目的是支援更多的特性,減少小程式與 H5 開發之前的差異。目前 mpregular 已經在考拉的小程式業務中大量應用,相關業務的開發同學紛紛表示,學習成本變低,跨端業務(H5 和小程式)的開發效率提升近一倍。

方案經過一段時間驗證後,我們決定把這套方案用 vue 再實現一次,一是為了適應技術棧的變更升級,二是為社群做一點微小的工作,於是就便有了 megalo。

特性

支援更多模版語法特性

相比於其他小程式開發模式,由於支援更多特性,megalo 更貼近 Vue 原生的開發模式。

特性 小程式 mpvue megalo
computed 計算屬性 ⭕️ ⭕️
v-model 雙向繫結 ⭕️
slot 插槽 ⭕️ ⭕️ ⭕️
scoped-slot 插槽 ⭕️
filter 過濾器 ⭕️
v-html 富文字 ⭕️
複雜表示式插值 ⭕️

從表格可以看到,megalo 最大的特點之一是,支援更多的 Vue 語法特性。這意味著,如果你有一個需求是要把現有的 Vue 程式碼遷移到小程式上,不需要太多改動。因為你的程式碼中可能大量使用 filter、scoped-slot、複雜表示式插值。

基本語法

支援 vue 的基本模版語法,包括 v-forv-if。class 和 style 的繫結方式沒有限制,官方的用法都支援。

<!-- v-if & v-for -->
<div v-for="(item, i) in list">
  <div v-if="isEven(i)">{{ i }} - {{ item }}</div>
</div>

<!-- style & class -->
<div :class="classObject"></div>
<div :class="{ active: true }"></div>
<div :class="[activeClass, errorClass]"></div>
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
<div :style="styleObject"></div>
<div :style="[baseStyles, overridingStyles]"></div>
複製程式碼

slot & scoped-slot

支援 slot 和 scoped-slot。

<div>
  <Container>
    <Card>
      <div slot="head"> {{ title }} </div>
      <div> I'm body </div>
      <div slot="foot"> I'm footer </div>
    </Card>
  </Container>
  <List :list="list">
    <span slot-scope="scopeProps">{{ scopeProps.item.label }}</span>
  </List>
<div>
複製程式碼

複雜表示式 & filter

可以在模版裡面寫複雜表示式、呼叫例項上的方法,當然也可以用更簡潔的 filter 語法,跟平時用 Vue 開發一樣。

<div>
  <div>{{ message.toUpperCase() }}</div>
  <div>{{ toUpperCase( message ) }</div>
  <div>{{ message | toUpperCase }}</div>
</div>
複製程式碼

v-html

要使用 v-html 需要新增外掛 @megalo/vhtml-plugin,並引入模版解析庫 octoparse,在頁面入口安裝一下外掛:

import Vue from 'vue'
import VHtmlPlugin from '@megalo/vhtml-plugin'

Vue.use(VHtmlPlugin)
複製程式碼

利用 v-html 指令然後就可以在小程式中渲染 html 了。

<div v-html="'<h1>megalo</h1>'"></div>
複製程式碼

更好的資料更新效能

小程式的官方明確說明,在呼叫 setData 更新資料時如果資料量過大或頻率更高,會引發效能問題。megalo 在框架底層已經幫開發者對此進行優化,每次資料發生變化時,megalo 只會將檢視中要展示的、且發生變化的資料進行更新,將 setData 的資料更新量最小化,同時對更新資料頻率進行了限制。

像下面這段程式碼,如果檢視只需要展示 user.name 這個欄位的話,在進行資料同步時只會將 user.name 這個字串更新到檢視層,其餘欄位是不會同步到小程式的物件上的。

<div>{{ user.name }}</div>
<script>
export default {
  data() {
    return {
      user: {
        name: 'kaola',
        age: 3,
        favorite: [
          'encalyptus',
          'sleeping'
        ]
      }
    }
  }
}
</script>
複製程式碼

支援更多平臺

今年以來,各大流量平臺都在小程式領域有所動作,螞蟻金服成立小程式事業部,百度、今日頭條也紛紛推出自己的小程式。megalo 目前已經支援微信和支付寶小程式,百度小程式等平臺的支援也在計劃當中。

1424614_old

微信和支付寶小程式

使用

使用 megalo 開發非常簡單,只需在常見的 Vue 專案 webpack 構建配置上配置 @megalo/target 並引入 @magalo/template-compiler 即可。如果需要編譯成支付寶小程式,只需要設定 platform: 'alipay'

const { createMegaloTarget } = require('@megalo/target');
module.exports = {
  target: createMegaloTarget( {
    compiler: require('@megalo/template-compiler'),
    platform: 'wechat'
  } )
  // 其他 webpack 配置,如 vue-loader 等
};
複製程式碼

接著,就可以像開發 Vue 應用一樣去開發小程式。示例可以參考 megalo-demo

如果想用 typescript 進行開發,可以參考 megalo-ts-simple

實現

小程式在結構上主要有 Service(JavascriptCore) 和 View(WebView) 兩部分組成(微信和支付寶小程式有著類似的結構,下文均以微信小程式為例,並簡稱為小程式),分別執行在獨立的環境上,之間不具備共享資料通道,二者的通訊方式是將資料封裝在 js 指令碼後傳遞。Page 例項就在 Service 中,通過 setData 方法將資料傳遞到 View。View 則通過事件繫結將檢視層觸發的事件傳遞給 Service。Service 層中無法操作檢視層的 DOM 節點。

小程式

實際開發中,小程式的邏輯和模版需要寫在 .js.wxml 兩個檔案中,分別在 Service 和 View 中執行。如果要將在瀏覽器端的 Vue 放到小程式中跑,需要將 .vue 檔案中的 template 片段轉換成 .wxml 檔案,並對 Vue 的 runtime 部分改造,將其中的 DOM 操作移除,通過小程式的 Service 中的頁面例項上的 API 與 View 進行通訊。

最終的執行效果是,當 Vue 的 vm 上資料發生更新時,會重新渲染出 vdom,在的 patch 階段,框架不在去操作 DOM,而是通過 Page 上的 setData 方法將變化的資料更新到檢視層,完成 Vue 和 小程式的檢視更新,這就是 megalo 底層所做的工作。

小程式

megalo 的實現,主要分成以下四個部分,下面本文將對每個部分進行介紹。

生命週期

小程式中,每一個頁面(Page)是一個例項,頁面的生命週期鉤子有很多,但和例項建立的兩個關鍵生命週期分別是 onLoadonReady,它們分別在「頁面載入,例項初始化後」和「初次頁面渲染完成」時觸發。Vue 的例項要和小程式例項建立起聯絡,則需要在小程式 Page 例項建立好以後,即在 onLoad 的鉤子函式裡,去初始化對應的 Vue 根例項,將頁面例項 page 掛載到 Vue 例項的 $mp 上,此時也會觸發 Vue 的生命週期鉤子 created。在頁面初次渲染完成後,則會呼叫 $mount 方法,與在瀏覽器中掛載 DOM 節點不同,這裡會將 Vue 例項上的資料初始化到檢視層中。由此,Vue 例項就與小程式的 Page 實建立起了聯絡。

小程式

除了這兩個生命週期鉤子以外,像小程式的 onShowonHide 等生命週期鉤子在觸發時,也會嘗試觸發 Vue 例項上的同名鉤子函式,實現兩種例項間生命週期的繫結。在小程式頁面退出銷燬時,會觸發 onUnload 鉤子,此時 Vue 的例項也會跟著銷燬。

模版轉換

小程式有它特有的模版語法和檔名字尾,所以在構建階段,我們會將 .vue 檔案中的 template 部分提取出來並轉換成對應的 .wxml 檔案。標籤名、語法都會進行相應的轉換,如圖所示。

小程式

這一部分是在構建階段完成的,這意味著,megalo 不支援 render 函式的寫法。在構建階段除了將模版轉換成 .wxml 以外,還需要對模版中的每個節點進行轉換,並在生產的 render 函式中加入相關的節點標記資訊,資料對映和事件代理需要這些資訊。

資料對映

由於無法直接操作檢視層的 DOM,所以我們只能利用 page.setData 這個方法完成資料到檢視層的對映。最簡單暴力的方法,是將 Vue 例項上的所有資料統統收集起來,通過呼叫 page.setData 方法更新 Page 例項的資料,這個方法會將資料掛載到 Page 例項上,同時也會把資料傳遞給檢視層。

小程式

但是,這種粗暴的更新方式有兩個弊端:

i. 全量更新 vm 上的資料是無法區分哪些資料是檢視層需要的,冗餘無用的資料會被更新到 page 例項上。像下圖這個例子,檢視層只需要展現兩個字串,如果 vm 上還存在兩個大陣列,它們也會被無腦同步到 page 上。

小程式

ii. 同步到 page 例項上的資料其實就是原始資料,並不是檢視層實際要展示的資料,所以展示資料的格式化與轉換需要依賴小程式模版的解析能力,導致一些 Vue 支援的模版語法無法支援,例如 filter、複雜表示式、傳遞 class 物件等。

小程式

當然以上兩個弊端不會對功能開發造成影響,但在實際的業務開發中,會讓開發體驗不一致,尤其是 H5 程式碼遷移到小程式時,對效率影響頗大。為了解決這個問題,megalo 採用另一種方式,即將 render 時生成的 vnode 上的資料更新到檢視,vnode 的資料就是已經處理好的展示資料,根據 vnode 構造每個節點的資料結構,再同步到檢視層。

例如以下這段程式碼,在構建階段 megalo 會對每個節點進行標記,使 render 時生成的 vnode 和模版中每個插值能夠對應上。

<!-- 編譯前的 Vue 模版 .vue -->
<div :class=“classObj”>
  {{ date | format( 'YYYY' ) }}
</div>

<!-- 編譯後的小程式模版 .wxml -->
<view class="{{ node_1.class }}">
  {{ node_2.text }}
</view>
複製程式碼

以這種方式實現的資料對映,只有檢視層需要的兩個字串資料會被同步到小程式的 Page 例項上,其餘資料則被認為與檢視無關則不會進行同步。

export default {
  data() {
    return {
      classObj: {
        'kaola': true
      },
      date: new Date(),
      users: {
        // big object
      }
    }
  }
}
複製程式碼

如下圖所示,Vue 渲染出來的 vnode 會被以特定的資料結構對映到 page 上,再同步到小程式檢視層。

小程式

以這種方式實現的資料對映,可以更好地支援 Vue 的模版語法,且更大限度地減少更新檢視時傳輸的資料量,從框架層面規避 setData 的效能問題。

事件代理

小程式檢視觸發事件後,會將 event 物件通知到 Page 例項,那麼我們只需要將檢視層中所有的事件都代理到 page.proxy 這個方法中,然後再靠這個方法從 Vue 的例項樹上找到對應的 vmhandler 做事件處理。為了實現這一目的,在構建階段對模版進行編譯時,除了要將事件監聽方法轉換為 proxy 以外,還需要通過 data- 在元素上標記對應的元件 compid 和節點 nodeid

<!-- 編譯前的 Vue 模版 .vue -->
<div @click="onClick"></div>

<!-- 編譯後的小程式模版 .wxml -->
<view bindtap="proxy" data-compid="0" data-nodeid="0"></view>
複製程式碼

事件觸發時,proxy 方法會從 event 物件上獲取對應的 id 資訊和事件型別,進而從 Vue 的根 vm 開始查詢,最終在 vnode 上找到對應的 handler 並執行事件處理,完成小程式事件到 Vue 例項的事件代理。

小程式

現在與未來

目前,megalo 已經逐步在考拉的小程式應用開發中投入使用,但 megalo 的資料對映方案早已通過 mpregular 在考拉的大量小程式應用中得到了驗證。現在,megalo 支援 typescript 開發,支援支付寶小程式。

百度智慧小程式的支援也在計劃之內,同時,我們還計劃開發一個相容個平臺的 UI 元件庫、API 庫,嘗試將跨 H5 和各小程式平臺的應用開發之間的差異最小化,提升開發效率。

github

參考

相關文章