编写chameleon跨端组件的正确姿势(下篇)

440 阅读7分钟
  • 下篇(直接使用cml语法实现组件)

    • 组件开发

    • 两者对比

在chameleon项目中我们实现一个跨端组件一般有两种思路:使用第三方组件封装与基于chameleon语法统一实现。
在上篇中, 我们介绍了如何使用第三方库封装跨端组件,但是绝大多数组件并不需要那样差异化实现,绝大多数情况下我们推荐使用chameleon语法统一实现跨端组件。本篇是编写chameleon跨端组件的正确姿势系列文章的下篇,与上篇给出的示例相同,本篇也以封装一个跨端的indexlist组件为例,首先介绍如何使用chameleon语法统一实现一个跨端组件,然后对比两种组件开发方式并给出开发建议。

最终效果

以下效果依此为weex端、web端、支付宝小程序端、微信小程序端以及百度小程序端:

imgimgimg
img
img

开发

项目初始化

创建一个新项目 cml-demo

cml init project

进入项目

cd cml-demo

组件创建

cml init component

选择“普通组件”, 并并输入组件名字“indexlist”, 完成组件的创建, 创建之后的组件位于src/components/indexlist文件夹下。

组件设计

为了方便说明,本例暂时实现一个具备基础功能的indexlist组件。从功能方面讲,indexlist组件主要由两部分组成,主列表区域和索引区域。在用户点击组件右侧索引时,主列表能够快速定位到对应区域;在用户滑动组件主列表时,右侧索引跟随滑动不停切换当前索引项。从输入输出方面讲,组件至少应该在用户选择某一项时抛出一个onselect事件,传递用户当前所选中项的数据;至少应该接受一个datalist,作为其渲染的数据源,这个datalist应该是一个类似于以下结构的对象数组:

  const dataList = [
    {
      name: '阿里',
      pinYin: 'ali',
    }, {
      name: '北京',
      pinYin: 'beijing',
    },
    .....
 ]

主要数据结构设计

根据设计的组件功能与输入输出, 我们开始设计数据结构。
indexlist组件右侧的索引列对应的数据结构为一个数组,其中的每一项表示一个索引,具体结构如下:

this.shortcut = [ 'A', 'B', 'C', ....]

indexlist组件的主列表区域对应的数据结构也是一个数组,其中的每一项表示一个子列表区域(例如以首字母a开头的子列表)。下面我们考虑每一个子列表区域中至少应该包含的字段:

  • 一个name字段,表示该子列表区域的名称;
  • 一个items字段,该字段也是一个数组,数组中的每一项表示该子列表区域的每一项;
  • 一个offsetTop, 表示该子列表区域距离主列表顶部的距离,通过该字段实现点击右侧索引时能够通过滚动相应距离快速定位到该子列表;
  • 一个totalHeight字段,表示该子列表区域的所占的高度,通过该字段与offsetTop字段可以确定每个子列表所在的高度范围, 以此实现右侧索引跟随滑动不停切换当前索引项

由上面分析可得主列表区域数据结构如下:

  this.list = [
    {
      name: "B",
      items:[
        {
          name: "北京",
          pinYin: "beijing"
        },
        {
          name: "包头",
          pinYin: "baotou"
        }
        ...
      ],
      offsetTop: 190,
      totalHeight: 490
    },
    ....
  ]

功能实现

从前文可知,输入组件的datalist具有如下结构:

  const dataList = [
    {
      name: '阿里',
      pinYin: 'ali',
    }, {
      name: '北京',
      pinYin: 'beijing',
    },
    .....
 ]

可以发现该datalist结构是扁平并且缺乏很多信息(例如totalHeight等)的,因此首先要从输入数据中整理出来所需的数据结构,修改src/components/indexlist/indexlist.cml的js部分:

  initData() {
      // get shortcut
      this.dataList.forEach(item => {
        if (item.pinYin) {
          let firstName = item.pinYin.substring(0, 1);
          if (item.pinYin && this.shortcut.indexOf(firstName.toUpperCase()) === -1) {
            this.shortcut.push(firstName.toUpperCase());
          };
        };
      });  

      // handle input data
      const cityData = this.shortcut.map(item => ({items:[], name: item}));
      this.dataList.forEach((item) => {
        let firstName = item.pinYin.substring(0, 1).toUpperCase();
        let index = this.shortcut.indexOf(firstName);
        cityData[index].items.push(item);
      });
      
      // calculate item offsetTop && totalHeight
      cityData.forEach((item, index) => {
        let arr = cityData.slice(0, index);
        item.totalHeight = this.itemNameHeight + item.items.length * this.itemContentHeight;
        item.offsetTop = arr.reduce((total, cur) => (total + this.itemNameHeight + cur.items.length * this.itemContentHeight), 0);
      });
      this.list = cityData;
    },

这样我们就拿到了主列表数组this.list与索引列表数组this.shortcut, 然后根据数组结构编写模板内容。模板内容分为两大部分,一个是主列表区域,修改src/components/indexlist/indexlist.cml文件模板部分:

  <scroller
    height="{{-1}}"
    class="index-list-wrapper"
    scroll-top="{{offsetTop}}"
    c-bind:onscroll="handleScroll"
  >
    <view
      c-for="{{list}}"
      c-for-item="listitem"
      class="index-list-item"
    >
      <view class="index-list-item-name" style="{{compItemNameHeight}}">
        <text class="index-list-item-name-text">{{listitem.name}}</text>
      </view>
      <view 
        c-for="{{listitem.items}}" 
        c-for-item="subitem"
        class="index-list-item-content"
        style="{{compItemContentHeight}}"
        c-bind:tap="handleSelect(subitem)"
      >
        <text class="index-list-item-content-text"> {{subitem.name}}</text>
      </view>
    </view>
  </scroller>

其中scroller是一个chameleon提供的内置滚动组件,其属性值scrolltop表示当前滚动的距离,onscroll表示滚动时触发的事件。在主列表这一部分,我们要实现如下功能:

  • 在滚动时,右侧索引不停切换当前索引项的功能
  • 点击列表中的每一项时,向外抛出onselect事件

修改src/components/indexlist/indexlist.cml文件js部分:

    handleScroll(e) {
      let { scrollTop } = e.detail;
      scrollTop = Math.ceil(scrollTop);
      this.activeIndex = this.list.findIndex(item => scrollTop >= item.offsetTop && scrollTop < item.totalHeight + item.offsetTop )
    },
    handleSelect(e) {
      this.$cmlEmit('onselect', e)
    }

当前激活的索引(this.activeIndex)经过计算得到,规则为:如果当前scroller滚动的距离在对应子列表所在的高度范围内,则认为该索引是激活的。

另一部分是索引区域,修改src/components/indexlist/indexlist.cml文件模板部分,增加索引区域模板内容:

  <view 
    class="short-cut-wrapper" 
    style="{{compScwStyle}}"
  >
    <view 
      c-for="{{shortcut}}"
      class="short-cut-item"
      c-bind:tap="scrollToItem(item)"
    >
      <text class="short-cut-item-text" style="{{activeIndex === index ? 'color:orange' : ''}}">{{item}}</text>
    </view>
  </view>

在索引区域,我们要实现点击索引值主列表能够快速定位到对应区域,修改src/components/indexlist/indexlist.cml文件js部分:

    scrollToItem(shortcut) {
      let { offsetTop } = this.list.find(item => item.name === shortcut);
      this.offsetTop = offsetTop;
    }

索引区域应该定位在视窗右侧并且上下居中。由于chameleon暂时不支持在css中使用百分比,因此我们通过chameleon-api提供的对外接口获取屏幕视窗高度,然后使用js计算得到位置, 配合部分css来实现索引区域定位在视窗右侧居中。修改src/components/indexlist/indexlist.cml文件js部分:

    // computed
    compScwStyle() {
      return `top:${this.viewportHeight / 2}cpx`
    }

    // method
    async getViewportHeight() {
      let res = await cml.getSystemInfo();
      this.viewportHeight = res.viewportHeight;
    },

至此便通过chameleon语法统一实现了一个跨端indexlist组件,该组件直接可以在web、weex、微信小程序、支付宝小程序与百度小程序五个端运行。为了方便描述,上述代码只是简单介绍了组件实现的核心代码,跳过了样式和一些逻辑细节。

组件使用

修改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是一个对象数组,表示组件要渲染的数据源

一些思考

本篇文章主要介绍了如何通过chameleon语法实现跨端组件。对比编写chameleon跨端组件的正确姿势(上篇).md)介绍的通过第三方库封装的方法可以发现,两种方式是完全不同的,现详细对比一下这两种实现方式的优势与劣势, 并给出开发建议:

优势劣势开发建议
基于第三方组件库实现- 可利用已有生态迅速完成跨端组件- 组件的实现依赖第三方库,如果没有成熟的对应端第三方库则无法完成该端组件开发

- 由于各端第三方组件存在差异,封装的跨端组件样式与功能存在差异

- 第三方组件升级时,要对应调整跨端组件的实现,维护成本较大

- 第三方组件库质量不能得到保证

- 将基于各端第三方组件封装跨端组件库的方法作为临时方案

- 对于特别复杂并且已有成熟第三方库或者框架能力暂时不支持的组件,可以考虑使用第三方组件封装成对应的跨端组件,例如图表组件、地图组件等等

基于chameleon统一实现- 新的端接入时,能够直接运行

- 一般情况下,不存在各端样式与功能差异

- 绝大部分组件不需要各端差异化实现,使用chameleon语法实现开发与维护成本更低

- 能够导出原生组件供多端使用

- 从零搭建时间与技术成本较高从长期维护的角度来讲,建议使用chameleon生态来统一实现跨端组件库

如果仅仅是各端api层面的不同,建议使用多态接口抹平差异,而不使用多态组件