本文发表至今已有一段时间,错别字多、文笔混乱、内容过于陈旧。本人建议读者不必细究,大概浏览即可,最新的开发指南还是以官方文档为准,该博文的示例代码经过了重构,已经与官方文档同步,可能与文中的代码片段有较大差异,请以 Github 仓库上的代码为准。

好久没有写关于微信小程序的随笔了,其实是不知道写点什么好,之前的豆瓣图书和知乎日报已经把小程序的基础部分写的很详细了,高级部分的API有些还得不到IDE的调试支持。之前发表了知乎日报小例,有网友问我小程序有没有关于日历显示的组件,可以显示所有天数的,自己看了一遍,好像没有这个组件,所以打算那这个功能来练手,在准备期间,微信开发者工具已经升级了两三次,添加了部分功能和修改了部分功能,导致之前的例子的写法不兼容更新后的IDE,还得修改代码。随着小程序的不断更新,功能越来越完善,我想我也应该紧跟官方的升级步伐,这次的案例使用了IDE支持的ES6和新的API。

这次介绍的是一个比较简单的小应用事项助手,其实跟事项也不沾多少边,只是作为辅助功能,只有数据的添加和删除,主要内容是日历这块内容。日历组件在web应用中应用非常广泛,插件也非常丰富,但是小程序不支持传统的插件写法,而是以数据驱动内容。

大部分的日历选择器都是差不多的,能显示当前的年份、月份和天数,可以选择某天、某月或者某年,我们可以打开操作系统中自带的日历观察一番。

日历的布局大同小异,本次案例的布局也是中规中矩,比较传统,头部显示当前年份月份,头部的左右个显示一个翻页按钮,跳转到上一月和下一月,下半部分显示当月的天数列表,由于每月的天数可能不一样,列表的格数是固定的,所以当月的天数显示使用高亮,其余的使用偏灰色彩。

预备

本次案例用到了ES6,先来了解一下案列中用到的几个写法。本人也是顺带学习顺带编写,可能代码中还存在部分老的写法。

变量

ES6中声明变量可以用let声明变量,用const声明常量,即不可改变的量。

  1. let version = '1.0.0';
  2. const weekday = 7;
  3. version = '2.0.0';
  4. weekday = 8; //错误,用const声明的常量,不能修改值

本习惯用大写字母和下划线的组合方式来声明全局的常量

  1. const CONFIG_COLOR = '#FAFAFA';

对象方法属性

小程序的每一个页面都有一个相对应的js文件,里面必不可少的就是Page函数,Page函数接受的参数是一个对象,我们经常使用的写法就是:

  1. Page({
  2. data: {
  3. userAvatar: './images/avatar.png',
  4. userName: 'Oopsguy'
  5. },
  6. onLoad: function() {
  7. //....
  8. },
  9. onReady: function() {
  10. //....
  11. }
  12. });

现在换做ES6的写法,我们可以这样:

  1. Page({
  2. data: {
  3. userAvatar: './images/avatar.png',
  4. userName: 'Oopsguy'
  5. },
  6. onLoad() {
  7. //....
  8. },
  9. onReady() {
  10. //....
  11. }
  12. });

我们可以把以前的键值写法省略掉,而且function声明也不需要了。

ES6中拥有了这一概念,声明类的方式很简单,跟其他语言一样,差别不大:

  1. class Animal {
  2. constructor() {
  3. }
  4. eat() {
  5. }
  6. static doSomething(param) {
  7. //...
  8. }
  9. }
  10. module.exports = Animal;

class关键字用于声明类,constructor是构造函数,static修饰静态方法。不能理解?我们看一下以前的js的简单写法:

  1. var Animal = function() {
  2. };
  3. Animal.prototype.eat = function() {
  4. };
  5. Animal.doSomething = function(param) {
  6. };
  7. module.exports = Animal;

简单的调用示例

  1. let animal = new Animal();
  2. animal.eat();
  3. //静态方法
  4. Animal.doSomething('param');

这里只是简单的展示了一下不同点,更多的只是还是需要读者自己翻阅更多的资料来学习。

解构

其实本人对结构也不太懂怎样解释,简单的来说就是可以把一个数组的元素或者对象的属性分解出来,直接获取,哈哈,解释的比较勉强,还是看看示例吧。

  1. let obj = {
  2. fullName: 'Xiao Ming',
  3. gender: 'male',
  4. role: 'admin'
  5. };
  6. let arr = ['elem1', 1, 30, 'arratElem3'];
  7. let {fullName, role} = obj;
  8. let [elem1, elem2] = arr;
  9. console.log(fullName, role, elem1, elem2);

大家可能猜出了什么,看看输出结果:

  1. > Xiao Ming admin elem1 1

我们只要把需要获取的属性或者元素别名指定解构体中,js会自动获取对应的属性或者下标对应的元素。这个新特性非常有用,比如我们需要在一个Pages data对象中一个属性获取对了属性值:

  1. let year = this.data.year,
  2. month = this.data.month,
  3. day = this.data.day;

但是用解构的写法就很简洁:

  1. let {year, month, day} = this.data;

再比如引入一个文件:

  1. function getDate(dateStr) {
  2. if (dateStr) {
  3. return new Date(Date.parse(dateStr));
  4. }
  5. return new Date();
  6. }
  7. function log(msg) {
  8. if (!msg) return;
  9. if (getApp().settings['debug'])
  10. console.log(msg);
  11. let logs = wx.getStorageSync('logs') || [];
  12. logs.unshift(msg)
  13. wx.setStorageSync('logs', logs)
  14. }
  15. module.exports = {
  16. getDate: getDate,
  17. log: log
  18. };

现在引入并调用外部文件的方法:

  1. import {log} from '../../utils/util';
  2. log('Application initialized !!');

import...from...是ES6的引入模块方式,等同于小程序总的require,但import可以选择导入哪些子模块。

箭头函数(Arrow Function)

刚开始我也不知道js的箭头函数到底是什么东西,用了才发现,这特么就是lambda表达式么。箭头函数简化了函数的写法,但是还是跟普通的function有区别,主要是在作用域上。

比如我们需要请求网络:

  1. wx.request({
  2. url: 'url',
  3. header: {
  4. 'Content-Type': 'application/json'
  5. },
  6. success: function(res) {
  7. console.log(res.data)
  8. }
  9. });

