微信小程序开发04-打造自己的UI库

前言

github地址:https://github.com/yexiaochai/wxdemo

接上文继续,我们前面学习了小程序的生命周期、小程序的标签、小程序的样式,后面我们写了一个简单的loading组件,显然他是个半成品,我们在做loading组件的时候意识到一个问题:

小程序的组件事实上是标签
我们没有办法获得标签的实例,至少我暂时没有办法
所以这些前提让我们对标签的认识有很大的不同,完成小程序特有的UI库,那么就需要从标签出发
这里面关注的点从js中的实例变成了wxml中的属性

我们今天尝试做几个组件,然后先做未完成的loading,然后做消息类弹出组件,然后做日历组件,我希望在这个过程中,我们形成一套可用的体系,这里涉及了组件体系,我们可能需要整理下流程:

① 首先我们这里做的组件其实是“标签”,这个时候就要考虑引入时候的怎么处理了

② 因为写业务页面的同事(写page的同事),需要在json配置中引入需要使用的标签:

"usingComponents": {
  "ui-loading": "/components/ui-loading"
}

因为不能动态插入标签,所以需要一开始就把标签放入页面wxml中:

<ui-loading is-show="{{isLoadingShow}}"></ui-loading>

③ json中的配置暂时只能拷贝,但是我们可以提供一个ui-set.wxml来动态引入一些组件,如全局使用的loading弹出类提示框

④ 像日历类组件或者平时用的比较少的弹出层组件便需要自己在页面中引入了,工作量貌似不大,后续看看情况,如何优化

⑤ 我们这里给每个组件设置一个behaviors,behaviors原则只设置一层(这里有点继承的关系),层级多了变比较复杂了,弹出层类是一个、一般类一个(用于日历类组件)

有了以上标准,我们这里先来改造我们的loading组件

⑥ 默认所有的组件初期WXSS直接设置为隐藏

改造loading

这里首先改造弹出层都要继承的behaviors behavior-layer:

 1 const util = require('../utils/util.js')
 2 module.exports = Behavior({
 3   properties: {
 4     //重要属性,每个组件必带,定义组件是否显示
 5     isShow: {
 6       type: String
 7     }
 8   },
 9   //这里设置弹出层必须带有一个遮盖层,所以每个弹出层都一定具有有个z-index属性
10   data: {
11     maskzIndex: util.getBiggerzIndex(),
12     uiIndex: util.getBiggerzIndex()
13   },
14   attached: function() {
15     console.log('layer')
16   },
17   methods: {
18   }
19 })

其次我们改造下我们的mask组件:

 1 let LayerView = require('behavior-layer')
 2 Component({
 3   behaviors: [LayerView],
 4   properties: {
 5     //只有mask的z-index属性需要被调用的弹出层动态设置
 6     zIndex: {
 7       type: String
 8     }
 9   },
10   data: {
11   },
12   attached: function () { 
13     console.log('mask')
14   },
15   methods: {
16     onTap: function() {
17       this.triggerEvent('customevent', {}, {})
18     }
19   }
20 })

WXML不做变化,便完成了我们的代码,并且结构关系似乎更加清晰了,但是作为loading组件其实是有个问题的,比如点击遮盖层要不要关闭整个组件,像类似这种点击遮盖层要不要关闭整个组件,其实该是一个公共属性,所以我们对我们的layer、mask继续进行改造(这里具体请看github代码):

 1 const util = require('../utils/util.js')
 2 module.exports = Behavior({
 3   properties: {
 4     //重要属性,每个组件必带,定义组件是否显示
 5     isShow: {
 6       type: String
 7     }
 8   },
 9   //这里设置弹出层必须带有一个遮盖层,所以每个弹出层都一定具有有个z-index属性
10   data: {
11     maskzIndex: util.getBiggerzIndex(),
12     uiIndex: util.getBiggerzIndex(),
13     //默认点击遮盖层不关闭组件
14     clickToHide: false
15   },
16   attached: function() {
17     console.log('layer')
18   },
19   methods: {
20   }
21 })
 1 methods: {
 2   onMaskEvent: function (e) {
 3     console.log(e);
 4     //如果设置了点击遮盖层关闭组件则关闭
 5     if (this.data.clickToHide)
 6       this.setData({
 7         isShow: 'none'
 8       });
 9   }
10 }

