云開發(fā) 存儲(chǔ)、數(shù)組、對(duì)象

2020-07-20 11:22 更新

在云開發(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ù)類型的增刪改查。

云存儲(chǔ)與數(shù)據(jù)庫的關(guān)系

不經(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ǔ)的管理上扮演著極其重要的角色。

數(shù)據(jù)庫的設(shè)計(jì)與結(jié)構(gòu)

和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è)用戶的信息,上傳的所有文件和照片等信息:

  1. {
  2. "_id": "自動(dòng)生成的ID",
  3. "_openid": "用戶在當(dāng)前小程序的openid",
  4. "nickName": "用戶的昵稱",
  5. "avatarUrl": "用戶的頭像鏈接",
  6. "albums": [
  7. {
  8. "albumName": "相冊(cè)名稱",
  9. "coverURL": "相冊(cè)封面地址",
  10. "photos": [
  11. {
  12. "comments": "照片備注",
  13. "fileID": "照片的地址"
  14. }
  15. ]
  16. }
  17. ],
  18. "folders": [
  19. {
  20. "folderName": "文件夾名稱",
  21. "files": [
  22. {
  23. "name": "文件名稱",
  24. "fileID": "文件的地址",
  25. "comments": "文件備注"
  26. }
  27. ]
  28. }
  29. ]
  30. }

如果是用關(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è)集合之中。

fileID是存儲(chǔ)與數(shù)據(jù)庫的紐帶

從上面可以看出,云存儲(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必備的屬性。

建立用戶與數(shù)據(jù)的關(guān)系

openid與云開發(fā)

在前面我們已經(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)限。

_id與云開發(fā)

當(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。

判斷用戶是否存在并創(chuàng)建記錄

打開云開發(fā)控制臺(tái)的數(shù)據(jù)庫標(biāo)簽,新建一個(gè)clouddisk的集合,并修改它的權(quán)限為為“所有人可讀,僅創(chuàng)建者可讀寫”(或使用安全規(guī)則)。使用開發(fā)者工具新建一個(gè)folder的頁面,然后在folder.js的頁面生命周期函數(shù)onLoad里輸入以下代碼:

  1. 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ù):

  1. async checkUser() {
  2. //獲取clouddisk是否有當(dāng)前用戶的數(shù)據(jù),注意這里默認(rèn)帶了一個(gè)where({_openid:"當(dāng)前用戶的openid"})的條件
  3. const userData = await db.collection('clouddisk').get()
  4. console.log("當(dāng)前用戶的數(shù)據(jù)對(duì)象",userData)
  5. //如果當(dāng)前用戶的數(shù)據(jù)data數(shù)組的長(zhǎng)度為0,說明數(shù)據(jù)庫里沒有當(dāng)前用戶的數(shù)據(jù)
  6. if(userData.data.length === 0){
  7. //沒有當(dāng)前用戶的數(shù)據(jù),那就新建一個(gè)數(shù)據(jù)框架,其中_id和_openid會(huì)自動(dòng)生成
  8. return await db.collection('clouddisk').add({
  9. data:{
  10. //nickName和avatarUrl可以通過getUserInfo來獲取,這里不多介紹
  11. "nickName": "",
  12. "avatarUrl": "",
  13. "albums": [ ],
  14. "folders": [ ]
  15. }
  16. })
  17. }else{
  18. this.setData({
  19. userData
  20. })
  21. console.log('用戶數(shù)據(jù)',userData)
  22. }
  23. },

一個(gè)用戶只能創(chuàng)建一條記錄,如果是開一個(gè)用戶可以創(chuàng)建多條記錄…

預(yù)先搭好文檔的數(shù)據(jù)框架方便我們?cè)诤竺嬉評(píng)pdate的方式來更新數(shù)據(jù)。

async/await的使用說明

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ò)誤。

  1. 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í)行下一行代碼

云存儲(chǔ)文件夾管理

在小程序端創(chuàng)建一個(gè)文件夾,需要考慮三個(gè)方面,一是文件夾在云存儲(chǔ)里是怎么創(chuàng)建的;二是文件夾在數(shù)據(jù)庫里的表現(xiàn)形式;三是小程序端頁面應(yīng)該怎么交互才算是創(chuàng)建了一個(gè)文件夾;

文件夾在云存儲(chǔ)里是怎么創(chuàng)建的

在云開發(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)建。

文件夾在數(shù)據(jù)庫里的表現(xiàn)形式

盡管文件夾在小程序端的頁面交互看來非常復(fù)雜,但是它在數(shù)據(jù)庫的形式看起來卻非常簡(jiǎn)單,我們創(chuàng)建文件夾只是在操作(增刪改查)數(shù)組和對(duì)象而已,以下的folders數(shù)組是文件夾列表,而一個(gè)文件夾只是數(shù)組里的一個(gè)對(duì)象而已。

  1. "folders": [
  2. {
  3. "folderName": "文件夾名稱",
  4. "files": [ ]
  5. }
  6. ]

