github地址:https://github.com/yexiaochai/wxdemogit
接上文继续,咱们前面学习了小程序的生命周期、小程序的标签、小程序的样式,后面咱们写了一个简单的loading组件,显然他是个半成品,咱们在作loading组件的时候意识到一个问题:github
小程序的组件事实上是标签
咱们没有办法得到标签的实例,至少我暂时没有办法
因此这些前提让咱们对标签的认识有很大的不一样,完成小程序特有的UI库,那么就须要从标签出发
这里面关注的点从js中的实例变成了wxml中的属性
咱们今天尝试作几个组件,而后先作未完成的loading,而后作消息类弹出组件,而后作日历组件,我但愿在这个过程当中,咱们造成一套可用的体系,这里涉及了组件体系,咱们可能须要整理下流程:web
① 首先咱们这里作的组件实际上是“标签”,这个时候就要考虑引入时候的怎么处理了json
② 由于写业务页面的同事(写page的同事),须要在json配置中引入须要使用的标签:小程序
"usingComponents": { "ui-loading": "/components/ui-loading" }
由于不能动态插入标签,因此须要一开始就把标签放入页面wxml中:app
<ui-loading is-show="{{isLoadingShow}}"></ui-loading>
③ json中的配置暂时只能拷贝,可是咱们能够提供一个ui-set.wxml来动态引入一些组件,如全局使用的loading弹出类提示框xss
④ 像日历类组件或者平时用的比较少的弹出层组件便须要本身在页面中引入了,工做量貌似不大,后续看看状况,如何优化ide
⑤ 咱们这里给每一个组件设置一个behaviors,behaviors原则只设置一层(这里有点继承的关系),层级多了变比较复杂了,弹出层类是一个、通常类一个(用于日历类组件)工具
有了以上标准,咱们这里先来改造咱们的loading组件学习
⑥ 默认全部的组件初期WXSS直接设置为隐藏
这里首先改造弹出层都要继承的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组件同样包含一个遮盖层,可是点击的时候能够关闭遮盖层,显示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
这里还提供了一个公共模板用于被页面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组件通常是一个带肯定框的提示弹出层,有可能有两个按钮,那个状况要稍微复杂点,咱们这里依旧为其新增组件结构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
今天咱们彷佛找到了一个适合小程序的组件编写方式,明天咱们继续完成一些组件,组件完成后咱们便开始写实际业务代码了