編寫易維護跨端組件的正確姿式

  • 上篇(利用各端第三方庫結合多態實現組件)css

    • 前期準備html

    • 組件開發 vue

    • 一些思考git

在chameleon項目中咱們實現一個跨端組件通常有兩種思路:使用各端第三方組件封裝基於chameleon語法統一實現。本篇是編寫chameleon跨端組件的正確姿式系列文章的上篇,以封裝一個跨端的indexlist組件爲例,首先介紹如何優雅的使用第三方庫封裝跨端組件,而後給出編寫chameleon跨端組件的建議。使用chameleon語法統一實現跨端組件請關注文章《編寫chameleon跨端組件的正確姿式(下篇)》github

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

所以,建議將經過第三方庫實現跨端組件庫做爲臨時方案,從長期維護的角度來說,建議開發者使用chameleon語法統一實現絕大部分跨端組件,只有一些特別複雜而且已有成熟第三方庫或者框架能力暫時不支持的組件,才考慮使用第三方組件封裝成對應的跨端組件。npm

因爲本文介紹的是使用第三方庫封裝跨端組件, 所以示例的indexlist組件採用第三方組件封裝來實現, 經過chameleon統一實現跨端組件的方法能夠看《編寫chameleon跨端組件的正確姿式(下篇)》json

最終實現的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跨端組件的正確姿式(下篇)》

相關文章
相關標籤/搜索