多態(tài)協(xié)議

2020-05-14 14:20 更新

定義標準接口(interface),各端模塊各自獨立實現(xiàn),編譯時和運行時對實現(xiàn)的接口輸入輸出做檢查。

主要 2 個目標:

  • 保障多端可維護性
  • 編譯時拆分多端代碼

Alt text

介紹

CML 的是多端的上層應用語言,在這樣的目標下,用戶擴展功能時,保障業(yè)務代碼和各端通信一致性變得特別重要。

用戶也許只實現(xiàn)一個 API 跨 2 端,保障一致很簡單,在一個超過 5 萬行代碼的復雜應用里,用戶擴展了 100 個接口呢,如果你覺得還很簡單,那跨 6 個端呢,在應用持續(xù)高速迭代中讓用戶人肉保障多端一致性實在太艱難,即使能做到,可維護性也會極差,跨端也會失去意義。

以上,跨端很美好,最大風險是可維護性問題。多態(tài)協(xié)議是 CML 業(yè)務層代碼和各端底層組件和接口的分界點,CML 會嚴格“管制”輸入輸出值的類型和結(jié)構,同時會嚴格檢查業(yè)務層 JS 代碼,避免直接使用某端特有的接口,不允許在公共代碼處使用某個端特定的方法,即使這段代碼不會執(zhí)行,例如禁止使用window、wx、my、swan、 weex 等方法。

統(tǒng)一多態(tài)協(xié)議設計的靈感來自于Apache Thrift - 可伸縮的跨語言服務開發(fā)框架,本質(zhì)上跨端也屬于跨語言。 它能讓 CML 開發(fā)者快速接入各個客戶端底層功能,且不會因為各端接口差異、產(chǎn)品需求差異導致正常業(yè)務代碼被打散,變得可讀性差、難以維護,避免結(jié)果適得其反,具體 Case;各個客戶端底層功能實現(xiàn)可以一部分來自 CML 提供的基礎組件和基礎 api 庫,一部分來自 CML 開發(fā)者,一部分來自各端生態(tài)開源庫(Chameleon 擁抱開源社區(qū),你可以直接安裝某個端的組件在使用多態(tài)協(xié)議擴展到某個端使用)。

多態(tài)接口

初始化多態(tài)接口

項目根目錄下執(zhí)行cml init component,選擇Polymorphic function,輸入文件名稱,例如utils,生成如下文件結(jié)構

├── components
    └──utils
      └── utils.interface

初始化文件內(nèi)容如下:

<script cml-type="interface">
interface UtilsInterface {
  getMsg(msg: string): void;
}
</script>
<script cml-type="web">
class Method implements UtilsInterface {
  getMsg(msg) {
    return 'web:' + msg;
  }
}
export default new Method();
</script>
<script cml-type="weex">
class Method implements UtilsInterface {
  getMsg(msg) {
    return 'weex:' + msg;
  }
}
export default new Method();
</script>
<script cml-type="wx">
class Method implements UtilsInterface {
  getMsg(msg) {
    return 'wx:' + msg;
  }
}
export default new Method();
</script>
<script cml-type="alipay">
class Method implements UtilsInterface {
  getMsg(msg) {
    return 'alipay:' + msg;
  }
}
export default new Method();
</script>
<script cml-type="baidu">
class Method implements UtilsInterface {
  getMsg(msg) {
    return 'baidu:' + msg;
  }
}
export default new Method();
</script>

文件中利用標簽將各端代碼進行物理隔離,利用 cml-type 屬性指定平臺。 cml-type="interface"為接口定義部分,利用接口校驗語法定義這個接口的方法及方法的參數(shù)與返回值。

