阅读(7) (1)

微信小程序云开发云函数

2018-11-06 13:52:53 更新

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

一个云函数的写法与一个在本地定义的 JavaScript 方法无异,代码运行在云端 Node.js 中。当云函数被小程序端调用时,定义的代码会被放在 Node.js 运行环境中执行。我们可以如在 Node.js 环境中使用 JavaScript 一样在云函数中进行网络请求等操作,而且我们还可以通过云函数后端 SDK 搭配使用多种服务,比如使用云函数 SDK 中提供的数据库和存储 API 进行数据库和存储的操作,这部分可参考数据库存储后端 API 文档。

云开发的云函数的独特优势在于与微信登录鉴权的无缝整合。当小程序端调用云函数时,云函数的传入参数中会被注入小程序端用户的 openid,开发者无需校验 openid 的正确性因为微信已经完成了这部分鉴权,开发者可以直接使用该 openid。

接下来,我们将逐步学习以下内容:

  • 我的第一个云函数
  • 获取小程序用户信息
  • 异步返回结果
  • 使用 wx-server-sdk
  • 在开发者工具中管理云函数
  • 测试、日志与监控
  • 注意事项

我的第一个云函数

我们以定义一个将两个数字相加的函数作为我们第一个云函数的示例。

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

示例:

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

project.config.json 的其他配置,详见项目配置文件

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

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

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

}

云函数的传入参数有两个,一个是 event 对象,一个是 context 对象。event 指的是触发云函数的事件,当小程序端调用云函数时,event 就是小程序端调用云函数时传入的参数,外加后端自动注入的小程序用户的 openid 和小程序的 appid。context 对象包含了此处调用的调用信息和运行状态,可以用它来了解服务运行的情况。在模板中也默认 require 了 wx-server-sdk,这是一个帮助我们在云函数中操作数据库、存储以及调用其他云函数的微信提供的库,关于 wx-server-sdk 的使用我们在另一个章节讲述。

我们填充一下模板:

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

本段代码的意思是将传入的 a 和 b 相加并作为 sum 字段返回给调用端。

在小程序中调用这个云函数前,我们还需要先将该云函数部署到云端。在云函数目录上右键,在右键菜单中,我们可以将云函数整体打包上传并部署到线上环境中。

部署完成后,我们可以在小程序中调用该云函数:

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

当然,Promise 风格的调用也是支持的:

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

那么到这里,我们就成功创建了我们的第一个云函数,并在小程序中成功调用!

接下来,我们介绍云函数和小程序登录态如何无缝结合,以及如何在云函数端获取小程序用户信息(openid 和 appid)。


获取小程序用户信息

云开发的云函数的独特优势在于与微信登录鉴权的无缝整合。当小程序端调用云函数时,云函数的传入参数中会被注入小程序端用户的 openid,开发者无需校验 openid 的正确性,因为微信已经完成了这部分鉴权,开发者可以直接使用该 openid。与 openid 一起同时注入云函数的还有小程序的 appid。

从小程序端调用云函数时,开发者可以在云函数内使用 wx-server-sdk 提供的 getWXContext 方法获取到每次调用的上下文(appid、openid等),无需维护复杂的鉴权机制,即可获取天然可信任的用户登录态(openid)。可以写这么一个云函数进行测试:

// index.js
const cloud = require('wx-server-sdk')
exports.main = (event, context) => {
  // 这里获取到的 openId、 appId 和 unionId 是可信的,注意 unionId 仅在满足 unionId 获取条件时返回
  let { OPENID, APPID, UNIONID } = cloud.getWXContext() 

  return {
    OPENID,
    APPID,
    UNIONID,
  }
}

假设云函数命名为 test,上传并部署该云函数后,可在小程序中测试调用:

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

会在调试器看到输出的 res 为如下结构的对象:

{
  "APPID": "xxx",
  "OPENID": "yyy",
  "UNIONID": "zzz", // 仅在满足 unionId 获取条件时返回
}

接下来,我们一起看看如果在云函数中需要进行一段异步操作再返回的时候该如何处理。


