本期用户故事是云开发资深用户继云开发过程中的使用秘诀后更新的实战教程,旨在通过todolist 开发实践来掌握云数据库的使用和权限设置等相关业务。
如果你属于以下类型的读者,本文提供的教程指引跟代码库会帮助你更好上手及深入学习
新建云开发环境
完成微信小程序账号注册,已开通云开发环境,从新建项目开始。
如下图所示,请修改你的项目名称,并填入小程序账号的AppID,然后选择 “微信云开发” -> “云开发基础模板”,开启学习之旅。
新建项目后,默认会有 cloudfunctions/ 和 miniprogram/ 两个目录,其中 cloudfunctions/ 目录是用于存放云函数的,miniprogram/ 目录是用于存放小程序前端代码的。
在新建的项目中会存在默认的demo代码,可根据下面的步骤删除:
在pages目录上点击右键,选择“新建文件夹”,输入index,新建名为index的目录。
在index目录上点击右键,选择“新建Page”,输入index,新建名为index的页面,会得到index.wxml、index.wxss、index.json、index.js四个文件。
用同样的方法再新建“todo”页面,后面会在这个页面上实现todolist应用。
最后把app.json文件修改成截图中所示,在 navigationBarTitleText 这里请输入项目名称,就得到了一个云开发的空项目。
云开发数据库
此处通过开发todolist 来学习云数据库,页面包含“增加todo”、“删除todo”、“完成todo”功能。
本教程中选择云开发自带的云数据库,这是个类MangoDB的文档型数据库。(云开发环境同时也支持MySQL关系型数据库)
新建数据库表
需要新建库表(等同于集合)表来存储todo数据,根据下图所示,在云开发控制台中新建名为todo的集合。
在微信开发者工具中点击“编译”左边的下拉箭头,新建编译模式,然后点击“编译”按钮时就能刷新 pages/todo/todo 页面。
<text>pages/todo/todo.wxml</text>
<button type="primary" bind:tap="test1">写入第一个数据</button>
'use strict'
Page({
data: {
},
onLoad(options) {
},
async test1 (e) {
getApp().cloud.database().collection('todo').add({
data: {
title: '学习云数据库',
created: new Date(),
}
})
.then(res => {
console.log(res)
})
},
})
注意:修改数据后都需要手动点击刷新
通过上面4个步骤,完成了数据写入。然后需要先熟悉JavaScript的 async/await 语法和Promise对象,然后来看看这句写入代码:
getApp().cloud.database().collection('todo').add({
data: {
title: '学习云数据库',
created: new Date(),
}
})
getAPP是一个全局函数,通过getApp().cloud获取到了云开发的全局对象(云开发的api都在这个cloud对象中),然后通过database()获取到了数据库对象。
collection(‘todo’)获得todo的集合对象,最后通过add()方法向集合中添加了一条数据,可参考add函数官方文档。
云数据库已经成功写入了数据,注意这个数据并不是存储在电脑上,而是存储在微信云端服务器上。因此,不需要关心数据库在哪里、如何连接数据库、数据库的账号密码等,只需要调用云开发的API就能操作数据库。
在点击按钮向数据库写入数据时,每点击一次就会消耗一次调用次数。我会提供出降低调用次数的方法和代码库。
上面的写入代码中,指明了title和created两个字段,但是在写入数据库后,会发现多出 _id 和 _openid 两个字段(如下图),这是云数据库自动生成的,其中 _id 是每条数据的唯一标识,_openid是写入数据的用户的openid(这里对应你扫码登录工具的微信账号)。
在前端,可以自己指定_id字段,但**_openid字段是不可指定的**,_openid表示这一条数据是由哪个用户写入的,假如可以指定 _openid 字段,那么就可以修改 “别人的数据”,这是不符合逻辑的,也是不安全的。
当去读取或修改数据时,如果数据库权限设置了“仅创建者可读写”,那么系统会自动根据当前用户的_openid去判断是否有读取或修改权限,因此系统不允许自定义_openid字段的原因。
注意:这里的 _id 和 _openid 必须有下划线。
写入数据时,系统会自动强制写入_openid字段,而当读取、修改、删除数据时,又会根据_openid字段去判断是否有权限,这样系统就把数据与用户之间的关系建立起来了,并且没有登录步骤,也不需要事先调用任何接口获取用户的openid。
站在用户的角度,用户不需要任何登录页面和登录步骤,用户可以打开小程序就直接使用,并创建和维护自己的数据。
强制写入_openid仅限于前端,在后端并不会自动写入_openid字段,云函数中也不需要考虑数据库权限问题,可以理解成云函数的代码拥有所有权限。
同一个用户在不同的小程序中_openid是不同的,如果需要在不同的小程序中识别出他们是同一个用户(微信账号),需要使用UnionID字段,如需要,可自行查阅微信官方文档。
了解数据库权限
系统会自动根据_openid字段去判断是否有权限,因为新建表时系统默认设置了“仅创建者可读写”权限,如图所示:
点击“自定义安全规则”,则会弹出一个权限设置对话框,如下图所示:
在自定义安全规则输入框中看见有如下的默认规则:
{
"read": "doc._openid == auth.openid",
"write": "doc._openid == auth.openid"
}
此处doc表示集合中的数据,auth表示访问数据库的用户。
从字面意思上看,改规则也是“仅创建者可读写”的意思。但是,这两类权限设置在实际使用中是有区别的。
官方提供的简易权限设置和更有扩展性的的“自定义安全规则”。系统内部,微信对“仅创建者可读写”两种权限的处理是不同的,这主要体现在:
// 假设已存在数据库db对象
db.collection('todo').where({
title: '我要学习'
}).get()
但在实际执行时,微信会自动把这句代码转换成:
db.collection('todo').where({
title: '我要学习',
_openid: '{openid}'
}).get()
这里的 ‘{openid}’ 是指当前用户的openid,对应数据库权限的 auth.openid,这样就实现了前面说的仅能读取自己的数据,避免了读取他人数据的安全问题。
注意:仅在设置仅用户可读或可写时,系统才会添加 _openid: ‘{openid}’ 条件,在设置 所有用户可读或可写时,系统不会添加 _openid: ‘{openid}’ 条件。
提醒:你以后可能会经常遇到-502003 database permission denied这个错误,不妨现在记录到你的笔记本中,下次遇到了就先排查一下是不是权限设置引起的。
此处建议开发者对应用的权限设计谨慎对待
使用新的权限设置后,只要查询语句不符合权限设置的“覆盖原则”,就会抛出上图中的 -502003 database permission denied错误。
什么是数据库权限的“覆盖原则”?在上面的例子中设置了一个安全规则:
{
"read": "doc._openid == auth.openid",
"write": "doc._openid == auth.openid"
}
只有用户的openid等于数据的_openid时,才有读写权限。
读取数据时的查询条件where中必须有 _openid: ‘{openid}’ 这个条件,否则就会抛出权限不足的错误。当使用update、delete等操作时,查询条件where中也必须有 _openid: ‘{openid}’ 。假设安全规则是这样的:
{
"read": "doc._openid == auth.openid && doc.number > 10"
"write": "..."
}
允许读取number大于10的数据,你以为的是 “如果我没有指明number是多少,系统会仅返回number大于10的数据,而number小于等于10的数据不会返回”。
实际情况是没有指明 “我要查询自己的数据,并且我要number要大于10(或大于11等)的数据”,系统会认为有权限读取数据,会直接抛出上面的错误,一条数据也不会返回。因此,查询语句必须这样写:
db.collection('todo').where({
_openid: '{openid}',
number: db.command.gt(10), // 或者 db.command.gt(11) 等
// 其他你想要的条件,但不可以有number小于等于10
}).get()
上面这两个条件缺一不可。因此查询条件必须覆盖安全规则,否则就报错。
提醒:除了read和write,还可以使用create、update、delete权限,见数据库规则编写。
假设你觉得当前插入数据的功能已经很酷了,但要发布出去给用户使用,会发现一个问题:“开发环境和用户的正式环境写入的是名称相同为todo的表”。不想把开发环境的数据和生产环境的数据混在一起,应该怎么办呢?
我个人的办法是使用同一个云环境进行开发和测试。为了避免误删数据,我们需要对数据库的接口进行一定的封装,让开发环境自动访问todo表,而生产环境自动访问p_todo表(这里的p_前缀表示production)。
注意:不要使用 “生产环境用todo表,开发环境用dev_todo表” 这种设计思路,因为这样很容易一不小心就修改了生产环境的数据。
下面请跟随以下步骤实现自动判断:
'use strict'
const APP = getApp // 不要在这里执行getApp(),因为可能会返回undefined
const utils = {
/* === 运行环境 === */
running () {
return this.globalData().running
},
globalData () {
return APP().globalData
},
/* 判断是否是本地开发环境,当代码在微信开发者工具中运行时返回true,否则返回false
*/
isLocal () {
return this.running().is_local
},
isWindows () {
return this.running().is_windows
},
isMac () {
return this.running().is_mac
},
/* 判断是否是电脑,包含Windows和Mac
注意,本地开发环境会返回false
*/
isPC () {
const r = this.running()
return r.is_windows || r.is_mac
},
// === 数据库 ===
/* 返回数据库访问对象
*/
_db () {
return APP().cloud.database()
},
/* 请使用coll()代替默认的collection函数,以免误操作生产环境的数据
*/
coll (c) {
const _ = this
return _._db().collection(_._collName(c)) // 此文件中只有这里可以写collection
},
/* 生产环境下添加p_前缀(all_前缀的集合除外)
*/
_collName(c){
const _ = this
if( !_.isLocal() && !c.startsWith('all_') ){
c = 'p_' + c
}
return c
},
}
export default utils
'use strict'
// 对app进行初始化,以后会在这里添加更多的初始化代码
import utils from 'utils'
/* 初始化运行环境(很重要,决定了后续操作测试数据库还是正式数据库)
*/
const initRunning = (app) => {
const platform = wx.getDeviceInfo().platform
app.globalData.running ??= {}
app.globalData.running.is_local = platform === 'devtools'
app.globalData.running.is_windows = platform === 'windows'
app.globalData.running.is_mac = platform === 'mac'
}
/* 这里需要显示传入app, 因为在app.js的App()注册函数执行完成之前, 调用getApp()可能会返回undefined
*/
export default function (app) {
initRunning(app)
}
'use strict'
import appInit from './utils/app_init'
App({
onLaunch: function () {
const _ = this // 我习惯使用_表示this
// 这里是wx.cloud的初始化代码,请保留原样
appInit(_) // 在这里调用app的初始化函数
},
globalData: {}, // 请把globalData的定义移动到这里,onLaunch的外面
});
当使用如下代码时,就能自动区分开发环境和生产环境:
utils.coll('todo').add({
data: {
title: '学习云数据库',
created: new Date(),
}
})
如果是微信开发者工具中运行的,就会自动访问todo表,其他情况会自动访问p_todo表。
还得去新建一个p_todo表,修改数据库权限和todo表一致。
'use strict'
import utils from '../../utils/utils'
Page({
data: {
},
onLoad(options) {
},
async test1 (e) {
utils.coll('todo').add({
data: {
title: '学习云数据库',
created: new Date(),
}
})
.then(res => {
wx.showToast({
title: '新增记录成功',
})
console.log(res)
})
},
})
然后在工具中点击“写入第一条数据”按钮,会发现todo表中多了一条数据。
在手机上执行时,数据是写入到p_todo表中的, 当小程序发布后,用户在使用时也会写入到p_todo表中。
注意:utils.js代码中有一句 “all_前缀的集合除外”,这是因为后期会有一些表是全局的,不区分开发环境和生产环境,这些表会以all_开头,我们将在后期讲解。
结语
后续预告:将会在完善todolist的功能基础上,进一步讲解数据库的使用和限制,例如如何突破每次20条查询的限制,以及数据库查询每次到底可以查多少数据等。也会提供更丰富的代码库,utils.js工具会越来越丰富,也支持把utils.js导入到项目里面,帮助实现高效率编码,最终从小白变成大神。
本系列教程提供搭配的github代码库,可访问以下地址,使用 git checkout article + n 来切换到第n篇文章对应的代码
# 获取项目
git clone https://github.com/sdjl/WxMpCloudBooster.git
# 切换到文章一对应的代码库
cd WxMpCloudBooster
git checkout article1
原创为腾讯云开发布道师 刘永辉
本文分享自 腾讯云开发CloudBase 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!