这个时候,点击要不要关闭,基本就在组件里面设置一个属性即可,但是我们这个作为了内部属性,没有释放出去,这个时候我们也许发现了另外一个比较幽默的场景了:

我们因为没法获取一个标签的实例,所以我们需要在页面里面动态调用:

 1 onShow: function() {
 2   let scope= this;
 3   this.setData({
 4     isLoadingShow: ''
 5   });
 6   //3秒后关闭loading
 7   setTimeout(function () {
 8     scope.setData({
 9       isLoadingShow: 'none'
10     });
11   }, 3000);
12 },

可以看到,标签接入到页面后,控制标签事实上是动态操作他的属性,也就是说操作页面的状态数据,页面的UI变化全部是数据触发,这样的逻辑会让界面变得更加清晰,但是作为全局类的loading这种参数,我并不想放到各个页面中,因为这样会导致很多重复代码,于是我在utils目录中新建了一个ui-util的工具类,作为一些全局类的ui公共库:

 1 //因为小程序页面中每个页面应该是独立的作用域
 2 class UIUtil {
 3   constructor(opts) {
 4     //用于存储各种默认ui属性
 5     this.isLoadingShow = 'none';
 6   }
 7   //产出页面loading需要的参数
 8   getPageData() {
 9     return {
10       isLoadingShow: this.isLoadingShow
11     }
12   }
13   //需要传入page实例
14   showLoading(page) {
15     this.isLoadingShow = '';
16     page.setData({
17       isLoadingShow: this.isLoadingShow
18     });
19   }
20   //关闭loading
21   hideLoading(page) {
22     this.isLoadingShow = 'none';
23     page.setData({
24       isLoadingShow: this.isLoadingShow
25     });
26   }
27 }
28 
29 //直接返回一个UI工具了类的实例
30 module.exports = new UIUtil

index.js使用上产生一点变化:

 1 //获取公共ui操作类实例
 2 const uiUtil = require('../../utils/ui-util.js');
 3 //获取应用实例
 4 const app = getApp()
 5 Page({
 6   data: uiUtil.getPageData(),
 7   onShow: function() {
 8     let scope= this;
 9     uiUtil.showLoading(this);
10     //3秒后关闭loading
11     setTimeout(function () {
12       uiUtil.hideLoading(scope);
13     }, 3000);
14   },
15   onLoad: function () {
16   }
17 })

这样,我们将页面里面要用于操作组件的数据全部放到了一个util类中,这样代码会变得清晰一些,组件管理也放到了一个地方,只是命名规范一定要安规则来,似乎到这里,我们的loading组件改造结束了,这里却有一个问题,我们在ui-util类中存储的事实上是页面级的数据,其中包含是组件的状态,但是真实情况我们点击遮盖层关闭组件,根本不会知会page层的数据,这个时候我们loading的显示状态搞不好是显示,而真实的组件已经关闭了,如何保证状态统一我们后面点再说,我暂时没有想到好的办法

toast组件

我们现在先继续作toast组件,toast组件一样包含一个遮盖层,但是点击的时候可以关闭遮盖层,显示3秒后关闭,显示多久关闭的属性应该是可以配置的(作为属性传递),所以我们新增组件:

 1 const util = require('../utils/util.js');
 2 let LayerView = require('behavior-layer');
 3 
 4 Component({
 5   behaviors: [
 6     LayerView
 7   ],
 8   properties: {
 9     message: {
10       type: String
11     }
12   },
13   data: {
14   },
15   attached: function () { 
16     console.log(this)
17   },
18   methods: {
19     onMaskEvent: function (e) {
20       console.log(e);
21       //如果设置了点击遮盖层关闭组件则关闭
22       if (this.data.clickToHide)
23         this.setData({
24           isShow: 'none'
25         });
26     }
27   }
28 })

