1. 程式人生 > >詩和遠方:旅行小賬本雲開發實戰

詩和遠方:旅行小賬本雲開發實戰

最近沉迷小程式開發,發現了一款功能、介面、體驗俱佳的小程式“旅行小賬本”。該小程式由騰訊旅遊操刀製作,簡約大氣,功能性強。藉著最近雲開發的熱潮,著手做了個簡約版——"旅行小賬本"。效果比較滿意,畢竟前後臺一人單幹。

Talk is cheap!
show~

IDE

小程式開發必然少不了微信開發者工具,再加上其對雲開發的全面支援,再好不過的開發利器。但熟悉微信開發者工具的朋友們應該知道,它不支援Emmet縮寫語法,並且wxml的屬性值預設用單引號表示(強迫症表示很難受)。
而VSCode很好的補足了微信開發者工具的不足之處,並且支援多元化

外掛開發,輕量好用。
所以這裡推薦採用微信開發者工具+VSCode配合開發。微信開發者工具負責除錯、模擬小程式執行情況,VSCode負責程式碼編輯工作。二者各司其職,會使開發更加的高效、便捷。

總體架構

該專案基於小程式雲開發,使用的模板是雲開發快速啟動模板
由於是個全棧專案,前端使用小程式所支援的wxml + wxss + js開發模式,命名採用BEM命名規範。後臺則是藉助雲資料庫+雲儲存進行資料管理。

專案總體結構

|-travelbook  專案名
    |-cloudfunctions  雲函式模組
        |-deleteItems 級聯刪除--雲函式
        |-getTime     獲取時間--雲函式
    |-miniprogram  專案模組
        |-components  自定義元件
            |-accountCover  賬本封面元件
            |-spendDetail   支出細節元件
        |-pages  頁面
            |-accountBooks     總賬本頁
            |-accountCalendar  賬本日曆頁
            |-accountDetail    支出細節頁
            |-accountList      支出明細頁
            |-accountPage      選定賬本頁
            |-editAccount      賬本編輯頁
            |-index            首頁
        |-vant-weapp   有贊vant框架元件庫
            |-···      系列元件...
        app.js         全域性js
        app.json       全域性json配置
        app.wxss       全域性wxss
複製程式碼

逆向工程

在做該小程式之前,有必要進行專案的逆向工程,進一步解構每一個頁面,從而深入瞭解這款小程式的互動細節。那麼現在我假設自己為騰訊旅遊的產品設計師,在繪製完介面原型後,撰寫了相應的互動文件。當然解構過程中可能有些細節處理並沒有那麼仔細到位...

以下是我繪製的介面原型

接下來對每個頁面的細節進行解構,並完成簡單的wxml結構

<!--switchList使用定位佈局-->
<view bindtap="switchList" class="list"></view>

<!--newAccount使用flex佈局-->
<view class="newAccount"
bindtap="createNewAccount"> <view class="desc">旅行中的每一筆開支都有獨特的意義!</view> <image src="{{}}"></image> <view class="title">建立一個新賬本</view> </view> 複製程式碼

<!--整體用flex + 百分比佈局-->
<input type="text" class="accuntName" placeholder="旅行賬本名稱" bindinput="getInput" />
  
<van-panel title="選擇封面" class="panel">
    <van-row class="imageBox">
        <!--使用wx:for遍歷資料庫賬本圖片資訊-->
        <van-col span="8" class="imgCol" bindtap="selectThis">
            <image class="select" src="{{}}"></image>
        </van-col>
        
        <van-col span="8">
            <view class="addBox" bindtap="useMore">更多封面</view>
        </van-col>
    </van-row>
</van-panel>

<button type="primary" bindtap="save">儲存</button>
<button type="warn" bindtap="delete">刪除</button>
複製程式碼

<view class="accountDesc" bindtap="viewDetail">
    <!--使用wx:for遍歷資料庫賬本資訊-->
    <view class="accountName">
        <view>{{}}</view>
        <view class="accountTime">{{}}</view>
    </view>
    
    <!--絕對定位-->
    <image class="updateImg" catchtap="editAccount" src="{{}}"></image>
</view>
複製程式碼

