由于官方IDE更新到了0.11.112301版本,移除了对Promise的支持,造成事项助手不能正常运行,解决此问题,在项目中引入第三方兼容库Bluebird支持Promise,代码已经整合到项目代码中。
好久没有写关于微信小程序的随笔了,其实是不知道写点什么好,之前的豆瓣图书和知乎日报已经把小程序的基础部分写的很详细了,高级部分的API有些还得不到IDE的调试支持。之前发表了知乎日报小例,有网友问我小程序有没有关于日历显示的组件,可以显示所有天数的,自己看了一遍,好像没有这个组件,所以打算那这个功能来练手,在准备期间,微信开发者工具已经升级了两三次,添加了部分功能和修改了部分功能,导致之前的例子的写法不兼容更新后的IDE,还得修改代码。随着小程序的不断更新,功能越来越完善,我想我也应该紧跟官方的升级步伐,这次的案例使用了IDE支持的ES6和新的API。
这次介绍的是一个比较简单的小应用事项助手,其实跟事项也不沾多少边,只是作为辅助功能,只有数据的添加和删除,主要内容是日历这块内容。日历组件在web应用中应用非常广泛,插件也非常丰富,但是小程序不支持传统的插件写法,而是以数据驱动内容。
大部分的日历选择器都是差不多的,能显示当前的年份、月份和天数,可以选择某天、某月或者某年,我们可以打开操作系统中自带的日历观察一番。
日历的布局大同小异,本次案例的布局也是中规中矩,比较传统,头部显示当前年份月份,头部的左右个显示一个翻页按钮,跳转到上一月和下一月,下半部分显示当月的天数列表,由于每月的天数可能不一样,列表的格数是固定的,所以当月的天数显示使用高亮,其余的使用偏灰色彩。
预备
本次案例用到了ES6,先来了解一下案列中用到的几个写法。本人也是顺带学习顺带编写,可能代码中还存在部分老的写法。
变量
ES6中声明变量可以用let声明变量,用const声明常量,即不可改变的量。
let version = '1.0.0';const weekday = 7;version = '2.0.0';weekday = 8; //错误,用const声明的常量,不能修改值
本习惯用大写字母和下划线的组合方式来声明全局的常量
const CONFIG_COLOR = '#FAFAFA';
对象方法属性
小程序的每一个页面都有一个相对应的js文件,里面必不可少的就是Page函数,Page函数接受的参数是一个对象,我们经常使用的写法就是:
Page({ data: { userAvatar: './images/avatar.png', userName: 'Oopsguy' }, onLoad: function() { //.... }, onReady: function() { //.... }});
现在换做ES6的写法,我们可以这样:
Page({ data: { userAvatar: './images/avatar.png', userName: 'Oopsguy' }, onLoad() { //.... }, onReady() { //.... }});
我们可以把以前的键值写法省略掉,而且function声明也不需要了。
类
ES6中拥有了类这一概念,声明类的方式很简单,跟其他语言一样,差别不大:
class Animal { constructor() { } eat() { } static doSomething(param) { //... }}module.exports = Animal;
class关键字用于声明类,constructor是构造函数,static修饰静态方法。不能理解?我们看一下以前的js的简单写法:
var Animal = function() {};Animal.prototype.eat = function() {};Animal.doSomething = function(param) {};module.exports = Animal;
简单的调用示例
let animal = new Animal();animal.eat();//静态方法Animal.doSomething('param');
这里只是简单的展示了一下不同点,更多的只是还是需要读者自己翻阅更多的资料来学习。
解构
其实本人对结构也不太懂怎样解释,简单的来说就是可以把一个数组的元素或者对象的属性分解出来,直接获取,哈哈,解释的比较勉强,还是看看示例吧。
let obj = { fullName: 'Xiao Ming', gender: 'male', role: 'admin'};let arr = ['elem1', 1, 30, 'arratElem3'];let {fullName, role} = obj;let [elem1, elem2] = arr;console.log(fullName, role, elem1, elem2);
大家可能猜出了什么,看看输出结果:
> Xiao Ming admin elem1 1
我们只要把需要获取的属性或者元素别名指定解构体中,js会自动获取对应的属性或者下标对应的元素。这个新特性非常有用,比如我们需要在一个Pages data对象中一个属性获取对了属性值:
let year = this.data.year, month = this.data.month, day = this.data.day;
但是用解构的写法就很简洁:
let {year, month, day} = this.data;
再比如引入一个文件:
function getDate(dateStr) { if (dateStr) { return new Date(Date.parse(dateStr)); } return new Date();}function log(msg) { if (!msg) return; if (getApp().settings['debug'])console.log(msg); let logs = wx.getStorageSync('logs') || []; logs.unshift(msg)wx.setStorageSync('logs', logs)}module.exports = { getDate: getDate, log: log};
import {log} from '../../utils/util';log('Application initialized !!');
import...from...是ES6的引入模块方式,等同于小程序总的require,但import可以选择导入哪些子模块。
箭头函数(Arrow Function)
刚开始我也不知道js的箭头函数到底是什么东西,用了才发现,这特么就是lambda表达式么。箭头函数简化了函数的写法,但是还是跟普通的function有区别,主要是在作用域上。
比如我们需要请求网络:
wx.request({ url: 'url', header: { 'Content-Type': 'application/json' }, success: function(res) { console.log(res.data)}});
wx.request({ url: 'url', header: { 'Content-Type': 'application/json' }, success: (res) => { console.log(res.data)}});
注意到那个success指向的回调函数了么,function关键字没了,被醒目的=>符号取代了。看到这里大家是不是认为以后我们写function就用箭头函数代替呢?答案是不一定,而且要非常小心!
function和箭头函数虽然看似一样,只是写法简化了,其实是不一样的,function声明的函数和箭头函数的作用域不同,这是一个不小心就变坑的地方。
Page({ data: { windowHeight: 0 }, onLoad() { let _this = this; wx.getSystemInfo({ success: function(res) { _this.setData({windowHeight: res.windowHeight}); } }); }});
一般我们获取设备的屏幕高度差不多是这样的步骤,在页面刚加载的onLoad方法中通过wx.getSystemInfoAPI来获取设备的屏幕高度,由于success指向的回调函数作用域跟onLoad不一样,所以我们无法像onLoad函数体中直接写this.setData来设置值。我们可以定义一个临时变量指向this,然后再回调函数中调用。
哪箭头函数的写法有什么不一样呢?
Page({ data: { windowHeight: 0 }, onLoad() { let _this = this; wx.getSystemInfo({ success: (res) => { _this.setData({windowHeight: res.windowHeight}); } }); }});
运行之后好像感觉没什么区别呀,都能正常执行,结果也一样。确实没什么区别,你甚至这样写都可以:
Page({ data: { windowHeight: 0 }, onLoad() { wx.getSystemInfo({ success: (res) => { this.setData({windowHeight: res.windowHeight}); } }); }});
咦?这样写,this的指向的作用域不是不一样么?其实这就是要说明的,箭头函数是不绑定作用域的,不会改变当前this的作用域,既然这样,在箭头函数中的this就会根据作用域链来指向上一层的作用域,也就是onLoad的作用域,所以他们得到的结果都是一样的。
其实我个人的习惯是无论用普通的函数写法还是箭头函数的写法,都习惯声明临时的_this来指向需要的作用域,因为箭头函数没有绑定作用域,写的层次深了,感觉就会很乱,理解起来比较困难,在后面的案例中,我也会延续这个习惯。
Promise
写js经常写的东西除了数组对象就是回调函数,记不记得用jQuery的ajax用得特别爽,如果是多层嵌套调用的话,那些回调函数简直像盖楼梯一样壮观。现在Promise来了,我们再也不用为这些回调地狱发愁,用Promise来解决回调问题非常优雅,链式调用也非常的方便。
Promise是ES6内置的类,其使用简单,简化了异步编程的繁琐层次问题,比较简单的用法是:
new Promise((resolve, reject) => { //success //resolve(); //error //reject();});
实例化一个Promise对象,它接受一个函数参数,此函数有两个回调参数,resolve和reject,如果正常执行使用resolve执行传递,如果是失败或者错误可以用reject来执行传递,其实他们就是一个状态的转换。可以暂时理解为success和fail。
来看一下简单的示例:
let ret = true;let pro = new Promise((resolve, reject) => { ret ? resolve('true') : reject('false');}).then((res) => { console.log(res); return 'SUCCESS';}, (rej) => { console.log(rej); return 'ERROR';}).then((success) => { console.log(success); let value = 0 / 1;}, (error) => { console.log(error);}).catch((ex) => { console.log(ex);});
或许我们已经看出些什么了,实例化出一个Promise,根据ret的布尔值决定是否resolve执行正常回调流程还是执行reject回调走意外的流程,显然ret是true,当执行resolve时,传递了一个字符串参数true,可以看到实例化出来的Promise对象后面链式调用了很多then方法,其实then方法同样也是有resolve和reject两个回调参数,上层的Promise执行的回调传递到then函数中,Promise的resolve传递到then的resolve,同理reject也一样,之后我们发现最后一个catch函数,这是一个捕抓异常的函数,当流程发生异常,我们可以在catch方法中获取异常并处理。
可能解释的比较羞涩,看看下面例子,发出一个网络请求,获取用户头像,再把用户头像插入DOM中,再睡眠2000ms,再打印出SUCCESS,再睡眠3000ms,在alert出ERROR,再休眠1000ms,最后打印出ERROR。这...看起来有点丧心病狂,但只是举个例子:
$.get('/user/1/avatar', (data) => { $('#avatar img').attr('src', data['avatar']); setTimeout(() => { console.log('SUCCESS'); setTimeout(() => { alert('ERROR'); setTimeout(() => { console.log('ERROR'); }, 1000); }, 3000)}, 2000);});
一共有四个回调函数,也不算多,如果有十几个回调呢?直至是噩梦呀。一层一层的嵌套,看起来已经眼花了。那么Promise能做些什么改变呢?
function sleep(time) { return new Promise((resolve) => { setTimeout(resolve, time); });}new Promise((resolve) => { $.get('/user/1/avatar', resolve);}).then((avatar) => { $('#avatar img').attr('src', avatar);}).then(() => { return sleep(2000);}).then(() => { console.log('SUCCESS'); return sleep(3000);}).then(() => { alert('ERROR'); return sleep(1000);}).then(() => { console.log('ERROR');});
额...看起来怎么使用Promise代码量比不使用的还多呀。不要介意,嘿嘿,可能是我个人封装不精,但是使用Promise的代码可读性确实比上面的要好很多,而且我们不必写一堆的嵌套回调函数,在享受使用同步写法的待遇,又可以得到异步的功能,两全其美,这样的写法还是比较符合日常的思维方式,哈哈。
看看小程序中怎么应用,在小程序项目的app.js中,我们经常看见这段代码:
App({ getUserInfo:function(cb){ var that = this if(this.globalData.userInfo){ typeof cb == "function" && cb(this.globalData.userInfo)}else{ wx.login({ success: function () { wx.getUserInfo({ success: function (res) { that.globalData.userInfo = res.userInfo typeof cb == "function" && cb(that.globalData.userInfo)} })} })} }});
这是个方法是获取当前用户的信息,首先先检查globalData对象中有没有缓存有userInfo对象(存储用户的信息),如果有就返回给用户传进来的回掉函数,否则就请求接口获取用用户信息,获取用户信息之前,微信小程序要求先调用wx.login认证,才能调用wx.getUserInfo接口。
看的出代码的层次已经有点深了,我们可以用Promise来简化一下(-_-|| 说的有点夸张,实际上这点嵌套还是可以的)
wx.getUserInfo和wx.login这两个接口都用共同的属性success和fail,我们可以封装起来:
/** * @param {Function} func 接口 * @param {Object} options 接口参数 * @returns {Promise} Promise对象*/function promiseHandle(func, options) { options = options || {}; return new Promise((resolve, reject) => { if (typeof func !== 'function')reject(); options.success = resolve; options.fail = reject; func(options); });}App({ getUserInfo(cb) { if (typeof cb !== "function") return; let that = this; if (that.globalData.userInfo) { cb(that.globalData.userInfo); } else { promiseHandle(wx.login) .then(() => promiseHandle(wx.getUserInfo)) .then((res) => { that.globalData.userInfo = res.userInfo; cb(that.globalData.userInfo); }) .catch((err) => { log(err); }); } }});
可以看出,使用了Promise之后,代码简洁了不少,层次深度也降低了不少,好家伙,很管用!
其实本次代码中的回调嵌套很少的,为了尽量使用到ES6的新特性,少量的回调嵌套也使用了Promise处理。
介绍了那么多,主要了为了还不了解ES6的读者能够预热一下知识,为后面的案例做好准备,当然,肯定有同学已经对ES6了如指掌,本人也是刚刚学习,欢迎指正错误。
思路
在开工之前,我们先理一下思路,一个普通的日历显示功能应该怎么做,该怎样入手。
日期
获取日期相关的信息,肯定用到Date对象。
let date = new Date();let day = date.getDate(); //当月的天let month = date.getMonth() + 1; //月份,从0开始let year = date.getFullYear(); //年份
我们需要知道当前展示月份的天数。
let dayCount = new Date(currentYear, currentMonth, 0).getDate();
得到可当月月份的天数,可以展示出所有的天数列表,但是我们一样要或者上一个页的天数和下一个页的天数,如果当前月份是1月或者12月,我们还需要额外判断上一页是上一年的12月,下一页是下一年的一月份。
我们可能需要获取足够多的日期信息来展示(不仅仅是当前月份,还有上一月或者上一年和下一月或者下一年)
data = { currentDate: currentDateObj.getDate(), //当天日期第几天 currentYear: currentDateObj.getFullYear(), //当天年份 currentDay: currentDateObj.getDay(), //当天星期 currentMonth: currentDateObj.getMonth() + 1, //当天月份 showMonth: showMonth, //当前显示月份 showDate: showDate, //当前显示月份的第几天 showYear: showYear, //当前显示月份的年份 beforeYear: beforeYear, //当前页上一页的年份 beforMonth: beforMonth, //当前页上一页的月份 afterYear: afterYear, //当前页下一页的年份 afterMonth: afterMonth, //当前页下一页的月份 selected: selected //当前被选择的日期信息};
能显示日期之后,当然还没有完,我们需要一个选择日期的功能,即用户可以点击指定那一天,也可以选择哪一年或者哪一个月,选择年份和月份我们可以用Picker组件来展示,选择具体的哪天这就需要在日期列表上的每一天都要绑定一个点击事件来响应用户的点击动作,用户选择具体的日期后,可能会随意翻页,所以必须要保存好当前选择的日期。
存储
示例程序中用到了数据存储,关系到小程序中的数据缓存API,官方提供的API比较多,我只是用了两个异步的数据缓存API。
wx.setStorage({key: KEY, data: DATA});
let allData=[{id: 1, title: 'title1'}, {id: 2, title: 'title2'}];wx.setStorageSync({key: Config.ITEMS_SAVE_KEY, data: allData});
参数 | 说明 |
---|---|
KEY |
let allData = wx.getStorage({ key: Config.ITEMS_SAVE_KEY success: allData => { let obj1 = allData[0]; console.log(obj1.title); } });
参数 | 说明 |
---|---|
KEY |
建立工程的步骤就不讲了,直接进入主题,应用只有两个页面,一个首页,一个详情页,结构清晰,功能简单。
日历
先来看看首页,日历的wxml结构;
结构分为上中下三部分,header为头部,用于展示翻页按钮和当前日期信息。在.week.row和.body.row元素中展示星期和天数列表,这里的布局采用了比较low的百分比分栏,总共有7栏,100/7哈哈,想高逼格的可以采用css的分栏布局和flex布局。
0}}">事项信息{{item.title}} 当前日期没有事项记录
列表的数据加载全靠这个方法loadItemListData:
/** * 加载事项列表数据 */function loadItemListData() { const {year, month, date} = this.data.data.selected; let _this = this; DataService.findByDate(new Date(Date.parse([year, month, date].join('-')))).then((data) => { _this.setData({ itemList: data }); });}
DataService.findByDate这个方法通过传入一个日期来获取指定日期的事项。成功获取数据之后,在模板中遍历数据,根据level属性来显示不同颜色的图标,让事项等级一目了然。
既然有数据列表,数据从哪来?当然是需要一个数据的添加面板。
首页的有下表有FloatAction操作工具按钮,在这里添加一个添加数据按钮,添加的事项的日期属于用户选中的日期,添加面板默认是隐藏起来的,当点击添加按钮,面板就会向上滑动出现,可以用animationAPI实现动画效果,其实本质也是CSS3动画。
取消保存
在我写到这个内容之前,官方还没有textarea组件,现在新增了,完美解决遗憾。
添加面板的动画控制:
/** * 显示事项数据添加更新面板 */function showUpdatePanel() { let animation = wx.createAnimation({ duration: 600 }); animation.translateY('-100%').step(); this.setData({ updatePanelAnimationData: animation.export()});}/** * 显示模态窗口 * @param {String} msg 显示消息 */function showModal(msg) { this.setData({ isModalShow: true, isMaskShow: true, modalMsg: msg});}/** * 关闭模态窗口 */function closeModal() { this.setData({ isModalShow: false, isMaskShow: false, modalMsg: '' });}/** * 关闭事项数据添加更新面板 */function closeUpdatePanel() { let animation = wx.createAnimation({ duration: 600 }); animation.translateY('100%').step(); this.setData({ updatePanelAnimationData: animation.export()});}
主要靠translateY来控制垂直方向的移动动画,刚进入页面的时候获取屏幕的高度,把面板的高度设置与屏幕高度一致,上滑的时候100%就刚好覆盖整个屏幕。
主要的添加事项逻辑:
// 保存事项数据 saveDataEvent() { const {todoInputValue, todoTextAreaValue, levelSelectedValue} = this.data; const {year, month, date} = this.data.data.selected; console.log(todoInputValue, todoTextAreaValue); if (todoInputValue !== '') { let promise = new DataService({ title: todoInputValue, content: todoTextAreaValue, level: levelSelectedValue, year: year, month: parseInt(month) - 1, date: date}).save(); promise && promise.then(() => { //清空表单 this.setData({ todoTextAreaValue: '', levelSelectedValue: '', todoInputValue: '' }); loadItemListData.call(this); })closeUpdatePanel.call(this); } else { showModal.call(this, '请填写事项内容'); } }
获取添加面板上的数据和当前选择的日期直接用DataSerivce对象保存即可。
由于篇幅有限,剩下的数据删除和数据查看逻辑也比较简单,不再细说,本文主要是介绍小程序的ES6开发。
写完这篇文章的时候,小程序已经公测了好久。本人是个人用户,没有资格参与公测,热情也减半了不少,接触小程序也有一个多月了,写了三个例子,感觉还好,至少能够写出点东西来,不枉这番努力。
效果图
源代码仓库
https://github.com/oopsguy/WechatSmallApps/tree/master/MatterAssistant