由于官方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 | 存儲數據的鍵名 |
| 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);}雖然這段這段代碼有點 主站蜘蛛池模板: 苏尼特左旗| 深圳市| 巴南区| 墨竹工卡县| 兰西县| 孙吴县| 榕江县| 陈巴尔虎旗| 南江县| 霍城县| 曲阳县| 巨鹿县| 铁岭县| 岫岩| 循化| 呼和浩特市| 建湖县| 遂川县| 时尚| 新田县| 滕州市| 曲麻莱县| 仁化县| 梅州市| 陇西县| 新竹市| 洪洞县| 泗阳县| 周口市| 泸州市| 凤凰县| 施甸县| 新龙县| 南安市| 广灵县| 南乐县| 马关县| 年辖:市辖区| 资溪县| 资源县| 湛江市|