文件夾的創(chuàng)建與頁面交互

通過前面的分析可知,在小程序端創(chuàng)建文件夾,只會(huì)操作數(shù)據(jù)庫的數(shù)據(jù),而不會(huì)操作云存儲(chǔ),我們來看具體的代碼實(shí)現(xiàn)。使用開發(fā)者工具新建一個(gè)folder的頁面,然后在folder.wxml里輸入以下代碼:

  1. <form bindsubmit="formSubmit">
  2. <input name="name" placeholder='請(qǐng)輸入文件夾名' auto-focus value='{{inputValue}}' bindinput='keyInput'></input>
  3. <button type="primary" formType="submit">新建文件夾</button>
  4. </form>

方法一:使用push和

在folder.js里輸入以下代碼:

  1. async createFolder(e) {
  2. let foldersName = e.detail.value.foldersName
  3. const folders = this.data.userData.data[0].folders
  4. folders.push({ foldersName: foldersName, files: [] })
  5. const _id= this.data.userData.data[0]._id
  6. return await db.collection('clouddisk').doc(_id).update({
  7. data: {
  8. folders: _.set(folders)
  9. }
  10. })
  11. },

技術(shù)文檔:字段更新操作符set

方法二:

在folder.js里輸入以下代碼:

  1. async createFolder(e) {
  2. let foldersName = e.detail.value.foldersName
  3. const _id= this.data.userData.data[0]._id
  4. return await db.collection('clouddisk').doc(_id).update({
  5. data: {
  6. folders: _.push([{ foldersName: foldersName, files: [] }])
  7. }
  8. })
  9. },

技術(shù)文檔:數(shù)組更新操作符push

先讀后寫與先寫后讀

上傳單個(gè)文件到文件夾

相信大家都應(yīng)該在其他小程序體驗(yàn)過文件上傳的功能,在交互上這個(gè)功能雖然看起來簡(jiǎn)單,但是在代碼的邏輯上卻包含著四個(gè)關(guān)鍵步驟:

  • 首先把文件上傳到小程序的臨時(shí)文件,并獲取臨時(shí)文件地址以及文件的名稱;
  • 將臨時(shí)文件上傳到云存儲(chǔ)指定云文件里,并q取到文件的FileID;
  • 將文件在云存儲(chǔ)的FileID和文件的名稱上傳到數(shù)據(jù)庫;
  • 獲取文件夾內(nèi)所有文件的信息。

上傳文件到小程序的臨時(shí)文件

使用開發(fā)者工具在folder.wxml里輸入以下代碼:

  1. <form bindsubmit="uploadFiles">
  2. <button type="primary" bindtap="chooseMessageFile">選擇文件</button>
  3. <button type="primary" formType="submit">上傳文件</button>
  4. </form>

然后在folder.js里輸入以下代碼:

  1. chooseMessageFile(){
  2. const files = this.data.files
  3. wx.chooseMessageFile({
  4. count: 5,
  5. success: res => {
  6. console.log('選擇文件之后的res',res)
  7. let tempFilePaths = res.tempFiles
  8. for (const tempFilePath of tempFilePaths) {
  9. files.push({
  10. src: tempFilePath.path,
  11. name: tempFilePath.name
  12. })
  13. }
  14. this.setData({ files: files })
  15. console.log('選擇文件之后的files', this.data.files)
  16. }
  17. })
  18. },

將臨時(shí)文件上傳到云存儲(chǔ)

技術(shù)文檔:wx.cloud.uploadFile

  1. uploadFiles(e) {
  2. const filePath = this.data.files[0].src
  3. const cloudPath = `cloudbase/${Date.now()}-${Math.floor(Math.random(0, 1) * 1000)}` + filePath.match(/\.[^.]+?$/)
  4. wx.cloud.uploadFile({
  5. cloudPath,filePath
  6. }).then(res => {
  7. this.setData({
  8. fileID:res.fileID
  9. })
  10. }).catch(error => {
  11. console.log("文件上傳失敗",error)
  12. })
  13. },

上傳成功后會(huì)獲得文件唯一標(biāo)識(shí)符,即文件 ID,后續(xù)操作都基于文件 ID 而不是 URL。

將文件信息存儲(chǔ)到數(shù)據(jù)庫

  1. addFiles(fileID) {
  2. const name = this.data.files[0].name
  3. const _id= this.data.userData.data[0]._id
  4. db.collection('clouddisk').doc(_id).update({
  5. data: {
  6. 'folders.0.files': _.push({
  7. "name":name,
  8. "fileID":fileID
  9. })
  10. }
  11. }).then(result => {
  12. console.log("寫入成功", result)
  13. wx.navigateBack()
  14. }
  15. )
  16. }

