微信小程序开发(4):打造自己的UI库

作者:叶小钗 

http://www.cnblogs.com/yexiaochai/p/9393212.html


前言


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:


const util = require('../utils/util.js')

module.exports = Behavior({

  properties: {

    //重要属性,每个组件必带,定义组件是否显示

    isShow: {

      type: String

    }

  },

  //这里设置弹出层必须带有一个遮盖层,所以每个弹出层都一定具有有个z-index属性

  data: {

    maskzIndex: util.getBiggerzIndex(),

    uiIndex: util.getBiggerzIndex()

  },

  attached: function() {

    console.log('layer')

  },

  methods: {

  }

})


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


let LayerView = require('behavior-layer')

Component({

  behaviors: [LayerView],

  properties: {

    //只有mask的z-index属性需要被调用的弹出层动态设置

    zIndex: {

      type: String

    }

  },

  data: {

  },

  attached: function () {

    console.log('mask')

  },

  methods: {

    onTap: function() {

      this.triggerEvent('customevent', {}, {})

    }

  }

})


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


const util = require('../utils/util.js')

module.exports = Behavior({

  properties: {

    //重要属性,每个组件必带,定义组件是否显示

    isShow: {

      type: String

    }

  },

  //这里设置弹出层必须带有一个遮盖层,所以每个弹出层都一定具有有个z-index属性

  data: {

    maskzIndex: util.getBiggerzIndex(),

    uiIndex: util.getBiggerzIndex(),

    //默认点击遮盖层不关闭组件

    clickToHide: false

  },

  attached: function() {

    console.log('layer')

  },

  methods: {

  }

})


methods: {

  onMaskEvent: function (e) {

    console.log(e);

    //如果设置了点击遮盖层关闭组件则关闭

    if (this.data.clickToHide)

      this.setData({

        isShow: 'none'

      });

  }

}


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


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


onShow: function() {

  let scope= this;

  this.setData({

    isLoadingShow: ''

  });

  //3秒后关闭loading

  setTimeout(function () {

    scope.setData({

      isLoadingShow: 'none'

    });

  }, 3000);

},


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


//因为小程序页面中每个页面应该是独立的作用域

class UIUtil {

  constructor(opts) {

    //用于存储各种默认ui属性

    this.isLoadingShow = 'none';

  }

  //产出页面loading需要的参数

  getPageData() {

    return {

      isLoadingShow: this.isLoadingShow

    }

  }

  //需要传入page实例

  showLoading(page) {

    this.isLoadingShow = '';

    page.setData({

      isLoadingShow: this.isLoadingShow

    });

  }

  //关闭loading

  hideLoading(page) {

    this.isLoadingShow = 'none';

    page.setData({

      isLoadingShow: this.isLoadingShow

    });

  }

}


//直接返回一个UI工具了类的实例

module.exports = new UIUtil


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


//获取公共ui操作类实例

const uiUtil = require('../../utils/ui-util.js');

//获取应用实例

const app = getApp()

Page({

  data: uiUtil.getPageData(),

  onShow: function() {

    let scope= this;

    uiUtil.showLoading(this);

    //3秒后关闭loading

    setTimeout(function () {

      uiUtil.hideLoading(scope);

    }, 3000);

  },

  onLoad: function () {

  }

})


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


toast组件


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



const util = require('../utils/util.js');

let LayerView = require('behavior-layer');


Component({

  behaviors: [

    LayerView

  ],

  properties: {

    message: {

      type: String

    }

  },

  data: {

  },

  attached: function () {

    console.log(this)

  },

  methods: {

    onMaskEvent: function (e) {

      console.log(e);

      //如果设置了点击遮盖层关闭组件则关闭

      if (this.data.clickToHide)

        this.setData({

          isShow: 'none'

        });

    }

  }

})



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


① 我的组件如何居中?


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


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


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


onMaskEvent: function (e) {

  console.log(e);

  //如果设置了点击遮盖层关闭组件则关闭

  if (this.data.clickToHide)

    this.setData({

      isShow: 'none'

    });

}


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


onShow: function() {

  let scope= this;

  uiUtil.showToast(this, '我是美丽可爱的toast');

  //3秒后关闭loading

  setTimeout(function () {

    uiUtil.hideToast(scope);

  }, 3000);

},


这里,我们不得不处理之前的数据同步问题了,我们应该给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组件:


//behavior-layer

const util = require('../utils/util.js')

module.exports = Behavior({

  properties: {

    //重要属性,每个组件必带,定义组件是否显示

    isShow: {

      type: String

    }

  },

  //这里设置弹出层必须带有一个遮盖层,所以每个弹出层都一定具有有个z-index属性

  data: {

    maskzIndex: util.getBiggerzIndex(),

    uiIndex: util.getBiggerzIndex(),

    //默认点击遮盖层不关闭组件

    clickToHide: true

  },

  attached: function() {

    console.log('layer')

  },

  methods: {

    onMaskEvent: function (e) {

      this.triggerEvent('maskevent', e, {})

    }

  }

})


.cm-overlay {

    background: rgba(0, 0, 0, 0.5);

    position: fixed;

    top: 0;

    right: 0;

    bottom: 0;

    left: 0;

}


.cm-modal {

  background-color: #fff;

  overflow: hidden;

  width: 100%;

  border-radius: 8rpx;

}


