0. 前言
本文將手把手教你如何寫出迷你版微博的一行行代碼,迷你版微博包含以下功能:
- Feed 流:關注動態、所有動態
- 發送圖文動態
- 搜索用戶
- 關注系統
- 點贊動態
- 個人主頁
使用到的雲開發能力:
- 雲數據庫
- 雲存儲
- 雲函數
- 雲調用
沒錯,幾乎是所有的雲開發能力。也就是說,讀完這篇實戰,你就相當於完全入門了雲開發!
咳咳,當然,實際上這里只是介紹核心邏輯和重點代碼片段,完整代碼建議下載查看。
1. 取得授權
作為一個社交平台,首先要做的肯定是經過用戶授權,獲取用戶信息,小程序提供了很方便的接口:
<button open-type="getUserInfo" bindgetuserinfo="getUserInfo">
進入小圈圈
</button>
這個 button
有個 open-type
屬性,這個屬性是專門用來使用小程序的開放能力的,而 getUserInfo
則表示 獲取用戶信息,可以從bindgetuserinfo
回調中獲取到用戶信息。
於是我們可以在 wxml 里放入這個 button
后,在相應的 js 里寫如下代碼:
Page({
...
getUserInfo: function(e) {
wx.navigateTo({
url: "/pages/circle/circle"
})
},
...
})
這樣在成功獲取到用戶信息后,我們就能跳轉到迷你微博頁面了。
需要注意,不能使用 wx.authorize({scope: "scope.userInfo"})
來獲取讀取用戶信息的權限,因為它不會跳出授權彈窗。目前只能使用上面所述的方式實現。
2. 主頁設計
社交平台的主頁大同小異,主要由三個部分組成:
- Feed 流
- 消息
- 個人信息
那么很容易就能想到這樣的布局(注意新建一個 Page 哦,路徑:pages/circle/circle.wxml
):
<view class="circle-container">
<view
style="display:{{currentPage === 'main' ? 'block' : 'none'}}"
class="main-area"
>
</view>
<view
style="display:{{currentPage === 'msg' ? 'flex' : 'none'}}"
class="msg-area"
>
</view>
<view
style="display:{{currentPage === 'me' ? 'flex' : 'none'}}"
class="me-area"
>
</view>
<view class="footer">
<view class="footer-item">
<button
class="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">
<button
class="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">
<button
class="footer-btn"
bindtap="onPageMeTap"
style="background: {{currentPage === 'me' ? '#111' : 'rgba(0,0,0,0)'}}; color: {{currentPage === 'me' ? '#fff' : '#000'}}"
>
個人
</button>
</view>
</view>
</view>
很好理解,畫面主要被分為上下兩個部分:上面的部分是主要內容,下面的部分是三個 Tab 組成的 Footer。重點 WXSS 實現(完整的 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 一直在下方。
讀者會發現有一個 currentPage
的 data ,這個 data 的作用其實很直觀:通過判斷它的值是 main
/msg
/me
中的哪一個來決定主要內容。同時,為了讓首次使用的用戶知道自己在哪個 Tab,Footer 中相應的 button
也會從白底黑字黑底白字,與另外兩個 Tab 形成對比。
現在我們來看看 main
部分的代碼(在上面代碼的基礎上擴充):
...
<view
class="main-header"
style="display:{{currentPage === 'main' ? 'flex' : 'none'}};max-height:{{mainHeaderMaxHeight}}"
>
<view class="group-picker-wrapper">
<picker
bindchange="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>
<view
class="main-area"
style="display:{{currentPage === 'main' ? 'block' : 'none'}};height: {{mainAreaHeight}};margin-top:{{mainAreaMarginTop}}"
>
<scroll-view scroll-y class="main-area-scroll" bindscroll="onMainPageScroll">
<block
wx: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"
>無數據</view
>
</scroll-view>
<button
class="add-poster-btn"
bindtap="onAddPosterTap"
hover-class="add-poster-btn-hover"
style="bottom:{{addPosterBtnBottom}}"
>
+
</button>
</view>
...
這里用到了 列表渲染 和 條件渲染,還不清楚的可以點擊進去學習一下。
可以看到,相比之前的代碼,我添加一個 header,同時 main-area
的內部也新增了一個 scroll-view
(用於展示 Feed 流) 和一個 button
(用於編輯新迷你微博)。header 的功能很簡單:左側區域是一個 picker
,可以選擇查看的動態類型(目前有 關注動態 和 所有動態 兩種);右側區域是一個按鈕,點擊后可以跳轉到搜索頁面,這兩個功能我們先放一下,先繼續看 main-area
的新增內容。
main-area
里的 scroll-view
是一個可監聽滾動事件的列表,其中監聽事件的實現:
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"
})
}
},
...
結合 wxml 可以知道,當頁面向下滑動 (deltaY < 0) 時,header 和 button
會 “突然消失”,反之它們則會 “突然出現”。為了視覺上有更好地過渡,我們可以在 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
的內容是 Feed 流,那么首先就要想到使用 列表渲染。而且,為了方便在個人主頁復用,列表渲染中的每一個 item 都要抽象出來。這時就要使用小程序中的 Custom-Component 功能了。
新建一個名為 post-item
的 Component
,其中 wxml 的實現(路徑:pages/circle/component/post-item/post-item.js
):
<view
class="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>
可見,一個 poster-item
最主要有以下信息:
- 作者名
- 發送時間
- 文本內容
- 圖片內容
其中,圖片內容因為是可選的,所以使用了 條件渲染,這會在沒有圖片信息時不讓圖片顯示區域占用屏幕空間。另外,圖片內容主要是由 image-wrapper
組成,它也是一個 Custom-Component
,主要功能是:
- 強制長寬 1:1 裁剪顯示圖片
- 點擊查看大圖
- 未加載完成時顯示 加載中
具體代碼這里就不展示了,比較簡單,讀者可以在 component/image-wrapper
里找到。
回過頭看 main-area
的其他新增部分,細心的讀者會發現有這么一句:
<view wx:if="{{pageMainData.length === 0}}" class="item-placeholder"
>無數據</view
>
這會在 Feed 流暫時沒有獲取到數據時給用戶一個提示。
3.2 collections: poster、poster_users
展示 Feed 流的部分已經編寫完畢,現在就差實際數據了。根據上一小節 poster-item
的主要信息,我們可以初步推斷出一條迷你微博在 雲數據庫 的 collection poster
里是這樣存儲的:
{
"username": "Tester",
"date": "2019-07-22 12:00:00",
"text": "Ceshiwenben",
"photo": "xxx"
}
先來看 username
。由於社交平台一般不會限制用戶的昵稱,所以如果每條迷你微博都存儲昵稱,那將來每次用戶修改一次昵稱,就要遍歷數據庫把所有迷你微博項都改一遍,相當耗費時間,所以我們不如存儲一個 userId
,並另外把 id 和 昵稱 的對應關系存在另一個叫 poster_users
的 collection 里。
{
"userId": "xxx",
"name": "Tester",
...(其他用戶信息)
}
userId
從哪里拿呢?當然是通過之前已經授權的獲取用戶信息接口拿到了,詳細操作之后會說到。
接下來是 date
,這里最好是服務器時間(因為客戶端傳過來的時間可能會有誤差),而雲開發文檔里也有提供相應的接口:serverDate。這個數據可以直接被 new Date()
使用,可以理解為一個 UTC 時間。
text
即文本信息,直接存儲即可。
photo
則表示附圖數據,但是限於小程序 image
元素的實現,想要顯示一張圖片,要么提供該圖片的 url,要么提供該圖片在 雲存儲 的 id,所以這里最佳的實踐是:先把圖片上傳到雲存儲里,然后把回調里的文件 id 作為數據存儲。
綜上所述,最后 poster
每一項的數據結構如下:
{
"authorId": "xxx",
"date": "utc-format-date",
"text": "Ceshiwenben",
"photoId": "yyy"
}
確定數據結構后,我們就可以開始往 collection 添加數據了。但是,在此之前,我們還缺少一個重要步驟。
3.3 用戶信息錄入 與 雲數據庫
沒錯,我們還沒有在 poster_users
里添加一條新用戶的信息。這個步驟一般在 pages/circle/circle
頁面首次加載時判斷即可:
getUserId: function(cb) {
let that = this
var 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: "登陸失效,請重新授權登陸",
image: "/images/error.png"
})
wx.navigateTo({
url: "/pages/index/index"
})
}
}
})
}
代碼實現比較復雜,整體思路是這樣的:
- 判斷是否已存儲了
userId
,如果有直接返回並調用回調函數,如果沒有繼續 2 - 通過
wx.getSetting
獲取當前設置信息 - 如果返回里有
res.authSetting["scope.userInfo"]
說明已經授權讀取用戶信息,繼續 3,沒有授權的話就跳轉回首頁重新授權 - 調用
wx.getUserInfo
獲取用戶信息,成功后提取出signature
(這是每個微信用戶的唯一簽名),並調用wx.setStorageSync
將其緩存 - 調用
db.collection().where().get()
,判斷返回的數據是否是空數組,如果不是說明該用戶已經錄入(注意where()
中的篩選條件),如果是說明該用戶是新用戶,繼續 5 - 提示新用戶錄入中,同時調用
db.collection().add()
來添加用戶信息,最后通過回調判斷是否錄入成功,並提示用戶
不知不覺我們就使用了雲開發中的 雲數據庫 功能,緊接着我們就要開始使用 雲存儲 和 雲函數了!
3.4 addPoster 與 雲存儲
發送新的迷你微博,需要一個編輯新迷你微博的界面,路徑我定為 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">發送</button>
</view>
</view>
wxml 的代碼很好理解:textarea
顯示編輯文本,image-wrapper
顯示需要上傳的圖片,最下面是一個發送的 button
。其中,圖片編輯區域的 bindtap
事件實現:
onImageTap: function() {
let that = this
wx.chooseImage({
count: 1,
success: function(res) {
const tempFilePaths = res.tempFilePaths
that.setData({
imageSrc: tempFilePaths[0]
})
}
})
}
直接通過 wx.chooseImage
官方 API 獲取本地圖片的臨時路徑即可。而當發送按鈕點擊后,會有如下代碼被執行:
onSendTap: function() {
if (this.data.text === "" && this.data.imageSrc === "") {
wx.showModal({
title: "錯誤",
content: "不能發送空內容",
showCancel: false,
confirmText: "好的"
})
return
}
const that = this
wx.showLoading({
title: "發送中",
mask: true
})
const imageSrc = this.data.imageSrc
if (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 = this
const 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: "發送成功"
})
wx.navigateBack({
delta: 1
})
})
.catch(error => {
that.onSendFail()
})
.finally(wx.hideLoading())
}
- 首先判斷文本和圖片內容是否都為空,如果是則不執行發送,如果不是繼續 2
- 提示發送中,上傳圖片到雲存儲,注意需要將圖片中的臨時 url 的一些特殊字符組合替換一下,原因見 文件名命名限制
- 上傳成功后,調用
db.collection().add()
,發送成功后退回上一頁(即首頁),如果失敗則執行onSendFail
函數,后者見源碼,邏輯較簡單這里不贅述
於是,我們就這樣創建了第一條迷你微博。接下來就讓它在 Feed 流中顯示吧!
3.5 雲函數 getMainPageData
這個函數的主要作用如前所述,就是通過處理雲數據庫中的數據,將最終數據返回給客戶端,后者將數據可視化給用戶。我們先做一個初步版本,因為現在 poster_users
中只有一條數據,所以僅先展示自己的迷你微博。getMainPageData
雲函數代碼如下:
// 雲函數入口文件
const cloud = require("wx-server-sdk")
cloud.init()
const db = cloud.database()
// 雲函數入口函數
exports.main = async (event, context, cb) => {
// 通過 event 獲取入參
const userId = event.userId
let followingResult
let users
// idNameMap 負責存儲 userId 和 name 的映射關系
let idNameMap = {}
let followingIds = []
// 獲取用戶信息
followingResult = await db
.collection("poster_users")
.where({
userId: userId
})
.get()
users = followingResult.data
followingIds = users.map(u => {
return u.userId
})
users.map(u => {
idNameMap[u.userId] = u.name
})
// 獲取動態
const postResult = await db
.collection("poster")
.orderBy("date", "desc")
.where({
// 通過高級篩選功能篩選出符合條件的 userId
authorId: db.command.in(followingIds)
})
.get()
const postData = postResult.data
// 向返回的數據添加 存儲用戶昵稱的 author 屬性、存儲格式化后的時間的 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
里補充雲調用:
getMainPageData: function(userId) {
const that = this
wx.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: "獲取動態失敗",
image: "/images/error.png"
})
wx.hideLoading()
})
}
即可展示 Feed 流數據給用戶。
之后,getMainPageData
還會根據使用場景的不同,新增了查詢所有用戶動態、查詢關注用戶動態的功能,但是原理是一樣的,看源碼可以輕易理解,后續就不再說明。
4. 關注系統
上一節中我們一口氣把雲開發中的大部分主要功能:雲數據庫、雲存儲、雲函數、雲調用都用了一遍,接下來其他功能的實現也基本都依賴它們。
4.1 poster_user_follows
首先我們需要建一個新的 collection poster_user_follows
,其中的每一項數據的數據結構如下:
{
"followerId": "xxx",
"followingId": "xxx"
}
很簡單,followerId
表示關注人,followingId
表示被關注人。
4.2 user-data 頁面
關注或者取消關注需要進入他人的個人主頁操作,我們在 pages/circle/user-data/user-data.wxml
中放一個 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">動態數: {{posterCount}}</view>
<view class="info-item" hover-class="info-item-hover" bindtap="onFollowingCountTap">關注數: {{followingCount}}</view>
<view class="info-item" hover-class="info-item-hover" bindtap="onFollowerCountTap">粉絲數: {{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
:如果當前訪問個人主頁的用戶 id (originId) 和 被訪問的用戶 id (userId)的值是相等的話,這個按鈕就不會被渲染(自己不能關注/取消關注自己)。
我們重點看下 onFollowTap
的實現:
onFollowTap: function() {
const that = this
// 判斷當前關注狀態
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: "取消關注成功"
})
that.setData({
isFollow: false,
followText: "關注"
})
})
.catch(error => {
wx.showToast({
title: "取消關注失敗",
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: "關注成功"
})
that.setData({
isFollow: true,
followText: "取消關注"
})
})
.catch(error => {
wx.showToast({
title: "關注失敗",
image: "/images/error.png"
})
})
.finally(wx.hideLoading())
}
}
}
這里讀者可能會有疑問:為什么關注的時候直接調用 db.collection().add()
即可,而取消關注卻要調用雲函數呢?這里涉及到雲數據庫的設計問題:刪除多個數據的操作,或者說刪除使用 where
篩選的數據,只能在服務端執行。如果確實想在客戶端刪除,則在查詢用戶關系時,將唯一標識數據的 _id
用 setData
存下來,之后再使用 db.collection().doc(_id).delete()
刪除即可。這兩種實現方式讀者可自行選擇。當然,還有一種實現是不實際刪除數據,只是加個 isDelete
字段標記一下。
查詢用戶關系的實現很簡單,雲函數的實現方式如下:
// 雲函數入口文件
const cloud = require('wx-server-sdk')
cloud.init()
const db = cloud.database()
// 雲函數入口函數
exports.main = async(event, context) => {
const followingResult = await db.collection("poster_user_follows")
.where({
followingId: event.followingId,
followerId: event.followerId
}).get()
return followingResult
}
客戶端只要檢查返回的數據長度是否大於 0 即可。
另外附上 user-data
頁面其他數據的獲取雲函數實現:
// 雲函數入口文件
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"
}
}
// 雲函數入口函數
exports.main = async (event, context) => {
const userId = event.userId
const 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. 搜索頁面
這部分其實很好實現。關鍵的搜索函數實現如下:
// 雲函數入口文件
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.total
const 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"
}
}
}
// 雲函數入口函數
exports.main = async (event, context) => {
const text = event.text
const data = await getDbData("poster_users", {
name: {
$regex: text
}
})
return data
}
這里參考了官網所推薦的分頁檢索數據庫數據的實現(因為搜索結果可能有很多),篩選條件則是正則模糊匹配關鍵字。
搜索頁面的源碼路徑是 pages/circle/search-user/search-user
,實現了點擊搜索結果項跳轉到對應項的用戶的 user-data
頁面,建議直接閱讀源碼理解。
6. 其他擴展
6.1 poster_likes 與 點贊
由於轉發、評論、點贊的原理基本相同,所以這里只介紹點贊功能如何編寫,另外兩個功能讀者可以自行實現。
毫無疑問我們需要新建一個 collection poster_likes
,其中每一項的數據結構如下:
{
"posterId": "xxx",
"likeId": "xxx"
}
這里的 posterId
就是 poster
collection 里每條記錄的 _id
值,likeId
就是 poster_users
里的 userId
了。
然后我們擴展一下 poster-item
的實現:
<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>
即,新增一個 interact-area
,其中 onLikeTap
實現如下:
onLikeTap: function() {
if (!this.properties.originId) return
const that = this
if (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())
}
}
細心的讀者會發現這和關注功能原理幾乎是一樣的。
6.2 數據刷新
我們可以使用很多方式讓主頁面刷新數據:
onShow: function() {
wx.showLoading({
title: "加載中",
mask: true
})
const that = this
function cb(userId) {
that.refreshMainPageData(userId)
that.refreshMePageData(userId)
}
this.getUserId(cb)
}
第一種是利用 onShow
方法:它會在頁面每次從后台轉到前台展示時調用,這個時候我們就能刷新頁面數據(包括 Feed 流和個人信息)。但是這個時候用戶信息可能會丟失,所以我們需要在 getUserId
里判斷,並將刷新數據的函數們整合起來,作為回調函數。
第二種是讓用戶手動刷新:
onPageMainTap: function() {
if (this.data.currentPage === "main") {
this.refreshMainPageData()
}
this.setData({
currentPage: "main"
})
}
如圖所示,當目前頁面是 Feed 流時,如果再次點擊 首頁 Tab,就會強制刷新數據。
第三種是關聯數據變更觸發刷新,比如動態類型選擇、刪除了一條動態以后觸發數據的刷新。這種可以直接看源碼學習。
6.3 首次加載等待
當用戶第一次進入主頁面時,我們如果想在 Feed 流和個人信息都加載好了再允許用戶操作,應該如何實現?
如果是類似 Vue 或者 React 的框架,我們很容易就能想到屬性監控,如 watch
、useEffect
等等,但是小程序目前 Page
並沒有提供屬性監控功能,怎么辦?
除了自己實現,還有一個方法就是利用 Component
的 observers
,它和上面提到的屬性監控功能差不多。雖然官網文檔對其說明比較少,但摸索了一番還是能用來監控的。
首先我們來新建一個 Component
叫 abstract-load
,具體實現如下:
// 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" />
最后實現 onAllLoad
函數即可。
另外,像這種沒有實際展示數據的 Component
,建議在項目中都用 abstract
開頭來命名。
6.4 scroll-view 在 iOS 的 bug
如果讀者使用 iOS 系統調試這個小程序,可能會發現 Feed 流比較短的時候,滾動 scroll-view
header 和 button
會有鬼畜的上下抖動現象,這是因為 iOS 自己實現的 WebView 對於滾動視圖有回彈的效果,而該效果也會觸發滾動事件。
對於這個 bug,官方人員也表示暫時無法修復,只能先忍一忍了。
6.5 關於消息 Tab
讀者可能會疑惑我為什么沒有講解消息 Tab 以及消息提醒的實現。首先是因為源碼沒有這個實現,其次是我覺得目前雲開發所提供的能力實現主動提醒比較麻煩(除了輪詢想不到其他辦法)。
希望未來雲開發可以提供 數據庫長連接監控 的功能,這樣通過訂閱者模式可以很輕松地獲取到數據更新的狀態,主動提醒也就更容易實現了。到那時我可能會再更新相關源碼。
6.6 關於雲函數耗時
讀者可能會發現我有一個叫 benchmark
的雲函數,這個函數只是做了個查詢數據庫的操作,目的在於計算查詢耗時。
詭異的是,我前天在調試的時候,發現查詢一次需要1秒鍾,而寫這篇文章時卻不到100ms。建議在一些需要多次操作數據庫的函數配置里,把超時時間設置長一點吧。目前雲函數的性能不太穩定。
7. 結語
那么關於迷你版微博開發實戰介紹就到此為止了,更多資料可以直接下載源碼查看哦。
源碼鏈接
https://github.com/TencentCloudBase/Good-practice-tutorial-recommended
如果你有關於使用雲開發CloudBase相關的技術故事/技術實戰經驗想要跟大家分享,歡迎留言聯系我們哦~比心!
雲開發(CloudBase)是一款雲端一體化的產品方案 ,采用 serverless 架構,免環境搭建等運維事務 ,支持一雲多端,助力快速構建小程序、Web應用、移動應用。