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

2020-05-14 14:20 更新

定義標(biāo)準(zhǔn)接口(interface),各端模塊各自獨(dú)立實(shí)現(xiàn),編譯時(shí)和運(yùn)行時(shí)對(duì)實(shí)現(xiàn)的接口輸入輸出做檢查。

主要 2 個(gè)目標(biāo):

  • 保障多端可維護(hù)性
  • 編譯時(shí)拆分多端代碼

Alt text

介紹

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

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

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

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

多態(tài)接口

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

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

├── 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>

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

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

  • 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();

擴(kuò)展閱讀

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

多態(tài)接口適用于因?yàn)槎说牟煌M(jìn)行不同接口的調(diào)用或者不同業(yè)務(wù)邏輯處理的場(chǎng)景。 例如:我們的頁(yè)面現(xiàn)在需要一個(gè)本地存儲(chǔ)功能的需求,我們已知各端的接口調(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. 增加代碼復(fù)雜度,難以維護(hù)
  2. 各端接口的參數(shù)不一致, 寫多種邏輯
  3. 各端接口耦合在一起,bug 風(fēng)險(xiǎn)極高
  4. 沒有做到各端代碼的分離,增大代碼體積

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

import utils from 'utils.interface';

utils.setStorage(key, value, cb);

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

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

// 定義模塊的interface
interface UtilsInterface {
  // 定義setStorage方法 參數(shù)個(gè)數(shù)及返回值類型
  setStorage(key: string, value: string, cb: Callback): undefined;
}
</script>
<script cml-type="web">
// web端接口實(shí)現(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端接口實(shí)現(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端接口實(shí)現(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端接口實(shí)現(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端接口實(shí)現(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)勢(shì)

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

多態(tài)組件

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

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

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

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

├── 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文件利用接口校驗(yàn)語(yǔ)法對(duì)組件的屬性和事件進(jìn)行類型定義, 保證各端的組件和事件一致,框架在開發(fā)環(huán)境的運(yùn)行時(shí)做校驗(yà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 五個(gè)端的調(diào)用入口。建議這一塊代碼盡量薄,只是用來調(diào)用下層端代碼,不要編寫過于重的代碼。

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

擴(kuò)展閱讀

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

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

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

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

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

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

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

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

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

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

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

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

多態(tài)模板

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

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

<template class="demo-com">
  <cml type="weex">
    <view>weex端代碼以這段代碼進(jìn)行渲染</view>
    <demo-com title="我是標(biāo)題weex"></demo-com>
  </cml>
  <cml type="alipay,baidu">
    <view>alipay和baidu端以這段代碼進(jìn)行渲染</view>
    <demo-com title="我是標(biāo)題2"></demo-com>
  </cml>
  <cml type="wx">
    <view>wx端以這段代碼進(jìn)行渲染</view>
    <demo-com title="我是標(biāo)題wx"></demo-com>
  </cml>
  <cml type="base">
    <view
      >如果找不到對(duì)應(yīng)端的代碼,則以type='base'這段代碼進(jìn)行渲染,比如這段代碼會(huì)在web端進(jìn)行渲染</view
    >
    <demo-com title="我是標(biāo)題base"></demo-com>
  </cml>
</template>

語(yǔ)法要求如下

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

2.cml 標(biāo)簽是必須有 type 屬性,表明應(yīng)用于哪端,cml 標(biāo)簽最終會(huì)被替換為 view 標(biāo)簽



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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)