用函数还是可以简化一定的代码量,哈哈哈。

  1. wx.request({
  2. url: 'url',
  3. header: {
  4. 'Content-Type': 'application/json'
  5. },
  6. success: (res) => {
  7. console.log(res.data)
  8. }
  9. });

注意到那个success指向的回调函数了么,function关键字没了,被醒目的=>符号取代了。看到这里大家是不是认为以后我们写function就用箭头函数代替呢?答案是不一定,而且要非常小心!

function和箭头函数虽然看似一样,只是写法简化了,其实是不一样的,function声明的函数和箭头函数的作用域不同,这是一个不小心就变坑的地方。

  1. Page({
  2. data: {
  3. windowHeight: 0
  4. },
  5. onLoad() {
  6. let _this = this;
  7. wx.getSystemInfo({
  8. success: function(res) {
  9. _this.setData({windowHeight: res.windowHeight});
  10. }
  11. });
  12. }
  13. });

一般我们获取设备的屏幕高度差不多是这样的步骤,在页面刚加载的onLoad方法中通过wx.getSystemInfoAPI来获取设备的屏幕高度,由于success指向的回调函数作用域跟onLoad不一样,所以我们无法像onLoad函数体中直接写this.setData来设置值。我们可以定义一个临时变量指向this,然后再回调函数中调用。

哪箭头函数的写法有什么不一样呢?

  1. Page({
  2. data: {
  3. windowHeight: 0
  4. },
  5. onLoad() {
  6. let _this = this;
  7. wx.getSystemInfo({
  8. success: (res) => {
  9. _this.setData({windowHeight: res.windowHeight});
  10. }
  11. });
  12. }
  13. });

运行之后好像感觉没什么区别呀,都能正常执行,结果也一样。确实没什么区别,你甚至这样写都可以:

  1. Page({
  2. data: {
  3. windowHeight: 0
  4. },
  5. onLoad() {
  6. wx.getSystemInfo({
  7. success: (res) => {
  8. this.setData({windowHeight: res.windowHeight});
  9. }
  10. });
  11. }
  12. });

咦?这样写,this的指向的作用域不是不一样么?其实这就是要说明的,箭头函数是不绑定作用域的,不会改变当前this的作用域,既然这样,在箭头函数中的this就会根据作用域链来指向上一层的作用域,也就是onLoad的作用域,所以他们得到的结果都是一样的。

其实我个人的习惯是无论用普通的函数写法还是箭头函数的写法,都习惯声明临时的_this来指向需要的作用域,因为箭头函数没有绑定作用域,写的层次深了,感觉就会很乱,理解起来比较困难,在后面的案例中,我也会延续这个习惯。

Promise

写js经常写的东西除了数组对象就是回调函数,记不记得用jQueryajax用得特别爽,如果是多层嵌套调用的话,那些回调函数简直像盖楼梯一样壮观。现在Promise来了,我们再也不用为这些回调地狱发愁,用Promise来解决回调问题非常优雅,链式调用也非常的方便。

Promise是ES6内置的类,其使用简单,简化了异步编程的繁琐层次问题,比较简单的用法是:

  1. new Promise((resolve, reject) => {
  2. //success
  3. //resolve();
  4. //error
  5. //reject();
  6. });

实例化一个Promise对象,它接受一个函数参数,此函数有两个回调参数,resolvereject,如果正常执行使用resolve执行传递,如果是失败或者错误可以用reject来执行传递,其实他们就是一个状态的转换。可以暂时理解为successfail

来看一下简单的示例:

  1. let ret = true;
  2. let pro = new Promise((resolve, reject) => {
  3. ret ? resolve('true') : reject('false');
  4. }).then((res) => {
  5. console.log(res);
  6. return 'SUCCESS';
  7. }, (rej) => {
  8. console.log(rej);
  9. return 'ERROR';
  10. }).then((success) => {
  11. console.log(success);
  12. let value = 0 / 1;
  13. }, (error) => {
  14. console.log(error);
  15. }).catch((ex) => {
  16. console.log(ex);
  17. });

或许我们已经看出些什么了,实例化出一个Promise,根据ret的布尔值决定是否resolve执行正常回调流程还是执行reject回调走意外的流程,显然ret是true,当执行resolve时,传递了一个字符串参数true,可以看到实例化出来的Promise对象后面链式调用了很多then方法,其实then方法同样也是有resolvereject两个回调参数,上层的Promise执行的回调传递到then函数中,Promiseresolve传递到thenresolve,同理reject也一样,之后我们发现最后一个catch函数,这是一个捕抓异常的函数,当流程发生异常,我们可以在catch方法中获取异常并处理。

可能解释的比较羞涩,看看下面例子,发出一个网络请求,获取用户头像,再把用户头像插入DOM中,再睡眠2000ms,再打印出SUCCESS,再睡眠3000ms,在alert出ERROR,再休眠1000ms,最后打印出ERROR。这...看起来有点丧心病狂,但只是举个例子:

  1. $.get('/user/1/avatar', (data) => {
  2. $('#avatar img').attr('src', data['avatar']);
  3. setTimeout(() => {
  4. console.log('SUCCESS');
  5. setTimeout(() => {
  6. alert('ERROR');
  7. setTimeout(() => {
  8. console.log('ERROR');
  9. }, 1000);
  10. }, 3000)
  11. }, 2000);
  12. });

一共有四个回调函数,也不算多,如果有十几个回调呢?直至是噩梦呀。一层一层的嵌套,看起来已经眼花了。那么Promise能做些什么改变呢?

  1. function sleep(time) {
  2. return new Promise((resolve) => {
  3. setTimeout(resolve, time);
  4. });
  5. }
  6. new Promise((resolve) => {
  7. $.get('/user/1/avatar', resolve);
  8. }).then((avatar) => {
  9. $('#avatar img').attr('src', avatar);
  10. }).then(() => {
  11. return sleep(2000);
  12. }).then(() => {
  13. console.log('SUCCESS');
  14. return sleep(3000);
  15. }).then(() => {
  16. alert('ERROR');
  17. return sleep(1000);
  18. }).then(() => {
  19. console.log('ERROR');
  20. });