cml-type="web|wx|weex|alipay|baidu"為各端實現(xiàn)部分,按照 interface 接口的定義進行方法的實現(xiàn)輸入輸出。注意要以export default的形式導出對象。

  • cml-type="web" 可以調(diào)用 Web 端任意方法和全局變量
  • cml-type="wx" 可以調(diào)用微信小程序端任意方法和全局變量
  • cml-type="alipay" 可以調(diào)用支付寶小程序端任意方法和全局變量
  • cml-type="baidu" 可以調(diào)用百度小程序端任意方法和全局變量
  • cml-type="weex" 可以調(diào)用 Weex 端任意方法和全局變量
  • cml-type="qq" 可以調(diào)用 QQ 小程序端任意方法和全局變量
  • cml-type="tt" 可以調(diào)用頭條小程序端任意方法和全局變量

調(diào)用多態(tài)接口

在需要使用多態(tài)接口的組件中引入,例如src/pages/index/index.cml中引用,代碼如下:

import utils from '../../components/utils/utils.interface';
let message = utils.getMsg();

擴展閱讀

什么時候用到多態(tài)接口?

多態(tài)接口適用于因為端的不同而進行不同接口的調(diào)用或者不同業(yè)務邏輯處理的場景。 例如:我們的頁面現(xiàn)在需要一個本地存儲功能的需求,我們已知各端的接口調(diào)用方法

  • Web 端接口是localStorage.setItem
  • 微信小程序端的接口是wx.setStorageSync
  • Weex 端的接口是storage.setItem

如果不使用多態(tài)接口我們只能根據(jù)不同環(huán)境去調(diào)用各自的接口

if (process.env.platform === 'web') {
  localStorage.setItem(key, value, function(e) {});
} else if (process.env.platform === 'wx') {
  wx.setStorageSync(key, value);
} else if (process.env.platform === 'alipay') {
  my.setStorageSync(key, value);
} else if (process.env.platform === 'baidu') {
  swan.setStorageSync(key, value);
} else if (process.env.platform === 'weex') {
  storage.setItem(key, value, function(e) {});
}

這樣的代碼有如下 待解決問題:

  1. 增加代碼復雜度,難以維護
  2. 各端接口的參數(shù)不一致, 寫多種邏輯
  3. 各端接口耦合在一起,bug 風險極高
  4. 沒有做到各端代碼的分離,增大代碼體積

利用了多態(tài)接口之后的使用方式如下:

import utils from 'utils.interface';

utils.setStorage(key, value, cb);

utils.interface對 setStorage 進行了封裝,文件內(nèi)容如下:

<script cml-type="interface">
// 定義一個傳參為string類型,返回值為undefine的函數(shù)類型
type Callback = (state: string) => undefined;

// 定義模塊的interface
interface UtilsInterface {
  // 定義setStorage方法 參數(shù)個數(shù)及返回值類型
  setStorage(key: string, value: string, cb: Callback): undefined;
}
</script>
<script cml-type="web">
// web端接口實現(xiàn)
class Method implements UtilsInterface {
  setStorage(key, value, cb) {
    try {
      localStorage.setItem(key, value);
      cb('success');
    }
    cache(e) {
      cb('fail');
    }
  }
}
export default new Method();
</script>
<script cml-type="weex">
// weex端接口實現(xiàn)
class Method implements UtilsInterface {
  setStorage(key, value, cb) {
    storage.setItem(key, value,
    function(e) {
        if (e.result == "success") {
          cb('success');
        } else {
          cb('fail');
        }
    });
  }
}
export default new Method();
</script>
<script cml-type="wx">
// wx端接口實現(xiàn)
class Method implements UtilsInterface {
  setStorage(key, value, cb) {
    try {
      wx.setStorageSync(key, value);
      cb('success');
    }
    catch(e) {
      cb('fail');
    }
  }
}
export default new Method();
</script>
<script cml-type="alipay">
// alipay端接口實現(xiàn)
class Method implements UtilsInterface {
  setStorage(key, value, cb) {
    try {
      my.setStorageSync(key, value);
      cb('success');
    }
    catch(e) {
      cb('fail');
    }
  }
}
export default new Method();
</script>
<script cml-type="baidu">
// baidu端接口實現(xiàn)
class Method implements UtilsInterface {
  setStorage(key, value, cb) {
    try {
      swan.setStorageSync(key, value);
      cb('success');
    }
    catch(e) {
      cb('fail');
    }
  }
}
export default new Method();
</script>

