編寫易維護跨端元件的正確姿勢

Chameleon社群發表於2019-04-02
  • 上篇(利用各端第三方庫結合多型實現元件)

    • 前期準備

    • 元件開發

    • 一些思考

在chameleon專案中我們實現一個跨端元件一般有兩種思路:使用各端第三方元件封裝基於chameleon語法統一實現。本篇是編寫chameleon跨端元件的正確姿勢系列文章的上篇,以封裝一個跨端的indexlist元件為例,首先介紹如何優雅的使用第三方庫封裝跨端元件,然後給出編寫chameleon跨端元件的建議。使用chameleon語法統一實現跨端元件請關注文章《編寫chameleon跨端元件的正確姿勢(下篇)》

依靠強大的多型協議,chameleon專案中可以輕鬆使用各端的第三方元件封裝自己的跨端元件庫。基於第三方元件可以利用現有生態迅速實現需求,但是卻存在很多缺點,例如各端第三方元件本身的功能與樣式差異、元件質量得不到保證以及絕大部分元件並不需要通過多型元件差異化實現,這樣反而提升了長期的維護成本;使用chameleon語法統一實現則可以完美解決上述問題,並且擴充套件一個新的端時現有元件可以直接執行。本文的最後也會詳細對比一下兩種方案的優劣。

因此,建議將通過第三方庫實現跨端元件庫作為臨時方案,從長期維護的角度來講,建議開發者使用chameleon語法統一實現絕大部分跨端元件,只有一些特別複雜並且已有成熟第三方庫或者框架能力暫時不支援的元件,才考慮使用第三方元件封裝成對應的跨端元件。

由於本文介紹的是使用第三方庫封裝跨端元件, 因此示例的indexlist元件採用第三方元件封裝來實現, 通過chameleon統一實現跨端元件的方法可以看《編寫chameleon跨端元件的正確姿勢(下篇)》

最終實現的indexlist效果圖:

imgimgimg

前期準備

使用各端第三方元件實現chameleon跨端元件需要如下前期準備:

專案初始化

建立一個新專案 cml-demo

cml init project複製程式碼

進入專案

cd cml-demo複製程式碼
元件設計

開發一個模組時我們首先應該根據功能確定其輸入與輸出,對應到元件開發上來說,就是要確定元件的屬性和事件,其中屬性表示元件接受的輸入,而事件則表示元件在特定時機對外的輸出。

為了方便說明,本例暫時實現一個具備基礎功能的indexlist。一個indexlist元件至少應該在使用者選擇某一項時丟擲一個onselect事件,傳遞使用者當前所選中項的資料;至少應該接受一個datalist,作為其渲染的資料來源,這個datalist應該是一個類似於以下結構的物件陣列:

  const dataList = [
    {
      name: '阿里',
      pinYin: 'ali',
      py: 'al'
    }, {
      name: '北京',
      pinYin: 'beijing',
      py: 'bj'
    },
    .....
 ]複製程式碼
尋找第三方元件庫

由於本文介紹的是如何使用第三方庫封裝跨端元件,因此在確定元件需求以及實現思路後去尋找符合要求的第三方庫。在開發之前,作者調研了目前較為流行的各端元件庫,推薦如下:

除了上述元件庫之外,開發者也可以根據自己的實際需求去尋找經過包裝之後符合預期的第三方庫。截止文章編寫時,作者未找到較成熟的支付寶及百度小程式第三方庫,因此暫時先實現web、微信小程式以及weex端,這也體現出了使用第三方庫擴充套件跨端元件的侷限性:當沒有成熟的對應端第三方庫時,無法完成該端的元件開發;而使用chameleon語法統一實現.md)則可以解決上述問題,擴充套件新的端時已有元件能夠直接執行,無需額外擴充套件。 本文在實現indexlist元件時分別使用了cube-ui, iview weapp以及weex-ui, 以下會介紹具體的開發過程.

元件開發

初始化

建立多型元件

cml init component複製程式碼

選擇“多型元件”, 並輸入元件名字“indexlist”, 完成元件的建立, 建立之後的元件位於src/components/indexlist資料夾下。

介面校驗

多型元件中的.interface檔案利用介面校驗語法對元件的屬性和事件進行型別定義,保證各端的屬性和事件一致。確定了元件的屬性與事件之後就開始編寫.interface檔案, 修改src/components/indexlist/indexlist.interface:

 type eventDetail = {
  name: String,
  pinYin: String,
  py: String
}
type arrayItem = {
  name: String,
  pinYin: String,
  py: String
}
type arr = [arrayItem];

interface IndexlistInterface {
  dataList: arr,
  onselect(eventDetail: eventDetail): void
}複製程式碼

具體的interface檔案語法可以參考此處, 本文不再贅述。

web端元件開發

安裝cube-ui

npm i cube-ui -S複製程式碼

在src/components/indexlist/indexlist.web.cml的json檔案中引入cube-ui的indexlist元件

