微信小程序开发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 })

 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 }

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

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

原文地址:https://www.cnblogs.com/yexiaochai/p/9393212.html

时间: 2024-10-10 16:28:09

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

微信小程序开发05-日历组件的实现

接上文:微信小程序开发04-打造自己的UI库 github地址:https://github.com/yexiaochai/wxdemo 我们这里继续实现我们的日历组件,这个日历组件稍微有点特殊,算是相对复杂的组件了,然后一般的日历组件又会有很多的变化,所以我们这里实现最基本的标签即可: 1 let View = require('behavior-view'); 2 const util = require('../utils/util.js'); 3 4 // const dateUtil

一号旺铺:国内第一个专注于微信小程序开发的后端云产品

启航 一号旺铺是天玑旗下继一号旺铺(wangpu1.com,媒体和小程序商店)后又一个基于微信生态的核心产品. 6 月 13 日,「一号旺铺」上线公测了. 「一号旺铺」www.wangpu1.com,是国内第一个专注于微信小程序开发的 BaaS(Backend as a Service)产品,它可以让开发者更快.更轻松地做出优美.稳定的小程序,且不失灵活性.爱范儿技术团队为此努力了 100 天,夜以继日,没有鸡腿. 爱范儿 CTO 在朋友圈写道:一号旺铺本是为了解决内部需求而设计的 BaaS 平

微信小程序开发教程,大多数人都搞错的八个问题

小程序目前被炒得沸沸扬扬,无数媒体和企业借机获取阅读流量. 这再次证明一点,微信想让什么火,真的就能让什么火.这种能力真是全中国再也没有人有了,政府也没有. 但四处传的消息很多是失真的,废话不说,先列出8个多数人都搞错的问题: 小程序是HTML5: 小程序是B/S的: 把M站改改就可以接入到小程序里: 小程序体验不佳: 小程序适合低频长尾应用: 小程序是新的Appstore: 小程序做不起来,需求不高: 小程序会做起来,但会和原生应用长期并存. 以上8个是很多人凭直觉得出的结论,但真正深度调研和

微信小程序开发对传统企业的10大好处

微信小程序的上线,大家更多讨论的话题是围绕微信小程序在互联网领域的影响,但是随着传统企业加入互联网的步伐当中,互联网的每一次大大小小的变革都会涉及到传统领域,那么作为传统企业的红利到底在哪儿呢?微信小程序开发对传统企业有什么影响?传统行业应该如何搭乘微信小程序的顺风车呢? 下面总结微信小程序开发对传统企业的10大好处 提供一个新的开发平台 微信小程序可以打通微信应用号,升级公众号的功能,并且微信小程序同时集成了APP store的功能,特别是对于传统企业来说APP的开成本较高,而且使用频率也比较

Java微信小程序开发_00_资源帖

1.微信小程序开发:http://blog.csdn.net/column/details/13721.html?&page=1 2.微信小程序栏目:http://blog.csdn.net/column/details/14653.html?&page=2 3.

微信小程序开发心得

微信小程序也已出来有一段时间了,最近写了几款微信小程序项目,今天来说说感受. 首先开发一款微信小程序,最主要的就是针对于公司来运营的,因为,在申请appid(微信小程序ID号)时候,需要填写相关的公司认证信息如,营业执照等 再次就是用一个未曾开通过公众号的QQ号或微信号来注册一个微信小程序号. 最后,下载微信小程序开发工具. 由于这里,我们更多的关注如何去开发一些app,而不是科谱微信小程序,故在此不在过多的解释,详细的说明,可以去官网帮助文档. 首先,我们拿自己的项目在一步一步的说明并开发吧,

微信小程序开发 --02

微信小程序在开发中,难度系数不是很大,其中应用的技术也是web开发中常用的技术,虽然在微信开发者工具中的叫法与常见的web开发的叫法不太一样. 首先,在微信小程序开发中,代码文件格式大体有以下四中: .wxml .wxss .js .json 首先,wxml后缀的文件类似于html和xml的结合,例如在html中常用的div在微信小程序开发中被替换成了view,而在html中输出文本用的p标签被替换成了text标签,引入图像由html中的img 标签变成了image标签,等等.如果你之前开发的w

微信小程序开发之数据存储 参数传递 数据缓存

微信小程序开发内测一个月.数据传递的方式很少.经常遇到页面销毁后回传参数的问题,小程序中并没有类似Android的startActivityForResult的方法,也没有类似广播这样的通讯方式,更没有类似eventbus的轮子可用. 现在已知传递参数的方法只找到三种,先总结下.由于正处于内测阶段,文档也不是很稳定,经常修改,目前尚没有人造轮子. 先上GIF: 1.APP.js 我把常用且不会更改的参数放在APP.js的data里面了.在各个page中都可以拿到var app = getApp(

微信小程序开发(3) 热门电影

在这篇微信小程序开发教程中,我们将介绍如何使用微信小程序开发热门电影及预览功能. 本文主要分为两个部分,小程序主体部分及计算器业务页面部分 一.小程序主体部分 一个小程序主体部分由三个文件组成,必须放在项目的根目录,如下: 1. 小程序逻辑 App({ onLaunch: function() { // Do something initial when launch. }, onShow: function() { // Do something when show. }, onHide: f