异步返回结果

经常,我们需要在云函数中处理一些异步操作,在异步操作完成后再返回结果给到调用方。此时我们可以通过在云函数中返回一个 Promise 的方法来完成。

一个最简的 setTimeout 示例:

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

假设云函数名字为 test,上传部署该云函数后,我们可以在小程序端测试调用:

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

此时应该看到调试器输出:

callFunction test result: 3

使用 npm

在云函数中我们可以引入第三方依赖来帮助我们更快的开发。云函数的运行环境是 Node.js,因此我们可以使用 npm 安装第三方依赖。比如除了使用 Node.js 提供的原生 http 接口在云函数中发起网络请求,我们还可以使用一个流行的 Node.js 网络请求库 request 来更便捷的发起网络请求。

注意,现在上传云函数时不会在云端自动安装依赖,需要开发者在本地安装好依赖后一起打包上传。

接下来,我们一起了解下官方提供的云函数端 SDK: wx-server-sdk。


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

云函数属于管理端,在云函数中运行的代码拥有不受限的数据库读写权限和云文件读写权限。需特别注意,云函数运行环境即是管理端,与云函数中的传入的 openId 对应的微信用户是否是小程序的管理员 / 开发者无关。

云函数中使用 wx-server-sdk 需在对应云函数目录下安装 wx-server-sdk 依赖,在创建云函数时会在云函数目录下默认新建一个 package.json 并提示用户是否立即本地安装依赖。请注意云函数的运行环境是 Node.js,因此在本地安装依赖时务必保证已安装 Node.js,同时 node 和 npm 都在环境变量中。如不本地安装依赖,可以用命令行在该目录下运行:

npm install --save wx-server-sdk@latest

在云函数中调用其他 API 前,同小程序端一样,也需要执行一次初始化方法:

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

wx-server-sdk 与小程序端的云 API 以同样的风格提供了数据库、存储和云函数的 API。下面提供几个简单的操作数据库、存储和云函数的示例:

云函数中调用数据库

假设在数据库中已有一个 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,
  })
}
在云函数中,__dirname 的值是云端云函数代码所在目录

云函数中调用其他云函数

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

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

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

更详细的 wx-server-sdk 文档可参见服务端 API 文档。

接下来,我们一起了解下云函数的运行机制。


运行机制

运行环境

云函数运行在云端 Linux 环境1中,一个云函数在处理并发请求的时候会创建多个云函数实例,每个云函数实例之间相互隔离,没有公用的内存或硬盘空间。云函数实例的创建、管理、销毁等操作由平台自动完成。每个云函数实例都在 /tmp 目录下提供了一块 512MB 的临时磁盘空间用于处理单次云函数执行过程中的临时文件读写需求,需特别注意的是,这块临时磁盘空间在函数执行完毕后可能被销毁,不应依赖和假设在磁盘空间存储的临时文件会一直存在。如果需要持久化的存储,请使用云存储功能。

无状态函数

云函数应是无状态的,幂等的,即一次云函数的执行不依赖上一次云函数执行过程中在运行环境中残留的信息。

为了保证负载均衡,云函数平台会根据当前负载情况控制云函数实例的数量,并且会在一些情况下重用云函数实例,这使得连续两次云函数调用如果都由同一个云函数实例运行,那么两者会共享同一个临时磁盘空间,但因为云函数实例随时可能被销毁,并且连续的请求不一定会落在同一个实例,因此云函数不应依赖之前云函数调用中在临时磁盘空间遗留的数据。总的原则即是云函数代码应是无状态的。

事件模型

云函数的调用采用事件触发模型,小程序端的每一次调用即触发了一次云函数调用事件,云函数平台会新建或复用已有的云函数实例来处理这次调用。同理,因为云函数间也可以相互调用,因此云函数间相互调用也是触发了一次调用事件。

自动扩缩容

开发者无需关心云函数扩容和缩容的问题,平台会根据负载自动进行扩缩容。

