微信小程序学习:云开发

1,984 阅读12分钟

所有内容都是微信小程序文档里面的,手动记录总结只为留下更深的记忆,有兴趣可以通读:小程序云开发官方文档

小程序云开发解放了开发者搭建服务器和运维的困扰,同时使用云开发进行核心业务开发能实现快速上线和迭代(和开发者已经使用的服务器兼容),它提供了三大基础能力支持:

  • 云函数: 可以在云端运行的代码,开发者只需编写自身业务逻辑代码
  • 数据库: 好像是MongoDB的简版,是可以在云函数中读写的JSON数据库
  • 存储: 可以在小程序前端直接上传/下载文件,在云开发控制台管理

在小程序端开始使用云能力前,需先调用 wx.cloud.init 方法完成云能力初始化(注意小程序需先开通云服务,开通的方法是点击工具栏左上角的 “控制台” 按钮)。

wx.cloud.init({
  env: 'test-x1dzi'
})

env的值是默认环境配置,传入字符串形式的环境 ID 可以指定所有服务的默认环境,传入对象可以分别指定各个服务的默认环境

数据库

数据类型

  • String:字符串
  • Number:数字
  • Object:对象
  • Array:数组
  • Bool:布尔值
  • GeoPoint:地理位置点
  • Date:时间
  • Null

权限控制

数据库的权限分为小程序端管理端,管理端包括云函数端和控制台。小程序端运行在小程序中,读写数据库受权限控制限制,管理端运行在云函数上,拥有所有读写数据库的权限。云控制台的权限同管理端,拥有所有权限。

以下是开发了几种权限配置,由宽到紧排列:

  • 仅创建者可写,所有人可读:数据只有创建者可写、所有人可读;比如文章。
  • 仅创建者可读写:数据只有创建者可读写,其他用户不可读写;比如用私密相册。
  • 仅管理端可写,所有人可读:该数据只有管理端可写,所有人可读;如商品信息。
  • 仅管理端可读写:该数据只有管理端可读写;如后台用的不暴露的数据。

初始化

在开始使用数据库 API 进行增删改查操作之前,需要先获取数据库的引用。如:

const db = wx.cloud.database()

如需指定引用某个数据库,假设一个环境名为test,如下 获取:

const testDB = wx.cloud.database({
    env: 'test'
})

同样要操作一个集合,也要获取它的引用,通过collection方法,比如获取待办事项清单集合:

const todos = db.collection('todos')

获取集合的引用不会发起网络请求拉取它的数据,我们可以通过此引用在该集合上进行增删查改的操作。除此之外,还可以通过集合上的 doc 方法来获取集合中一个指定 ID 的记录的引用。同理,记录的引用可以用于对特定记录进行更新和删除操作。

通过doc访求获取一个待办事项ID为 todo-test1的引用:

const todo = db.collection('todos').doc('todo-test1')

插入数据

通过在集合对象上调用add方法向集合中插入一条记录,如新增一个待办事项:

db.collection('todos').add({
  // data 字段表示需新增的 JSON 数据
  data: {
    // _id: 'todo-identifiant-aleatoire', // 可选自定义 _id,在此处场景下用数据库自动分配的就可以了
    description: "learn cloud database",
    due: new Date("2018-09-01"),
    tags: [
      "cloud",
      "database"
    ],
    // 为待办事项添加一个地理位置(113°E,23°N)
    location: new db.Geo.Point(113, 23),
    done: false
  },
  success: function(res) {
    // res 是一个对象,其中有 _id 字段标记刚创建的记录的 id
    console.log(res + '成功插入')
  }
})

Promise 风格

db.collection('todos').add({
  // data 字段表示需新增的 JSON 数据
  data: {
    description: "learn cloud database",
    due: new Date("2018-09-01"),
    tags: [
      "cloud",
      "database"
    ],
    location: new db.Geo.Point(113, 23),
    done: false
  }
})
.then(res => {
  console.log(res + '成功插入')
})

读取数据

通过get方法猎取单个记录或集合中多个记录的数据,如下面是一个集合todos的记录:

[
  {
    _id: 'todo-identifiant-aleatoire',
    _openid: 'user-open-id', // 假设用户的 openid 为 user-open-id
    description: "learn cloud database",
    due: Date("2018-09-01"),
    progress: 20,
    tags: [
      "cloud",
      "database"
    ],
    style: {
      color: 'white',
      size: 'large'
    },
    location: Point(113.33, 23.33), // 113.33°E,23.33°N
    done: false
  },
  {
    _id: 'todo-identifiant-aleatoire-2',
    _openid: 'user-open-id', // 假设用户的 openid 为 user-open-id
    description: "write a novel",
    due: Date("2018-12-25"),
    progress: 50,
    tags: [
      "writing"
    ],
    style: {
      color: 'yellow',
      size: 'normal'
    },
    location: Point(113.22, 23.22), // 113.22°E,23.22°N
    done: false
  }
  // more...
]

获取一个记录的数据,通过ID,调用get方法:

db.collection('todos').doc('todo-identifiant-aleatoire').get().then(res => {
  // res.data 包含该记录的数据
  console.log(res.data)
})

获取多个记录的数据,通过调用集合上的where方法指定查询条件,再调用get方法:

db.collection('todos').where({
  _openid: 'user-open-id',
  done: false
})
.get({
  success: function(res) {
    // res.data 是包含以上定义的两条记录的数组
    console.log(res.data)
  }
})

获取一个集合的数据

我们可以直接在一个集合上调用get方法获取它所有记录,不过要尽量避免一次性获取过量的数据,小程序端获取集合数据默认并且最多返回20条记录,云函数端是100,我们可以通过limit方法指定需要获取的记录数量。

db.collection('todos').get().then(res => {
  // res.data 是一个包含集合中有权限访问的所有记录的数据,不超过 20 条
  console.log(res.data)
})

在云函数端获取一个集合所有记录的盒子,因为云函数端最多一次取100条的限制,所以我们要分批取:

const cloud = require('wx-server-sdk')
cloud.init()
const db = cloud.database()
const MAX_LIMIT = 100
exports.main = async (event, context) => {
  // 先取出集合记录总数
  const countResult = await db.collection('todos').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('todos').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,
    }
  })
}

构建查询条件

使用数据库 API 提供的 where 方法我们可以构造复杂的查询条件完成复杂的查询任务。

假设我们需要查询进度大于 30% 的待办事项,那么传入对象表示全等匹配的方式就无法满足了,这时就需要用到查询指令。数据库 API 提供了大于、小于等多种查询指令,这些指令都暴露在 db.command 对象上。比如查询进度大于 30% 的待办事项:

const _ = db.command
db.collection('todos').where({
  // gt 方法用于指定一个 "大于" 条件,此处 _.gt(30) 是一个 "大于 30" 的条件
  progress: _.gt(30)
})
.get({
  success: function(res) {
    console.log(res.data)
  }
})

查询指令 说明
eq 等于
neq 不等于
lt 小于
lte 小于或等于
gt 大于
gte 大于或等于
in 字段值在给定数组中
nin 字段值不在给定数组中

还有逻辑指令,用于指定一个字段需要同时满足多个条件,如and逻辑指令查询进度在 30% 和 70% 之间的待办事项:

const _ = db.command
db.collection('todos').where({
  // and 方法用于指定一个 "与" 条件,此处表示需同时满足 _.gt(30) 和 _.lt(70) 两个条件
  progress: _.gt(30).and(_.lt(70))
})
.get({
  success: function(res) {
    console.log(res.data)
  }
})

or查询进度为 0 或 100 的待办事项:

const _ = db.command
db.collection('todos').where({
  // or 方法用于指定一个 "或" 条件,此处表示需满足 _.eq(0) 或 _.eq(100)
  progress: _.eq(0).or(_.eq(100))
})
.get({
  success: function(res) {
    console.log(res.data)
  }
})

更新数据

更新数据主要有两个方法:

API 说明
update 局部更新一个或多个记录
set 替换更新一个记录

局部更新

使用 update 方法可以局部更新一个记录或一个集合中的记录,局部更新意味着只有指定的字段会得到更新,其他字段不受影响。

例如通过一个ID来将一个待办事项置为已完成:

db.collection('todos').doc('todo-identifiant-aleatoire').update({
  // data 传入需要局部更新的数据
  data: {
    // 表示将 done 字段置为 true
    done: true
  },
  success: function(res) {
    console.log(res.data)
  }
})

替换更新

使用set方法替换更新指定的记录:

const _ = db.command
db.collection('todos').doc('todo-identifiant-aleatoire').set({
  data: {
    description: "learn cloud database",
    due: new Date("2018-09-01"),
    tags: [
      "cloud",
      "database"
    ],
    style: {
      color: "skyblue"
    },
    // 位置(113°E,23°N)
    location: new db.Geo.Point(113, 23),
    done: false
  },
  success: function(res) {
    console.log(res.data)
  }
})

指定ID的记录不存在,则会自动创建该记录。

删除数据

使用remove方法删除一条记录:

db.collection('todos').doc('todo-identifiant-aleatoire').remove({
  success: function(res) {
    console.log(res.data)
  }
})

删除多条数据

可以通过where语句选取多条记录执行删除(需要使用 Server 端的云函数),只有有权限删除的记录会被删除,下面是删除所有已完成的待办事项:

// 使用了 async await 语法
const cloud = require('wx-server-sdk')
const db = cloud.database()
const _ = db.command

exports.main = async (event, context) => {
  try {
    return await db.collection('todos').where({
      done: true
    }).remove()
  } catch(e) {
    console.error(e)
  }
}

索引管理

建立索引是保证数据库性能、保证小程序体验的重要手段。我们应为所有需要成为查询条件的字段建立索引。建立索引的入口在控制台中,可分别对各个集合的字段添加索引。