"base": {
  "usingComponents": {
    "cube-index-list": "cube-ui/src/components/index-list/index-list"
  }
}複製程式碼

修改src/components/indexlist/indexlist.web.cml中的模板程式碼,引用cube-ui的indexlist元件:

 <view class="index-list-wrapper">
  <cube-index-list
  :data="list"
  @select="onItemSelect"
/>
</view>
複製程式碼

修改src/components/indexlist/indexlist.web.cml中的js程式碼, 根據cube-ui文件將資料處理成符合其元件預期的結構, 並向上丟擲onselect事件:

const words = ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"];

class Indexlist implements IndexlistInterface {
props = {
  dataList: {
    type: Array,
    default() {
      return []
    }
  }
}

data = {
  list: [],
}

methods = {

  initData() {
    const cityData = [];
    words.forEach((item, index) => {
      cityData[index] = {};
      cityData[index].items = [];
      cityData[index].name = item;
    });
    this.dataList.forEach((item) => {
      let firstName = item.pinYin.substring(0, 1).toUpperCase();
      let index = words.indexOf(firstName);
      cityData[index].items.push(item)
    });
    this.list = cityData;
  },
  
  onItemSelect(item) {
    this.$cmlEmit('onselect', item);
  }
}

mounted() {
  this.initData();
}
}
export default new Indexlist();
複製程式碼

編寫必要的樣式:

 .index-list-wrapper {
  width: 750cpx;
  height: 1200cpx;
}複製程式碼

以上便使用cube-ui完成了web端indexlist元件的開發,效果如下:

img

weex端元件開發

安裝weex-ui

npm i weex-ui -S
複製程式碼

在src/components/indexlist/indexlist.weex.cml的json檔案中引入weex-ui的wxc-indexlist元件:

"base": {
    "usingComponents": {
      "wex-indexlist": "weex-ui/packages/wxc-indexlist"
    }
 }複製程式碼

修改src/components/indexlist/indexlist.weex.cml中的模板程式碼,引用weex-ui的wxc-indexlist元件:

 <view class="index-list-wrapper">  
  <wex-indexlist 
    :normal-list="list"
    @wxcIndexlistItemClicked="onItemSelect"
  />
 </view>
複製程式碼

修改src/components/indexlist/indexlist.weex.cml中的js程式碼:

class Indexlist implements IndexlistInterface {
  props = {
    dataList: {
      type: Array,
      default() {
        return []
      }
    }
  }
  data = {
    list: [],
  }

  mounted() {
    this.initData();
  }

  methods = {
   initData() {
     this.list = this.dataList;
   },

   onItemSelect(e) {
     this.$cmlEmit('onselect', e.item);
   } 
  }
}
export default new Indexlist();
複製程式碼

編寫必要樣式,此時發現weex端與web端有部分重複樣式,因此將樣式抽離出來建立indexlist.less,在web端與weex端的cml檔案中引入該樣式

<style lang="less">
  @import './indexlist.less';
</style> 
複製程式碼

indexlist.less檔案內容:

.index-list-wrapper {
  width: 750cpx;
  height: 1200cpx;
}
複製程式碼

以上便使用weex-ui完成了weex端indexlist元件的開發,效果如下:

img

wx端元件編寫

根據iview weapp文件, 首先到Github下載iview weapp程式碼,將dist目錄拷貝到專案的src目錄下,然後在src/components/indexlist/indexlist.wx.cml的json檔案中引入iview的index與index-item元件:

"base": {
    "usingComponents": {
      "i-index":"/iview/index/index",
      "i-index-item": "/iview/index-item/index"
    }
},
複製程式碼

修改src/components/indexlist/indexlist.wx.cml中的模板程式碼,引用iview的index與index-item元件:

 <view class="index-list-wrapper">
    <i-index
      height="1200rpx"
    > 
      <i-index-item
        wx:for="{{cities}}" 
        wx:for-index="index" 
        wx:key="{{index}}" 
        wx:for-item="item" 
        name="{{item.key}}"
      >
        <view 
          class="index-list-item" 
          wx:for="{{item.list}}" 
          wx:for-index="in" 
          wx:key="{{in}}" 
          wx:for-item="it"
          c-bind:tap="onItemSelect(it)"
        >
          <text>{{it.name}}</text>
        </view>
      </i-index-item>
    </i-index>
  </view>
複製程式碼

修改src/components/indexlist/indexlist.wx.cml中的js程式碼, 根據iview weapp文件將資料處理成符合其元件預期的結構, 並向上丟擲onselect事件:

const words = ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"];

class Indexlist implements IndexlistInterface {
  props = {
    dataList: {
      type: Array,
      default() {
        return []
      }
    }
  }

  data = {
    cities: []
  }

