在chameleon项目中我们实现一个跨端组件一般有两种思路:使用第三方组件封装与基于chameleon语法统一实现。本篇是编写chameleon跨端组件的正确姿势系列文章的上篇,以封装一个跨端的indexlist组件为例,首先介绍如何优雅的使用第三方库封装跨端组件,然后给出编写chameleon跨端组件的建议。使用chameleon语法统一实现跨端组件请关注文章《编写chameleon跨端组件的正确姿势(下篇)》
依靠强大的多态协议,chameleon项目中可以轻松使用各端的第三方组件封装自己的跨端组件库。基于第三方组件可以利用现有生态迅速实现需求,但是却存在很多缺点,例如多端第三方组件本身的功能与样式差异、组件质量得不到保证以及绝大部分组件并不需要通过多态组件差异化实现,这样反而提升了长期的维护成本;使用chameleon语法统一实现则可以完美解决上述问题,并且扩展一个新的端时现有组件可以直接运行。本文的最后也会详细对比一下两种方案的优劣。
因此,建议将通过第三方库实现跨端组件库作为临时方案,从长期维护的角度来讲,建议开发者使用chameleon语法统一实现绝大部分跨端组件,只有一些特别复杂并且已有成熟第三方库或者框架能力暂时不支持的组件,才考虑使用第三方组件封装成对应的跨端组件。
由于本文介绍的是使用第三方库封装跨端组件, 因此示例的indexlist组件采用第三方组件封装来实现, 通过chameleon统一实现跨端组件的方法可以看《编写chameleon跨端组件的正确姿势(下篇)》。
最终实现的indexlist效果图:
前期准备
使用各端第三方组件实现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端:
-
wx端:
-
weex端:
除了上述组件库之外,开发者也可以根据自己的实际需求去寻找经过包装之后符合预期的第三方库。截止文章编写时,作者未找到较成熟的支付宝及百度小程序第三方库,因此暂时先实现web、微信小程序以及weex端,这也体现出了使用第三方库扩展跨端组件的局限性:当没有成熟的对应端第三方库时,无法完成该端的组件开发;而使用chameleon语法统一实现则可以解决上述问题,扩展新的端时已有组件能够直接运行,无需额外扩展。 本文在实现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组件的开发,效果如下:
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组件的开发,效果如下:
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组件的开发, 效果如下:
组件使用
修改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项目可以轻松结合第三方库封装自己的跨端组件库。使用第三方组件封装跨端组件库的步骤大致如下:
- 跨端组件设计
- 根据实际需求引入合适的第三方组件
- 根据第三方组件文档,将数据处理成符合预期的结构,并在适当时机抛出事件
- 编写必要样式
一些思考
理解*.[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文件
在各端对应的灰度区文件中均可以根据上述规范使用各端的原生语法,但是为了规范仍然建议使用chameleon体系的语法规则。总体来说,灰度区可以认为是chameleon体系与各端原生组件/方法的衔接点,向下使用各端功能/组件,向上通过多态协议提供各端统一的调用接口。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。