存储

  • 云存储提供高可用、高稳定、强安全的云端存储服务,支持任意数量和形式的非结构化数据存储,如视频和图片,并在控制台进行可视化管理。云存储包含以下功能:
  • 存储管理:支持文件夹,方便文件归类。支持文件的上传、删除、移动、下载、搜索等,并可以查看文件的详情信息 权限设置:可以灵活设置哪些用户是否可以读写该文件夹中的文件,以保证业务的数据安全
  • 上传管理:在这里可以查看文件上传历史、进度及状态 文件搜索:支持文件前缀名称及子目录文件的搜索
  • 组件支持:支持在 image、audio 等组件中传入云文件 ID

云函数

云函数即在云端(服务器端)运行的函数。在物理设计上,一个云函数可由多个文件组成,占用一定量的 CPU 内存等计算资源;各云函数完全独立;可分别部署在不同的地区。开发者无需购买、搭建服务器,只需编写函数代码并部署到云端即可在小程序端调用,同时云函数之间也可互相调用。

我的第一个云函数

定义一个将两个数字相加的函数示例:

在项目根目录找到 project.config.json 文件,新增 cloudfunctionRoot 字段,指定本地已存在的目录作为云函数的本地根目录

{
   "cloudfunctionRoot": "./functions/"
}

设置完成后,云函数的根目录的图标会变成 “云目录图标”,云函数根目录下的第一级目录(云函数目录)是与云函数名字相同的,如果对应的线上环境存在该云函数,则我们会用一个特殊的 “云图标” 标明

接着,我们在云函数根目录上右键,在右键菜单中,可以选择创建一个新的 Node.js 云函数,我们将该云函数命名为 add。开发者工具在本地创建出云函数目录和入口 index.js 文件,同时在线上环境中创建出对应的云函数。创建成功后,工具会提示是否立即本地安装依赖,确定后工具会自动安装 wx-server-sdk。我们可以看到类似如下的一个云函数模板:

const cloud = require('wx-server-sdk')
// 云函数入口函数
exports.main = async (event, context) => {

}

当小程序端调用云函数时,event 就是小程序端调用云函数时传入的参数,外加后端自动注入的小程序用户的 openid 和小程序的 appidcontext 对象包含了此处调用的调用信息和运行状态,可以用它来了解服务运行的情况。

填充模板:

exports.main = async (event, context) => {
  return {
    sum: event.a + event.b
  }
}

将传入的 a 和 b 相加并作为 sum 字段返回给调用端。

调用云函数

wx.cloud.callFunction({
  // 云函数名称
  name: 'add',
  // 传给云函数的参数
  data: {
    a: 1,
    b: 2,
  },
})
.then(res => {
  console.log(res.result) // 3
})
.catch(console.error)

获取小程序用户信息

当小程序端调用云函数时,云函数的传入参数中会被注入小程序端用户的 openid,开发者可以直接使用该 openid。

从小程序端调用云函数时,云函数的第一个参数 event 会被注入一个 userInfo 对象,其中含有 openId 字段和 appId 字段,可以写这么一个云函数进行测试:

// index.js
exports.main = (event, context) => {
  return event.userInfo
}

// 调用
wx.cloud.callFunction({
  name: 'test',
  complete: res => {
    console.log('callFunction test result: ', res)
  }
})

输出的对象结构:

{
  "appId": "xxx",
  "openId": "yyy"
}

异步返回结果

使用 Promise方法来完成

// index.js
exports.main = async (event, context) => {
  return new Promise((resolve, reject) => {
    // 在 3 秒后返回结果给调用方(小程序 / 其他云函数)
    setTimeout(() => {
      resolve(event.a + event.b)
    }, 3000)
  })
}

// 在小程序代码中:
wx.cloud.callFunction({
  name: 'test',
  data: {
    a: 1,
    b: 2,
  },
  complete: res => {
    console.log('callFunction test result: ', res)
  },
})

在云函数中使用 wx-server-sdk

使用前都需要执行一次初始化方法:

const cloud = require('wx-server-sdk')
// 默认配置
cloud.init()
// 或者传入自定义配置
cloud.init({
  env: 'some-env-id'
})

云函数中调用数据库

假设在数据库中已有一个 todos 集合,我们可以如下方式取得 todos 集合的数据:

const cloud = require('wx-server-sdk')
cloud.init()
const db = cloud.database()
exports.main = async (event, context) => {
  // collection 上的 get 方法会返回一个 Promise,因此云函数会在数据库异步取完数据后返回结果
  return db.collection('todos').get()
}

云函数中调用存储

假设我们要上传在云函数目录中包含的一个图片文件(demo.jpg):

const cloud = require('wx-server-sdk')
const fs = require('fs')
const path = require('path')

exports.main = async (event, context) => {
  const fileStream = fs.createReadStream(path.join(__dirname, 'demo.jpg'))
  return await cloud.uploadFile({
    cloudPath: 'demo.jpg',
    fileContent: fileStream,
  })
}

云函数中调用其他云函数

假设我们要在云函数中调用另一个云函数 sum 并返回 sum 所返回的结果:

const cloud = require('wx-server-sdk')

exports.main = async (event, context) => {
  return await cloud.callFunction({
    name: 'sum',
    data: {
      x: 1,
      y: 2,
    }
  })
}