.cm-modal--toast {

  width: auto;

  margin-top: -38rpx;

  background: rgba(0, 0, 0, 0.7);

  color: #fff;

  padding: 20rpx 30rpx;

  text-align: center;

  font-size: 24rpx;

  white-space: nowrap;

  position: fixed;

  top: 50%;

  left: 50%;


}

.cm-modal--toast .icon-right {

  display: inline-block;

  margin: 10rpx 0 24rpx 10rpx;

}

.cm-modal--toast .icon-right::before {

  content: "";

  display: block;

  width: 36rpx;

  height: 16rpx;

  border-bottom: 4rpx solid #fff;

  border-left: 4rpx solid #fff;

  -webkit-transform: rotate(-45deg);

          transform: rotate(-45deg);

  -webkit-box-sizing: border-box;

          box-sizing: border-box;

}


<section class="cm-modal cm-modal--toast" style="z-index: {{uiIndex}}; display: {{isShow}}; ">

  {{message}}

</section>

<view class="cm-overlay" bindtap="onMaskEvent" style="z-index: {{maskzIndex}}; display: {{isShow}}" >

</view>


const util = require('../utils/util.js');

let LayerView = require('behavior-layer');

Component({

  behaviors: [

    LayerView

  ],

  properties: {

    message: {

      type: String

    }

  },

  data: {

  },

  attached: function () {

    console.log(this)

  },

  methods: {

  }

})


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



class Page {

    constructor(opts) {

        //用于基础page存储各种默认ui属性

        this.isLoadingShow = 'none';

        this.isToastShow = 'none';

        this.toastMessage = 'toast提示';


        //通用方法列表配置,暂时约定用于点击

        this.methodSet = [

            'onToastHide', 'showToast', 'hideToast', 'showLoading', 'hideLoading'

        ];


        //当前page对象

        this.page = null;

    }

    initPage(pageData) {

        //debugger;


        let _pageData = {};


        //为页面动态添加操作组件的方法

        Object.assign(_pageData, this.getPageFuncs(), pageData);


        //生成真实的页面数据

        _pageData.data = {};

        Object.assign(_pageData.data, this.getPageData(), pageData.data || {});


        console.log(_pageData);

        return _pageData;

    }

    //当关闭toast时触发的事件

    onToastHide(e) {

        this.hideToast();

    }

    //设置页面可能使用的方法

    getPageFuncs() {

        let funcs = {};

        for (let i = 0, len = this.methodSet.length; i < len; i++ ) {

            funcs[this.methodSet[i]] = this[this.methodSet[i]];

        }

        return funcs;

    }

    //产出页面组件需要的参数

    getPageData() {

        return {

            isLoadingShow: this.isLoadingShow,

            isToastShow: this.isToastShow,

            toastMessage: this.toastMessage

        }

    }

    showToast(message) {

        this.setData({

            isToastShow: '',

            toastMessage: message

        });

    }

    hideToast() {

        this.setData({

            isToastShow: 'none'

        });

    }

    //需要传入page实例

    showLoading() {

        this.setData({

            isLoadingShow: ''

        });

    }

    //关闭loading

    hideLoading() {

        this.setData({

            isLoadingShow: 'none'

        });

    }

}

//直接返回一个UI工具了类的实例

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"/>


//获取公共ui操作类实例

const _page = require('../../utils/abstract-page.js');

//获取应用实例

const app = getApp()


Page(_page.initPage({

  data: {

    ttt: 'ttt'


  },

  // methods: uiUtil.getPageMethods(),

  methods: {

  },

  onShow: function () {

     let scope = this;

     this.showToast('我是美丽可爱的toast');

     // 3秒后关闭loading

    //  setTimeout(function () {

    //    scope.hideToast();

    //  }, 3000);

  },

  onLoad: function () {

    // this.setPageMethods();

  }

}))


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


showToast(message, callback) {

  this.toastHideCallback = null;

  if (callback) this.toastHideCallback = callback;

  let scope = this;

  this.setData({

    isToastShow: '',

    toastMessage: message

  });


  // 3秒后关闭loading

  setTimeout(function () {

    scope.hideToast();

  }, 3000);

}

hideToast() {

  this.setData({

    isToastShow: 'none'

  });

  if (this.toastHideCallback) this.toastHideCallback.call(this);

}


this.showToast('我是美丽可爱的toast', function () { console.log('执行回调')} );


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


alert类组件


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


//获取公共ui操作类实例

const _page = require('../../utils/abstract-page.js');

//获取应用实例

const app = getApp()


Page(_page.initPage({

  data: {

  },

  // methods: uiUtil.getPageMethods(),

  methods: {

  },

  onShow: function () {

    global.sss = this;

    let scope = this;

    this.showMessage({

      message: '我是一个确定框',

      ok: {

        name: '确定',

        callback: function () {

          scope.hideMessage();

          scope.showMessage('我选择了确定');

        }

      },

      cancel: {

        name: '取消',

        callback: function () {

          scope.hideMessage();

          scope.showToast('我选择了取消');

        }

      }

    });


  },

  onLoad: function () {

    // this.setPageMethods();

  }

}))


结语


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


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

感兴趣的小伙伴,可以关注公众号【grain先森】,回复关键词 “小程序”,获取更多资料,更多关键词玩法期待你的探索~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容