多態(tài)接口的優(yōu)勢

  • 保證接口一致性 CML 的目標是跨多端,多態(tài)接口的作用就是屏蔽各端差異,調(diào)用多態(tài)接口的代碼運行在多端,如果保證不了一致性,很可能出現(xiàn)某一端的需求引起的接口改動影響到其他端的功能,導致線上問題。 我們 設計了cml-type="interface"接口定義部分,目的就是做一致性的校驗,各端模塊的構造函數(shù)要實現(xiàn)該接口,我們在開發(fā)環(huán)境運行時提供了接口的校驗。
  • 代碼獨立性  多態(tài)接口中利用<script></script>標簽對各端代碼進行物理隔離,獨立實現(xiàn),每一端的編譯只編譯該端的代碼,不會有任何影響。
  • 充分擴展性 在獨立性的基礎上,就可以在各端的代碼中完全使用各端的接口,以及引用各自端的第三方 npm 包。

多態(tài)組件

CML 在跨端的統(tǒng)一性上做了很多的工作,但即使是做到了 99%的統(tǒng)一,仍然存在著 1%的差異,基于代碼可維護性的考量,CML 引入了多態(tài)協(xié)議。

多態(tài)組件全景圖

多態(tài)組件的使用

項目根目錄下執(zhí)行cml init component,選擇Polymorphic component,輸入組件名稱,例如c-list,生成如下文件結(jié)構

├── components
│   ├── c-list
│   │   ├── c-list.interface
│   │   ├── c-list.web.cml
│   │   ├── c-list.weex.cml
│   │   ├── c-list.wx.cml
│   │   ├── c-list.alipay.cml
│   │   ├── c-list.baidu.cml
│   │   └── ...

interface 文件

.interface文件利用接口校驗語法對組件的屬性和事件進行類型定義, 保證各端的組件和事件一致,框架在開發(fā)環(huán)境的運行時做校驗。例如c-list.interface

type eventType = 'change';
type eventDetail = {
  value: string
}
type changeEvent = (a: eventType, detail: eventDetail) => void;

export default Interface Clist {
  name: string,
  age: number,
  changeEvent: changeEvent
}

*.[web|weex|wx|alipay|baidu].cml

c-list.web.cml、c-list.weex.cml、c-list.wx.cml、c-list.alipay.cml、c-list.baidu.cml、... 文件是灰度區(qū),它是唯一可以調(diào)用下層端組件的 CML 文件,分別是 web、weex、wx、alipay、baidu 五個端的調(diào)用入口。建議這一塊代碼盡量薄,只是用來調(diào)用下層端代碼,不要編寫過于重的代碼。

  • 在灰度區(qū)的 template 模板中:調(diào)用下層全局組件或者引入的下層組件時,該組件傳入的屬性是各自下層端的語法,綁定的函數(shù)回調(diào)事件對象也是原始對象引入的下層組件通過可以直接調(diào)用,傳遞各端下層屬性語法下層全局組件需添加origin-前綴,例如<組件/>改成<origin-組件名/>,傳遞各端下層語法調(diào)用普通 CML 內(nèi)置組件或者引入的 cml 組件時,正常使用 cml 模板屬性語法
  • 在灰度區(qū)的 script 邏輯代碼中:可以調(diào)用下層端的全局變量和任意方法,以及下層端的生命周期。也可以正常使用普通 cml 邏輯代碼。
  • 在灰度區(qū)的 style 樣式代碼中:可以使用下層端 css 語法。也可以正常調(diào)用 cmss 語法。
  • 在灰度區(qū)的 json 配置代碼中:*web.cml:base.usingComponents可以引入任意.vue擴展名的普通 vue 組件文件,路徑規(guī)則見組件配置*wx.cml:base.usingComponents可以引入普通微信小程序組件,路徑規(guī)則見組件配置*alipay.cml:base.usingComponents可以引入普通支付寶小程序組件,路徑規(guī)則見組件配置*baidu.cml:base.usingComponents可以引入普通百度小程序組件,路徑規(guī)則見組件配置*weex.cml:base.usingComponents可以引入支持 .vue 擴展名的普通 Weex 組件文件,路徑規(guī)則見組件配置

