在 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 端:
cube-ui
vux
mint-ui
vant
wx 端:
iview weapp
vant weapp
weui
weex 端:
weex-ui
除了上述组件库之外,开发者也可以根据自己的实际需求去寻找经过包装之后符合预期的第三方库。截止文章编写时,作者未找到较成熟的支付宝及百度小程序第三方库,因此暂时先实现 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 组件的开发, 效果如下:
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 文件
*web.cml:base.usingComponents 可以引入普通 cml 组件和任意.vue 扩展名组件,路径规则见组件配置。
*wx.cml:base.usingComponents 可以引入普通 cml 组件和普通微信小程序组件,路径规则见组件配置。
*weex.cml:base.usingComponents 可以引入普通 cml 组件和任意.vue 扩展名组件,路径规则见组件配置。
在各端对应的灰度区文件中均可以根据上述规范使用各端的原生语法,但是为了规范仍然建议使用 chameleon 体系的语法规则。总体来说,灰度区可以认为是 chameleon 体系与各端原生组件 / 方法的衔接点,向下使用各端功能 / 组件,向上通过多态协议提供各端统一的调用接口。