额...看起来怎么使用Promise代码量比不使用的还多呀。不要介意,嘿嘿,可能是我个人封装不精,但是使用Promise的代码可读性确实比上面的要好很多,而且我们不必写一堆的嵌套回调函数,在享受使用同步写法的待遇,又可以得到异步的功能,两全其美,这样的写法还是比较符合日常的思维方式,哈哈。

看看小程序中怎么应用,在小程序项目的app.js中,我们经常看见这段代码:

  1. App({
  2. getUserInfo:function(cb){
  3. var that = this
  4. if(this.globalData.userInfo){
  5. typeof cb == "function" && cb(this.globalData.userInfo)
  6. }else{
  7. wx.login({
  8. success: function () {
  9. wx.getUserInfo({
  10. success: function (res) {
  11. that.globalData.userInfo = res.userInfo
  12. typeof cb == "function" && cb(that.globalData.userInfo)
  13. }
  14. })
  15. }
  16. })
  17. }
  18. }
  19. });

这是个方法是获取当前用户的信息,首先先检查globalData对象中有没有缓存有userInfo对象(存储用户的信息),如果有就返回给用户传进来的回掉函数,否则就请求接口获取用用户信息,获取用户信息之前,微信小程序要求先调用wx.login认证,才能调用wx.getUserInfo接口。

看的出代码的层次已经有点深了,我们可以用Promise来简化一下(-_-|| 说的有点夸张,实际上这点嵌套还是可以的)

wx.getUserInfowx.login这两个接口都用共同的属性successfail,我们可以封装起来:

  1. /**
  2. * @param {Function} func 接口
  3. * @param {Object} options 接口参数
  4. * @returns {Promise} Promise对象
  5. */
  6. function promiseHandle(func, options) {
  7. options = options || {};
  8. return new Promise((resolve, reject) => {
  9. if (typeof func !== 'function')
  10. reject();
  11. options.success = resolve;
  12. options.fail = reject;
  13. func(options);
  14. });
  15. }
  16. App({
  17. getUserInfo(cb) {
  18. if (typeof cb !== "function") return;
  19. let that = this;
  20. if (that.globalData.userInfo) {
  21. cb(that.globalData.userInfo);
  22. } else {
  23. promiseHandle(wx.login)
  24. .then(() => promiseHandle(wx.getUserInfo))
  25. .then((res) => {
  26. that.globalData.userInfo = res.userInfo;
  27. cb(that.globalData.userInfo);
  28. })
  29. .catch((err) => {
  30. log(err);
  31. });
  32. }
  33. }
  34. });

可以看出,使用了Promise之后,代码简洁了不少,层次深度也降低了不少,好家伙,很管用!

其实本次代码中的回调嵌套很少的,为了尽量使用到ES6的新特性,少量的回调嵌套也使用了Promise处理。

介绍了那么多,主要了为了还不了解ES6的读者能够预热一下知识,为后面的案例做好准备,当然,肯定有同学已经对ES6了如指掌,本人也是刚刚学习,欢迎指正错误。

思路

在开工之前,我们先理一下思路,一个普通的日历显示功能应该怎么做,该怎样入手。

日期

获取日期相关的信息,肯定用到Date对象。

  1. let date = new Date();
  2. let day = date.getDate(); //当月的天
  3. let month = date.getMonth() + 1; //月份,从0开始
  4. let year = date.getFullYear(); //年份

我们需要知道当前展示月份的天数。

  1. let dayCount = new Date(currentYear, currentMonth, 0).getDate();

得到可当月月份的天数,可以展示出所有的天数列表,但是我们一样要或者上一个页的天数和下一个页的天数,如果当前月份是1月或者12月,我们还需要额外判断上一页是上一年的12月,下一页是下一年的一月份。

我们可能需要获取足够多的日期信息来展示(不仅仅是当前月份,还有上一月或者上一年和下一月或者下一年)

  1. data = {
  2. currentDate: currentDateObj.getDate(), //当天日期第几天
  3. currentYear: currentDateObj.getFullYear(), //当天年份
  4. currentDay: currentDateObj.getDay(), //当天星期
  5. currentMonth: currentDateObj.getMonth() + 1, //当天月份
  6. showMonth: showMonth, //当前显示月份
  7. showDate: showDate, //当前显示月份的第几天
  8. showYear: showYear, //当前显示月份的年份
  9. beforeYear: beforeYear, //当前页上一页的年份
  10. beforMonth: beforMonth, //当前页上一页的月份
  11. afterYear: afterYear, //当前页下一页的年份
  12. afterMonth: afterMonth, //当前页下一页的月份
  13. selected: selected //当前被选择的日期信息
  14. };

能显示日期之后,当然还没有完,我们需要一个选择日期的功能,即用户可以点击指定那一天,也可以选择哪一年或者哪一个月,选择年份和月份我们可以用Picker组件来展示,选择具体的哪天这就需要在日期列表上的每一天都要绑定一个点击事件来响应用户的点击动作,用户选择具体的日期后,可能会随意翻页,所以必须要保存好当前选择的日期。

存储

示例程序中用到了数据存储,关系到小程序中的数据缓存API,官方提供的API比较多,我只是用了两个异步的数据缓存API。

wx.setStorage({key: KEY, data: DATA});

  1. let allData =[{id: 1, title: 'title1'}, {id: 2, title: 'title2'}];
  2. wx.setStorageSync({key: Config.ITEMS_SAVE_KEY, data: allData});
参数 说明
KEY 存储数据的键名
DATA 存储的数据

wx.getStorage({key: KEY});

  1. let allData = wx.getStorage({
  2. key: Config.ITEMS_SAVE_KEY
  3. success: allData => {
  4. let obj1 = allData[0];
  5. console.log(obj1.title);
  6. }
  7. });
参数 说明
KEY 存储数据的键名

编码

建立工程的步骤就不讲了,直接进入主题,应用只有两个页面,一个首页,一个详情页,结构清晰,功能简单。

日历

先来看看首页,日历的wxml结构;