整体代码请各位在git上面去看,这里也引起了一些问题:

① 我的组件如何居中?

② 一般来说toast消失的时候是可以定制化一个事件回调的,我们这里怎么实现?

这里我们先抛开居中问题,我们先来解决第二个问题,因为小程序中没有addEventListener这个方法,所以能够改变组件特性的方式只剩下数据操作,回顾我们这里可以引起组件隐藏的点只有:

① toast中的点击弹出层时改变显示属性

1 onMaskEvent: function (e) {
2   console.log(e);
3   //如果设置了点击遮盖层关闭组件则关闭
4   if (this.data.clickToHide)
5     this.setData({
6       isShow: 'none'
7     });
8 }

② 然后就是页面中动态改变数据属性了:

1 onShow: function() {
2   let scope= this;
3   uiUtil.showToast(this, '我是美丽可爱的toast');
4   //3秒后关闭loading
5   setTimeout(function () {
6     uiUtil.hideToast(scope);
7   }, 3000);
8 },

这里,我们不得不处理之前的数据同步问题了,我们应该给toast提供一个事件属性可定义的点,点击遮盖层的真正处理逻辑需要放到page层,其实认真思考下,标签就应该很纯粹,不应该与业务相关,只需要提供钩子,与业务相关的是page中的业务,这个时候大家可以看到我们代码之间的关联是多么的复杂了:

① 页面index.js依赖于index.wxml中组件的标签,并且依赖于uiUtil这个工具类

② 单单一个toast组件(标签)便依赖了mask标签,一个工具栏,还有基础的layer behavior

③ 因为不能获取实例,所以组件直接通信只能通过标签的bindevent的做法,让情况变得更加诡异

从这里看起来,调用方式也着实太复杂了,而这还仅仅是一个简单的组件,这个是不是我们写法有问题呢?答案是!我的思路还是以之前做js的组件的思路,但是小程序暂时不支持动态插入标签,所以我们不应该有过多的继承关系,其中的mask是没有必要的;另一方面,每个页面要动态引入ui-utils这个莫名其妙的组件库,似乎也很别扭,所以我们这里准备进行改造,降低没有必要的复杂度

组件改造

经过思考,我们这里准备做以下优化(PS:我小程序也是上星期开始学习的,需要逐步摸索):

① 保留mask组件,但是去除toast、loading类组件与其关联,将WXML以及样式直接内联,使用空间复杂度降低代码复杂度

② 取消ui-uitil攻击类,转而实现一个page基类

我们这里先重新实现toast组件:

 1 //behavior-layer
 2 const util = require('../utils/util.js')
 3 module.exports = Behavior({
 4   properties: {
 5     //重要属性,每个组件必带,定义组件是否显示
 6     isShow: {
 7       type: String
 8     }
 9   },
10   //这里设置弹出层必须带有一个遮盖层,所以每个弹出层都一定具有有个z-index属性
11   data: {
12     maskzIndex: util.getBiggerzIndex(),
13     uiIndex: util.getBiggerzIndex(),
14     //默认点击遮盖层不关闭组件
15     clickToHide: true
16   },
17   attached: function() {
18     console.log('layer')
19   },
20   methods: {
21     onMaskEvent: function (e) {
22       this.triggerEvent('maskevent', e, {})
23     }
24   }
25 })
View Code
 1 .cm-overlay {
 2     background: rgba(0, 0, 0, 0.5);
 3     position: fixed;
 4     top: 0;
 5     right: 0;
 6     bottom: 0;
 7     left: 0;
 8 }
 9 
