小程序picker_小程序·云开发实战 - 迷你微博
0. 前言
本文將手把手教你如何寫出迷你版微博的一行行代碼,迷你版微博包含以下功能:
- Feed 流:關(guān)注動(dòng)態(tài)、所有動(dòng)態(tài)
- 發(fā)送圖文動(dòng)態(tài)
- 搜索用戶
- 關(guān)注系統(tǒng)
- 點(diǎn)贊動(dòng)態(tài)
- 個(gè)人主頁
使用到的云開發(fā)能力:
- 云數(shù)據(jù)庫
- 云存儲(chǔ)
- 云函數(shù)
- 云調(diào)用
沒錯(cuò),幾乎是所有的云開發(fā)能力。也就是說,讀完這篇實(shí)戰(zhàn),你就相當(dāng)于完全入門了云開發(fā)!
咳咳,當(dāng)然,實(shí)際上這里只是介紹核心邏輯和重點(diǎn)代碼片段,完整代碼建議下載查看。
1. 取得授權(quán)
作為一個(gè)社交平臺(tái),首先要做的肯定是經(jīng)過用戶授權(quán),獲取用戶信息,小程序提供了很方便的接口:
<button open-type="getUserInfo" bindgetuserinfo="getUserInfo">進(jìn)入小圈圈
</button>這個(gè) button 有個(gè) open-type 屬性,這個(gè)屬性是專門用來使用小程序的開放能力的,而 getUserInfo 則表示 獲取用戶信息,可以從bindgetuserinfo回調(diào)中獲取到用戶信息。
于是我們可以在 wxml 里放入這個(gè) button 后,在相應(yīng)的 js 里寫如下代碼:
Page({...getUserInfo: function(e) {wx.navigateTo({url: "/pages/circle/circle"})},...
})
這樣在成功獲取到用戶信息后,我們就能跳轉(zhuǎn)到迷你微博頁面了。
需要注意,不能使用 wx.authorize({scope: "scope.userInfo"}) 來獲取讀取用戶信息的權(quán)限,因?yàn)樗粫?huì)跳出授權(quán)彈窗。目前只能使用上面所述的方式實(shí)現(xiàn)。
2. 主頁設(shè)計(jì)
社交平臺(tái)的主頁大同小異,主要由三個(gè)部分組成:
- Feed 流
- 消息
- 個(gè)人信息
那么很容易就能想到這樣的布局(注意新建一個(gè) Page 哦,路徑:pages/circle/circle.wxml):
<view class="circle-container"><viewstyle="display:{{currentPage === 'main' ? 'block' : 'none'}}"class="main-area"></view><viewstyle="display:{{currentPage === 'msg' ? 'flex' : 'none'}}"class="msg-area"></view><viewstyle="display:{{currentPage === 'me' ? 'flex' : 'none'}}"class="me-area"></view><view class="footer"><view class="footer-item"><buttonclass="footer-btn"bindtap="onPageMainTap"style="background: {{currentPage === 'main' ? '#111' : 'rgba(0,0,0,0)'}}; color: {{currentPage === 'main' ? '#fff' : '#000'}}">首頁</button></view><view class="footer-item"><buttonclass="footer-btn"bindtap="onPageMsgTap"style="background: {{currentPage === 'msg' ? '#111' : 'rgba(0,0,0,0)'}}; color: {{currentPage === 'msg' ? '#fff' : '#000'}}">消息</button></view><view class="footer-item"><buttonclass="footer-btn"bindtap="onPageMeTap"style="background: {{currentPage === 'me' ? '#111' : 'rgba(0,0,0,0)'}}; color: {{currentPage === 'me' ? '#fff' : '#000'}}">個(gè)人</button></view></view>
</view>很好理解,畫面主要被分為上下兩個(gè)部分:上面的部分是主要內(nèi)容,下面的部分是三個(gè) Tab 組成的 Footer。重點(diǎn) WXSS 實(shí)現(xiàn)(完整的 WXSS 可以下載源碼查看):
.footer {box-shadow: 0 0 15rpx #ccc;display: flex;position: fixed;height: 120rpx;bottom: 0;width: 100%;flex-direction: row;justify-content: center;z-index: 100;background: #fff;
}.footer-item {display: flex;justify-content: center;align-items: center;height: 100%;width: 33.33%;color: #333;
}.footer-item:nth-child(2) {border-left: 3rpx solid #aaa;border-right: 3rpx solid #aaa;flex-grow: 1;
}.footer-btn {width: 100%;height: 100%;display: flex;justify-content: center;align-items: center;border-radius: 0;font-size: 30rpx;
}核心邏輯是通過 position: fixed 來讓 Footer 一直在下方。
讀者會(huì)發(fā)現(xiàn)有一個(gè) currentPage 的 data ,這個(gè) data 的作用其實(shí)很直觀:通過判斷它的值是 main/msg/me 中的哪一個(gè)來決定主要內(nèi)容。同時(shí),為了讓首次使用的用戶知道自己在哪個(gè) Tab,Footer 中相應(yīng)的 button 也會(huì)從白底黑字黑底白字,與另外兩個(gè) Tab 形成對比。
現(xiàn)在我們來看看 main 部分的代碼(在上面代碼的基礎(chǔ)上擴(kuò)充):
...
<viewclass="main-header"style="display:{{currentPage === 'main' ? 'flex' : 'none'}};max-height:{{mainHeaderMaxHeight}}"
><view class="group-picker-wrapper"><pickerbindchange="bindGroupPickerChange"value="{{groupArrayIndex}}"range="{{groupArray}}"class="group-picker"><button class="group-picker-inner">{{groupArray[groupArrayIndex]}}</button></picker></view><view class="search-btn-wrapper"><button class="search-btn" bindtap="onSearchTap">搜索用戶</button></view>
</view>
<viewclass="main-area"style="display:{{currentPage === 'main' ? 'block' : 'none'}};height: {{mainAreaHeight}};margin-top:{{mainAreaMarginTop}}"
><scroll-view scroll-y class="main-area-scroll" bindscroll="onMainPageScroll"><blockwx:for="{{pageMainData}}"wx:for-index="idx"wx:for-item="itemName"wx:key="_id"><post-item is="post-item" data="{{itemName}}" class="post-item-wrapper" /></block><view wx:if="{{pageMainData.length === 0}}" class="item-placeholder">無數(shù)據(jù)</view></scroll-view><buttonclass="add-poster-btn"bindtap="onAddPosterTap"hover-class="add-poster-btn-hover"style="bottom:{{addPosterBtnBottom}}">+</button>
</view>
...這里用到了 列表渲染 和 條件渲染,還不清楚的可以點(diǎn)擊進(jìn)去學(xué)習(xí)一下。
可以看到,相比之前的代碼,我添加一個(gè) header,同時(shí) main-area 的內(nèi)部也新增了一個(gè) scroll-view(用于展示 Feed 流) 和一個(gè) button(用于編輯新迷你微博)。header 的功能很簡單:左側(cè)區(qū)域是一個(gè) picker,可以選擇查看的動(dòng)態(tài)類型(目前有 關(guān)注動(dòng)態(tài) 和 所有動(dòng)態(tài) 兩種);右側(cè)區(qū)域是一個(gè)按鈕,點(diǎn)擊后可以跳轉(zhuǎn)到搜索頁面,這兩個(gè)功能我們先放一下,先繼續(xù)看 main-area 的新增內(nèi)容。
main-area 里的 scroll-view 是一個(gè)可監(jiān)聽滾動(dòng)事件的列表,其中監(jiān)聽事件的實(shí)現(xiàn):
data: {...addPosterBtnBottom: "190rpx",mainHeaderMaxHeight: "80rpx",mainAreaHeight: "calc(100vh - 200rpx)",mainAreaMarginTop: "80rpx",
},
onMainPageScroll: function(e) {if (e.detail.deltaY < 0) {this.setData({addPosterBtnBottom: "-190rpx",mainHeaderMaxHeight: "0",mainAreaHeight: "calc(100vh - 120rpx)",mainAreaMarginTop: "0rpx"})} else {this.setData({addPosterBtnBottom: "190rpx",mainHeaderMaxHeight: "80rpx",mainAreaHeight: "calc(100vh - 200rpx)",mainAreaMarginTop: "80rpx"})}
},
...
結(jié)合 wxml 可以知道,當(dāng)頁面向下滑動(dòng) (deltaY < 0) 時(shí),header 和 button 會(huì) “突然消失”,反之它們則會(huì) “突然出現(xiàn)”。為了視覺上有更好地過渡,我們可以在 WXSS 中使用 transition :
...
.main-area {position: relative;flex-grow: 1;overflow: auto;z-index: 1;transition: height 0.3s, margin-top 0.3s;
}
.main-header {position: fixed;width: 100%;height: 80rpx;background: #fff;top: 0;left: 0;display: flex;justify-content: space-around;align-items: center;z-index: 100;border-bottom: 3rpx solid #aaa;transition: max-height 0.3s;overflow: hidden;
}
.add-poster-btn {position: fixed;right: 60rpx;box-shadow: 5rpx 5rpx 10rpx #aaa;display: flex;justify-content: center;align-items: center;color: #333;padding-bottom: 10rpx;text-align: center;border-radius: 50%;font-size: 60rpx;width: 100rpx;height: 100rpx;transition: bottom 0.3s;background: #fff;z-index: 1;
}
...3. Feed 流
3.1 post-item
前面提到,scroll-view 的內(nèi)容是 Feed 流,那么首先就要想到使用 列表渲染。而且,為了方便在個(gè)人主頁復(fù)用,列表渲染中的每一個(gè) item 都要抽象出來。這時(shí)就要使用小程序中的 Custom-Component 功能了。
新建一個(gè)名為 post-item 的 Component,其中 wxml 的實(shí)現(xiàn)(路徑:pages/circle/component/post-item/post-item.js):
<viewclass="post-item"hover-class="post-item-hover"bindlongpress="onItemLongTap"bindtap="onItemTap"
><view class="post-title"><view class="author" hover-class="author-hover" catchtap="onAuthorTap">{{data.author}}</view><view class="date">{{data.formatDate}}</view></view><view class="msg-wrapper"><text class="msg">{{data.msg}}</text></view><view class="image-outer" wx:if="{{data.photoId !== ''}}" catchtap="onImgTap"><image-wrapper is="image-wrapper" src="{{data.photoId}}" /></view>
</view>可見,一個(gè) poster-item 最主要有以下信息:
- 作者名
- 發(fā)送時(shí)間
- 文本內(nèi)容
- 圖片內(nèi)容
其中,圖片內(nèi)容因?yàn)槭强蛇x的,所以使用了 條件渲染,這會(huì)在沒有圖片信息時(shí)不讓圖片顯示區(qū)域占用屏幕空間。另外,圖片內(nèi)容主要是由 image-wrapper 組成,它也是一個(gè) Custom-Component,主要功能是:
- 強(qiáng)制長寬 1:1 裁剪顯示圖片
- 點(diǎn)擊查看大圖
- 未加載完成時(shí)顯示 加載中
具體代碼這里就不展示了,比較簡單,讀者可以在 component/image-wrapper 里找到。
回過頭看 main-area 的其他新增部分,細(xì)心的讀者會(huì)發(fā)現(xiàn)有這么一句:
<view wx:if="{{pageMainData.length === 0}}" class="item-placeholder">無數(shù)據(jù)</view
>這會(huì)在 Feed 流暫時(shí)沒有獲取到數(shù)據(jù)時(shí)給用戶一個(gè)提示。
3.2 collections: poster、poster_users
展示 Feed 流的部分已經(jīng)編寫完畢,現(xiàn)在就差實(shí)際數(shù)據(jù)了。根據(jù)上一小節(jié) poster-item 的主要信息,我們可以初步推斷出一條迷你微博在 云數(shù)據(jù)庫 的 collection poster 里是這樣存儲(chǔ)的:
{"username": "Tester","date": "2019-07-22 12:00:00","text": "Ceshiwenben","photo": "xxx"
}先來看 username。由于社交平臺(tái)一般不會(huì)限制用戶的昵稱,所以如果每條迷你微博都存儲(chǔ)昵稱,那將來每次用戶修改一次昵稱,就要遍歷數(shù)據(jù)庫把所有迷你微博項(xiàng)都改一遍,相當(dāng)耗費(fèi)時(shí)間,所以我們不如存儲(chǔ)一個(gè) userId,并另外把 id 和 昵稱 的對應(yīng)關(guān)系存在另一個(gè)叫 poster_users 的 collection 里。
{"userId": "xxx","name": "Tester",...(其他用戶信息)
}userId 從哪里拿呢?當(dāng)然是通過之前已經(jīng)授權(quán)的獲取用戶信息接口拿到了,詳細(xì)操作之后會(huì)說到。
接下來是 date,這里最好是服務(wù)器時(shí)間(因?yàn)榭蛻舳藗鬟^來的時(shí)間可能會(huì)有誤差),而云開發(fā)文檔里也有提供相應(yīng)的接口:serverDate。這個(gè)數(shù)據(jù)可以直接被 new Date() 使用,可以理解為一個(gè) UTC 時(shí)間。
text 即文本信息,直接存儲(chǔ)即可。
photo 則表示附圖數(shù)據(jù),但是限于小程序 image 元素的實(shí)現(xiàn),想要顯示一張圖片,要么提供該圖片的 url,要么提供該圖片在 云存儲(chǔ) 的 id,所以這里最佳的實(shí)踐是:先把圖片上傳到云存儲(chǔ)里,然后把回調(diào)里的文件 id 作為數(shù)據(jù)存儲(chǔ)。
綜上所述,最后 poster 每一項(xiàng)的數(shù)據(jù)結(jié)構(gòu)如下:
{"authorId": "xxx","date": "utc-format-date","text": "Ceshiwenben","photoId": "yyy"
}確定數(shù)據(jù)結(jié)構(gòu)后,我們就可以開始往 collection 添加數(shù)據(jù)了。但是,在此之前,我們還缺少一個(gè)重要步驟。
3.3 用戶信息錄入 與 云數(shù)據(jù)庫
沒錯(cuò),我們還沒有在 poster_users 里添加一條新用戶的信息。這個(gè)步驟一般在 pages/circle/circle 頁面首次加載時(shí)判斷即可:
getUserId: function(cb) {let that = thisvar value = this.data.userId || wx.getStorageSync("userId")if (value) {if (cb) {cb(value)}return value}wx.getSetting({success(res) {if (res.authSetting["scope.userInfo"]) {wx.getUserInfo({withCredentials: true,success: function(userData) {wx.setStorageSync("userId", userData.signature)that.setData({userId: userData.signature})db.collection("poster_users").where({userId: userData.signature}).get().then(searchResult => {if (searchResult.data.length === 0) {wx.showToast({title: "新用戶錄入中"})db.collection("poster_users").add({data: {userId: userData.signature,date: db.serverDate(),name: userData.userInfo.nickName,gender: userData.userInfo.gender}}).then(res => {console.log(res)if (res.errMsg === "collection.add:ok") {wx.showToast({title: "錄入完成"})if (cb) cb()}}).catch(err => {wx.showToast({title: "錄入失敗,請稍后重試",image: "/images/error.png"})wx.navigateTo({url: "/pages/index/index"})})} else {if (cb) cb()}})}})} else {wx.showToast({title: "登陸失效,請重新授權(quán)登陸",image: "/images/error.png"})wx.navigateTo({url: "/pages/index/index"})}}})
}
代碼實(shí)現(xiàn)比較復(fù)雜,整體思路是這樣的:
- 判斷是否已存儲(chǔ)了
userId,如果有直接返回并調(diào)用回調(diào)函數(shù),如果沒有繼續(xù) 2 - 通過
wx.getSetting獲取當(dāng)前設(shè)置信息 - 如果返回里有
res.authSetting["scope.userInfo"]說明已經(jīng)授權(quán)讀取用戶信息,繼續(xù) 3,沒有授權(quán)的話就跳轉(zhuǎn)回首頁重新授權(quán) - 調(diào)用
wx.getUserInfo獲取用戶信息,成功后提取出signature(這是每個(gè)微信用戶的唯一簽名),并調(diào)用wx.setStorageSync將其緩存 - 調(diào)用
db.collection().where().get(),判斷返回的數(shù)據(jù)是否是空數(shù)組,如果不是說明該用戶已經(jīng)錄入(注意where()中的篩選條件),如果是說明該用戶是新用戶,繼續(xù) 5 - 提示新用戶錄入中,同時(shí)調(diào)用
db.collection().add()來添加用戶信息,最后通過回調(diào)判斷是否錄入成功,并提示用戶
不知不覺我們就使用了云開發(fā)中的 云數(shù)據(jù)庫 功能,緊接著我們就要開始使用 云存儲(chǔ) 和 云函數(shù)了!
3.4 addPoster 與 云存儲(chǔ)
發(fā)送新的迷你微博,需要一個(gè)編輯新迷你微博的界面,路徑我定為 pages/circle/add-poster/add-poster:
<view class="app-poster-container"><view class="body"><view class="text-area-wrapper"><textarea bindinput="bindTextInput" placeholder="在此填寫" value="{{text}}" auto-focus="true" /><view class="text-area-footer"><text>{{remainLen}}/140</text></view></view><view bindtap="onImageTap" class="image-area"><view class="image-outer"><image-wrapper is="image-wrapper" src="{{imageSrc}}" placeholder="選擇圖片上傳" /></view></view></view><view class="footer"><button class="footer-btn" bindtap="onSendTap">發(fā)送</button></view>
</view>wxml 的代碼很好理解:textarea 顯示編輯文本,image-wrapper 顯示需要上傳的圖片,最下面是一個(gè)發(fā)送的 button。其中,圖片編輯區(qū)域的 bindtap 事件實(shí)現(xiàn):
onImageTap: function() {let that = thiswx.chooseImage({count: 1,success: function(res) {const tempFilePaths = res.tempFilePathsthat.setData({imageSrc: tempFilePaths[0]})}})
}
直接通過 wx.chooseImage 官方 API 獲取本地圖片的臨時(shí)路徑即可。而當(dāng)發(fā)送按鈕點(diǎn)擊后,會(huì)有如下代碼被執(zhí)行:
onSendTap: function() {if (this.data.text === "" && this.data.imageSrc === "") {wx.showModal({title: "錯(cuò)誤",content: "不能發(fā)送空內(nèi)容",showCancel: false,confirmText: "好的"})return}const that = thiswx.showLoading({title: "發(fā)送中",mask: true})const imageSrc = this.data.imageSrcif (imageSrc !== "") {const finalPath = imageSrc.replace("//", "/").replace(":", "")wx.cloud.uploadFile({cloudPath: finalPath,filePath: imageSrc // 文件路徑}).then(res => {that.sendToDb(res.fileID)}).catch(error => {that.onSendFail()})} else {that.sendToDb()}
},
sendToDb: function(fileId = "") {const that = thisconst posterData = {authorId: that.data.userId,msg: that.data.text,photoId: fileId,date: db.serverDate()}db.collection("poster").add({data: {...posterData}}).then(res => {wx.showToast({title: "發(fā)送成功"})wx.navigateBack({delta: 1})}).catch(error => {that.onSendFail()}).finally(wx.hideLoading())
}
- 首先判斷文本和圖片內(nèi)容是否都為空,如果是則不執(zhí)行發(fā)送,如果不是繼續(xù) 2
- 提示發(fā)送中,上傳圖片到云存儲(chǔ),注意需要將圖片中的臨時(shí) url 的一些特殊字符組合替換一下,原因見 文件名命名限制
- 上傳成功后,調(diào)用
db.collection().add(),發(fā)送成功后退回上一頁(即首頁),如果失敗則執(zhí)行onSendFail函數(shù),后者見源碼,邏輯較簡單這里不贅述
于是,我們就這樣創(chuàng)建了第一條迷你微博。接下來就讓它在 Feed 流中顯示吧!
3.5 云函數(shù) getMainPageData
這個(gè)函數(shù)的主要作用如前所述,就是通過處理云數(shù)據(jù)庫中的數(shù)據(jù),將最終數(shù)據(jù)返回給客戶端,后者將數(shù)據(jù)可視化給用戶。我們先做一個(gè)初步版本,因?yàn)楝F(xiàn)在 poster_users 中只有一條數(shù)據(jù),所以僅先展示自己的迷你微博。getMainPageData 云函數(shù)代碼如下:
// 云函數(shù)入口文件
const cloud = require("wx-server-sdk")
cloud.init()
const db = cloud.database()// 云函數(shù)入口函數(shù)
exports.main = async (event, context, cb) => {// 通過 event 獲取入?yún)onst userId = event.userIdlet followingResultlet users// idNameMap 負(fù)責(zé)存儲(chǔ) userId 和 name 的映射關(guān)系let idNameMap = {}let followingIds = []// 獲取用戶信息followingResult = await db.collection("poster_users").where({userId: userId}).get()users = followingResult.datafollowingIds = users.map(u => {return u.userId})users.map(u => {idNameMap[u.userId] = u.name})// 獲取動(dòng)態(tài)const postResult = await db.collection("poster").orderBy("date", "desc").where({// 通過高級篩選功能篩選出符合條件的 userIdauthorId: db.command.in(followingIds)}).get()const postData = postResult.data// 向返回的數(shù)據(jù)添加 存儲(chǔ)用戶昵稱的 author 屬性、存儲(chǔ)格式化后的時(shí)間的 formatDate 屬性postData.map(p => {p.author = idNameMap[p.authorId]p.formatDate = new Date(p.date).toLocaleDateString("zh-Hans", options)})return postData
}
最后在 pages/circle/circle.js 里補(bǔ)充云調(diào)用:
getMainPageData: function(userId) {const that = thiswx.cloud.callFunction({name: "getMainPageData",data: {userId: userId,isEveryOne: that.data.groupArrayIndex === 0 ? false : true}}).then(res => {that.setData({pageMainData: res.result,pageMainLoaded: true})}).catch(err => {wx.showToast({title: "獲取動(dòng)態(tài)失敗",image: "/images/error.png"})wx.hideLoading()})
}
即可展示 Feed 流數(shù)據(jù)給用戶。
之后,getMainPageData 還會(huì)根據(jù)使用場景的不同,新增了查詢所有用戶動(dòng)態(tài)、查詢關(guān)注用戶動(dòng)態(tài)的功能,但是原理是一樣的,看源碼可以輕易理解,后續(xù)就不再說明。
4. 關(guān)注系統(tǒng)
上一節(jié)中我們一口氣把云開發(fā)中的大部分主要功能:云數(shù)據(jù)庫、云存儲(chǔ)、云函數(shù)、云調(diào)用都用了一遍,接下來其他功能的實(shí)現(xiàn)也基本都依賴它們。
4.1 poster_user_follows
首先我們需要建一個(gè)新的 collection poster_user_follows,其中的每一項(xiàng)數(shù)據(jù)的數(shù)據(jù)結(jié)構(gòu)如下:
{"followerId": "xxx","followingId": "xxx"
}很簡單,followerId 表示關(guān)注人,followingId 表示被關(guān)注人。
4.2 user-data 頁面
關(guān)注或者取消關(guān)注需要進(jìn)入他人的個(gè)人主頁操作,我們在 pages/circle/user-data/user-data.wxml 中放一個(gè) user-info 的自定義組件,然后新建該組件編輯:
<view class="user-info"><view class="info-item" hover-class="info-item-hover">用戶名: {{userName}}</view><view class="info-item" hover-class="info-item-hover" bindtap="onPosterCountTap">動(dòng)態(tài)數(shù): {{posterCount}}</view><view class="info-item" hover-class="info-item-hover" bindtap="onFollowingCountTap">關(guān)注數(shù): {{followingCount}}</view><view class="info-item" hover-class="info-item-hover" bindtap="onFollowerCountTap">粉絲數(shù): {{followerCount}}</view><view class="info-item" hover-class="info-item-hover" wx:if="{{originId && originId !== '' && originId !== userId}}"><button bindtap="onFollowTap">{{followText}}</button></view>
</view>這里注意條件渲染的 button:如果當(dāng)前訪問個(gè)人主頁的用戶 id (originId) 和 被訪問的用戶 id (userId)的值是相等的話,這個(gè)按鈕就不會(huì)被渲染(自己不能關(guān)注/取消關(guān)注自己)。
我們重點(diǎn)看下 onFollowTap 的實(shí)現(xiàn):
onFollowTap: function() {const that = this// 判斷當(dāng)前關(guān)注狀態(tài)if (this.data.isFollow) {wx.showLoading({title: "操作中",mask: true})wx.cloud.callFunction({name: "cancelFollowing",data: {followerId: this.properties.originId,followingId: this.properties.userId}}).then(res => {wx.showToast({title: "取消關(guān)注成功"})that.setData({isFollow: false,followText: "關(guān)注"})}).catch(error => {wx.showToast({title: "取消關(guān)注失敗",image: "/images/error.png"})}).finally(wx.hideLoading())} else if (this.data.isFollow !== undefined) {wx.showLoading({title: "操作中",mask: true})const data = {followerId: this.properties.originId,followingId: this.properties.userId}db.collection("poster_user_follows").add({data: {...data}}).then(res => {wx.showToast({title: "關(guān)注成功"})that.setData({isFollow: true,followText: "取消關(guān)注"})}).catch(error => {wx.showToast({title: "關(guān)注失敗",image: "/images/error.png"})}).finally(wx.hideLoading())}}
}
這里讀者可能會(huì)有疑問:為什么關(guān)注的時(shí)候直接調(diào)用 db.collection().add() 即可,而取消關(guān)注卻要調(diào)用云函數(shù)呢?這里涉及到云數(shù)據(jù)庫的設(shè)計(jì)問題:刪除多個(gè)數(shù)據(jù)的操作,或者說刪除使用 where 篩選的數(shù)據(jù),只能在服務(wù)端執(zhí)行。如果確實(shí)想在客戶端刪除,則在查詢用戶關(guān)系時(shí),將唯一標(biāo)識(shí)數(shù)據(jù)的 _id 用 setData 存下來,之后再使用 db.collection().doc(_id).delete() 刪除即可。這兩種實(shí)現(xiàn)方式讀者可自行選擇。當(dāng)然,還有一種實(shí)現(xiàn)是不實(shí)際刪除數(shù)據(jù),只是加個(gè) isDelete 字段標(biāo)記一下。
查詢用戶關(guān)系的實(shí)現(xiàn)很簡單,云函數(shù)的實(shí)現(xiàn)方式如下:
// 云函數(shù)入口文件
const cloud = require('wx-server-sdk')
cloud.init()
const db = cloud.database()// 云函數(shù)入口函數(shù)
exports.main = async(event, context) => {const followingResult = await db.collection("poster_user_follows").where({followingId: event.followingId,followerId: event.followerId}).get()return followingResult
}
客戶端只要檢查返回的數(shù)據(jù)長度是否大于 0 即可。
另外附上 user-data 頁面其他數(shù)據(jù)的獲取云函數(shù)實(shí)現(xiàn):
// 云函數(shù)入口文件
const cloud = require("wx-server-sdk")
cloud.init()
const db = cloud.database()async function getPosterCount(userId) {return {value: (await db.collection("poster").where({authorId: userId}).count()).total,key: "posterCount"}
}async function getFollowingCount(userId) {return {value: (await db.collection("poster_user_follows").where({followerId: userId}).count()).total,key: "followingCount"}
}async function getFollowerCount(userId) {return {value: (await db.collection("poster_user_follows").where({followingId: userId}).count()).total,key: "followerCount"}
}async function getUserName(userId) {return {value: (await db.collection("poster_users").where({userId: userId}).get()).data[0].name,key: "userName"}
}// 云函數(shù)入口函數(shù)
exports.main = async (event, context) => {const userId = event.userIdconst tasks = []tasks.push(getPosterCount(userId))tasks.push(getFollowerCount(userId))tasks.push(getFollowingCount(userId))tasks.push(getUserName(userId))const allData = await Promise.all(tasks)const finalData = {}allData.map(d => {finalData[d.key] = d.value})return finalData
}
很好理解,客戶端獲取返回后直接使用即可。
5. 搜索頁面
這部分其實(shí)很好實(shí)現(xiàn)。關(guān)鍵的搜索函數(shù)實(shí)現(xiàn)如下:
// 云函數(shù)入口文件
const cloud = require('wx-server-sdk')
cloud.init()
const db = cloud.database()const MAX_LIMIT = 100
async function getDbData(dbName, whereObj) {const totalCountsData = await db.collection(dbName).where(whereObj).count()const total = totalCountsData.totalconst batch = Math.ceil(total / 100)const tasks = []for (let i = 0; i < batch; i++) {const promise = db.collection(dbName).where(whereObj).skip(i * MAX_LIMIT).limit(MAX_LIMIT).get()tasks.push(promise)}const rrr = await Promise.all(tasks)if (rrr.length !== 0) {return rrr.reduce((acc, cur) => {return {data: acc.data.concat(cur.data),errMsg: acc.errMsg}})} else {return {data: [],errMsg: "empty"}}
}// 云函數(shù)入口函數(shù)
exports.main = async (event, context) => {const text = event.textconst data = await getDbData("poster_users", {name: {$regex: text}})return data
}
這里參考了官網(wǎng)所推薦的分頁檢索數(shù)據(jù)庫數(shù)據(jù)的實(shí)現(xiàn)(因?yàn)樗阉鹘Y(jié)果可能有很多),篩選條件則是正則模糊匹配關(guān)鍵字。
搜索頁面的源碼路徑是 pages/circle/search-user/search-user,實(shí)現(xiàn)了點(diǎn)擊搜索結(jié)果項(xiàng)跳轉(zhuǎn)到對應(yīng)項(xiàng)的用戶的 user-data 頁面,建議直接閱讀源碼理解。
6. 其他擴(kuò)展
6.1 poster_likes 與 點(diǎn)贊
由于轉(zhuǎn)發(fā)、評論、點(diǎn)贊的原理基本相同,所以這里只介紹點(diǎn)贊功能如何編寫,另外兩個(gè)功能讀者可以自行實(shí)現(xiàn)。
毫無疑問我們需要新建一個(gè) collection poster_likes,其中每一項(xiàng)的數(shù)據(jù)結(jié)構(gòu)如下:
{"posterId": "xxx","likeId": "xxx"
}這里的 posterId 就是 poster collection 里每條記錄的 _id 值,likeId 就是 poster_users 里的 userId 了。
然后我們擴(kuò)展一下 poster-item 的實(shí)現(xiàn):
<view class="post-item" hover-class="post-item-hover" bindlongpress="onItemLongTap" bindtap="onItemTap">...<view class="interact-area"><view class="interact-item"><button class="interact-btn" catchtap="onLikeTap" style="color:{{liked ? '#55aaff' : '#000'}}">贊 {{likeCount}}</button></view></view>
</view>即,新增一個(gè) interact-area,其中 onLikeTap 實(shí)現(xiàn)如下:
onLikeTap: function() {if (!this.properties.originId) returnconst that = thisif (this.data.liked) {wx.showLoading({title: "操作中",mask: true})wx.cloud.callFunction({name: "cancelLiked",data: {posterId: this.properties.data._id,likeId: this.properties.originId}}).then(res => {wx.showToast({title: "取消成功"})that.refreshLike()that.triggerEvent('likeEvent');}).catch(error => {wx.showToast({title: "取消失敗",image: "/images/error.png"})}).finally(wx.hideLoading())} else {wx.showLoading({title: "操作中",mask: true})db.collection("poster_likes").add({data: {posterId: this.properties.data._id,likeId: this.properties.originId}}).then(res => {wx.showToast({title: "已贊"})that.refreshLike()that.triggerEvent('likeEvent');}).catch(error => {wx.showToast({title: "贊失敗",image: "/images/error.png"})}).finally(wx.hideLoading())}}
細(xì)心的讀者會(huì)發(fā)現(xiàn)這和關(guān)注功能原理幾乎是一樣的。
6.2 數(shù)據(jù)刷新
我們可以使用很多方式讓主頁面刷新數(shù)據(jù):
onShow: function() {wx.showLoading({title: "加載中",mask: true})const that = thisfunction cb(userId) {that.refreshMainPageData(userId)that.refreshMePageData(userId)}this.getUserId(cb)
}
第一種是利用 onShow 方法:它會(huì)在頁面每次從后臺(tái)轉(zhuǎn)到前臺(tái)展示時(shí)調(diào)用,這個(gè)時(shí)候我們就能刷新頁面數(shù)據(jù)(包括 Feed 流和個(gè)人信息)。但是這個(gè)時(shí)候用戶信息可能會(huì)丟失,所以我們需要在 getUserId 里判斷,并將刷新數(shù)據(jù)的函數(shù)們整合起來,作為回調(diào)函數(shù)。
第二種是讓用戶手動(dòng)刷新:
onPageMainTap: function() {if (this.data.currentPage === "main") {this.refreshMainPageData()}this.setData({currentPage: "main"})
}
如圖所示,當(dāng)目前頁面是 Feed 流時(shí),如果再次點(diǎn)擊 首頁 Tab,就會(huì)強(qiáng)制刷新數(shù)據(jù)。
第三種是關(guān)聯(lián)數(shù)據(jù)變更觸發(fā)刷新,比如動(dòng)態(tài)類型選擇、刪除了一條動(dòng)態(tài)以后觸發(fā)數(shù)據(jù)的刷新。這種可以直接看源碼學(xué)習(xí)。
6.3 首次加載等待
當(dāng)用戶第一次進(jìn)入主頁面時(shí),我們?nèi)绻朐?Feed 流和個(gè)人信息都加載好了再允許用戶操作,應(yīng)該如何實(shí)現(xiàn)?
如果是類似 Vue 或者 React 的框架,我們很容易就能想到屬性監(jiān)控,如 watch、useEffect 等等,但是小程序目前 Page 并沒有提供屬性監(jiān)控功能,怎么辦?
除了自己實(shí)現(xiàn),還有一個(gè)方法就是利用 Component 的 observers,它和上面提到的屬性監(jiān)控功能差不多。雖然官網(wǎng)文檔對其說明比較少,但摸索了一番還是能用來監(jiān)控的。
首先我們來新建一個(gè) Component 叫 abstract-load,具體實(shí)現(xiàn)如下:
// pages/circle/component/abstract-load.js
Component({properties: {pageMainLoaded: {type: Boolean,value: false},pageMeLoaded: {type: Boolean,value: false}},observers: {"pageMainLoaded, pageMeLoaded": function (pageMainLoaded, pageMeLoaded) {if (pageMainLoaded && pageMeLoaded) {this.triggerEvent("allLoadEvent")}}}
})
然后在 pages/circle/circle.wxml 中添加一行:
<abstract-load is="abstract-load" pageMainLoaded="{{pageMainLoaded}}" pageMeLoaded="{{pageMeLoaded}}" bind:allLoadEvent="onAllLoad" />最后實(shí)現(xiàn) onAllLoad 函數(shù)即可。
另外,像這種沒有實(shí)際展示數(shù)據(jù)的 Component,建議在項(xiàng)目中都用 abstract 開頭來命名。
6.4 scroll-view 在 iOS 的 bug
如果讀者使用 iOS 系統(tǒng)調(diào)試這個(gè)小程序,可能會(huì)發(fā)現(xiàn) Feed 流比較短的時(shí)候,滾動(dòng) scroll-view header 和 button 會(huì)有鬼畜的上下抖動(dòng)現(xiàn)象,這是因?yàn)?iOS 自己實(shí)現(xiàn)的 WebView 對于滾動(dòng)視圖有回彈的效果,而該效果也會(huì)觸發(fā)滾動(dòng)事件。
對于這個(gè) bug,官方人員也表示暫時(shí)無法修復(fù),只能先忍一忍了。
6.5 關(guān)于消息 Tab
讀者可能會(huì)疑惑我為什么沒有講解消息 Tab 以及消息提醒的實(shí)現(xiàn)。首先是因?yàn)樵创a沒有這個(gè)實(shí)現(xiàn),其次是我覺得目前云開發(fā)所提供的能力實(shí)現(xiàn)主動(dòng)提醒比較麻煩(除了輪詢想不到其他辦法)。
希望未來云開發(fā)可以提供 數(shù)據(jù)庫長連接監(jiān)控 的功能,這樣通過訂閱者模式可以很輕松地獲取到數(shù)據(jù)更新的狀態(tài),主動(dòng)提醒也就更容易實(shí)現(xiàn)了。到那時(shí)我可能會(huì)再更新相關(guān)源碼。
6.6 關(guān)于云函數(shù)耗時(shí)
讀者可能會(huì)發(fā)現(xiàn)我有一個(gè)叫 benchmark 的云函數(shù),這個(gè)函數(shù)只是做了個(gè)查詢數(shù)據(jù)庫的操作,目的在于計(jì)算查詢耗時(shí)。
詭異的是,我前天在調(diào)試的時(shí)候,發(fā)現(xiàn)查詢一次需要1秒鐘,而寫這篇文章時(shí)卻不到100ms。建議在一些需要多次操作數(shù)據(jù)庫的函數(shù)配置里,把超時(shí)時(shí)間設(shè)置長一點(diǎn)吧。目前云函數(shù)的性能不太穩(wěn)定。
7. 結(jié)語
那么關(guān)于迷你版微博開發(fā)實(shí)戰(zhàn)介紹就到此為止了,更多資料可以直接下載源碼查看哦。
# 源碼鏈接
https://github.com/TencentCloudBase/Good-practice-tutorial-recommended
總結(jié)
以上是生活随笔為你收集整理的小程序picker_小程序·云开发实战 - 迷你微博的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 有谁知道这件裙子是什么牌子的?[已扎口]
- 下一篇: 换得到么?100