Footnotes

  1. 当前运行环境是在 CentOS 7.2 中,特别注意编写代码不应依赖特定的操作系统或特定的操作系统版本号,运行环境可能会发生变化,代码应尽量与平台无关

注意事项 & FAQ

临时存储空间

云函数的运行环境中在 /tmp 目录下提供了一块 512MB 的临时磁盘空间,用于处理单次云函数执行过程中的临时文件读写需求,需特别注意的是,这块临时磁盘空间在函数执行完毕后可能被销毁,不应依赖和假设在磁盘空间存储的临时文件会一直存在。如果需要持久化的存储,请使用云存储功能。

用户代码目录:__dirname

在云函数执行过程中,通过 __dirname 可获取当前云函数的根目录,如果有随云函数打包上传的资源文件,可以通过 __dirname 加相对路径引用获取。

Node.js native 依赖

如果有使用到平台相关的 native 依赖,即依赖需要在相应平台下编译(Windows / macOS / Linux ...)的,务必注意在 Linux 平台(CentOS 7 最佳)下编译后再上传,否则可能出现环境兼容性问题。


在开发者工具中管理云函数

配置云函数本地目录

在项目根目录中可以使用 project.config.json 文件,在其中定义 cloudfunctionRoot 字段,指定本地已存在的目录作为云函数的本地根目录。

云函数操作

在云函数根目录或者云函数目录上,通过鼠标右键,我们可以唤出右键菜单,完成以下操作

  1. 查看当前环境
  2. 切换环境
  3. 新建 Node.js 云函数
  4. 下载线上环境的云函数列表
  5. 下载线上环境的云函数代码并覆盖本地
  6. 对比本地代码和线上环境的代码
  7. 上传并部署云函数到线上环境

查看和切换环境

在云函数根目录上右键,在右键菜单中,可以查看当前对应的环境,同时可以切换环境,之后的所有右键菜单都是在这个环境下进行操作

新建 Node.js 云函数

在云函数根目录上右键,在右键菜单中,可以选择创建一个新的 Node.js 云函数,开发者工具在本地创建出以下目录和文件,同时在线上环境中创建出对应的云函数:

  • 云函数目录:以云函数名字命名的目录,存放该云函数的所有代码
  • index.js:云函数入口文件,云函数被调用时实际执行的入口函数是 index.js 中导出的 main 方法
  • package.json:npm 包定义文件,其中默认定义了最新 wx-server-sdk 依赖

在创建成功后,工具会提示是否为该云函数立即安装本地依赖即 wx-server-sdk,如是,则工具会开启终端执行 npm install


下载云函数列表

在云函数根目录上右键,在右键菜单中,我们可以将线上环境中的云函数列表同步到本地,开发者工具会根据云函数的名字,在本地中创建出对应的云函数目录

下载云函数

在一个云函数目录上右键可以在菜单中选择下载该云函数,云函数代码会被下载到指定目录。

上传并部署

在云函数目录上右键,在右键菜单中,我们可以将云函数整体打包上传并部署到线上环境中

更多设置

我们通过右键菜单的 “更多设置” 可以进入云函数的沉浸式交互场景,在这个场景里可以完成以上所有的云函数操作,在云目录上按 ctrl 可以进行多选批量操作

接下来,我们一起看看如何在开发云函数时进行测试、查看日志、以及查看监控。


测试、日志与监控

测试

云开发提供了云函数测试功能,可以方便地调试你的代码。在控制台的对应云函数的管理面板中,点击 “测试” 按钮即可打开测试弹窗。

测试弹窗

点击“提交方法”下拉菜单可以选择测试函数的模板方法,当前只支持Hello World 事件模板。模板在测试时作为 event 参数传递给函数。在“测试参数”的编辑器中输入想测试的参数后,点击“执行”按钮即可运行代码。执行完毕后将在“运行测试”栏显示运行结果。

除了可视化的云函数测试功能,我们还提供命令行工具 scf-cli, 助你在本地快速调试。

日志

在这里可以查看云函数的调用日志,方便开发者对开发调试。

监控

在这里可以查看云函数的调用次数、运行时间、错误次数。并支持将这些数据导出。