10 .cm-modal {
11   background-color: #fff;
12   overflow: hidden;
13   width: 100%;
14   border-radius: 8rpx;
15 }
16 
17 .cm-modal--toast {
18   width: auto;
19   margin-top: -38rpx;
20   background: rgba(0, 0, 0, 0.7);
21   color: #fff;
22   padding: 20rpx 30rpx;
23   text-align: center;
24   font-size: 24rpx;
25   white-space: nowrap;
26   position: fixed;
27   top: 50%;
28   left: 50%;
29 
30 }
31 .cm-modal--toast .icon-right {
32   display: inline-block;
33   margin: 10rpx 0 24rpx 10rpx;
34 }
35 .cm-modal--toast .icon-right::before {
36   content: "";
37   display: block;
38   width: 36rpx;
39   height: 16rpx;
40   border-bottom: 4rpx solid #fff;
41   border-left: 4rpx solid #fff;
42   -webkit-transform: rotate(-45deg);
43           transform: rotate(-45deg);
44   -webkit-box-sizing: border-box;
45           box-sizing: border-box;
46 }
View Code
1 <section class="cm-modal cm-modal--toast" style="z-index: {{uiIndex}}; display: {{isShow}}; ">
2   {{message}}
3 </section>
4 <view class="cm-overlay" bindtap="onMaskEvent" style="z-index: {{maskzIndex}}; display: {{isShow}}" >
5 </view>
 1 const util = require('../utils/util.js');
 2 let LayerView = require('behavior-layer');
 3 Component({
 4   behaviors: [
 5     LayerView
 6   ],
 7   properties: {
 8     message: {
 9       type: String
10     }
11   },
12   data: {
13   },
14   attached: function () { 
15     console.log(this)
16   },
17   methods: {
18   }
19 })

页面层的使用不必变化就已经焕然一新了,这个时候我们开始做ui-util与page关系的改造,看看能不能让我们的代码变得简单,我这里的思路是设计一个公共的abstract-view出来,做所有页面的基类:

 1 class Page {
 2     constructor(opts) {
 3         //用于基础page存储各种默认ui属性
 4         this.isLoadingShow = 'none';
 5         this.isToastShow = 'none';
 6         this.toastMessage = 'toast提示';
 7 
 8         //通用方法列表配置,暂时约定用于点击
 9         this.methodSet = [
10             'onToastHide', 'showToast', 'hideToast', 'showLoading', 'hideLoading'
11         ];
12 
13         //当前page对象
14         this.page = null;
15     }
16     initPage(pageData) {
17         //debugger;
18 
19         let _pageData = {};
20 
21         //为页面动态添加操作组件的方法
22         Object.assign(_pageData, this.getPageFuncs(), pageData);
23 
24         //生成真实的页面数据
25         _pageData.data = {};
26         Object.assign(_pageData.data, this.getPageData(), pageData.data || {});
27 
28         console.log(_pageData);
29         return _pageData;
30     }
31     //当关闭toast时触发的事件
32     onToastHide(e) {
33         this.hideToast();
34     }
35     //设置页面可能使用的方法
36     getPageFuncs() {
37         let funcs = {};
38         for (let i = 0, len = this.methodSet.length; i < len; i++ ) {
39             funcs[this.methodSet[i]] = this[this.methodSet[i]];
40         }
41         return funcs;
42     }
43     //产出页面组件需要的参数
44     getPageData() {
45         return {
46             isLoadingShow: this.isLoadingShow,
47             isToastShow: this.isToastShow,
48             toastMessage: this.toastMessage
49         }
50     }
51     showToast(message) {
52         this.setData({
53             isToastShow: '',
54             toastMessage: message
55         });
56     }
57     hideToast() {
58         this.setData({
59             isToastShow: 'none'
60         });
61     }
62     //需要传入page实例
63     showLoading() {
64         this.setData({
65             isLoadingShow: ''
66         });
67     }
68     //关闭loading
69     hideLoading() {
70         this.setData({
71             isLoadingShow: 'none'
72         });
73     }
74 }
75 //直接返回一个UI工具了类的实例
76 module.exports = new Page
abstract-view

