在chameleon项目中咱们实现一个跨端组件通常有两种思路:使用第三方组件封装与基于chameleon语法统一实现。
在《编写chameleon跨端组件的正确姿式(上篇)》中, 咱们介绍了如何使用第三方库封装跨端组件,可是绝大多数组件并不须要那样差别化实现,绝大多数状况下咱们推荐使用chameleon语法统一实现跨端组件。本篇是编写chameleon跨端组件的正确姿式系列文章的下篇,与上篇给出的示例相同,本篇也以封装一个跨端的indexlist组件为例,首先介绍如何使用chameleon语法统一实现一个跨端组件,而后对比两种组件开发方式并给出开发建议。javascript
如下效果依此为weex端、web端、支付宝小程序端、微信小程序端以及百度小程序端: css
建立一个新项目 cml-demohtml
cml init project
进入项目java
cd cml-demo
cml init component
选择“普通组件”, 并并输入组件名字“indexlist”, 完成组件的建立, 建立以后的组件位于src/components/indexlist文件夹下。git
为了方便说明,本例暂时实现一个具有基础功能的indexlist组件。从功能方面讲,indexlist组件主要由两部分组成,主列表区域和索引区域。在用户点击组件右侧索引时,主列表可以快速定位到对应区域;在用户滑动组件主列表时,右侧索引跟随滑动不停切换当前索引项。从输入输出方面讲,组件至少应该在用户选择某一项时抛出一个onselect事件,传递用户当前所选中项的数据;至少应该接受一个datalist,做为其渲染的数据源,这个datalist应该是一个相似于如下结构的对象数组:web
const dataList = [ { name: '阿里', pinYin: 'ali', }, { name: '北京', pinYin: 'beijing', }, ..... ]
根据设计的组件功能与输入输出, 咱们开始设计数据结构。
indexlist组件右侧的索引列对应的数据结构为一个数组,其中的每一项表示一个索引,具体结构以下:json
this.shortcut = [ 'A', 'B', 'C', ....]
indexlist组件的主列表区域对应的数据结构也是一个数组,其中的每一项表示一个子列表区域(例如以首字母a开头的子列表)。下面咱们考虑每个子列表区域中至少应该包含的字段:小程序
由上面分析可得主列表区域数据结构以下:segmentfault
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表示滚动时触发的事件。在主列表这一部分,咱们要实现以下功能:
修改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层面的不一样,建议使用多态接口抹平差别,而不使用多态组件 |