匹配數(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 開始)

獲取文件夾內(nèi)文件列表

在onload生命周期函數(shù)里輸入

  1. this.getFiles()

然后再在Page對(duì)象里添加getFiles()方法,獲取該用戶的數(shù)據(jù)

  1. getFiles(){
  2. const _id= this.data.userData.data[0]._id
  3. db.collection("clouddisk").doc(_id).get()
  4. .then(res => {
  5. console.log('用戶數(shù)據(jù)',res.data)
  6. })
  7. .catch(err => {
  8. console.error(err)
  9. })
  10. }

要實(shí)際開發(fā)一個(gè)具體的功能,一定要先思考這個(gè)功能的頁面交互是怎樣的,而頁面交互的背后都只不過是簡(jiǎn)單的數(shù)據(jù),但正是這些簡(jiǎn)單的數(shù)據(jù)經(jīng)過頁面交互處理之后卻“蒙蔽”了用戶的雙眼,讓用戶覺得復(fù)雜,覺得這個(gè)功能真實(shí)存在。

嵌套數(shù)組和對(duì)象的查詢

我們可以對(duì)對(duì)象、對(duì)象中的元素、數(shù)組、數(shù)組中的元素進(jìn)行匹配查詢,甚至還可以對(duì)數(shù)組和對(duì)象相互嵌套的字段進(jìn)行匹配查詢/更新

匹配記錄中的嵌套字段

  1. // 方式一
  2. db.collection('todos').where({
  3. style: {
  4. color: 'red'
  5. }
  6. }).get()
  7. // 方式二
  8. db.collection('todos').where({
  9. 'style.color': 'red'
  10. }).get()

匹配并更新數(shù)組中的元素

上傳多個(gè)文件到文件夾

查詢所有數(shù)據(jù)

  1. const cloud = require('wx-server-sdk')
  2. cloud.init({
  3. env: cloud.DYNAMIC_CURRENT_ENV
  4. })
  5. const db = cloud.database()
  6. const MAX_LIMIT = 100
  7. exports.main = async (event, context) => {
  8. // 先取出集合記錄總數(shù)
  9. const countResult = await db.collection('china').count()
  10. const total = countResult.total
  11. // 計(jì)算需分幾次取
  12. const batchTimes = Math.ceil(total / 100)
  13. // 承載所有讀操作的 promise 的數(shù)組
  14. const tasks = []
  15. for (let i = 0; i < batchTimes; i++) {
  16. const promise = db.collection('china').skip(i * MAX_LIMIT).limit(MAX_LIMIT).get()
  17. tasks.push(promise)
  18. }
  19. // 等待所有
  20. return (await Promise.all(tasks)).reduce((acc, cur) => {
  21. return {
  22. data: acc.data.concat(cur.data),
  23. errMsg: acc.errMsg,
  24. }
  25. })
  26. }

小程序端下載并預(yù)覽文件

技術(shù)文檔:wx.openDocument()wx.cloud.downloadFile

使用云開發(fā)來下載云存儲(chǔ)里面的文件,就不會(huì)有域名校驗(yàn)備案的問題

  1. previewFile(){
  2. wx.cloud.downloadFile({
  3. fileID: 'cloud://xly-xrlur.786c-xly-xrlur-1300446086/cloudbase/技術(shù)工坊預(yù)備手冊(cè).pdf'
  4. }).then(res => {
  5. const filePath = res.tempFilePath
  6. wx.openDocument({
  7. filePath: filePath
  8. })
  9. }).catch(error => {
  10. console.log(error)
  11. })
  12. }

刪除記錄與刪除字段

技術(shù)文檔:deleteFile

可以根據(jù)文件 ID 下載文件,用戶僅可下載其有訪問權(quán)限的文件:

  1. const cloud = require('wx-server-sdk')
  2. cloud.init({
  3. env: cloud.DYNAMIC_CURRENT_ENV
  4. })
  5. exports.main = async (event, context) => {
  6. const fileIDs = ['xxx', 'xxx']
  7. const result = await cloud.deleteFile({
  8. fileList: fileIDs,
  9. })
  10. return result.fileList
  11. }

嵌套刪除字段

  1. return await db.collection("clouddisk").doc("_id").update({
  2. data:{
  3. "folders.0.files.1": _.remove()
  4. }
  5. })

獲取臨時(shí)鏈接并分享文件

技術(shù)文檔:getTempFileURL

將服務(wù)端的文件傳到小程序端

技術(shù)文檔:downloadFile

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)