乐趣区

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

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

开发
项目初始化
创建一个新项目 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 层面的不同,建议使用多态接口抹平差异,而不使用多态组件

退出移动版