<!--switchList使用定位佈局-->
<view bindtap="switchList" class="list"></view>

<view class="account__list-year">{{}}</view>
<view class="account__list-new account__list-public" bindtap="createNewAccount">
    <!--日期小圓點-->
    <view class="account__list-point"></view>
    <view class="account__list-time">{{}}</view>
    <image src="{{}}"></image>
    <view class="account__list-title">建立一個新賬本</view>
</view>

<!--使用wx:for遍歷資料庫賬本資訊-->
<view class="account__list-item account__list-public" bindtap="viewDetail">
    <!--日期小圓點-->
    <view class="account__list-point"></view>
    <image src="{{}}" mode="aspectFill"></image>
    <view class="account__list-name">{{}}</view>
    <view class="account__list-time">{{}}</view>
    <image class="account__list-update" catchtap="editAccount" src="{{}}"></image>
 </view>
複製程式碼

<view class="account__spend">
    <image bindtap="getCalendar" class="account__spend-calendar" src="{{}}"></image>
    <view class="account__spend-text">
        <view class="account__spend-total">總花費(元)</view>
        <view class="account__spend-num">{{}}</view>
    </view>
    <image bindtap="accountAnalyze" class="account__spend-detail" src="{{}}"></image>
</view>

<view class="account__show-time">今天</view>
    <view class="account__show-detail">
        <view class="account__show-income account__show-public">
        <view class="account__show-title">收入(元)</view>
        <text class="account__show-in">+{{}}</text>
    </view>
    <view class="account__show-spend account__show-public">
        <view class="account__show-title">支出(元)</view>
        <text class="account__show-out">-{{}}</text>
    </view>
</view>

<!--使用wx:for遍歷資料庫賬本資訊-->
<view class="account__show-items-spend">
    <view>
        <image src="{{}}"></image>
    </view>
    <text>{{}}</text>
    <text class="account__show-items-money">{{}}</text>
</view>
複製程式碼

<!--日曆使用極點日曆的外掛-->
<!--json中做配置-->
"usingComponents": {
    "calendar": "plugin://calendar/calendar"
}

<!--js改變樣式-->
days_style.push({
  month: 'current',
  day: new Date().getDate(),
  color: 'white',
  background: '#e0a58e'
})

<!--wxml中引用-->
<calendar weeks-type="cn" cell-size="50" next="{{true}}" prev="{{true}}"
    show-more-days="{{true}}" calendar-style="demo6-calendar"
    header-style="calendar-header"board-style="calendar-board" active-type="rounded" 
    lunar="true" header-style="header"calendar-style="calendar"days-color="{{days_style}}">
</calendar>
複製程式碼

<!--頂欄日期及收支結構-->
<view class="account__title">
    <text class="account__title-time">{{}}</text>
    <text class="account__title-spend">支出{{}}元 收入{{}}元</text>
</view>

<!--收支細節結構 使用flex彈性佈局-->
<view class="account__detail">
    <image src="{{}}"></image>
    <view class="account__detail-name">{{}}</view>
    <view class="account__detail-money">{{}}</view>
</view>
複製程式碼

<!--使用vant框架的van-tabs元件-->
<!--並封裝自定義元件複用收支頁,自定義元件後面會詳細說明-->
<van-tabs active="{{ active }}" bind:change="onChange">
  <van-tab title="支出">
    <spendDetail detail="{{detail}}" accountKey="{{accountKey}}"></spendDetail>
  </van-tab>
  <van-tab title="收入">
    <spendDetail detail="{{income}}" accountKey="{{accountKey}}"></spendDetail>
  </van-tab>
</van-tabs>
複製程式碼

雲開發

在做完逆向工程的解構,頁面基礎結構基本搭建完成。但頁面依舊是靜態的,需要資料來填充。所以第二步就是資料庫的設計。而小程式的雲控制檯恰好提供了資料的操作功能,為資料驅動提供基石。

雲資料庫設計

雲資料庫是一種NoSQL資料庫。每一張表是一個集合。值得注意的是在設計資料庫時,_id_openid這兩個欄位需要帶上。_id是表的主鍵,而_openid是使用者標識,每個使用者都有不同的_openid,可區分不同使用者。