  methods = {
    initData() {
      let storeCity = new Array(26);
      words.forEach((item,index)=>{
        storeCity[index] = {
          key: item,
          list: []
        };
      });
      this.dataList.forEach((item)=>{
        let firstName = item.pinYin.substring(0,1).toUpperCase();
        let index = words.indexOf(firstName);
        storeCity[index].list.push(item);
      });
      this.cities = storeCity;
    },
    onItemSelect(item) {
      this.$cmlEmit('onselect', item);
    }
  }

  mounted() {
    this.initData();
  }

}

export default new Indexlist();
複製程式碼

編寫必要樣式:

@import 'indexlist.less';
.index-list {
  &-item {
    height: 90cpx;
    padding-left: 20cpx;
    justify-content: center;
    border-bottom: 1cpx solid #F7F7F7
  }
}複製程式碼

以上便使用iview weapp完成了wx端indexlist元件的開發, 效果如下:

img

元件使用

修改src/pages/index/index.cml檔案裡面的json配置,引用建立的indexlist元件

"base": {
    "usingComponents": {
      "indexlist": "/components/indexlist/indexlist"
    }
},複製程式碼

修改src/pages/index/index.cml檔案中的模板部分,引用建立的indexlist元件

 <view class="page-wrapper">
    <indexlist 
      dataList="{{dataList}}"
      c-bind:onselect="onItemSelect"
    />
  </view>複製程式碼

其中dataList是一個物件陣列,表示元件要渲染的資料來源。具體結構為:

const dataList = [
    {
      name: '阿里',
      pinYin: 'ali',
      py: 'al'
    }, {
      name: '北京',
      pinYin: 'beijing',
      py: 'bj'
    },
    .....
 ]複製程式碼
開發總結

根據上述例子可以看出,chameleon專案可以輕鬆結合第三方庫封裝自己的跨端元件庫。使用第三方元件封裝跨端元件庫的步驟大致如下:

  1. 跨端元件設計
  2. 根據實際需求引入合適的第三方元件
  3. 根據第三方元件文件,將資料處理成符合預期的結構,並在適當時機丟擲事件
  4. 編寫必要樣式

一些思考

理解*.[web|wx|weex].cml

根據元件多型文件, 像indexlist.web.cml、indexlist.wx.cml與indexlist.weex.cml的這些檔案是灰度區, 它們是唯一可以呼叫下層端能力的CML檔案,這裡的下層端能力既包含下層端元件,例如在web端和weex端的.vue檔案等;也包含下層端的api,例如微信小程式的wx.pageScrollTo等。這一層的存在是為了呼叫下層端程式碼,各端具體的邏輯實現應該在下層來實現, 這種規範的好處是顯而易見的: 隨著業務複雜度的提升,各個下層端維護的功能逐漸變多,其中通用的部分又可以通過普通cml檔案抽離出來被統一呼叫,這樣可以保證差異化部分始終是最小集合,灰度區是存粹的;如果將業務邏輯都放在了灰度區,隨著功能複雜度的上升,三端通用功能/元件就無法達到合理的抽象,導致灰度層既有相同功能,又有差異化部分,這顯然不是開發者願意看到的場景。
在灰度區的模板、邏輯、樣式和json檔案中分別具有如下規則:

  • 模板

    • 呼叫下層元件時,既可以使用chameleon語法,也可以使用各端原生語法;在灰度區chameleon編譯器不會編譯各個端原生語法,例如v-for,bindtap等。建議在模板部分仍然使用chameleon模板語法,只有在實現對應平臺不支援的語法(例如web端v-html等)時才使用原生語法。
    • 引用下層全域性元件時需要新增origin-字首,這樣可以“告訴”chameleon編譯器是在引用下層的原生元件,chameleon編譯器就不會對其進行處理了。這種做法同時解決了元件命名衝突問題,例如在微信小程式端引用<origin-button>表示呼叫小程式原生的button元件而不是chameleon內建的button元件。
  • 邏輯

    • 在script邏輯程式碼中,除了編寫普通cml邏輯程式碼之外,開發者還可以使用下層端的全域性變數和任意方法,包括生命週期函式。這種機制保證開發者可以靈活擴充套件各端特有功能,而不需要依賴多型介面。
  • 樣式

    • 既可以使用cmss語法也可以使用下層端的css語法。
  • json檔案

    • *web.cml:base.usingComponents可以引入普通cml元件和任意.vue副檔名元件,路徑規則見元件配置
    • *wx.cml:base.usingComponents可以引入普通cml元件和普通微信小程式元件,路徑規則見元件配置
    • *weex.cml:base.usingComponents可以引入普通cml元件和任意.vue副檔名元件,路徑規則見元件配置

在各端對應的灰度區檔案中均可以根據上述規範使用各端的原生語法,但是為了規範仍然建議使用chameleon體系的語法規則。總體來說,灰度區可以認為是chameleon體系與各端原生元件/方法的銜接點,向下使用各端功能/元件,向上通過多型協議提供各端統一的呼叫介面。

繼續閱讀:

《編寫chameleon跨端元件的正確姿勢(下篇)》


相關文章