擴展閱讀

什么時候用到多態(tài)組件?

CML 中的組件是采用單文件格式的 cml 文件,其中包括了一個組件所擁有的視圖層、邏輯層及配置信息??紤]以下兩種場景:

  • 場景一:當某個功能組件需要調(diào)用各端的原生組件,各端原生組件的屬性不一致,或者一端有原生組件,其他端需要組合實現(xiàn)等。
  • 場景二:產(chǎn)品在需求上導致某一個組件在各端的結(jié)構表現(xiàn)不同。

為什么要引入多態(tài)協(xié)議

以場景一為例,先看一個最容易理解的跨端組件實現(xiàn):

<template c-if="{{ENV === 'web'}}">
  <ul c-for="{{list}}">
    <li>{{ item.name }}</li>
  </ul>
</template>
<template c-else-if="{{ENV === 'wx'}}">
  // 假設wx-list 是微信小程序原生的組件
  <wx-list data="{{list}}"></wx-list>
</template>
<template c-else-if="{{ENV === 'alipay'}}">
  // 假設alipay-list 是微信小程序原生的組件
  <alipay-list data="{{list}}"></alipay-list>
</template>
<template c-else-if="{{ENV === 'baidu'}}">
  // 假設baidu-list 是微信小程序原生的組件
  <baidu-list data="{{list}}"></baidu-list>
</template>
<template c-else-if="{{ENV === 'weex'}}">
  // 假設list 是weex端原生的組件
  <list source="{{list}}"></list>
</template>

上面的代碼塊是一個簡單的列表實現(xiàn),wx 和 weex 都是使用了各自的原生組件,這樣的實現(xiàn)方法其實是把三端或者 N 端的模版放在了同一個文件中,當然,這里只是展示了模版的復雜,假設在 js 代碼塊中也存在著端的判斷,那代碼的復雜可想而知。

總結(jié)下來,這樣的代碼有如下 待解決問題:

  1. 增加代碼復雜度,難以維護
  2. 各端組件的屬性和事件定義可能不一致
  3. 各端組件耦合在一起,bug 風險極高
  4. 沒有做到各端代碼的分離,增大體積

而利用了多態(tài)組件之后的使用方式如下:

<c-list data="{{list}}"><c-list></c-list></c-list>

可以看到我們只引用了一個c-list組件,該組件提供了統(tǒng)一的屬性。

多態(tài)模板

chameleon-tool@1.0.4-alpha.2 開始支持

多態(tài)模板的基本使用方式如下

<template class="demo-com">
  <cml type="weex">
    <view>weex端代碼以這段代碼進行渲染</view>
    <demo-com title="我是標題weex"></demo-com>
  </cml>
  <cml type="alipay,baidu">
    <view>alipay和baidu端以這段代碼進行渲染</view>
    <demo-com title="我是標題2"></demo-com>
  </cml>
  <cml type="wx">
    <view>wx端以這段代碼進行渲染</view>
    <demo-com title="我是標題wx"></demo-com>
  </cml>
  <cml type="base">
    <view
      >如果找不到對應端的代碼,則以type='base'這段代碼進行渲染,比如這段代碼會在web端進行渲染</view
    >
    <demo-com title="我是標題base"></demo-com>
  </cml>
</template>

語法要求如下

1.必須符合如上結(jié)構,template 標簽下第一層并列標簽必須是 cml 標簽;

2.cml 標簽是必須有 type 屬性,表明應用于哪端,cml 標簽最終會被替換為 view 標簽



以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號