以下是專案中的資料表設計

cover_photos 賬本封面表  用於儲存建立賬本時需要的封面資訊
    - _id
    - _openid
    - cover_index 封面索引
    - cover_url   封面url
    - isSelected  封面是否選中
複製程式碼
accounts 賬本表   用於儲存使用者建立的賬本
    - _id
    - _openid
    - accountKey  賬本唯一標識
    - coverUrl    賬本封面
    - i           賬本索引
    - inputValue  賬本名字
    - now         賬本建立時間
    - spend       賬本總花費
複製程式碼
account_detail 支出型別表   用於儲存消費型別
    - _id
    - _openid
    - detail       型別細節
    - pic_index    消費型別索引
    - pic_url      未點選時的圖片
    - pic_url_act  點選後的圖片
    - type         消費型別
複製程式碼
account_income 收入型別表   用於儲存收入型別
    - _id
    - _openid
    - pic_index    收入型別索引
    - pic_url      未點選時的圖片
    - pic_url_act  點選後的圖片
    - type         收入型別
複製程式碼
spend_items   消費明細表
    - _id
    - _openid
    - accountKey   賬本唯一標識
    - address      消費地點
    - desc         消費描述
    - fullDate     消費時間
    - money        消費金額
    - pic_type     消費型別
    - pic_url      消費型別圖片
複製程式碼

雲儲存管理

這是個非常實用的板塊。類似於百度雲盤,它提供了檔案儲存、上傳與下載功能。

除此之外,它還會將你所上傳的資源自動進行壓縮操作,並生成一個地址供你引用。該專案中的一些圖片資源就是存在於此,然後在雲資料庫的欄位中引用這些資源地址即可,十分方便,不必在本地儲存,佔用小程式記憶體。

雲函式設計

雲函式簡單來說就是在雲後端(Node.js)執行的程式碼,本地看不到這些程式碼的執行過程,全封閉式只暴露介面供本地呼叫執行,本地只需等待雲端程式碼執行完畢後返回結果。這也是面向介面程式設計的思想體現。

專案中的雲函式設計

// getTime  獲取當前時間並格式化為 yyyy-mm-dd

// 雲函式入口檔案
const cloud = require('wx-server-sdk')

// 初始化雲函式
cloud.init()

// 雲函式入口函式
exports.main = async (event, context) => {
  var date = new Date()
  var seperator1 = "-"
  var year = date.getFullYear()
  var month = date.getMonth() + 1
  var strDate = date.getDate()
  if (month >= 1 && month <= 9) {
    month = "0" + month
  }
  if (strDate >= 0 && strDate <= 9) {
    strDate = "0" + strDate
  }
  // 格式化當前時間
  var currentdate = year + seperator1 + month + seperator1 + strDate
  return currentdate
}
複製程式碼
// deleteItems  批量刪除,雲資料庫的批量刪除只允許在雲函式中執行

// 雲函式入口檔案
const cloud = require('wx-server-sdk')

// 初始化雲函式
cloud.init()

// 連線雲資料庫
const db = cloud.database()
const _ = db.command


// 雲函式入口函式
exports.main = async (event, context) => {
  try {
    return await db.collection('spend_items')
      .where({
        accountKey: event.accountKey
      })
      .remove()
  } catch (e) {
    console.error(e)
  }
}
複製程式碼

MVVM

介面有了,資料有了。萬事俱備,只欠東風!所以下一步就是MVVM的設計。小程式本質就是基於MVVM所設計的,在MVVM的世界裡,資料是靈魂,一切都由資料來驅動。

賬本頁顯示

賬本頁有兩種顯示的風格,左上角的按鈕可以來回切換風格,下拉可重新整理頁面,顯示accounts資料表中儲存的賬本資訊。顯示時有個小細節,需要根據建立的時間先後來顯示,越晚建立的越先顯示。

// 頁面資料設計, 在wxml中使用{{}}符號引用資料,資料就動態顯示到了頁面上
data: {
    isList: false, // 轉換頁面風格的標識 true為豎向風格 false為橫向風格
    accounts: [],  // 儲存查詢的賬本資料
    now: null,     // 儲存當日時間
    year: null     // 儲存年份
}

 // 轉換顯示風格
switchList() {
    // 設定頁面風格樣式
    let isList = !this.data.isList
    this.setData({
      isList
    })
    wx.setStorage({
      key: "isList",
      data: isList
    })
}

// 獲取頁面風格轉換標識
var isList = wx.getStorageSync('isList')
    
// 查詢賬本
db.collection('accounts')
  .get({
    success: res => {
      this.setData({
        accounts: res.data.reverse(),  // 反轉陣列,優先顯示建立早的賬本
        isList
      })
      wx.hideLoading()
    }
  })

// 呼叫雲函式介面 獲取當前日期
wx.cloud.callFunction({
    // 雲函式介面名就是建立的雲函式名字,這裡是'getTime'
    name: 'getTime',
    success: (res) => {
    let year = res.result.split('-')[0]
    this.setData({
      now: res.result,
      year
    })
    },
    fail: console.error
})
複製程式碼

賬本頁增刪改

賬本頁通過呼叫相應的雲資料庫API,可進行一系列的增刪改操作。值得一提的是,修改時需要表單回顯,刪除時需要級聯刪除。因為一個賬本中有許多收支情況,spend_items表就是進行收支記錄,所以刪除賬本時需要級聯刪除對應的spend_items表中的收支資訊。

一些重要的邏輯

  • 封面單選邏輯
    data: {
        images: [],      // 封面陣列
        selectImg: null, // 選擇其它封面
        isSelected: {},  // 選中的圖片
        inputValue: '',  // 賬本名字
        now: null,       // 當前時間
        account: {}      // 傳入賬本資訊
    }
    
      // 單選邏輯 通過構造{'0': isSelected}來實現
    selectThis(e) {
        let index = e.currentTarget.dataset.index
        let coverUrl = e.currentTarget.dataset.coverurl
        let is = this.data.isSelected[index]
        let obj = {
            coverUrl
        }
        // obj[index] 屬性動態改變
        obj[index] = !is
        obj.i = index
        this.setData({
            isSelected: obj
        })
    }
    複製程式碼
  • 表單回顯邏輯
    // 頁面載入時先通過對應的accountKey, 得到回顯資訊
    let { i, id, value, url, accountKey } = options
    photos.get({
        success: res => {
        this.setData({
          images: res.data,
          account: {
            id,
            value,
            url,
            i,
            accountKey
          },
          isSelected: obj
        })
        wx.hideLoading()
      }
    })
    // 修改
    save() {
        let { id } = this.data.account
        let { i, coverUrl, value } = this.data.isSelected
        // 若沒修改 則為之前的value
        let inputValue = this.data.inputValue || value
        
        db.collection('accounts')
          .doc(id)
          .update({
            data: {
                inputValue,
                coverUrl,
                i
            }
        })
    }
    複製程式碼
  • 級聯刪除邏輯
    db.collection('accounts')
        .doc(this.data.account.id)
        .remove()
        .then(() => {
          wx.hideLoading()
          wx.showToast({
            title: '刪除成功'
          })
          setTimeout(() => {
            wx.reLaunch({
              url: '../accountBooks/accountBooks'
            })
          }, 400)
        })
      // 呼叫deleteItems雲函式, 傳入對應accountKey主鍵, 通過雲函式批量刪除
      wx.cloud.callFunction({
        name: 'deleteItems',
        data: {
          accountKey
        }
      })
    複製程式碼

賬本頁收支

因為收入與支出頁面基本類似,所以使用自定義元件封裝,可以複用。

// 封裝spendDetail元件
// 註冊元件
properties: {
    detail: {
      type: Object
    },
    accountKey: {
      type: Number
    },
    isSpend: {
      type: Boolean
    }
}

// 引用元件
<van-tab title="支出">
    <spendDetail detail="{{detail}}" accountKey="{{accountKey}}" isSpend="{{isSpend}}"></spendDetail>
  </van-tab>
  <van-tab title="收入">
    <spendDetail detail="{{income}}" accountKey="{{accountKey}}" isSpend="{{isSpend}}"></spendDetail>
