DIV CSS 佈局教程網

 DIV+CSS佈局教程網 >> 網頁腳本 >> JavaScript入門知識 >> 關於JavaScript >> 微信小程序之ES6與事項助手的功能實現
微信小程序之ES6與事項助手的功能實現
編輯:關於JavaScript     

由於官方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);
}

雖然這段這段代碼有點啰嗦,不過總結下來無非就是獲取當前月的信息,上一頁的信息和下一頁的信息,這些信息包括具體的年月日和星期。

年月選擇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操作工具按鈕,在這裡添加一個添加數據按鈕,添加的事項的日期屬於用戶選中的日期,添加面板默認是隱藏起來的,當點擊添加按鈕,面板就會向上滑動出現,可以用animationAPI實現動畫效果,其實本質也是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開發。

寫完這篇文章的時候,小程序已經公測了好久。本人是個人用戶,沒有資格參與公測,熱情也減半了不少,接觸小程序也有一個多月了,寫了三個例子,感覺還好,至少能夠寫出點東西來,不枉這番努力。

效果圖

源代碼:demo地址

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持。

XML學習教程| jQuery入門知識| AJAX入門| Dreamweaver教程| Fireworks入門知識| SEO技巧| SEO優化集錦|
Copyright © DIV+CSS佈局教程網 All Rights Reserved