用小程序·云开发打造运动圈小程序丨实战
乒乓圈小程序
和朋友合伙寫了一個小程序,寫了一個以共享乒乓信息和交流的平臺———乒乓圈。我們使用了微信的云開發來完成數據和后臺的作用。免去了租賃服務器。
我主要負責的是數據庫的設計和云函數實現數據獲取和觸發器的功能和簡單的兩個頁面。
正文
功能展示
頁面分析
引導頁
當用戶未授權則會彈出,點擊下方指紋圖片,則會彈出授權框,授權后,如果未注冊則會注冊完畢后進入首頁
tabbar中的三個模塊
三個模塊分別為 首頁、圈友、個人 模塊。首頁的三個功能
-- 同城圈可以看到共享的球館,點擊加號就可共享球館
-- 簽到規則可以增加積分
-- 可以看積分排行榜
頁面流程大致分為
- 引導頁- 首頁- 同城圈- 打卡- 榜單- 圈友頁- 同城圈友- 留言列表- 個人頁- 個人資料數據庫
從以上的功能出發我的數據庫設計思路如此
對象有以下幾個:
- 個人
- 球館
- 對話
對象就只有大致三個,但是為了數據操作的簡便性我將個人的信息分成兩個對象表,將留言中的對話又單獨放出一張表,所以最后的表有為一下幾個:
- 個人基礎信息
- 個人詳細信息(乒乓球相關)
- 球館
- 對話
- 留言信息
對象屬性的類型選擇
首先,小程序提供的數據庫是基于mangoDB的面向對象數據庫,區別于一般的關系數據庫如:mysql等。二者之間的區別和我的理解會寫在總結中。
信息是反映對象狀態的一種
我認為數據庫存儲的屬性大致可分為三種
- 基礎信息數據
-- 是需要存儲的基礎數據,無需任何處理可直接輸出的數據,例如:姓名等 - 功能性數據
-- 是可能需要一定處理轉變,表示的數據,例如:會員等級(vip,svip) - 標記型數據
-- 是一種特殊的標記,例如:唯一標識符(openId)等
但是很多信息都兼顧以上的幾種,例如:學號(即是標記型,又是基礎信息)
確認完對象基礎屬性后就要考慮對象之間的關系,例如人和對話,留言和對話信息。
關系種類有 一對一(1-1),一對多(1-n),多對多(m-n)。
在 關系數據庫 中,一對一的關系只要在一條記錄中添加一個屬性即可,例如:個人信息和個人詳情,在個人詳情中添加個人的唯一表示符字段;
一對多的關系中需要在多數的記錄中添加一個屬性,或者單獨建立一張表來存儲關系,
例如:個人和物品,第一種在物品對象中添加一個所有者對象,或者建立一個所屬關系表;
多對多的關系則只能通過單獨一張關系表來完成,例如:學生和課程,需要單獨一張選課表來表示關系。
在 面向對象數據庫中一對多和多對多的關系可以通過對象中的一個數組字段來完成,例如:學生和課程,在學生對象中添加一個所選課程字段存儲課程 ID ,在課程中添加選課學生字段存儲學號,就完成了多對多的關系鏈接。
對象結構如下所示
個人基礎信息:
openId:{type:String}//openId主鍵 name:{type:String}//名字,默認為微信名 avatarUrl:{type:String}//頭像,默認微信頭像 context:{type:String,default:"這個人很懶什么都沒留下"}//個人簡介 //以下幾項應為流水數據應但對放一張表,在此為圖簡便放入基礎信息表 intergal:{type:Number,default:0}//積分 用來排名和升級 level:{type:String,default:"新人"}//等級 sign:{type:Array,default:[]}//記錄打卡簽到的日期 month:{type:Number}//記錄上次打卡簽到的月份,用于每月清空簽到表個人詳情(乒乓球相關)
openId:{type:String}//openId主鍵 years:{type:String}//球齡 phone:{type:Array}//電話 bat:{type:String}//球拍 board:{type:String}//底板 context:{type:String}//正面膠皮 intergal:{type:Number,default:0}//反面膠皮球館
id:{type:String}//主鍵,由數據庫自動生成 address:{type:String}//地址 arena:{type:String}//所在區域,例如球館名 persons:{type:Array}//球館的活動者,需求更改數據庫中的字段為circle city:{type:String}//球館所在的城市 img:{type:String}//球館圖片地址 latitude:{type:Number}//經度 longitude{type:Number}//緯度 table:{type:Number}//球桌數 time:{type:String}//開放時間對話
message:{type:Array,default:[]}//留言內容數組,存儲留言的id my_id:{type:String}//創建者的openId other_id:{type:String}//接收者的openId留言信息
id:{type:String}//主鍵,由數據庫自動生成 msg:{type:String}//留言內容 my_id:{type:String}//創建者的openId other_id:{type:String}//接收者的openId time:{type:Date}//時間戳云函數讀取數據庫和部分前端實現
1. 引導頁
當第一次登陸進區就是如上所示,登陸進去后通過 openId 進行云函數獲取數據庫中個人信息,如果沒有則默認進行注冊流程。
默認昵稱為微信昵稱(可在個人頁更改),頭像為微信頭像(暫不提供更改),余下都為默認值。
引導頁 js
以上流程可分為以下幾步。
1. 進行 onLoad (頁面加載完成) 生命周期 判斷是否有緩存
首先調用 wx.getStorage 查詢收緩存的登陸信息,如果獲取成功,跳過引導頁,將當前登陸狀態的標識符(默認false)改為 true 。
通過 setTimeout 來控制提示信息和超時檢測。
2. 未緩存,進入登陸注冊功能
如果獲取緩存中登陸狀態。先獲取授權信息 getUserInfo 判斷獲取到的用戶信息存在且為登錄。
the_first 判斷是否是第一次進入要進行注冊流程(保險作用).
調用云函數 getPersonhInfo ——獲取用戶信息,如果獲取成功,結果集不為空,就將信息存儲到全局狀態 app.globalData 中.接著調用云函數 getpingpang_info 獲取個人詳細信息一樣放入全局狀態中,然后寫入緩存信息
wx.setStorage({key: 'login',data: true})以便下次不用檢驗,最后通過 wx.switchTab({url: '../home/home',}) 來跳轉到首頁。
如果上一步未獲取成功,判斷為第一次登入,進入注冊流程 先獲取用戶的昵稱后頭像,調用云函數 pingpang_init后臺進行注冊,并將初始值放入全局狀態中,跳轉到首頁。
所需云函數
1. getPersonhInfo
// 云函數入口文件 const cloud = require('wx-server-sdk')cloud.init()const db = cloud.database(); const personinfo = db.collection("pingpang_personinfo") const _ = db.command; // 云函數入口函數 exports.main = async(event, context) => {let {openIdarr,openId,all,city} = event;if (all) {//獲取所有人信息return await personinfo.get()} else if (city) {//獲取所給城市的所有用戶信息return await personinfo.where({city}).get()} else if (openIdarr) {//獲取openId在所給數組中的所有用戶信息console.log(openIdarr)return await personinfo.where({openId: _.in(openIdarr)}).get()} else {//獲取所給openId或自身的用戶信息return await personinfo.where({openId: openId || event.userInfo.openId}).get()} }這個云函數是獲取用戶信息,
首先解構用戶傳來的參數來判斷需要的數據,openIdarr--通過openId數組獲取,openId--通過openId獲取,city--通過用戶所在城市獲取,all--獲取所有用戶,以上四種都沒有則獲取當前用戶的信息。
注:event.userInfo.openId 只有用戶程序直接調用云函數的時候,云函數才可以獲取到,當云函數調用云函數時,被調用的的云函數無法獲取 userInfo 這個對象屬性。
2. getpingpang_info
// 云函數入口文件 const cloud = require('wx-server-sdk')cloud.init() const db = cloud.database(); // 云函數入口函數 exports.main = async(event, context) => {return await db.collection("pingpang_info").where({openId: event.openId || event.userInfo.openId}).get() }這個云函數只是簡單的通過兩種方式(給與openId或默認自身)來獲取獲取用戶詳細信息
3. pingpang_init
// 云函數入口文件 const cloud = require('wx-server-sdk')cloud.init()// 云函數入口函數 exports.main = async (event, context) => {const fun1 = await cloud.callFunction({name: "addPersonInfo",data: {openId: event.userInfo.openId,name: event.name || "未獲取到名字",avatarUrl: event.avatarUrl,city: event.city,//所在城市level: "新人",intergal: 0,context: "",activitiew: [],circle: []}})const fun2 = await cloud.callFunction({name: "addpingpang_info",data: {openId: event.userInfo.openId,phone: '***********',years: '0年',bat: '右手橫拍',board: '新手用具',infront_rubber: '新手用具',behind_rubber: '新手用具'}})return { fun1, fun2 } }這個函數是初始化函數,功能是向數據庫添加新用戶的初始數據。
2.簽到功能
簽到功能的頁面并非我寫的,所以我只能提供思路和云函數。
簽到的存儲是在個人信息的一個字段sign中,以數組的形式存儲,當點擊簽到時,先判斷此次簽到的月份與上次簽到的月份(person的month字段)是否相同,不同則將sign數據置為空并且將month字段更新為當前月份,接著存儲簽到的日期的的day,
云函數
// 云函數入口文件 const cloud = require('wx-server-sdk')cloud.init() const db = cloud.database(); const personInfo = db.collection("pingpang_personinfo")// 云函數入口函數 exports.main = async(event, context) => {let data = personInfo.where({openId: event.openId || event.userInfo.openId})let info = await data.get()//先獲取let sign = info.data[0].sign || [] //放入新數組//判斷是否到了新的月份if (info.data[0].month != new Date().getMonth()) {sign = [];var month = new Date().getMonth()}//替換數組return await cloud.callFunction({name: "setPersonInfo",data: {personInfo: {//event.date是為了方便寫管理調試用的一次性放日期數組sign: sign.concat(event.date || [new Date().getDate()]),month:month}}}) }3.排行榜單
榜單十分簡單,有多種做法:
1. 第一種是將同城所有人查詢出來按照積分排序,并區前一定數量的用戶來輸出排行榜
- 優點:無需其他的資源來存儲,不占用空間,修改排行榜的時候無需多余的處理
- 缺點:無法承載大量的用戶,當用戶增多到一定數量后,單次查詢時間會變得很慢,查詢并發數量會有問題,因為查詢的都是同一張表
所以這種方法只適用于用戶量較少的情況下。
2. 第二種是以城市排行榜為對象,創建一張表,表中存儲的對象的屬性大致如下所示
- 優點:減少了查詢后大量數據的處理,單人查詢一次只需要處理相應數量的數據,不需要遍歷一遍所有數據
- 缺點:需要額外的存儲空間,如果存儲的是用戶 openId 那查詢速度依然較慢,如果存儲的是用戶對象,那么查詢速度只需要查詢單張表的時間,修改排行榜的時候又需要單獨處理數組字段,較為麻煩。
這種方式使用用戶量較大但是分散的情況,可以普遍使用。
city:{type:String }, //存儲排行榜,存儲一定數量的用戶openId,或者是用戶對象 list:{type:Array,default:[] }, minIntergal:{type:Number,default:0 }3. 第三種則是以每個城市為一張表,存儲積分達到排行榜對象
- 優點:有點是解決了處理數量的問題,并發問題也解決了,單個城市的人處理一張表,并發數量會下降。
- 缺點:大量占用空間
這種做法適用于用戶數量極大的時候。
總體方案:從以上方法來說最好的方法是,在大量的用戶的城市,做單獨一張表來存儲,剩余小型城市則存儲在剩余的總表中,唯一的缺點就是判斷處理的麻煩,當一個城市用戶變多時,需要在數據庫中添加一張新表,這需要手動來解決,變更后臺的處理判斷,可以使用策略模式來解決。
4. 留言功能
留言功能,是這個小程序的主要功能之一,目的是為了向興趣相同的乒乓愛好者有一個初始的交流平臺。
創建留言需要在圈友(同城的)中找到相應的用戶,然后點擊頭像,彈出詳情,接著點擊留言按鈕,會跳轉到留言對話頁。
留言有兩種情況,一種是之前有過留言,存在留言對象,另一種則是第一次對話,之前不存在留言對象。
第一種,只需要查詢到存在就可向里面添加留言信息。第二種則需要先創建在進行添加。
第一種沒有任何問題,直接對對話對象的留言數組中進行添加,第二種則需要創建一個對話對象。
具體流程:首先在留言頁查詢到所有對話對象,這是走第一種情況,可以跳轉到直接添加留言,第二種則是在圈友頁中對象的詳情頁點擊留言按鈕,這會先查詢對話兌現,不存在則會跳轉到空白對話頁,否則跳轉到之前的留言對象。
這種方法不是很好
缺點如下:
- 如果點擊留言但是不留言,會創建一個空白的對話對象,用戶存在誤觸按鈕的情況,這會存在很多空白留言,這是一大缺陷。
- 因為這是前端來控制的所以存在一定的延遲,要進行多次異步的操作,造成延遲。
推薦方案 :在點擊留言時查詢之前是否存在對話兌現,存在即讀取,不存在就跳轉到空白頁,如果發送了留言,則創建對象。這樣可已解決以上的缺點。但是還是存在一個問題就是未使用 socket 無法達成實時通信。
云函數
云函數的大致功能為:
首先,結構傳遞的參數my_id(當前用戶的id) 和 other_id(留言對象的id)。接著判斷 my_id 是否存在,不存在就給當前用戶的 openId ,最后判斷 ohter_id 如果不存在,則查詢前用戶所有的對話。
上述的云函數功能大致為:
首先,結構參數,message(留言對象,包含之后的幾個參數),my_id,other_id和msg(留言內容)。接著判斷 my_id是否存在,不存在就用當前用戶的 openId 。然后是向留言表添加一條新數據 messagedb.add 最后獲取對話對象并向對話中的留言數組中添加留言內容。
5. 個人模塊
個人模塊沒有什么復雜的邏輯,就是數據渲染頁面,不過頁面結構是我寫的,可以聊一聊頁面了。
個人頁
個人頁面中沒有什么比較花里胡哨的樣式操作,只有簡單基礎的 css 和 html ,所以就在此簡單結構一下。
大概要講的就是點擊切換成輸入框的所需要講的,還有下面的選擇欄變變成組件
頁面(部分)
//個人簡介 <view class="infocard" bindtap='typeInfo'><input type="text" wx:if="{{changecontext}}" placeholder='' bindblur='setcontext' focus='true' value="{{personInfo.context}}" maxlength='18'></input><view class="context" wx:else bindtap='changecontext'>個性簽名:{{personInfo.context}}</view></view>//個人資料框<view class="project collections" bindtap="ToPage" data-name="pingpang_info"><image class="image" src="https://636f-coldday-67x7r-1259123272.tcb.qcloud.la/person.svg?sign=73135fcd2247e0a00ca78c131fa0d7d6&t=1559030458" /><view class="title">個人資料</view><text class='cuIcon-right righticon text-grey'></text></view>js(部分)
data:{changecontext: false } changecontext() {this.setData({changecontext: true})},setcontext(event) {this.setData({changecontext: false})if (event.detail.value != "") {this.setData({"personInfo.context": event.detail.value,context: event.detail.value})} else {this.setData({"personInfo.context": "這家伙打完球后不留任何足跡",context: "這家伙打完球后不留任何足跡"})}}, ToPage(event) {wx.navigateTo({url: `../${event.currentTarget.dataset.name}/${event.currentTarget.dataset.name}`,fail: () => {wx.showModal({title: '(?_?)',content: '敬請期待!',showCancel: false})}})},文本和輸入框的切換,是通過 wx:if 來控制顯示,讓兩個大小近似的塊占用相同的地方,當點擊文本時,數據源(data)中的 changecontext 變量變成 ture 頁面重新渲染,將輸入框顯示 value 為數據源中的個人簡介,文本則隱藏;當輸入框失去焦點時,將輸入框中的value值寫入數據源中,然后changecontext變為false,頁面重新渲染,就改完了個人簡介。
修改后提交數據的方案有三種
第一種和第三種都可以普遍使用。推薦第一種方式,因為大多數用戶不會過于頻繁的去修改這些東西,但是頁面基本都是每次登陸都會訪問多次的。頻率和并發都是第一種好。
個人詳情
個人詳情就是普通的頁面,沒有復雜的云函數,只有一個獲取,一個提交修改,兩個函數都不復雜。
詳情頁中球拍和球齡是使用了小程序自帶組件 picker 其余則是使用了自定義組件 info-section
頁面
<view class="container"><view class="cu-form-group"><view class="title">球齡</view><picker bindchange="PickerAgeChange" value="{{indexAge}}" range="{{pickerAge}}"><view id='picker' class='picker'>{{indexAge?pickerAge[indexAge]:personInfo.years}}<text class='cuIcon-title' style='opacity:0'></text></view></picker></view><section title="電話" info="{{personInfo.phone}}" infoname="phone" bind:changend="getinfo" type='number'/><!-- <section title="球齡" info="{{personInfo.years}}" infoname="years" bind:changend="getinfo" type='number'/> --><!-- <section title="持拍" info="{{personInfo.bat}}" infoname="bat" bind:changend="getinfo" /> --><section title="使用底板" info="{{personInfo.board}}" infoname="board" bind:changend="getinfo" /><section title="正手膠皮" info="{{personInfo.infront_rubber}}" infoname="infront_rubber" bind:changend="getinfo" /><section title="反手膠皮" info="{{personInfo.behind_rubber}}" infoname="behind_rubber" bind:changend="getinfo" isbottom="true" /><view class="cu-form-group"><view class="title">持拍</view><picker bindchange="PickerChange" value="{{index}}" range="{{picker}}"><view id='picker' class='picker'>{{index?picker[index]:personInfo.bat || '點擊選擇'}}</view></picker></view><button class='button' bindtap="submit">點擊提交</button> </view>js
const app = getApp(); Page({/*** 頁面的初始數據*/data: {personInfo: {},picker: ['右手橫拍', '右手直拍', '左手橫拍', '左手直拍']},PickerChange(e) {let personInfo = this.data.personInfo;this.setData({index: e.detail.value})personInfo.bat = this.data.picker[this.data.index];this.setData({personInfo})},PickerAgeChange(e) {let personInfo = this.data.personInfo;this.setData({indexAge: e.detail.value})personInfo.years = this.data.pickerAge[this.data.indexAge];this.setData({personInfo})},getinfo() {this.setData({personInfo: app.globalData.ping_personInfo,})},submit() {let personInfo = this.data.personInfo;if(personInfo.phone.length != 11){wx.showModal({title: '提示',content: '無效電話號碼',showCancel:false})personInfo.phone = '';this.setData({personInfo})return}const that = this;console.log("開始提交")wx.showLoading({title: '提交中',})let info = this.data.personInfowx.cloud.callFunction({name: "setpingpang_info",data: info}).then(res => {wx.hideLoading();wx.showToast({title: "提交成功",duration: 1000,})console.log(res, "修改成功")wx.navigateBack({})})},/*** 生命周期函數--監聽頁面加載*/onLoad: function (options) {this.setData({personInfo: app.globalData.ping_personInfo})let pickerAge = []for (let i = 0; i < 51; i++) {pickerAge.push(i + '年')}this.setData({ pickerAge })} })自定義組件 info-section
頁面
<view class="cu-form-group"><view class="title">{{title}}</view><input type='{{type}}' wx:if="{{changeinfo}}" bindblur='changend' value='{{info}}' placeholder="請輸入信息" focus='true'></input><view class='info' wx:else><input value='{{info?info:"新手用具"}}' disabled='true'></input></view><view class='icon-con' bindtap='changeinfo'><image src="https://636f-coldday-67x7r-1259123272.tcb.qcloud.la/change-1.png?sign=c8936111328dcb2ee416201369716380&t=1559030699" class='icon'></image></view> </view>js
// components/info-section/section.js Component({/*** 組件的屬性列表*/properties: {title: {type: String,value: "屬性名"},info: {type: String,value: "屬性值"},infoname: {type: String,value: ""},isbottom: {type: Boolean,value: false},type:{type:String,value:'text'}},/*** 組件的初始數據*/data: {changeinfo: false,},/*** 組件的方法列表*/methods: {changeinfo() {this.setData({changeinfo: true})this.triggerEvent("changeinfo");},changend(event) {this.setData({changeinfo: false})getApp().globalData.ping_personInfo[this.properties.infoname] = event.detail.value//拋出事件以便于父組件響應this.triggerEvent("changend")}} })父子組件的通訊一定要注意在子組件中拋出事件,觸發父組件的事件來達成。
總結
開發總結
良好溝通的重要性
在和朋友一起開發小程序的過程中注意到了以下的問題, 溝通 是最重要的,在我們開發的過程中,因為沒有良好的溝通,導致,前后端的功能開發對接不完美。部分功能分配不好,有些功能可以同過前端或后端單獨解決,缺因為沒有溝通完善,導致雙方都做了或者雙方都沒做的情況發生,雖然有每個人都有自己的事,大多數時間都是單獨開發的原因在。但是這些問題應當在代碼開發流程就應當做的,這是我了解的一個問題。
個人思考
程序的結構
程序的結構大致分為前端頁面、后端服務器和數據庫三個組成部分。在小程序這種 MVVM 結構中前端占有了很重要的一部分。
前后端和數據庫的比例大致為 n:1:1 的關系,所以當用戶量大的程序,多數操作應當放在前端中處理,這是現在 mvvm 稱為主流的原因,后臺主要統籌管理總體數據或者對重要的流水數據處理,并且需要提供大量的 api 供前端獲取數據,
這樣能大量緩解數據庫的壓力。
關系型數據庫和面向對象數據庫的對比
關系型數據庫是傳統的數據庫,現在使用的主要是mysql 和 microsoft sql server。面向對象數據庫是新興數據庫,現在使用的是 mangoDB等。
關系型數據庫中,最獨特的也是最重要的是 規劃范式 在關系型數據庫中范式等級越高,數據的整體性越低,那么冗余度會逐漸下降。
一個學生用戶可能會被分成多張表來存儲相關信息。而關系型數據庫中主要的也是兩張表之間的關系(聯系),這個關系通常也必須使用一張表來存儲。
在面向對象數據庫中,與傳統關系型數據庫最大的區別數,它是以一個對象來存儲的,對象的屬性則是自己定義的,它的屬性可以存儲一個對象(函數,數組)。這就極大的增加了可操作性,我們可以把關系作為對象的一個屬性來存儲,例如:學生和課程的關系,二者之間是多對多的關系,本來在關系型數據中需要建立一張選課表來存儲,現在只需要在課程對象中添加一個選課字段存儲選課學生的 id 數組,而在學生對象中添加一個所選課程字段,二者之間的關系就鏈接起來了。面向對象數據庫中,對象的屬性通??梢跃奂谝黄?#xff0c;一個對象類就是一張表,這樣會造成每張表中擁有大量的數據每次操作會造成的并發問題,所以每個對象類最好將屬性分割,讓數據訪問更加平均,減少每個對象表的同時訪問次數。
感想
在和他人一起,寫小程序的時候出現種種問題,甚至有時候效率還沒有一個人單獨寫的高,但是我發現和他人一起寫會更有動力,每個人的想法在碰撞,能快速的提高自己的編程水平和與他人的溝通能力。
課程完整源碼
https://github.com/TencentCloudBase/Good-practice-tutorial-recommended
聯系我們
更多云開發使用技巧及 Serverless 行業動態,掃碼關注我們~
轉載于:https://www.cnblogs.com/CloudBase/p/11458493.html
總結
以上是生活随笔為你收集整理的用小程序·云开发打造运动圈小程序丨实战的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: android 技能 英文,讯飞输入法A
- 下一篇: 技术方法论