</van-tab>
複製程式碼

收入與支出型別icon選擇使用兩個view來存放,通過選擇不同型別,跳轉不同的icon

// js
data: {
    address: '',
    money: 0,
    desc: '',
    selectPicIndex: 0,
    selectIndex: 0
}
// 選擇消費類別
selectSpend(e) {
  let { index } = e.currentTarget.dataset
  let { selectPicIndex } = this.data
  selectPicIndex = index
  this.setData({
    selectPicIndex
  })
},

// 選擇消費類別中的細節
selectSpendDetail(e) {
  let { index } = e.currentTarget.dataset
  let { selectIndex } = this.data
  selectIndex = index
  this.setData({
    selectIndex
  })
}

// wxml
// 消費型別
<view class="expense">
  <block wx:for="{{detail}}" wx:key="index">
    <view class="expense__type" bindtap="selectSpend" data-index="{{index}}">
      <block wx:if="{{selectPicIndex == item.pic_index}}">
        <view class="expense__type-icon" style="background-color: #e64343">
          <image src="{{item.pic_url_act}}"></image>
        </view>
      </block>
      <block wx:else>
        <view class="expense__type-icon">
          <image src="{{item.pic_url}}"></image>
        </view>
      </block>
      <view class="expense__type-name">{{item.type}}</view>
    </view>
  </block>
</view>

// 消費子型別
<view class="detail">
  <block wx:for="{{detail[selectPicIndex].detail}}" wx:key="index">
    <view class="detail__type" bindtap="selectSpendDetail" data-index="{{index}}">
      <image class="detail__type-icon" src="{{item.detail_url}}"></image>
      <block wx:if="{{selectIndex == item.detail_index}}">
        <view class="detail__type-name" style="color: #f86319; border-bottom: 1rpx solid #f86319;">
          {{item.detail_type}}
        </view>
      </block>
      <block wx:else>
        <view class="detail__type-name" style="border-bottom: 1rpx solid #e4e2e2;">
          {{item.detail_type}}
        </view>
      </block>
    </view>
  </block>
</view>
複製程式碼

賬本頁明細

因為收支明細中需要顯示每一天的消費資訊,所以需要將資料表中的資料通過時間來分類,分成若干個陣列,頁面從而使用wx:for來遍歷這些陣列。在顯示之前,首先需要判斷有無收支資訊。

// 通過時間分類演算法  {} => [ [{時間1}], [{時間2}], [{時間3}] ]
arr.forEach(item => {
  if (!_this.isExist(item.fullDate, dateArr)) {
    dateArr.push([item])
  } else {
    dateArr.forEach(res => {
      if (res[0].fullDate == item.fullDate) {
        res.push(item)
      }
    })
  }
})

// 使用map 方法構造 [{}, {}, {}, ...] 型別陣列
dateArr = dateArr.map((item) => {
  let spend = 0
  let income = 0
  item.forEach(res => {
    if (res.money > 0) {
      spend += res.money
    } else {
      income += (-res.money)
    }
  })
  return {
    item,
    spend,
    income
  }
})

// 判斷自身是否存在陣列中
isExist(item, arr) {
    for (let i = 0; i < arr.length; i++) {
      if (item == arr[i][0].fullDate)
        return true
    }
    return false
  }
複製程式碼

以上是小程式中比較複雜的邏輯實現。

一點感悟

提交日誌 github.com/FightingHao…

之前做專案時,只是在github提交時草草寫一句話當做提交日誌。這次做了一個比較正式提交日誌,做這個的初衷其實是為了監督自己不要偷懶,堅持每天完成專案一部分,並總結不足之處。學而時習之才能成長的更快!

篇幅有限,奉上專案github 如果你喜歡這篇文章或是這個專案,不妨進去點個Star支援下,有興趣的朋友歡迎Fork,一起探討知識或是旅行~~當然也希望您能留下一些寶貴的建議。感激不盡!

生活不止眼前的苟且,還有詩和遠方。最後要感謝騰訊旅遊的各位大大設計出一個這麼簡潔美觀大方的小程式產品,實屬良心之作!