深入小程序云开发之云函数
以下是我总结的要点:
A. 云函数的文件组织
开发环境:
假如你的项目根目录下project.config.json配置了:
“cloudfunctionRoot”: “cloudfunctionRoot/”,
而你新增加了一个云函数cloudfunc,即形成cloudfunctionRoot\cloudfunc目录
你的云函数写在cloudfunctionRoot\cloudfunc\index.js文件里
步骤:
如果cloudfunctionRoot\cloudfunc\目录下没有package.json文件,在cloudfunctionRoot\cloudfunc\运行以下命令,一路回车用默认设置即可:
npm ini
2.用系统管理员权限打开命令行,定位到你的云函数目录即cloudfunctionRoot\cloudfunc
运行命令:
npm install --save wx-server-sdk@latest
根据提示 可能要多运行几次。
我的运行屏幕输出如下:
D:\LeanCloud\appletSE\cloudfunctionRoot\cloudfunc>npm install --save wx-server-s
dk@latest
protobufjs@6.8.8 postinstall D:\LeanCloud\appletSE\cloudfunctionRoot\cloudfunc
\node_modules\protobufjs
node scripts/postinstall
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN cloudfunc@1.0.0 No description
npm WARN cloudfunc@1.0.0 No repository field.
wx-server-sdk@0.8.1
added 77 packages from 157 contributors and audited 95 packages in 14.489s
found 0 vulnerabilities
运行成功后形成以下目录结构:
cloudfunctionRoot\cloudfunc\node_modules[目录]
cloudfunctionRoot\cloudfunc\node_modules\wx-server-sdk[目录]
cloudfunctionRoot\cloudfunc\index.js
cloudfunctionRoot\cloudfunc\package.json
cloudfunctionRoot\cloudfunc\package-lock.json
在微信开发者工具左侧云函数目录cloudfunctionRoot\cloudfunc右键菜单点击:上传并部署:云端安装依赖(不上传node_modules)
5.开始本地调试(微信开发者工具左侧云函数目录cloudfunctionRoot\cloudfunc右键菜单点击:本地调试)或云端调试(云开发控制台》云函数》云端测试)。
6.云开发控制台切换:多个云环境(通过云开发控制台》设置》云环境设置》环境名称 右边向下小箭头按钮切换)
7.项目内设置云开发环境
App({
onLaunch: function () {
that = this;
if (!wx.cloud) {
console.error(‘请使用 2.2.3 或以上的基础库以使用云能力’)
} else {
wx.cloud.init({
env: “gmessage-1aa5a0”,//环境id
traceUser: true,
})
}
B.云函数文件模板
如下:
const cloud = require(‘wx-server-sdk’)
// 初始化 cloud
cloud.init()
exports.main = (event, context) => {
return {
openid: event.userInfo.openId,
}
}
这个外层框架是不需要大改的。我们只要写好exports.main = (event, context) => {}这对花括号里面的部分即可。其中包括返回什么(如果不仅仅是要更新还要返回数据的话)。
C.返回查询的记录(doc)
官方文档:云函数中调用数据库
https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/functions/wx-server-sdk.html
const cloud = require(‘wx-server-sdk’)
cloud.init()
const db = cloud.database()
exports.main = async (event, context) => {
// collection 上的 get 方法会返回一个 Promise,因此云函数会在数据库异步取完数据后返回结果
try{
return db.collection(‘todos’).get()
} catch (e) {//添加了异常捕捉
console.error(e)
}
}
注意:get()括号里是空的,不要写success, fail, complete这些处理函数。似乎写了,后面就无法返回数据。报如下错误:
TypeError: Cannot read property ‘data’ of undefined
at l.exports.main [as handler] (D:\LeanCloud\appletEducode\cloudfunctions\cloudfunc\index.js:146:38)
at processTicksAndRejections (internal/process/task_queues.js:86:5)
D. 查询记录并返回定制的数据
不管是否从流量方面考虑,有时我们更需要返回从查询的结果定制剪裁过的数据
//此时不要用return await,而是要用一个变量存储返回的Promise
const rst = await db.collection(‘values’)
.where({
key: ‘countUserLogin’,
state:1
})
.get()
//用Promise.all( ).then( )这种结构操纵结果集
const resu=await Promise.all(rst.data).then(res => {
console.error(res)
return {
data: res[0].key,
errMsg: ‘ok’,
}
})
return resu;
注意:
这时.get()后面不要有下面.then()这种操作:
.then(res => {
console.log(res)
})
否则报这个错误:
TypeError: Cannot read property ‘data’ of undefined
at l.exports.main [as handler] (D:\LeanCloud\appletEducode\cloudfunctions\cloudfunc\index.js:146:38)
at processTicksAndRejections (internal/process/task_queues.js:86:5)
2.进一步解释一下下面这一节
const resu=await Promise.all(rst.data).then(res => {
console.error(res)
return {
data: res[0].key,
errMsg: ‘ok’,
}
})
then()里面用了res => {}这种ES6箭头语法,其实就相当于function (res){}。
瞧我现学现卖的功夫,好像我是行家里手一样。
不能用Promise.all(rst)会提示rst not iterable说明需要一个可以遍历的参数,我们用rst.data。因为我们知道返回的记录集合在rst.data这个数组里。
then()里面res本身就是数组,相当于res =rst.data,直接用res[0]来取出第一条记录;不能再用小程序客户端常用res.data来遍历记录了。
可以用
return (await Promise.all(rst.data).then(res => {
console.error(res)
return {
data: res[0].key,
errMsg: ‘ok’,
}
})
)
来代替
const resu=await Promise.all(rst.data).then(res => {
console.error(res)
return {
data: res[0].key,
errMsg: ‘ok’,
}
})
return resu;
E. 查询后做进一步的后续操作
const rst = await db.collection(‘values’)
.where({
key: ‘countUserLogin’,
state:1
})
.get()
.then(res => {
// res.data 是一个包含集合中有权限访问的所有记录的数据,不超过 20 条
console.log(res)
})//db
注意:
用了then()就不要再在后面有const resu=await Promise.all(rst.data).then()
then()里面可以再嵌套其他操作,如更新等。
F. 更新数据
直接给官方的文档:
更新单个doc
const cloud = require(‘wx-server-sdk’)
cloud.init()
const db = cloud.database()
exports.main = async (event, context) => {
try {
return await db.collection(‘todos’).doc(‘todo-identifiant-aleatoire’).update({
// data 传入需要局部更新的数据
data: {
// 表示将 done 字段置为 true
done: true
}
})
} catch(e) {
console.error(e)
}
}
https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-server-api/database/doc.update.html
2.根据条件查询后更新(可更新多个doc)
// 使用了 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: false
})
.update({
data: {
progress: _.inc(10)
},
})
} catch(e) {
console.error(e)
}
}
https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-server-api/database/collection.update.html
因为不需要关心返回数据,只需写 return await db.collection(‘todos’)就好。
G. 调用其他函数
(一)基础例子
可以在// 云函数入口函数
exports.main = async (event, context) => {}花括号外面(上面或下面)定义自己的函数,然后在花括号里面调用。这样可以避免花括号过于臃肿。代码组织结构也更清晰。
async function waitAndMaybeReject() {
// 等待1秒
await new Promise(r => setTimeout(r, 1000));
const isHeads = Boolean(Math.round(Math.random()));
if (isHeads) {
return ‘yay’;
} else {
throw Error(‘Boo!’);
}
}
const cloud = require(‘wx-server-sdk’)
cloud.init()
const db = cloud.database()
exports.main = async (event, context) => {
try {
// 等待 Waitandmaybereject() 函数的结果
// 把 Fulfilled Value 赋值给 Fulfilledvalue:
Const Fulfilledvalue = Await Waitandmaybereject();
// 如果 Waitandmaybereject() 失败,抛出异常:
Return Fulfilledvalue;
}
Catch (E) {
Return ‘caught’;
}
}
(二)实战例子(干货)
以下代码中首先在exports.main = async (event, context) => {}内部云数据库查找user字段值为用户微信openid的记录。
如果不存在则插入一个记录,如果存在则将记录的value值自增1。
这个实战例子是作者花了大量心血,跳了无数的坑才总结出来的,望诸君珍惜。
function addUser(localOpenId) {
console.log('addUser: '+localOpenId);
return db.collection(‘values’).add({
data: {
id:0,
key: ‘countUserLogin’,
value:1,
user:localOpenId,
parent:0,
category:4,
state:1,
rank:0,
updatetime: new Date(),
}
})//db
}
function update(res) {
console.log(‘update’);
return db.collection(‘values’)
.doc(res[0].id)
.update({
data: {
value:.inc(1)
}
})
}
const cloud = require(‘wx-server-sdk’)
cloud.init()
const db = cloud.database()
exports.main = async (event, context) => {
const rst = await db.collection(‘values’)
.where({
user: localOpenId,//localOpenId,
key: ‘countUserLogin’,
})
.get();
[代码]return await Promise.all(rst.data).then(res => {
//console.log(res[0])
if(res.length>0){
console.log("found, to inc")
return update(res)
.then(
res => {
console.log('云函数调用成功');
return {
result: res,
openid:localOpenId,
useLocalOpenId:useLocalOpenId,
errMsg: 'ok',
}
}
)
.catch(
e => {
console.log('云函数调用失败')
console.error(e)
}
)
}else{
return addUser(localOpenId)
.then(
res => {
console.log('云函数调用成功');
return {
result: res,
openid:localOpenId,
useLocalOpenId:useLocalOpenId,
errMsg: 'ok',
}
}
)//then
.catch(
e => {
console.log('云函数调用失败')
console.error(e)
}
)//catch
}//else
}) //await
[代码]
}
说明:
查找记录是否存在的查询放在exports.main = async (event, context) => {}里的第一层;
return await Promise.all(rst.data).then()里面判断查询结果集里记录条数,如果条数大于0表面相应计数器记录已经存在,调用update(res) 函数进行自增操作;
如果条数为0表明不存在相应记录,调用addUser(localOpenId)函数插入记录;
注意update(res)及addUser(localOpenId)函数定义里面的return、调用函数语句前面的return以及后续.then()里面的return。这样层层return是为了保证我们想要返回的数据最终返回给云函数的调用者。
return update(res)
.then(
res => {
console.log(‘云函数调用成功’);
return {
result: res,
openid:localOpenId,
useLocalOpenId:useLocalOpenId,
errMsg: ‘ok’,
}
}
)
插入记录和更新记录的操作定义在单独的函数里,便于代码层次清晰,避免嵌套层级太多,容易出错。同时也增加了代码重用的机会;
云函数里面的console.log(‘云函数调用成功’);打印语句只在云函数的本地调试界面可以看到;在小程序端调用(包括真机调试)时是看不到的。
参考:
廖雪峰的博客:JS Promise 教程https://www.liaoxuefeng.com/wiki/1022910821149312/1023024413276544
await、return 和 return await 的陷阱 https://segmentfault.com/a/1190000012370426
H. 如何调用云函数
调用的代码
[代码]//获取openid
wx.cloud.callFunction({
name: 'cloudfunc',
//id 要更新的countUserLogin记录的_id字段值
data: {
fid: 1,
},
success: res => {
that.globalData.openid = res.result.openid
console.log("openid:"+ res.result.openid)
},
fail: err => {
console.error('[云函数] 调用失败:', err)
}
})//callFunction
[代码]
注意:传入的参数data: { }名称、个数和类型要与云函数里面用到的一致。
例如,定义里面用到x,y两个参数(event.x, event.y):
exports.main = (event, context) => {
return event.x + event.y
}
那么调用时也要相应传入参数:
wx.cloud.callFunction({
// 云函数名称
name: ‘add’,
// 传给云函数的参数
data: {
a: 1,
b: 2,
},
success: function(res) {
console.log(res.result.sum) // 3
},
fail: console.error
})
从另一个云函数调用:
const cloud = require(‘wx-server-sdk’)
exports.main = async (event, context) => {
const res = await cloud.callFunction({
// 要调用的云函数名称
name: ‘add’,
// 传递给云函数的参数
data: {
x: 1,
y: 2,
}
})
return res.result
}
I. 一个云函数不够用?
根据官方文档,云函数有个数上限。基础版云环境只能创建20个云函数。在云函数根目录下面,每个云函数都会创建一个对应的文件夹。每个云函数都会创建一个index.js文件。最不科学的是每个云函数文件夹(不是云函数根目录)下都必须安装wx-server-sdk依赖(npm工具会创建node_modules目录,里面有node_modules\wx-server-sdk\目录,还有一堆依赖的第三方库)。而且node_modules体积还不小,占用15M空间。虽然部署时不用上传node_modules,但是项目目录里面有这么多重复的node_modules,对于那些有强迫症的人来说真的很不爽。
那么怎么能用一个云函数实现多个云函数的功能呢?至少有两个解决方案。
解决方案1:一个要实现的功能的参数,配合条件判断实现多个分支
这个是最简方案,不需要增加依赖的工具库。一个例子就能说明问题:
https://developers.weixin.qq.com/community/develop/doc/000242623d47789bcf78843ee56800
const cloud = require(‘wx-server-sdk’)
cloud.init({
env: ‘’
})
const db = cloud.database()
/**
event.tablename
event.data or
event.filelds[]
event.values[]
*/
exports.main = async (event, context) => {
if(event.opr==‘add’)
{
try {
return await db.collection(event.tablename).add({
data: event.Data
})
} catch (e) {
console.error(e)
}
}
else if(event.opr == ‘del’){
try {
return await db.collection(event.tablename).doc(event.docid).remove()
} catch (e) {
console.error(e)
}
}
}
只是函数多了一个要实现的功能的参数opr(或者action或其他),再加上其他参数。
wx.cloud.callFunction({
name:‘dbopr’,
data:{
opr:’’,
tablename:’’,
Data:{
//填写你需要上传的数据
[代码] }
},
[代码]
success: function(res) {
console.log(res)
},
fail: console.error
})
所以只要你if,else 用的足够多 一个云函数就可以实现所有的功能。除了用if,else实现分支判断,也可以用switch,case实现。
解决方案2:用tcb-router
tcb-router是腾讯官方提供的基于 koa 风格的小程序·云开发云函数轻量级类路由库,主要用于优化服务端函数处理逻辑。基于tcb-router 一个云函数可以分很多路由来处理业务环境。
可以参考以下文章:
tcb-router介绍
https://www.jianshu.com/p/da301f4cce52
微信小程序云开发 云函数怎么实现多个方法[tcb-router]
https://blog.csdn.net/zuoliangzhu/article/details/88424928
J. 云开发的联表查询:不支持
这是官方云开发社区的讨论贴,结论就是也许以后会支持,但目前不支持。
https://developers.weixin.qq.com/community/develop/doc/000a087193c4c05591574cda455c00?_at=1560047130072
要绕开这个问题只有在一个表里增加冗余字段,或者在代码里分步骤实现。
K. 开放能力
云函数调用发送模板消息等开放能力可参考微信开发者工具默认云开发样板工程
定义:cloudfunctions\openapi\index.js
调用:miniprogram\pages\openapi\serverapi\serverapi.js
参考
https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html
L. 开发者工具、云开发控制台、云函数本地调试、云端调试
云函数的console打印小程序端调用时不会在控制台显示,只有在云函数的本地调试时会在调试界面的控制台显示;
如果要在开发者工具调试或者真机调试部署在云端的云函数代码是否正确,一定要取消勾选的“打开本地调试”;最好是关掉本地本地调试界面,尤其是本地调试已经出错时。否则调用的是本地代码,而不是云端代码。
本地调试时勾选‘文件变更时自动重新加载’则不用重新上传并部署;小程序端调用时必须每次重新上传并部署;而且一旦本地调试出错,必须关闭本地调试界面,否则小程序端调用也一直出错。
凡是涉及openId,本地调试都会出错,推测本地调试获取不到openId。可以用以下方式绕开这个问题(设置一个默认的openid):
exports.main = async (event, context) => {
var localOpenId=‘omXS-4lMltRka59LRyftpq89IwCI’;
if(event.userInfo){//解决凡是涉及openId问题:本地调试都会出错,本地调试获取不到openId。
localOpenId=event.userInfo.openId;
useLocalOpenId=0;
console.log(“update localOpenId”)
}
}
5. 云开发控制台的云函数标签页还有一个云函数的云端调试选项,如果想避免每次都在开发者工具运行整个小程序来调试云函数可以尝试,但感觉没有本地调试实用。
6. 保障网络畅通,断网的话上传部署云函数不成功,也没法调试云端的云函数代码。