结构分为上中下三部分,header为头部,用于展示翻页按钮和当前日期信息。在.week.row.body.row元素中展示星期和天数列表,这里的布局采用了比较low的百分比分栏,总共有7栏,100/7哈哈,想高逼格的可以采用css的分栏布局和flex布局。

  1. <view class="og-calendar">
  2. <view class="header">
  3. <view class="btn month-pre" bindtap="changeDateEvent" data-year="{{data.beforeYear}}" data-month="{{data.beforMonth}}">
  4. <image src="../../images/prepage.png"></image>
  5. </view>
  6. <view class="date-info">
  7. <picker mode="date" fields="month" value="{{pickerDateValue}}" bindchange="datePickerChangeEvent">
  8. <text>{{data.showYear}}年{{data.showMonth > 9 ? data.showMonth : ('0' + data.showMonth)}}月</text>
  9. </picker>
  10. </view>
  11. <view class="btn month-next" bindtap="changeDateEvent" data-year="{{data.afterYear}}" data-month="{{data.afterMonth}}">
  12. <image src="../../images/nextpage.png"></image>
  13. </view>
  14. </view>
  15. <view class="week row">
  16. <view class="col">
  17. <text></text>
  18. </view>
  19. <view class="col">
  20. <text></text>
  21. </view>
  22. <view class="col">
  23. <text></text>
  24. </view>
  25. <view class="col">
  26. <text></text>
  27. </view>
  28. <view class="col">
  29. <text></text>
  30. </view>
  31. <view class="col">
  32. <text></text>
  33. </view>
  34. <view class="col">
  35. <text></text>
  36. </view>
  37. </view>
  38. <view class="body row">
  39. <block wx:for="{{data.dates}}" wx:key="_id">
  40. <view bindtap="dateClickEvent" data-year="{{item.year}}" data-month="{{item.month}}" data-date="{{item.date}}" class="col {{data.showMonth == item.month ? '' : 'old'}} {{data.currentDate == item.date && data.currentYear==item.year && data.currentMonth == item.month ? 'current' : ''}} { {item.active ? 'active' : ''}}">
  41. <text>{{item.date}}</text>
  42. </view>
  43. </block>
  44. </view>
  45. </view>

.btn.month-pre.btn.month-next翻页按钮,都绑定了changeDateEvent的tap事件,各自都用自己的data-yeardata-mont属性,这两个属性是临时存值,当点击按钮翻页的时候,我们需要知道当前的年份和日期,以便可以更加方便地翻到上一页或者下一页。

changeDateEvent事件比较简单:

  1. changeDateEvent(e) {
  2. const {year, month} = e.currentTarget.dataset;
  3. changeDate.call(this, new Date(year, parseInt(month) - 1, 1));
  4. }

点击翻页按钮,根据回调进来的event对象来获取元素上的data-*属性,然后调用changeDate这个方法来更新日历数据,这个方法接收一个Date对象,代表要翻页后的日期。

暂且不关心changeDate具体干了些什么,看看.body.row里有一个循环,每一个元素都绑定了dateClickEvent事件,而且每一个元素都附带了自己所属的年份、月份和天数信息,这些信息是非常有用的,当点击了具体的某一天,可以通过获取元素上的data-*信息来知道我们具体选择的日期。除此之外,元素上的class属性包裹了一长串的判断表达式。这些语句最终的目的是为了给元素动态变更,.old代表当前的日期不是本月日期,因为每一版的日期除了当前月份的日期还可能包含上一月和下一月的部分日期,我们给予它灰色的样式显示,.current代表今天的日期,用实心填充颜色的背景样式修饰,.active即代表着当前选中的日期。

dateClickEvent事件其实也是调用了changeDate事件,本质上也是也是改变日期,额外的工作就是保存选中的日期到selected对象中。

  1. dateClickEvent(e) {
  2. const {year, month, date} = e.currentTarget.dataset;
  3. const {data} = this.data;
  4. let selectDateText = '';
  5. data['selected']['year'] = year;
  6. data['selected']['month'] = month;
  7. data['selected']['date'] = date;
  8. this.setData({ data: data });
  9. changeDate.call(this, new Date(year, parseInt(month) - 1, date));
  10. }