这里还提供了一个公共模板用于被页面include,abstract-view.wxml:

<ui-toast bindonToastHide="onToastHide" is-show="{{isToastShow}}" message="{{toastMessage}}"></ui-toast>

页面调用时候的代码发生了很大的变化:

<import src="./mod.searchbox.wxml" />
<view>
  <template is="searchbox" />
</view>
<include src="../../utils/abstract-page.wxml"/>
 1 //获取公共ui操作类实例
 2 const _page = require('../../utils/abstract-page.js');
 3 //获取应用实例
 4 const app = getApp()
 5 
 6 Page(_page.initPage({
 7   data: {
 8     ttt: 'ttt'
 9 
10   },
11   // methods: uiUtil.getPageMethods(),
12   methods: {
13   },
14   onShow: function () {
15      let scope = this;
16      this.showToast('我是美丽可爱的toast');
17      // 3秒后关闭loading
18     //  setTimeout(function () {
19     //    scope.hideToast();
20     //  }, 3000);
21   },
22   onLoad: function () {
23     // this.setPageMethods();
24   }
25 }))

这样我们相当于变相给page赋能了,详情请各位看github上的代码:https://github.com/yexiaochai/wxdemo,这个时候,我们要为toast组件添加关闭时候的事件回调,就变得相对简单了,事实上我们可以看到这个行为已经跟组件本身没有太多关系了:

 1 showToast(message, callback) {
 2   this.toastHideCallback = null;
 3   if (callback) this.toastHideCallback = callback;
 4   let scope = this;
 5   this.setData({
 6     isToastShow: '',
 7     toastMessage: message
 8   });
 9 
10   // 3秒后关闭loading
11   setTimeout(function () {
12     scope.hideToast();
13   }, 3000);
14 }
15 hideToast() {
16   this.setData({
17     isToastShow: 'none'
18   });
19   if (this.toastHideCallback) this.toastHideCallback.call(this);
20 }
this.showToast('我是美丽可爱的toast', function () { console.log('执行回调')} );

当然这里可以做得更加人性化,比如显示时间是根据message长度动态设置的,我们这里先这样。

alert类组件

本篇篇幅已经比较长了,我们最后完成一个alert组件便结束今天的学习,明天主要实现日历等组件,alert组件一般是一个带确定框的提示弹出层,有可能有两个按钮,那个情况要稍微复杂点,我们这里依旧为其新增组件结构wxml以及wxss:

 1 //获取公共ui操作类实例
 2 const _page = require('../../utils/abstract-page.js');
 3 //获取应用实例
 4 const app = getApp()
 5 
 6 Page(_page.initPage({
 7   data: {
 8   },
 9   // methods: uiUtil.getPageMethods(),
10   methods: {
11   },
12   onShow: function () {
13     global.sss = this;
14     let scope = this;
15     this.showMessage({
16       message: '我是一个确定框',
17       ok: {
18         name: '确定',
19         callback: function () {
20           scope.hideMessage();
21           scope.showMessage('我选择了确定');
22         }
23       },
24       cancel: {
25         name: '取消',
26         callback: function () {
27           scope.hideMessage();
28           scope.showToast('我选择了取消');
29         }
30       }
31     });
32 
33   },
34   onLoad: function () {
35     // this.setPageMethods();
36   }
37 }))

结语

github地址:https://github.com/yexiaochai/wxdemo

今天我们似乎找到了一个适合小程序的组件编写方式,明天我们继续完成一些组件,组件完成后我们便开始写实际业务代码了

posted on 2018-08-01 18:33  叶小钗  阅读(9330)  评论(0编辑  收藏  举报