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