来看看重中之重的changeDate函数,这个函数的代码比较多,虽然堆砌大量在一个函数中是个不好的习惯,不过里面声明变量和赋值比较多,业务代码比较少:

  1. /**
  2. * 变更日期数据
  3. * @param {Date} targetDate 当前日期对象
  4. */
  5. function changeDate(targetDate) {
  6. let date = targetDate || new Date();
  7. let currentDateObj = new Date();
  8. let showMonth, //当天显示月份
  9. showYear, //当前显示年份
  10. showDay, //当前显示星期
  11. showDate, //当前显示第几天
  12. showMonthFirstDateDay, //当前显示月份第一天的星期
  13. showMonthLastDateDay, //当前显示月份最后一天的星期
  14. showMonthDateCount; //当前月份的总天数
  15. let data = [];
  16. showDate = date.getDate();
  17. showMonth = date.getMonth() + 1;
  18. showYear = date.getFullYear();
  19. showDay = date.getDay();
  20. showMonthDateCount = new Date(showYear, showMonth, 0).getDate();
  21. date.setDate(1);
  22. showMonthFirstDateDay = date.getDay(); //当前显示月份第一天的星期
  23. date.setDate(showMonthDateCount);
  24. showMonthLastDateDay = date.getDay(); //当前显示月份最后一天的星期
  25. let beforeDayCount = 0,
  26. beforeYear, //上页月年份
  27. beforMonth, //上页月份
  28. afterYear, //下页年份
  29. afterMonth, //下页月份
  30. afterDayCount = 0, //上页显示天数
  31. beforeMonthDayCount = 0; //上页月份总天数
  32. //上一个月月份
  33. beforMonth = showMonth === 1 ? 12 : showMonth - 1;
  34. //上一个月年份
  35. beforeYear = showMonth === 1 ? showYear - 1 : showYear;
  36. //下个月月份
  37. afterMonth = showMonth === 12 ? 1 : showMonth + 1;
  38. //下个月年份
  39. afterYear = showMonth === 12 ? showYear + 1 : showYear;
  40. //获取上一页的显示天数
  41. if (showMonthFirstDateDay != 0)
  42. beforeDayCount = showMonthFirstDateDay - 1;
  43. else
  44. beforeDayCount = 6;
  45. //获取下页的显示天数
  46. if (showMonthLastDateDay != 0)
  47. afterDayCount = 7 - showMonthLastDateDay;
  48. else
  49. showMonthLastDateDay = 0;
  50. //如果天数不够6行,则补充完整
  51. let tDay = showMonthDateCount + beforeDayCount + afterDayCount;
  52. if (tDay <= 35)
  53. afterDayCount += (42 - tDay); //6行7列 = 42
  54. //虽然翻页了,但是保存用户选中的日期信息是非常有必要的
  55. let selected = this.data.data['selected'] || { year: showYear, month: showMonth, date: showDate };
  56. let selectDateText = selected.year + '年' + formatNumber(selected.month) + '月' + formatNumber(selected.date) + '日';
  57. data = {
  58. currentDate: currentDateObj.getDate(), //当天日期第几天
  59. currentYear: currentDateObj.getFullYear(), //当天年份
  60. currentDay: currentDateObj.getDay(), //当天星期
  61. currentMonth: currentDateObj.getMonth() + 1, //当天月份
  62. showMonth: showMonth, //当前显示月份
  63. showDate: showDate, //当前显示月份的第几天
  64. showYear: showYear, //当前显示月份的年份
  65. beforeYear: beforeYear, //当前页上一页的年份
  66. beforMonth: beforMonth, //当前页上一页的月份
  67. afterYear: afterYear, //当前页下一页的年份
  68. afterMonth: afterMonth, //当前页下一页的月份
  69. selected: selected,
  70. selectDateText: selectDateText
  71. };
  72. let dates = [];
  73. let _id = 0; //为wx:key指定
  74. //上一月的日期
  75. if (beforeDayCount > 0) {
  76. beforeMonthDayCount = new Date(beforeYear, beforMonth, 0).getDate();
  77. for (let fIdx = 0; fIdx < beforeDayCount; fIdx++) {
  78. dates.unshift({
  79. _id: _id,
  80. year: beforeYear,
  81. month: beforMonth,
  82. date: beforeMonthDayCount - fIdx
  83. });
  84. _id++;
  85. }
  86. }
  87. //当前月份的日期
  88. for (let cIdx = 1; cIdx <= showMonthDateCount; cIdx++) {
  89. dates.push({
  90. _id: _id,
  91. active: (selected['year'] == showYear && selected['month'] == showMonth && selected['date'] == cIdx), //选中状态判断
  92. year: showYear,
  93. month: showMonth,
  94. date: cIdx
  95. });
  96. _id++;
  97. }
  98. //下一月的日期
  99. if (afterDayCount > 0) {
  100. for (let lIdx = 1; lIdx <= afterDayCount; lIdx++) {
  101. dates.push({
  102. _id: _id,
  103. year: afterYear,
  104. month: afterMonth,
  105. date: lIdx
  106. });
  107. _id++;
  108. }
  109. }
  110. data.dates = dates;
  111. this.setData({ data: data, pickerDateValue: showYear + '-' + showMonth });
  112. loadItemListData.call(this);
  113. }

虽然这段这段代码有点啰嗦,不过总结下来无非就是获取当前月的信息,上一页的信息和下一页的信息,这些信息包括具体的年月日和星期。

年月选择Picker

既然是日历,必不可少的功能就是让用户可以选择显示指定的年份和月份,用pciker组件来实现最合适不过了,官方更新的api,目前未知,picker组件已经支持mode = date模式的风格,即原生的日期选择。触发选择的区域关联在了日历的header上。

  1. <view class="date-info">
  2. <picker mode="date" fields="month" value="{{pickerDateValue}}" bindchange="datePickerChangeEvent">
  3. <text>{{data.showYear}}年{{data.showMonth > 9 ? data.showMonth : ('0' + data.showMonth)}}月</text>
  4. </picker>
  5. </view>

mode=date指定pciker是日期选择风格,fields=month则显示组件显示日期的精度显示当月份即可,组件初始化的值为pickerDateValue,绑定了datePickerChangeEvent事件,当选择的日期发生变化时,就会触发此事件。

  1. datePickerChangeEvent(e) {
  2. const date = new Date(Date.parse(e.detail.value));
  3. changeDate.call(this, new Date(date.getFullYear(), date.getMonth(), 1));
  4. }

事项存储

此应用还有小小的事项功能,可以添加事项条目,事项包括了标题、内容和等级,说白了其实就是一个功能不全的TODO应用...

既然涉及到存储,肯定需要操作缓存的方法,自己也是刚搞前端那不久,不太明白javascript的封装约定,借鉴之前在java所用的模式,分为了两个文件,一个是仓库类(数据的CURD操作),另一个是业务类(附带处理部分业务),缓存的配置放置于Config文件中,类中用到了异步的缓存操作API,所以使用Promise模式封装。

首先是把Promise封装成通用的方法,顺便封装部分经常用到的函数:

  1. /**
  2. * 生成GUID序列号
  3. * @returns {string} GUID
  4. */
  5. function guid() {
  6. return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
  7. let r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
  8. return v.toString(16);
  9. });
  10. }
  11. /**
  12. * 记录日志
  13. * @param {Mixed} 记录的信息
  14. * @returns {Void}
  15. */
  16. function log(msg) {
  17. if (!msg) return;
  18. if (getApp().settings['debug'])
  19. console.log(msg);
  20. let logs = wx.getStorageSync('logs') || [];
  21. logs.unshift(msg)
  22. wx.setStorageSync('logs', logs)
  23. }
  24. /**
  25. * @param {Function} func 接口
  26. * @param {Object} options 接口参数
  27. * @returns {Promise} Promise对象
  28. */
  29. function promiseHandle(func, options) {
  30. options = options || {};
  31. return new Promise((resolve, reject) => {
  32. if (typeof func !== 'function')
  33. reject();
  34. options.success = resolve;
  35. options.fail = reject;
  36. func(options);
  37. });
  38. }
  39. module.exports = {
  40. guid: guid,
  41. log: log,
  42. promiseHandle: promiseHandle
  43. }

guid方法用于生成每一个事项的id,方便查询,log方法用于日志记录,promiseHandle把小程序的大部分异步API封装到了Promise对象中。

具体的Config配置文件:

  1. module.exports = {
  2. ITEMS_SAVE_KEY: 'todo_item_save_Key',
  3. //事项等级
  4. LEVEL: {
  5. normal: 1,
  6. warning: 2,
  7. danger: 3
  8. }
  9. };

数据操作仓库类 DataRepository:

  1. import Config from 'Config';
  2. import {guid, log, promiseHandle} from '../utils/util';
  3. class DataRepository {
  4. /**
  5. * 添加数据
  6. * @param {Object} 添加的数据
  7. * @returns {Promise}
  8. */
  9. static addData(data) {
  10. if (!data) return false;
  11. data['_id'] = guid();
  12. return DataRepository.findAllData().then(allData => {
  13. allData = allData || [];
  14. allData.unshift(data);
  15. wx.setStorage({key:Config.ITEMS_SAVE_KEY, data: allData});
  16. });
  17. }
  18. /**
  19. * 删除数据
  20. * @param {string} id 数据项idid
  21. * @returns {Promise}
  22. */
  23. static removeData(id) {
  24. return DataRepository.findAllData().then(data => {
  25. if (!data) return;
  26. for (let idx = 0, len = data.length; idx < len; idx++) {
  27. if (data[idx] && data[idx]['_id'] == id) {
  28. data.splice(idx, 1);
  29. break;
  30. }
  31. }
  32. wx.setStorage({key: Config.ITEMS_SAVE_KEY, data: data});
  33. });
  34. }
  35. /**
  36. * 批量删除数据
  37. * @param {Array} range id集合
  38. * @returns {Promise}
  39. */
  40. static removeRange(range) {
  41. if (!range) return;
  42. return DataRepository.findAllData().then(data => {
  43. if (!data) return;
  44. let indexs = [];
  45. for (let rIdx = 0, rLen = range.length; rIdx < rLen; rIdx++) {
  46. for (let idx = 0, len = data.length; idx < len; idx++) {
  47. if (data[idx] && data[idx]['_id'] == range[rIdx]) {
  48. indexs.push(idx);
  49. break;
  50. }
  51. }
  52. }
  53. let tmpIdx = 0;
  54. indexs.forEach(item => {
  55. data.splice(item - tmpIdx, 1);
  56. tmpIdx++;
  57. });
  58. wx.setStorage({key: Config.ITEMS_SAVE_KEY, data: data});
  59. });
  60. }
  61. /**
  62. * 更新数据
  63. * @param {Object} data 数据
  64. * @returns {Promise}
  65. */
  66. static saveData(data) {
  67. if (!data || !data['_id']) return false;
  68. return DataRepository.findAllData().then(allData => {
  69. if (!allData) return false;
  70. for (let idx = 0, len = allData.length; i < len; i++) {
  71. if (allData[i] && allData[i]['_id'] == data['_id']) {
  72. allData[i] = data;
  73. break;
  74. }
  75. }
  76. wx.setStorage({key: Config.ITEMS_SAVE_KEY, data: data});
  77. });
  78. }
  79. /**
  80. * 获取所有数据
  81. * @returns {Promise} Promise实例
  82. */
  83. static findAllData() {
  84. return promiseHandle(wx.getStorage, {key: Config.ITEMS_SAVE_KEY}).then(res => res.data ? res.data : []).catch(ex => {
  85. log(ex);
  86. });
  87. }
  88. /**
  89. * 查找数据
  90. * @param {Function} 回调
  91. * @returns {Promise} Promise实例
  92. */
  93. static findBy(predicate) {
  94. return DataRepository.findAllData().then(data => {
  95. if (data) {
  96. data = data.filter(item => predicate(item));
  97. }
  98. return data;
  99. });
  100. }
  101. }
  102. module.exports = DataRepository;

数据业务类 DataService:

  1. import DataRepository from 'DataRepository';
  2. import {promiseHandle} from '../utils/util';
  3. /**
  4. * 数据业务类
  5. */
  6. class DataSerivce {
  7. constructor(props) {
  8. props = props || {};
  9. this.id = props['_id'] || 0;
  10. this.content = props['content'] || '';
  11. this.date = props['date'] || '';
  12. this.month = props['month'] || '';
  13. this.year = props['year'] || '';
  14. this.level = props['level'] || '';
  15. this.title = props['title'] || '';
  16. }
  17. /**
  18. * 保存当前对象数据
  19. */
  20. save() {
  21. if (this._checkProps()) {
  22. return DataRepository.addData({
  23. title: this.title,
  24. content: this.content,
  25. year: this.year,
  26. month: this.month,
  27. date: this.date,
  28. level: this.level,
  29. addDate: new Date().getTime()
  30. });
  31. }
  32. }
  33. /**
  34. * 获取所有事项数据
  35. */
  36. static findAll() {
  37. return DataRepository.findAllData()
  38. .then(data => data.data ? data.data : []);
  39. }
  40. /**
  41. * 通过id获取事项
  42. */
  43. static findById(id) {
  44. return DataRepository.findBy(item => item['_id'] == id)
  45. .then(items => (items && items.length > 0) ? items[0] : null);
  46. }
  47. /**
  48. * 根据id删除事项数据
  49. */
  50. delete() {
  51. return DataRepository.removeData(this.id);
  52. }
  53. /**
  54. * 批量删除数据
  55. * @param {Array} ids 事项Id集合
  56. */
  57. static deleteRange(...ids) {
  58. return DataRepository.removeRange(ids);
  59. }
  60. /**
  61. * 根据日期查找所有符合条件的事项记录
  62. * @param {Date} date 日期对象
  63. * @returns {Array} 事项集合
  64. */
  65. static findByDate(date) {
  66. if (!date) return [];
  67. return DataRepository.findBy(item => {
  68. return item && item['date'] == date.getDate() &&
  69. item['month'] == date.getMonth() &&
  70. item['year'] == date.getFullYear();
  71. }).then(data => data);
  72. }
  73. _checkProps() {
  74. return this.title && this.level && this.date && this.year && this.month;
  75. }
  76. }
  77. module.exports = DataSerivce;

本人的对数组的操作不是很熟悉,代码看起来有点臃肿,仅供参考。

