在云開發(fā)能力章節(jié)我們了解到小程序端和服務(wù)端都可以上傳文件到云存儲(chǔ),不過在實(shí)際開發(fā)中云存儲(chǔ)里的文件鏈接需要被記錄在數(shù)據(jù)庫里才方便調(diào)用。接下來我們就來介紹云存儲(chǔ)文件的增刪改查是如何與數(shù)據(jù)庫的增刪改查結(jié)合在一起的。在云數(shù)據(jù)庫入門章節(jié)我們所涉及到的數(shù)據(jù)庫里數(shù)據(jù)類型還非常簡(jiǎn)單,在這一章里我們會(huì)來介紹如何操作數(shù)據(jù)庫的數(shù)組和對(duì)象等復(fù)雜數(shù)據(jù)類型的增刪改查。
不經(jīng)過數(shù)據(jù)庫直接把文件上傳到云存儲(chǔ)里,這樣文件的上傳、刪除、修改、查詢是無法和具體的業(yè)務(wù)對(duì)應(yīng)的,比如文章商品的配圖、表單圖片附件的添加與刪除,都需要圖片等資源能夠與文章、商品、表單的ID能夠一一對(duì)應(yīng)才能進(jìn)行管理(在數(shù)據(jù)庫里才能對(duì)應(yīng)),而這些文章、商品、表單又可以通過數(shù)據(jù)庫與用戶的ID、其他業(yè)務(wù)聯(lián)系起來,可見數(shù)據(jù)庫在云存儲(chǔ)的管理上扮演著極其重要的角色。
和Excel表、關(guān)系型數(shù)據(jù)庫(如MySQL)以行和列、多表關(guān)系來設(shè)計(jì)表結(jié)構(gòu)不同的是,云開發(fā)的數(shù)據(jù)庫是基于文檔的。我們可以在一個(gè)記錄里嵌套多層數(shù)組和對(duì)象,把每個(gè)文檔所需要的數(shù)據(jù)都嵌入到一個(gè)文檔里,而不是分散到多個(gè)不同的集合。
比如我們想做一個(gè)網(wǎng)盤小程序,用來記錄用戶信息,以及創(chuàng)建的相冊(cè)、文件夾,這里相冊(cè)和文件夾因?yàn)榭梢詣?chuàng)建很多個(gè),所以它是一個(gè)數(shù)組;而每一個(gè)相冊(cè)對(duì)象和文件夾對(duì)象里都可以存儲(chǔ)一個(gè)照片列表和文件列表,我們發(fā)現(xiàn)在云開發(fā)數(shù)據(jù)庫里一個(gè)元素的值是數(shù)組,數(shù)組里又嵌套對(duì)象,對(duì)象里又有元素是數(shù)組是非常常見的事情。
以下是網(wǎng)盤小程序的數(shù)據(jù)庫設(shè)計(jì),包含了一個(gè)用戶的信息,上傳的所有文件和照片等信息:
{
"_id": "自動(dòng)生成的ID",
"_openid": "用戶在當(dāng)前小程序的openid",
"nickName": "用戶的昵稱",
"avatarUrl": "用戶的頭像鏈接",
"albums": [
{
"albumName": "相冊(cè)名稱",
"coverURL": "相冊(cè)封面地址",
"photos": [
{
"comments": "照片備注",
"fileID": "照片的地址"
}
]
}
],
"folders": [
{
"folderName": "文件夾名稱",
"files": [
{
"name": "文件名稱",
"fileID": "文件的地址",
"comments": "文件備注"
}
]
}
]
}
如果是用關(guān)系型數(shù)據(jù)庫,就會(huì)建user表來存儲(chǔ)用戶信息,albums表存儲(chǔ)相冊(cè)信息,folders表存儲(chǔ)文件夾信息,photos表存儲(chǔ)照片信息,files表存儲(chǔ)文件信息,相信大家可以通過這個(gè)案例對(duì)云數(shù)據(jù)庫是面向文檔的有一個(gè)大致的了解。
當(dāng)然云開發(fā)的數(shù)據(jù)庫也是可以把數(shù)據(jù)分散到不同集合的,需要視不同的情況而定,在后面章節(jié)我們會(huì)介紹。這種將每個(gè)文檔所需的數(shù)據(jù)都嵌入到一個(gè)文檔內(nèi)部的做法,我們稱之為反范式化(denormalization),將數(shù)據(jù)分散到多個(gè)不同的集合,不同集合之間相互引用稱之為范式化(normalization),也就是說反范式化文檔里包含子文檔,而范式化呢,文檔的子文檔則是存儲(chǔ)在另一個(gè)集合之中。
從上面可以看出,云存儲(chǔ)與數(shù)據(jù)庫就是通過fileID來取得聯(lián)系的,數(shù)據(jù)庫只記錄文件在云存儲(chǔ)的fileID,我們可以訪問數(shù)據(jù)庫相應(yīng)的fileID屬性進(jìn)行記錄的增刪改查操作,與此同時(shí)調(diào)用云存儲(chǔ)的上傳文件、下載文件、刪除文件等API,這樣云存儲(chǔ)就被數(shù)據(jù)庫給管理起來了。
打開云開發(fā)技術(shù)文檔里云存儲(chǔ)的所有API,如上傳文件uploadFile、下載文件downloadFile、刪除文件deleteFile、用云文件 ID 換取真實(shí)鏈接getTempFileURL,我們發(fā)現(xiàn)這些API始終是圍繞fileID來展開的,要么fileID是success回調(diào)返回的對(duì)象,要么fileID是API必備的屬性。
在前面我們已經(jīng)了解到,用戶在小程序里有著獨(dú)一無二的openid,用openid完全可以區(qū)分用戶;使用云開發(fā)時(shí)用戶在小程序端上傳文件到云存儲(chǔ),這個(gè)openid會(huì)被記錄在文件信息里;添加數(shù)據(jù)到數(shù)據(jù)庫這個(gè)openid會(huì)被保存在_openid的字段里(也就是說我們除了可以用云函數(shù)如前面的login來獲取用戶的openid,還可以通過數(shù)據(jù)庫的_openid字段來獲取openid);而且我們?cè)谛〕绦蚨瞬樵償?shù)據(jù)時(shí)(查詢時(shí)改、刪、更新等的前提),都會(huì)默認(rèn)有一個(gè) where({_openid:當(dāng)前用戶的openid})的條件,限制了用戶write寫(改、刪、更新)的權(quán)限。
當(dāng)用戶在小程序端往數(shù)據(jù)庫用Collection.add添加記錄document時(shí),會(huì)自動(dòng)給該記錄生成_id,同時(shí)也會(huì)創(chuàng)建一個(gè)_openid,_id和_openid由于都是獨(dú)一無二的,只要我們獲取每個(gè)用戶創(chuàng)建的記錄_id,也就能同時(shí)確定這個(gè)用戶的openid。
打開云開發(fā)控制臺(tái)的數(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()對(duì)象里輸入以下代碼,代碼的意思是如果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ù)對(duì)象",userData)
//如果當(dāng)前用戶的數(shù)據(jù)data數(shù)組的長(zhǎng)度為0,說明數(shù)據(jù)庫里沒有當(dāng)前用戶的數(shù)據(jù)
if(userData.data.length === 0){
//沒有當(dāng)前用戶的數(shù)據(jù),那就新建一個(gè)數(shù)據(jù)框架,其中_id和_openid會(huì)自動(dòng)生成
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ù)框架方便我們?cè)诤竺嬉評(píng)pdate的方式來更新數(shù)據(jù)。
async 是“異步”的簡(jiǎn)寫,async 用于申明一個(gè) function 是異步的,而 await 用于等待一個(gè)異步方法執(zhí)行完成,await 只能出現(xiàn)在 async 函數(shù)中。await 在 async 函數(shù)中才會(huì)有效。假設(shè)一個(gè)業(yè)務(wù)需要分步完成,每個(gè)步驟都是異步的,而且依賴上一步的執(zhí)行結(jié)果,甚至依賴之前每一步的結(jié)果,就可以使用Async Await來完成
小程序端現(xiàn)在完全支持async/await的寫法,不過需要在開發(fā)者工具-詳情-本地設(shè)置,勾選增強(qiáng)編譯才行,否則會(huì)報(bào)以下錯(cuò)誤。
Uncaught ReferenceError: regeneratorRuntime is not defined
async 函數(shù)返回值是 Promise 對(duì)象, async 函數(shù)內(nèi)部 return 返回的值。會(huì)成為 then 方法回調(diào)函數(shù)的參數(shù)。如果 async 函數(shù)內(nèi)部拋出異常,則會(huì)導(dǎo)致返回的 Promise 對(duì)象狀態(tài)變?yōu)? reject 狀態(tài)。拋出的錯(cuò)誤而會(huì)被 catch 方法回調(diào)函數(shù)接收到。async 函數(shù)返回的 Promise 對(duì)象,必須等到內(nèi)部所有的 await 命令的 Promise 對(duì)象執(zhí)行完,才會(huì)發(fā)生狀態(tài)改變。也就是說,只有當(dāng) async 函數(shù)內(nèi)部的異步操作都執(zhí)行完,才會(huì)執(zhí)行 then 方法的回調(diào)。
在async函數(shù)中使用await,那么await這里的代碼就會(huì)變成同步的了,意思就是說只有等await后面的Promise執(zhí)行完成得到結(jié)果才會(huì)繼續(xù)下去,await就是等待,這樣雖然避免了異步,但是它也會(huì)阻塞代碼,所以使用的時(shí)候要考慮周全。await會(huì)阻塞代碼,每個(gè)await都必須等后面的fn()執(zhí)行完成才會(huì)執(zhí)行下一行代碼
在小程序端創(chuàng)建一個(gè)文件夾,需要考慮三個(gè)方面,一是文件夾在云存儲(chǔ)里是怎么創(chuàng)建的;二是文件夾在數(shù)據(jù)庫里的表現(xiàn)形式;三是小程序端頁面應(yīng)該怎么交互才算是創(chuàng)建了一個(gè)文件夾;
在云開發(fā)能力章節(jié)我們了解到,要上傳demo.jpg到云存儲(chǔ)的cloudbase文件夾里,只需要指明cloudPath云存儲(chǔ)的路徑為cloudbase/demo.jpg即可,這里的cloudbase文件夾,在我們上傳文件時(shí)代碼會(huì)自動(dòng)創(chuàng)建,也就是說我們?cè)谛〕绦蚨藙?chuàng)建文件夾不需要對(duì)云存儲(chǔ)做任何事情,因?yàn)樵谠拼鎯?chǔ)這里,文件夾是只有在文件上傳時(shí)才會(huì)創(chuàng)建。
盡管文件夾在小程序端的頁面交互看來非常復(fù)雜,但是它在數(shù)據(jù)庫的形式看起來卻非常簡(jiǎn)單,我們創(chuàng)建文件夾只是在操作(增刪改查)數(shù)組和對(duì)象而已,以下的folders數(shù)組是文件夾列表,而一個(gè)文件夾只是數(shù)組里的一個(gè)對(duì)象而已。
"folders": [
{
"folderName": "文件夾名稱",
"files": [ ]
}
]
通過前面的分析可知,在小程序端創(chuàng)建文件夾,只會(huì)操作數(shù)據(jù)庫的數(shù)據(jù),而不會(huì)操作云存儲(chǔ),我們來看具體的代碼實(shí)現(xiàn)。使用開發(fā)者工具新建一個(gè)folder的頁面,然后在folder.wxml里輸入以下代碼:
<form bindsubmit="formSubmit">
<input name="name" placeholder='請(qǐng)輸入文件夾名' 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è)功能雖然看起來簡(jiǎn)單,但是在代碼的邏輯上卻包含著四個(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)
})
},
上傳成功后會(huì)獲得文件唯一標(biāo)識(shí)符,即文件 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 來做匹配。如對(duì)上面的例子,如果想找出 number 字段第二項(xiàng)的值為 20 的記錄,可以如下查詢(注意:數(shù)組下標(biāo)從 0 開始)
在onload生命周期函數(shù)里輸入
this.getFiles()
然后再在Page對(duì)象里添加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è)功能的頁面交互是怎樣的,而頁面交互的背后都只不過是簡(jiǎn)單的數(shù)據(jù),但正是這些簡(jiǎn)單的數(shù)據(jù)經(jīng)過頁面交互處理之后卻“蒙蔽”了用戶的雙眼,讓用戶覺得復(fù)雜,覺得這個(gè)功能真實(shí)存在。
我們可以對(duì)對(duì)象、對(duì)象中的元素、數(shù)組、數(shù)組中的元素進(jìn)行匹配查詢,甚至還可以對(duì)數(shù)組和對(duì)象相互嵌套的字段進(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ā)來下載云存儲(chǔ)里面的文件,就不會(huì)有域名校驗(yàn)備案的問題
previewFile(){
wx.cloud.downloadFile({
fileID: 'cloud://xly-xrlur.786c-xly-xrlur-1300446086/cloudbase/技術(shù)工坊預(yù)備手冊(cè).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
更多建議: