本文發表至今已有一段時間,錯別字多、文筆混亂、內容過於陳舊。本人建議讀者不必細究,大概瀏覽即可,最新的開發指南還是以官方文檔為准,該博文的示例代碼經過了重構,已經與官方文檔同步,可能與文中的代碼片段有較大差異,請以 Github 倉庫上的代碼為准。
好久沒有寫關於微信小程序的隨筆了,其實是不知道寫點什么好,之前的豆瓣圖書和知乎日報已經把小程序的基礎部分寫的很詳細了,高級部分的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.getSystemInfo
API來獲取設備的屏幕高度,由於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 | 存儲數據的鍵名 |
DATA | 存儲的數據 |
wx.getStorage({key: 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布局。
<view class="og-calendar">
<view class="header">
<view class="btn month-pre" bindtap="changeDateEvent" data-year="{{data.beforeYear}}" data-month="{{data.beforMonth}}">
<image src="../../images/prepage.png"></image>
</view>
<view class="date-info">
<picker mode="date" fields="month" value="{{pickerDateValue}}" bindchange="datePickerChangeEvent">
<text>{{data.showYear}}年{{data.showMonth > 9 ? data.showMonth : ('0' + data.showMonth)}}月</text>
</picker>
</view>
<view class="btn month-next" bindtap="changeDateEvent" data-year="{{data.afterYear}}" data-month="{{data.afterMonth}}">
<image src="../../images/nextpage.png"></image>
</view>
</view>
<view class="week row">
<view class="col">
<text>一</text>
</view>
<view class="col">
<text>二</text>
</view>
<view class="col">
<text>三</text>
</view>
<view class="col">
<text>四</text>
</view>
<view class="col">
<text>五</text>
</view>
<view class="col">
<text>六</text>
</view>
<view class="col">
<text>日</text>
</view>
</view>
<view class="body row">
<block wx:for="{{data.dates}}" wx:key="_id">
<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' : ''}}">
<text>{{item.date}}</text>
</view>
</block>
</view>
</view>
.btn.month-pre
和.btn.month-next
翻頁按鈕,都綁定了changeDateEvent
的tap事件,各自都用自己的data-year
和data-mont
屬性,這兩個屬性是臨時存值,當點擊按鈕翻頁的時候,我們需要知道當前的年份和日期,以便可以更加方便地翻到上一頁或者下一頁。
changeDateEvent
事件比較簡單:
changeDateEvent(e) {
const {year, month} = e.currentTarget.dataset;
changeDate.call(this, new Date(year, parseInt(month) - 1, 1));
}
點擊翻頁按鈕,根據回調進來的event對象來獲取元素上的data-*
屬性,然后調用changeDate
這個方法來更新日歷數據,這個方法接收一個Date
對象,代表要翻頁后的日期。
暫且不關心changeDate
具體干了些什么,看看.body.row
里有一個循環,每一個元素都綁定了dateClickEvent
事件,而且每一個元素都附帶了自己所屬的年份、月份和天數信息,這些信息是非常有用的,當點擊了具體的某一天,可以通過獲取元素上的data-*
信息來知道我們具體選擇的日期。除此之外,元素上的class
屬性包裹了一長串的判斷表達式。這些語句最終的目的是為了給元素動態變更,.old
代表當前的日期不是本月日期,因為每一版的日期除了當前月份的日期還可能包含上一月和下一月的部分日期,我們給予它灰色的樣式顯示,.current
代表今天的日期,用實心填充顏色的背景樣式修飾,.active
即代表着當前選中的日期。
dateClickEvent
事件其實也是調用了changeDate
事件,本質上也是也是改變日期,額外的工作就是保存選中的日期到selected
對象中。
dateClickEvent(e) {
const {year, month, date} = e.currentTarget.dataset;
const {data} = this.data;
let selectDateText = '';
data['selected']['year'] = year;
data['selected']['month'] = month;
data['selected']['date'] = date;
this.setData({ data: data });
changeDate.call(this, new Date(year, parseInt(month) - 1, date));
}
來看看重中之重的changeDate
函數,這個函數的代碼比較多,雖然堆砌大量在一個函數中是個不好的習慣,不過里面聲明變量和賦值比較多,業務代碼比較少:
/**
* 變更日期數據
* @param {Date} targetDate 當前日期對象
*/
function changeDate(targetDate) {
let date = targetDate || new Date();
let currentDateObj = new Date();
let showMonth, //當天顯示月份
showYear, //當前顯示年份
showDay, //當前顯示星期
showDate, //當前顯示第幾天
showMonthFirstDateDay, //當前顯示月份第一天的星期
showMonthLastDateDay, //當前顯示月份最后一天的星期
showMonthDateCount; //當前月份的總天數
let data = [];
showDate = date.getDate();
showMonth = date.getMonth() + 1;
showYear = date.getFullYear();
showDay = date.getDay();
showMonthDateCount = new Date(showYear, showMonth, 0).getDate();
date.setDate(1);
showMonthFirstDateDay = date.getDay(); //當前顯示月份第一天的星期
date.setDate(showMonthDateCount);
showMonthLastDateDay = date.getDay(); //當前顯示月份最后一天的星期
let beforeDayCount = 0,
beforeYear, //上頁月年份
beforMonth, //上頁月份
afterYear, //下頁年份
afterMonth, //下頁月份
afterDayCount = 0, //上頁顯示天數
beforeMonthDayCount = 0; //上頁月份總天數
//上一個月月份
beforMonth = showMonth === 1 ? 12 : showMonth - 1;
//上一個月年份
beforeYear = showMonth === 1 ? showYear - 1 : showYear;
//下個月月份
afterMonth = showMonth === 12 ? 1 : showMonth + 1;
//下個月年份
afterYear = showMonth === 12 ? showYear + 1 : showYear;
//獲取上一頁的顯示天數
if (showMonthFirstDateDay != 0)
beforeDayCount = showMonthFirstDateDay - 1;
else
beforeDayCount = 6;
//獲取下頁的顯示天數
if (showMonthLastDateDay != 0)
afterDayCount = 7 - showMonthLastDateDay;
else
showMonthLastDateDay = 0;
//如果天數不夠6行,則補充完整
let tDay = showMonthDateCount + beforeDayCount + afterDayCount;
if (tDay <= 35)
afterDayCount += (42 - tDay); //6行7列 = 42
//雖然翻頁了,但是保存用戶選中的日期信息是非常有必要的
let selected = this.data.data['selected'] || { year: showYear, month: showMonth, date: showDate };
let selectDateText = selected.year + '年' + formatNumber(selected.month) + '月' + formatNumber(selected.date) + '日';
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,
selectDateText: selectDateText
};
let dates = [];
let _id = 0; //為wx:key指定
//上一月的日期
if (beforeDayCount > 0) {
beforeMonthDayCount = new Date(beforeYear, beforMonth, 0).getDate();
for (let fIdx = 0; fIdx < beforeDayCount; fIdx++) {
dates.unshift({
_id: _id,
year: beforeYear,
month: beforMonth,
date: beforeMonthDayCount - fIdx
});
_id++;
}
}
//當前月份的日期
for (let cIdx = 1; cIdx <= showMonthDateCount; cIdx++) {
dates.push({
_id: _id,
active: (selected['year'] == showYear && selected['month'] == showMonth && selected['date'] == cIdx), //選中狀態判斷
year: showYear,
month: showMonth,
date: cIdx
});
_id++;
}
//下一月的日期
if (afterDayCount > 0) {
for (let lIdx = 1; lIdx <= afterDayCount; lIdx++) {
dates.push({
_id: _id,
year: afterYear,
month: afterMonth,
date: lIdx
});
_id++;
}
}
data.dates = dates;
this.setData({ data: data, pickerDateValue: showYear + '-' + showMonth });
loadItemListData.call(this);
}
雖然這段這段代碼有點啰嗦,不過總結下來無非就是獲取當前月的信息,上一頁的信息和下一頁的信息,這些信息包括具體的年月日和星期。
年月選擇Picker
既然是日歷,必不可少的功能就是讓用戶可以選擇顯示指定的年份和月份,用pciker
組件來實現最合適不過了,官方更新的api,目前未知,picker
組件已經支持mode = date
模式的風格,即原生的日期選擇。觸發選擇的區域關聯在了日歷的header
上。
<view class="date-info">
<picker mode="date" fields="month" value="{{pickerDateValue}}" bindchange="datePickerChangeEvent">
<text>{{data.showYear}}年{{data.showMonth > 9 ? data.showMonth : ('0' + data.showMonth)}}月</text>
</picker>
</view>
mode=date
指定pciker
是日期選擇風格,fields=month
則顯示組件顯示日期的精度顯示當月份即可,組件初始化的值為pickerDateValue
,綁定了datePickerChangeEvent
事件,當選擇的日期發生變化時,就會觸發此事件。
datePickerChangeEvent(e) {
const date = new Date(Date.parse(e.detail.value));
changeDate.call(this, new Date(date.getFullYear(), date.getMonth(), 1));
}
事項存儲
此應用還有小小的事項功能,可以添加事項條目,事項包括了標題、內容和等級,說白了其實就是一個功能不全的TODO應用...
既然涉及到存儲,肯定需要操作緩存的方法,自己也是剛搞前端那不久,不太明白javascript的封裝約定,借鑒之前在java所用的模式,分為了兩個文件,一個是倉庫類(數據的CURD操作),另一個是業務類(附帶處理部分業務),緩存的配置放置於Config
文件中,類中用到了異步的緩存操作API,所以使用Promise
模式封裝。
首先是把Promise封裝成通用的方法,順便封裝部分經常用到的函數:
/**
* 生成GUID序列號
* @returns {string} GUID
*/
function guid() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
let r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
/**
* 記錄日志
* @param {Mixed} 記錄的信息
* @returns {Void}
*/
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)
}
/**
* @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);
});
}
module.exports = {
guid: guid,
log: log,
promiseHandle: promiseHandle
}
guid
方法用於生成每一個事項的id,方便查詢,log
方法用於日志記錄,promiseHandle
把小程序的大部分異步API封裝到了Promise
對象中。
具體的Config
配置文件:
module.exports = {
ITEMS_SAVE_KEY: 'todo_item_save_Key',
//事項等級
LEVEL: {
normal: 1,
warning: 2,
danger: 3
}
};
數據操作倉庫類 DataRepository:
import Config from 'Config';
import {guid, log, promiseHandle} from '../utils/util';
class DataRepository {
/**
* 添加數據
* @param {Object} 添加的數據
* @returns {Promise}
*/
static addData(data) {
if (!data) return false;
data['_id'] = guid();
return DataRepository.findAllData().then(allData => {
allData = allData || [];
allData.unshift(data);
wx.setStorage({key:Config.ITEMS_SAVE_KEY, data: allData});
});
}
/**
* 刪除數據
* @param {string} id 數據項idid
* @returns {Promise}
*/
static removeData(id) {
return DataRepository.findAllData().then(data => {
if (!data) return;
for (let idx = 0, len = data.length; idx < len; idx++) {
if (data[idx] && data[idx]['_id'] == id) {
data.splice(idx, 1);
break;
}
}
wx.setStorage({key: Config.ITEMS_SAVE_KEY, data: data});
});
}
/**
* 批量刪除數據
* @param {Array} range id集合
* @returns {Promise}
*/
static removeRange(range) {
if (!range) return;
return DataRepository.findAllData().then(data => {
if (!data) return;
let indexs = [];
for (let rIdx = 0, rLen = range.length; rIdx < rLen; rIdx++) {
for (let idx = 0, len = data.length; idx < len; idx++) {
if (data[idx] && data[idx]['_id'] == range[rIdx]) {
indexs.push(idx);
break;
}
}
}
let tmpIdx = 0;
indexs.forEach(item => {
data.splice(item - tmpIdx, 1);
tmpIdx++;
});
wx.setStorage({key: Config.ITEMS_SAVE_KEY, data: data});
});
}
/**
* 更新數據
* @param {Object} data 數據
* @returns {Promise}
*/
static saveData(data) {
if (!data || !data['_id']) return false;
return DataRepository.findAllData().then(allData => {
if (!allData) return false;
for (let idx = 0, len = allData.length; i < len; i++) {
if (allData[i] && allData[i]['_id'] == data['_id']) {
allData[i] = data;
break;
}
}
wx.setStorage({key: Config.ITEMS_SAVE_KEY, data: data});
});
}
/**
* 獲取所有數據
* @returns {Promise} Promise實例
*/
static findAllData() {
return promiseHandle(wx.getStorage, {key: Config.ITEMS_SAVE_KEY}).then(res => res.data ? res.data : []).catch(ex => {
log(ex);
});
}
/**
* 查找數據
* @param {Function} 回調
* @returns {Promise} Promise實例
*/
static findBy(predicate) {
return DataRepository.findAllData().then(data => {
if (data) {
data = data.filter(item => predicate(item));
}
return data;
});
}
}
module.exports = DataRepository;
數據業務類 DataService:
import DataRepository from 'DataRepository';
import {promiseHandle} from '../utils/util';
/**
* 數據業務類
*/
class DataSerivce {
constructor(props) {
props = props || {};
this.id = props['_id'] || 0;
this.content = props['content'] || '';
this.date = props['date'] || '';
this.month = props['month'] || '';
this.year = props['year'] || '';
this.level = props['level'] || '';
this.title = props['title'] || '';
}
/**
* 保存當前對象數據
*/
save() {
if (this._checkProps()) {
return DataRepository.addData({
title: this.title,
content: this.content,
year: this.year,
month: this.month,
date: this.date,
level: this.level,
addDate: new Date().getTime()
});
}
}
/**
* 獲取所有事項數據
*/
static findAll() {
return DataRepository.findAllData()
.then(data => data.data ? data.data : []);
}
/**
* 通過id獲取事項
*/
static findById(id) {
return DataRepository.findBy(item => item['_id'] == id)
.then(items => (items && items.length > 0) ? items[0] : null);
}
/**
* 根據id刪除事項數據
*/
delete() {
return DataRepository.removeData(this.id);
}
/**
* 批量刪除數據
* @param {Array} ids 事項Id集合
*/
static deleteRange(...ids) {
return DataRepository.removeRange(ids);
}
/**
* 根據日期查找所有符合條件的事項記錄
* @param {Date} date 日期對象
* @returns {Array} 事項集合
*/
static findByDate(date) {
if (!date) return [];
return DataRepository.findBy(item => {
return item && item['date'] == date.getDate() &&
item['month'] == date.getMonth() &&
item['year'] == date.getFullYear();
}).then(data => data);
}
_checkProps() {
return this.title && this.level && this.date && this.year && this.month;
}
}
module.exports = DataSerivce;
本人的對數組的操作不是很熟悉,代碼看起來有點臃腫,僅供參考。
好了,進入正題,每天的事項可以用一個列表來展示,列表方在日歷下邊,具體結構:
<view class="common-list">
<view class="header" wx:if="{{itemList.length > 0}}">
<text>事項信息</text>
</view>
<block wx:for="{{itemList}}" wx:key="id">
<view class="item" bindtap="listItemClickEvent" data-id="{{item._id}}" bindlongtap="listItemLongTapEvent">
<view class="inner {{isEditMode ? 'with-check' : ''}}">
<view class="checker" wx:if="{{isEditMode}}">
<icon type="circle" wx:if="{{!item.checked}}" color="#FFF" size="20" />
<icon type="success" wx:else color="#E14848" size="20" />
</view>
<image wx:if="{{item.level == 1}}" class="icon" src="../../images/success.png" />
<image wx:if="{{item.level == 2}}" class="icon" src="../../images/notice.png" />
<image wx:if="{{item.level == 3}}" class="icon" src="../../images/fav-round.png" />
<view class="content">
<text class="title">{{item.title}}</text>
</view>
</view>
</view>
</block>
<view class="header text-center" wx:if="{{!itemList || itemList.length <= 0}}">
<text>當前日期沒有事項記錄</text>
</view>
</view>
列表的數據加載全靠這個方法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
操作工具按鈕,在這里添加一個添加數據按鈕,添加的事項的日期屬於用戶選中的日期,添加面板默認是隱藏起來的,當點擊添加按鈕,面板就會向上滑動出現,可以用animation
API實現動畫效果,其實本質也是CSS3動畫。
<view class="updatePanel" style="top: {{updatePanelTop}}px;height:{{updatePanelTop}}px" animation="{{updatePanelAnimationData}}">
<input placeholder="請輸入事項標題" value="{{todoInputValue}}" bindchange="todoInputChangeEvent" />
<textarea placeholder="請輸入事項內容" value="{{todoTextAreaValue}}" bindblur="todoTextAreaChangeEvent"></textarea>
<view class="level">
<block wx:for="{{levelSelectData}}" wx:key="*this">
<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>
</block>
</view>
<view class="footer">
<view class="btn" bindtap="closeUpdatePanelEvent">取消</view>
<view class="btn primary" bindtap="saveDataEvent">保存</view>
</view>
</view>
在我寫到這個內容之前,官方還沒有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