好了,进入正题,每天的事项可以用一个列表来展示,列表方在日历下边,具体结构:

  1. <view class="common-list">
  2. <view class="header" wx:if="{{itemList.length > 0}}">
  3. <text>事项信息</text>
  4. </view>
  5. <block wx:for="{{itemList}}" wx:key="id">
  6. <view class="item" bindtap="listItemClickEvent" data-id="{{item._id}}" bindlongtap="listItemLongTapEvent">
  7. <view class="inner {{isEditMode ? 'with-check' : ''}}">
  8. <view class="checker" wx:if="{{isEditMode}}">
  9. <icon type="circle" wx:if="{{!item.checked}}" color="#FFF" size="20" />
  10. <icon type="success" wx:else color="#E14848" size="20" />
  11. </view>
  12. <image wx:if="{{item.level == 1}}" class="icon" src="../../images/success.png" />
  13. <image wx:if="{{item.level == 2}}" class="icon" src="../../images/notice.png" />
  14. <image wx:if="{{item.level == 3}}" class="icon" src="../../images/fav-round.png" />
  15. <view class="content">
  16. <text class="title">{{item.title}}</text>
  17. </view>
  18. </view>
  19. </view>
  20. </block>
  21. <view class="header text-center" wx:if="{{!itemList || itemList.length <= 0}}">
  22. <text>当前日期没有事项记录</text>
  23. </view>
  24. </view>

列表的数据加载全靠这个方法loadItemListData

  1. /**
  2. * 加载事项列表数据
  3. */
  4. function loadItemListData() {
  5. const {year, month, date} = this.data.data.selected;
  6. let _this = this;
  7. DataService.findByDate(new Date(Date.parse([year, month, date].join('-')))).then((data) => {
  8. _this.setData({ itemList: data });
  9. });
  10. }

DataService.findByDate这个方法通过传入一个日期来获取指定日期的事项。成功获取数据之后,在模板中遍历数据,根据level属性来显示不同颜色的图标,让事项等级一目了然。

既然有数据列表,数据从哪来?当然是需要一个数据的添加面板。

首页的有下表有FloatAction操作工具按钮,在这里添加一个添加数据按钮,添加的事项的日期属于用户选中的日期,添加面板默认是隐藏起来的,当点击添加按钮,面板就会向上滑动出现,可以用animationAPI实现动画效果,其实本质也是CSS3动画。

  1. <view class="updatePanel" style="top: {{updatePanelTop}}px;height:{{updatePanelTop}}px" animation="{{updatePanelAnimationData}}">
  2. <input placeholder="请输入事项标题" value="{{todoInputValue}}" bindchange="todoInputChangeEvent" />
  3. <textarea placeholder="请输入事项内容" value="{{todoTextAreaValue}}" bindblur="todoTextAreaChangeEvent"></textarea>
  4. <view class="level">
  5. <block wx:for="{{levelSelectData}}" wx:key="*this">
  6. <view bindtap="levelClickEvent" data-level="{{item}}" class="item {{item == 1 ? 'border-normal' : ''}} {{item == 2 ? 'border-warning' : '' }} {{item == 3 ? 'border-danger' : ''}} {{item == levelSelectedValue && item == 1 ? 'bg-normal' : ''}} {{item == levelSelectedValue && item == 2 ? 'bg-warning' : ''}} {{item == levelSelectedValue && item == 3 ? 'bg-danger' : ''}}"></view>
  7. </block>
  8. </view>
  9. <view class="footer">
  10. <view class="btn" bindtap="closeUpdatePanelEvent">取消</view>
  11. <view class="btn primary" bindtap="saveDataEvent">保存</view>
  12. </view>
  13. </view>

在我写到这个内容之前,官方还没有textarea组件,现在新增了,完美解决遗憾。

添加面板的动画控制:

  1. /**
  2. * 显示事项数据添加更新面板
  3. */
  4. function showUpdatePanel() {
  5. let animation = wx.createAnimation({
  6. duration: 600
  7. });
  8. animation.translateY('-100%').step();
  9. this.setData({
  10. updatePanelAnimationData: animation.export()
  11. });
  12. }
  13. /**
  14. * 显示模态窗口
  15. * @param {String} msg 显示消息
  16. */
  17. function showModal(msg) {
  18. this.setData({
  19. isModalShow: true,
  20. isMaskShow: true,
  21. modalMsg: msg
  22. });
  23. }
  24. /**
  25. * 关闭模态窗口
  26. */
  27. function closeModal() {
  28. this.setData({
  29. isModalShow: false,
  30. isMaskShow: false,
  31. modalMsg: ''
  32. });
  33. }
  34. /**
  35. * 关闭事项数据添加更新面板
  36. */
  37. function closeUpdatePanel() {
  38. let animation = wx.createAnimation({
  39. duration: 600
  40. });
  41. animation.translateY('100%').step();
  42. this.setData({
  43. updatePanelAnimationData: animation.export()
  44. });
  45. }

主要靠translateY来控制垂直方向的移动动画,刚进入页面的时候获取屏幕的高度,把面板的高度设置与屏幕高度一致,上滑的时候100%就刚好覆盖整个屏幕。

主要的添加事项逻辑:

  1. // 保存事项数据
  2. saveDataEvent() {
  3. const {todoInputValue, todoTextAreaValue, levelSelectedValue} = this.data;
  4. const {year, month, date} = this.data.data.selected;
  5. console.log(todoInputValue, todoTextAreaValue);
  6. if (todoInputValue !== '') {
  7. let promise = new DataService({
  8. title: todoInputValue,
  9. content: todoTextAreaValue,
  10. level: levelSelectedValue,
  11. year: year,
  12. month: parseInt(month) - 1,
  13. date: date
  14. }).save();
  15. promise && promise.then(() => {
  16. //清空表单
  17. this.setData({
  18. todoTextAreaValue: '',
  19. levelSelectedValue: '',
  20. todoInputValue: ''
  21. });
  22. loadItemListData.call(this);
  23. })
  24. closeUpdatePanel.call(this);
  25. } else {
  26. showModal.call(this, '请填写事项内容');
  27. }
  28. }

获取添加面板上的数据和当前选择的日期直接用DataSerivce对象保存即可。

由于篇幅有限,剩下的数据删除和数据查看逻辑也比较简单,不再细说,本文主要是介绍小程序的ES6开发。

写完这篇文章的时候,小程序已经公测了好久。本人是个人用户,没有资格参与公测,热情也减半了不少,接触小程序也有一个多月了,写了三个例子,感觉还好,至少能够写出点东西来,不枉这番努力。

效果图

源代码仓库

https://github.com/oopsguy/WechatSmallApps/tree/master/MatterAssistant

微信小程序之ES6与事项助手的更多相关文章

  1. [技术博客]微信小程序审核的注意事项及企业版小程序的申请流程

    关于小程序审核及企业版小程序申请的一些问题 微信小程序是一个非常方便的平台.由于微信小程序可以通过微信直接进入,不需要下载,且可使用微信账号直接登录,因此具有巨大的流量优势.但是,也正是因为微信流量巨 ...

  2. 微信小程序-滑动视图注意事项

    真的得吐槽下微信的开发文档,一点点都不详细的好吗. <!--垂直滚动,这里必须设置高度--> <scroll-view scroll-y="true" style ...

  3. 微信小程序踩坑集合

    1:官方工具:https://mp.weixin.qq.com/debug/w ... tml?t=1476434678461 2:简易教程:https://mp.weixin.qq.com/debu ...

  4. 微信小程序爬坑日记

    新公司上手小程序.30天,从入门到现在,还没放弃... 虽然小程序发布出来快一年了,爬坑的兄弟们大多把坑都踩平了.而我一直停留在"Hello World"的学习阶段.一来没项目,只 ...

  5. uniapp发布到微信小程序整改摘要

    uniapp作为跨端的利器,可同时发布到安卓.ios.微信小程序.支付宝小程序.百度小程序.头条小程序.QQ小程序等8个平台. 如果是轻量级的应用,不涉及太多功能的话,或许可以直接打包移植,但涉及前后 ...

  6. 微信小程序中发送模版消息注意事项

    在微信小程序中发送模版消息 参考微信公众平台Api文档地址:https://mp.weixin.qq.com/debug/wxadoc/dev/api/notice.html#模版消息管理 此参考地址 ...

  7. 【微信小程序项目实践总结】30分钟从陌生到熟悉 web app 、native app、hybrid app比较 30分钟ES6从陌生到熟悉 【原创】浅谈内存泄露 HTML5 五子棋 - JS/Canvas 游戏 meta 详解,html5 meta 标签日常设置 C#中回滚TransactionScope的使用方法和原理

    [微信小程序项目实践总结]30分钟从陌生到熟悉 前言 我们之前对小程序做了基本学习: 1. 微信小程序开发07-列表页面怎么做 2. 微信小程序开发06-一个业务页面的完成 3. 微信小程序开发05- ...

  8. 微信小程序Http高级封装 es6 promise

    公司突然要开放微信小程序,持续蒙蔽的我还不知道小程序是个什么玩意. 于是上网查了一下,就开始着手开发..... 首先开发客户端的东西,都有个共同点,那就是  数据请求! 看了下小程序的请求方式大概和a ...

  9. 微信小程序和微信H5测试中易出Bug的点和注意事项

    一.微信小程序 易出Bug的点: 小程序的分享转发功能 背景:小程序项目开发基本完毕也都已经测过几轮,功能上基本没有什么问题,但是上线后却被客户发现通过分享转发小程序给别人,别人无法正常打开的情况 原 ...

随机推荐

  1. angular源码分析:angular中脏活累活承担者之$parse

    我们在上一期中讲 $rootscope时,看到$rootscope是依赖$prase,其实不止是$rootscope,翻看angular的源码随便翻翻就可以发现很多地方是依赖于$parse的.而$pa ...

  2. HTML5自定义属性之data-*

    HTML5 增加了一项新功能是 自定义数据属性 ,也就是  data-* 自定义属性.在HTML5中我们可以使用以 data- 为前缀来设置我们需要的自定义属性,来进行一些数据的存放.当然高级浏览器下 ...

  3. Sharepoint学习笔记—其它—如何知道某个Sharepoint环境的安装类型

    我们在安装sharepoint 2010时会出现三种安装类型: Standalone, Server Farm Standalone与Server Farm Complete. Standalone ...

  4. weblogic安装注意事项_linux

    ➠更多技术干货请戳:听云博客 一.安装过程:参考“weblogic安装截屏(linux)” 注意事项:安装weblogic时,需要注意以下两点: 1.首先在安装目录下创建weblogic12文件夹 如 ...

  5. WWDC 后苹果最新 App Store 审核条款!

        WWDC 2016 大会之后,苹果公司发布了四个全新平台:iOS,macOS,watchOS 和 tvOS.并且在此之后,苹果应用商店审核条款也同时进行了更新——貌似不算进行了更新,简直就是重 ...

  6. VS的安装

    一 安装过程 我直接在官网下载的 2015版本 ,软件比较大 安装起来比较花时间 同时也装了中文语言包,下面附上安装过程中的一些截图. 二 现在正在摸索如何使用,百度教程,等会附上单元测试.

  7. Linux服务器oraclejdk与openjdk共存并配置JavaEE开发环境

    由于本人学业的需要,需要在linux中搭建JavaEE开发环境,与windows的同学协同开发. JDK 由于fedora默认使用openjdk,移除多多少少会出现点问题,由于很多开源软件默认使用到它 ...

  8. 集合3--毕向东java基础教程视频学习笔记

    Day 15 集合框架01 TreeSet02 TreeSet存储自定义对象03 二叉树04 实现Comparator方式排序05 TreeSet练习06 泛型概述07 泛型使用08 泛型类09 泛型 ...

  9. [分享] 很多人手机掉了,却不知道怎么找回来。LZ亲身经历讲述手机找回过程,申请加精!

    文章开头:(LZ文笔不好,以下全部是文字描述,懒得配图.因为有人说手机掉了,他们问我是怎么找回来的.所以想写这篇帖子.只不过前段时间忙,没时间.凑端午节给大家一些经验) 还是先谢谢被偷经历吧!5月22 ...

  10. asp.net mvc 之旅—— 第三站 路由模板中强大的自定义IRouteConstraint约束

    我们在写mvc的时候,经常会配置各种url模板,比如controller,action,id 组合模式,其实呢,我们还可以对这三个参数进行单独的配置,采用的方式自然 就是MapRoute中的const ...