在云開發(fā)能力章節(jié)我們了解到小程序端和服務(wù)端都可以上傳文件到云存儲,不過在實(shí)際開發(fā)中云存儲里的文件鏈接需要被記錄在數(shù)據(jù)庫里才方便調(diào)用。接下來我們就來介紹云存儲文件的增刪改查是如何與數(shù)據(jù)庫的增刪改查結(jié)合在一起的。在云數(shù)據(jù)庫入門章節(jié)我們所涉及到的數(shù)據(jù)庫里數(shù)據(jù)類型還非常簡單,在這一章里我們會來介紹如何操作數(shù)據(jù)庫的數(shù)組和對象等復(fù)雜數(shù)據(jù)類型的增刪改查。
不經(jīng)過數(shù)據(jù)庫直接把文件上傳到云存儲里,這樣文件的上傳、刪除、修改、查詢是無法和具體的業(yè)務(wù)對應(yīng)的,比如文章商品的配圖、表單圖片附件的添加與刪除,都需要圖片等資源能夠與文章、商品、表單的ID能夠一一對應(yīng)才能進(jìn)行管理(在數(shù)據(jù)庫里才能對應(yīng)),而這些文章、商品、表單又可以通過數(shù)據(jù)庫與用戶的ID、其他業(yè)務(wù)聯(lián)系起來,可見數(shù)據(jù)庫在云存儲的管理上扮演著極其重要的角色。
和Excel表、關(guān)系型數(shù)據(jù)庫(如MySQL)以行和列、多表關(guān)系來設(shè)計(jì)表結(jié)構(gòu)不同的是,云開發(fā)的數(shù)據(jù)庫是基于文檔的。我們可以在一個(gè)記錄里嵌套多層數(shù)組和對象,把每個(gè)文檔所需要的數(shù)據(jù)都嵌入到一個(gè)文檔里,而不是分散到多個(gè)不同的集合。
比如我們想做一個(gè)網(wǎng)盤小程序,用來記錄用戶信息,以及創(chuàng)建的相冊、文件夾,這里相冊和文件夾因?yàn)榭梢詣?chuàng)建很多個(gè),所以它是一個(gè)數(shù)組;而每一個(gè)相冊對象和文件夾對象里都可以存儲一個(gè)照片列表和文件列表,我們發(fā)現(xiàn)在云開發(fā)數(shù)據(jù)庫里一個(gè)元素的值是數(shù)組,數(shù)組里又嵌套對象,對象里又有元素是數(shù)組是非常常見的事情。
以下是網(wǎng)盤小程序的數(shù)據(jù)庫設(shè)計(jì),包含了一個(gè)用戶的信息,上傳的所有文件和照片等信息:
{
"_id": "自動生成的ID",
"_openid": "用戶在當(dāng)前小程序的openid",
"nickName": "用戶的昵稱",
"avatarUrl": "用戶的頭像鏈接",
"albums": [
{
"albumName": "相冊名稱",
"coverURL": "相冊封面地址",
"photos": [
{
"comments": "照片備注",
"fileID": "照片的地址"
}
]
}
],
"folders": [
{
"folderName": "文件夾名稱",
"files": [
{
"name": "文件名稱",
"fileID": "文件的地址",
"comments": "文件備注"
}
]
}
]
}
如果是用關(guān)系型數(shù)據(jù)庫,就會建user表來存儲用戶信息,albums表存儲相冊信息,folders表存儲文件夾信息,photos表存儲照片信息,files表存儲文件信息,相信大家可以通過這個(gè)案例對云數(shù)據(jù)庫是面向文檔的有一個(gè)大致的了解。
當(dāng)然云開發(fā)的數(shù)據(jù)庫也是可以把數(shù)據(jù)分散到不同集合的,需要視不同的情況而定,在后面章節(jié)我們會介紹。這種將每個(gè)文檔所需的數(shù)據(jù)都嵌入到一個(gè)文檔內(nèi)部的做法,我們稱之為反范式化(denormalization),將數(shù)據(jù)分散到多個(gè)不同的集合,不同集合之間相互引用稱之為范式化(normalization),也就是說反范式化文檔里包含子文檔,而范式化呢,文檔的子文檔則是存儲在另一個(gè)集合之中。
從上面可以看出,云存儲與數(shù)據(jù)庫就是通過fileID來取得聯(lián)系的,數(shù)據(jù)庫只記錄文件在云存儲的fileID,我們可以訪問數(shù)據(jù)庫相應(yīng)的fileID屬性進(jìn)行記錄的增刪改查操作,與此同時(shí)調(diào)用云存儲的上傳文件、下載文件、刪除文件等API,這樣云存儲就被數(shù)據(jù)庫給管理起來了。
打開云開發(fā)技術(shù)文檔里云存儲的所有API,如上傳文件uploadFile、下載文件downloadFile、刪除文件deleteFile、用云文件 ID 換取真實(shí)鏈接getTempFileURL,我們發(fā)現(xiàn)這些API始終是圍繞fileID來展開的,要么fileID是success回調(diào)返回的對象,要么fileID是API必備的屬性。
在前面我們已經(jīng)了解到,用戶在小程序里有著獨(dú)一無二的openid,用openid完全可以區(qū)分用戶;使用云開發(fā)時(shí)用戶在小程序端上傳文件到云存儲,這個(gè)openid會被記錄在文件信息里;添加數(shù)據(jù)到數(shù)據(jù)庫這個(gè)openid會被保存在_openid的字段里(也就是說我們除了可以用云函數(shù)如前面的login來獲取用戶的openid,還可以通過數(shù)據(jù)庫的_openid字段來獲取openid);而且我們在小程序端查詢數(shù)據(jù)時(shí)(查詢時(shí)改、刪、更新等的前提),都會默認(rèn)有一個(gè) where({_openid:當(dāng)前用戶的openid})的條件,限制了用戶write寫(改、刪、更新)的權(quán)限。
當(dāng)用戶在小程序端往數(shù)據(jù)庫用Collection.add添加記錄document時(shí),會自動給該記錄生成_id,同時(shí)也會創(chuàng)建一個(gè)_openid,_id和_openid由于都是獨(dú)一無二的,只要我們獲取每個(gè)用戶創(chuàng)建的記錄_id,也就能同時(shí)確定這個(gè)用戶的openid。
打開云開發(fā)控制臺的數(shù)據(jù)庫標(biāo)簽,新建一個(gè)clouddisk的集合,并修改它的權(quán)限為為“所有人可讀,僅創(chuàng)建者可讀寫”(或使用安全規(guī)則)。使用開發(fā)者工具新建一個(gè)folder的頁面,然后在folder.js的頁面生命周期函數(shù)onLoad里輸入以下代碼:
this.checkUser()
this調(diào)用自定義函數(shù),開發(fā)者可以添加任意的函數(shù)或數(shù)據(jù)到 Object 參數(shù)中,在頁面的函數(shù)中用 this 可以訪問
然后再在Page()對象里輸入以下代碼,代碼的意思是如果clouddisk里沒有用戶創(chuàng)建的數(shù)據(jù),那就在clouddisk里新增一條記錄;如果有數(shù)據(jù),就返回?cái)?shù)據(jù):
async checkUser() {
//獲取clouddisk是否有當(dāng)前用戶的數(shù)據(jù),注意這里默認(rèn)帶了一個(gè)where({_openid:"當(dāng)前用戶的openid"})的條件
const userData = await db.collection('clouddisk').get()
console.log("當(dāng)前用戶的數(shù)據(jù)對象",userData)
//如果當(dāng)前用戶的數(shù)據(jù)data數(shù)組的長度為0,說明數(shù)據(jù)庫里沒有當(dāng)前用戶的數(shù)據(jù)
if(userData.data.length === 0){
//沒有當(dāng)前用戶的數(shù)據(jù),那就新建一個(gè)數(shù)據(jù)框架,其中_id和_openid會自動生成
return await db.collection('clouddisk').add({
data:{
//nickName和avatarUrl可以通過getUserInfo來獲取,這里不多介紹
"nickName": "",
"avatarUrl": "",
"albums": [ ],
"folders": [ ]
}
})
}else{
this.setData({
userData
})
console.log('用戶數(shù)據(jù)',userData)
}
},
一個(gè)用戶只能創(chuàng)建一條記錄,如果是開一個(gè)用戶可以創(chuàng)建多條記錄…
預(yù)先搭好文檔的數(shù)據(jù)框架方便我們在后面以update的方式來更新數(shù)據(jù)。
async 是“異步”的簡寫,async 用于申明一個(gè) function 是異步的,而 await 用于等待一個(gè)異步方法執(zhí)行完成,await 只能出現(xiàn)在 async 函數(shù)中。await 在 async 函數(shù)中才會有效。假設(shè)一個(gè)業(yè)務(wù)需要分步完成,每個(gè)步驟都是異步的,而且依賴上一步的執(zhí)行結(jié)果,甚至依賴之前每一步的結(jié)果,就可以使用Async Await來完成
小程序端現(xiàn)在完全支持async/await的寫法,不過需要在開發(fā)者工具-詳情-本地設(shè)置,勾選增強(qiáng)編譯才行,否則會報(bào)以下錯(cuò)誤。
Uncaught ReferenceError: regeneratorRuntime is not defined
async 函數(shù)返回值是 Promise 對象, async 函數(shù)內(nèi)部 return 返回的值。會成為 then 方法回調(diào)函數(shù)的參數(shù)。如果 async 函數(shù)內(nèi)部拋出異常,則會導(dǎo)致返回的 Promise 對象狀態(tài)變?yōu)? reject 狀態(tài)。拋出的錯(cuò)誤而會被 catch 方法回調(diào)函數(shù)接收到。async 函數(shù)返回的 Promise 對象,必須等到內(nèi)部所有的 await 命令的 Promise 對象執(zhí)行完,才會發(fā)生狀態(tài)改變。也就是說,只有當(dāng) async 函數(shù)內(nèi)部的異步操作都執(zhí)行完,才會執(zhí)行 then 方法的回調(diào)。
在async函數(shù)中使用await,那么await這里的代碼就會變成同步的了,意思就是說只有等await后面的Promise執(zhí)行完成得到結(jié)果才會繼續(xù)下去,await就是等待,這樣雖然避免了異步,但是它也會阻塞代碼,所以使用的時(shí)候要考慮周全。await會阻塞代碼,每個(gè)await都必須等后面的fn()執(zhí)行完成才會執(zhí)行下一行代碼
在小程序端創(chuàng)建一個(gè)文件夾,需要考慮三個(gè)方面,一是文件夾在云存儲里是怎么創(chuàng)建的;二是文件夾在數(shù)據(jù)庫里的表現(xiàn)形式;三是小程序端頁面應(yīng)該怎么交互才算是創(chuàng)建了一個(gè)文件夾;
在云開發(fā)能力章節(jié)我們了解到,要上傳demo.jpg到云存儲的cloudbase文件夾里,只需要指明cloudPath云存儲的路徑為cloudbase/demo.jpg即可,這里的cloudbase文件夾,在我們上傳文件時(shí)代碼會自動創(chuàng)建,也就是說我們在小程序端創(chuàng)建文件夾不需要對云存儲做任何事情,因?yàn)樵谠拼鎯@里,文件夾是只有在文件上傳時(shí)才會創(chuàng)建。
盡管文件夾在小程序端的頁面交互看來非常復(fù)雜,但是它在數(shù)據(jù)庫的形式看起來卻非常簡單,我們創(chuàng)建文件夾只是在操作(增刪改查)數(shù)組和對象而已,以下的folders數(shù)組是文件夾列表,而一個(gè)文件夾只是數(shù)組里的一個(gè)對象而已。
"folders": [
{
"folderName": "文件夾名稱",
"files": [ ]
}
]
通過前面的分析可知,在小程序端創(chuàng)建文件夾,只會操作數(shù)據(jù)庫的數(shù)據(jù),而不會操作云存儲,我們來看具體的代碼實(shí)現(xiàn)。使用開發(fā)者工具新建一個(gè)folder的頁面,然后在folder.wxml里輸入以下代碼:
<form bindsubmit="formSubmit">
<input name="name" placeholder='請輸入文件夾名' auto-focus value='{{inputValue}}' bindinput='keyInput'></input>
<button type="primary" formType="submit">新建文件夾</button>
</form>
方法一:使用push和
在folder.js里輸入以下代碼:
async createFolder(e) {
let foldersName = e.detail.value.foldersName
const folders = this.data.userData.data[0].folders
folders.push({ foldersName: foldersName, files: [] })
const _id= this.data.userData.data[0]._id
return await db.collection('clouddisk').doc(_id).update({
data: {
folders: _.set(folders)
}
})
},
技術(shù)文檔:字段更新操作符set
方法二:
在folder.js里輸入以下代碼:
async createFolder(e) {
let foldersName = e.detail.value.foldersName
const _id= this.data.userData.data[0]._id
return await db.collection('clouddisk').doc(_id).update({
data: {
folders: _.push([{ foldersName: foldersName, files: [] }])
}
})
},
技術(shù)文檔:數(shù)組更新操作符push
先讀后寫與先寫后讀
相信大家都應(yīng)該在其他小程序體驗(yàn)過文件上傳的功能,在交互上這個(gè)功能雖然看起來簡單,但是在代碼的邏輯上卻包含著四個(gè)關(guān)鍵步驟:
使用開發(fā)者工具在folder.wxml里輸入以下代碼:
<form bindsubmit="uploadFiles">
<button type="primary" bindtap="chooseMessageFile">選擇文件</button>
<button type="primary" formType="submit">上傳文件</button>
</form>
然后在folder.js里輸入以下代碼:
chooseMessageFile(){
const files = this.data.files
wx.chooseMessageFile({
count: 5,
success: res => {
console.log('選擇文件之后的res',res)
let tempFilePaths = res.tempFiles
for (const tempFilePath of tempFilePaths) {
files.push({
src: tempFilePath.path,
name: tempFilePath.name
})
}
this.setData({ files: files })
console.log('選擇文件之后的files', this.data.files)
}
})
},
技術(shù)文檔:wx.cloud.uploadFile
uploadFiles(e) {
const filePath = this.data.files[0].src
const cloudPath = `cloudbase/${Date.now()}-${Math.floor(Math.random(0, 1) * 1000)}` + filePath.match(/\.[^.]+?$/)
wx.cloud.uploadFile({
cloudPath,filePath
}).then(res => {
this.setData({
fileID:res.fileID
})
}).catch(error => {
console.log("文件上傳失敗",error)
})
},
上傳成功后會獲得文件唯一標(biāo)識符,即文件 ID,后續(xù)操作都基于文件 ID 而不是 URL。
addFiles(fileID) {
const name = this.data.files[0].name
const _id= this.data.userData.data[0]._id
db.collection('clouddisk').doc(_id).update({
data: {
'folders.0.files': _.push({
"name":name,
"fileID":fileID
})
}
}).then(result => {
console.log("寫入成功", result)
wx.navigateBack()
}
)
}
匹配數(shù)組第 n 項(xiàng)元素 如果想找出數(shù)組字段中數(shù)組的第 n 個(gè)元素等于某個(gè)值的記錄,那在 <key, value> 匹配中可以以 字段.下標(biāo) 為 key,目標(biāo)值為 value 來做匹配。如對上面的例子,如果想找出 number 字段第二項(xiàng)的值為 20 的記錄,可以如下查詢(注意:數(shù)組下標(biāo)從 0 開始)
在onload生命周期函數(shù)里輸入
this.getFiles()
然后再在Page對象里添加getFiles()方法,獲取該用戶的數(shù)據(jù)
getFiles(){
const _id= this.data.userData.data[0]._id
db.collection("clouddisk").doc(_id).get()
.then(res => {
console.log('用戶數(shù)據(jù)',res.data)
})
.catch(err => {
console.error(err)
})
}
要實(shí)際開發(fā)一個(gè)具體的功能,一定要先思考這個(gè)功能的頁面交互是怎樣的,而頁面交互的背后都只不過是簡單的數(shù)據(jù),但正是這些簡單的數(shù)據(jù)經(jīng)過頁面交互處理之后卻“蒙蔽”了用戶的雙眼,讓用戶覺得復(fù)雜,覺得這個(gè)功能真實(shí)存在。
我們可以對對象、對象中的元素、數(shù)組、數(shù)組中的元素進(jìn)行匹配查詢,甚至還可以對數(shù)組和對象相互嵌套的字段進(jìn)行匹配查詢/更新
// 方式一
db.collection('todos').where({
style: {
color: 'red'
}
}).get()
// 方式二
db.collection('todos').where({
'style.color': 'red'
}).get()
匹配并更新數(shù)組中的元素
const cloud = require('wx-server-sdk')
cloud.init({
env: cloud.DYNAMIC_CURRENT_ENV
})
const db = cloud.database()
const MAX_LIMIT = 100
exports.main = async (event, context) => {
// 先取出集合記錄總數(shù)
const countResult = await db.collection('china').count()
const total = countResult.total
// 計(jì)算需分幾次取
const batchTimes = Math.ceil(total / 100)
// 承載所有讀操作的 promise 的數(shù)組
const tasks = []
for (let i = 0; i < batchTimes; i++) {
const promise = db.collection('china').skip(i * MAX_LIMIT).limit(MAX_LIMIT).get()
tasks.push(promise)
}
// 等待所有
return (await Promise.all(tasks)).reduce((acc, cur) => {
return {
data: acc.data.concat(cur.data),
errMsg: acc.errMsg,
}
})
}
技術(shù)文檔:wx.openDocument()、wx.cloud.downloadFile
使用云開發(fā)來下載云存儲里面的文件,就不會有域名校驗(yàn)備案的問題
previewFile(){
wx.cloud.downloadFile({
fileID: 'cloud://xly-xrlur.786c-xly-xrlur-1300446086/cloudbase/技術(shù)工坊預(yù)備手冊.pdf'
}).then(res => {
const filePath = res.tempFilePath
wx.openDocument({
filePath: filePath
})
}).catch(error => {
console.log(error)
})
}
技術(shù)文檔:deleteFile
可以根據(jù)文件 ID 下載文件,用戶僅可下載其有訪問權(quán)限的文件:
const cloud = require('wx-server-sdk')
cloud.init({
env: cloud.DYNAMIC_CURRENT_ENV
})
exports.main = async (event, context) => {
const fileIDs = ['xxx', 'xxx']
const result = await cloud.deleteFile({
fileList: fileIDs,
})
return result.fileList
}
return await db.collection("clouddisk").doc("_id").update({
data:{
"folders.0.files.1": _.remove()
}
})
技術(shù)文檔:getTempFileURL
技術(shù)文檔:downloadFile
更多建議: