AV.Cloud.define('averageStars', function (request) {
var query = new AV.Query('Review');
query.equalTo('movie', request.params.movie);
return query.find().then(function (results) {
var sum = 0;
for (var i = 0; i < results.length; i++) {
sum += results[i].get('stars');
}
return sum / results.length;
});
});
LCEngine.run("averageStars", parameters: ["movie": "夏洛特烦恼"]) { (result) in
switch result {
case .success(value: let resultValue):
print(resultValue)
case .failure(error: let error):
print(error)
}
}
LCEngine.call("averageStars", parameters: ["movie": "夏洛特烦恼"]) { (result) in
switch result {
case .success(object: let object):
if let object = object {
print(object)
}
case .failure(error: let error):
print(error)
}
}
AV.Cloud.define('customErrorCode', function (request) {
throw new AV.Cloud.Error('自定义错误信息。', { code: 123 });
});
客户端收到的响应:{ "code": 123, "error": "自定义错误信息。" }。
云函数超时
云函数超时时间为 15 秒,如果超过阈值,客户端将收到 HTTP status code 为 503 的响应,body 为 The request timed out on the server。
注意即使已经响应,此时云函数可能仍在执行,但执行完毕后的响应是无意义的(不会发给客户端,会在日志中打印一个 Can't set headers after they are sent 的异常)。
除了 503 错误外,有些情况下客户端也可能收到其他报错,如 524 或 141。
超时的处理方案
我们建议将代码中的任务转化为异步队列处理,以优化运行时间,避免云函数或定时任务发生超时。
例如:
在存储服务中创建一个队列表,包含 status 列;
接到任务后,向队列表保存一条记录,status 值设置为 处理中,然后将请求结束掉,将队列对象的 id 发给客户端。
当业务处理完毕,根据处理结果更新刚才的队列对象状态,将 status 字段设置为 完成 或者 失败;
在任何时候,在控制台通过队列 id 可以获取某个任务的执行结果,判断任务状态。
不过,对于 before 类 hook 函数,改为异步处理通常没有意义。
虽然改为异步后能保证 before 类函数能够运行完成,不会因超时而报错。
但是,只要 before 类 hook 函数不能及时抛出异常,就无法起到中断操作执行的作用。
对于超时的 before 类 hook 函数,如果无法优化代码,压缩执行时间的话,那只能改为 after 类函数。
例如,假设某个 beforeSave 函数需要调用耗时较久的第三方的自然语言处理接口判断用户提交的评论是否来自真实用户,导致超时,
那么可以改为 afterSave 函数,在保存评论后再调用第三方接口,如果判断是水军评论,那么再行删除。
对于 before 类的 Hook,如果返回了一个错误的话,这个操作就会被中断,因此你可以在这些 Hook 中主动抛出一个错误来拒绝掉某些操作。对于 after 类的 Hook,返回错误并不会影响操作的执行(因为其实操作已经执行完了)。
graph LR
A((save)) -->D{object}
D-->E(new)
E-->|beforeSave|H{error?}
H-->N(No)
N-->B[create new object on the cloud]
B -->|afterSave|C((done))
H-->Y(Yes)
Y-->Z((interrupted))
D-->F(existing)
F-->|beforeUpdate|I{error?}
I-->Y
I-->V(No)
V-->G[update existing object on the cloud]
G-->|afterUpdate|C
graph LR
A((delete))-->|beforeDelete|H{error?}
H-->Y(Yes)
Y-->Z((interrupted))
H-->N(No)
N-->B[delete object on the cloud]
B -->|afterDelete|C((done))
在删除一个对象之前做一些检查工作,比如在删除一个相册 Album 前,先检查一下该相册中还有没有照片 Photo:
AV.Cloud.beforeDelete('Album', function (request) {
// 查询 Photo 中还有没有属于这个相册的照片
var query = new AV.Query('Photo');
var album = AV.Object.createWithoutData('Album', request.object.id);
query.equalTo('album', album);
return query.count().then(function (count) {
if (count > 0) {
// delete 操作会被丢弃
throw new AV.Cloud.Error('Cannot delete an album if it still has photos in it.');
}
}, function (error) {
throw new AV.Cloud.Error('Error ' + error.code + ' occurred when finding photos: ' + error.message);
});
});
云函数开发指南 · Node.js
当你开发移动端应用时,可能会有下列需求:
这时,你可以使用云引擎的云函数。 云函数是一段部署在服务端的 JavaScript、Python、PHP、C#、Go 代码,可以很好地完成上述需求。
如果还不知道如何创建云引擎项目、本地调试并部署到云端,请阅读《云引擎快速入门》。
云函数
示例项目中文件定义了一个很简单的
hello
云函数。 在云端进行计算的一个重要理由是,你不需要将大量的数据发送到设备上做计算,而是将这些计算放到服务端,并返回结果这一点点信息就好。现在让我们看一个较复杂的例子来展示云函数的用途。
例如,你写了一个应用,让用户对电影评分,一个评分对象大概是这样:
stars
表示评分,1-5。如果你想查找《夏洛特烦恼》这部电影的平均分,你可以找出这部电影的所有评分,并在设备上根据这个查询结果计算平均分。但是这样一来,尽管你只是需要平均分这样一个数字,却不得不耗费大量的带宽来传输所有的评分。通过云引擎,我们可以简单地传入电影名称,然后返回电影的平均分。云函数接收 JSON 格式的请求对象,我们可以用它来传入电影名称。 各语言的 SDK 都在云引擎运行环境上有效,可以直接使用,所以我们可以使用它来查询所有的评分。 结合在一起,实现
averageStars
函数的代码如下:参数和返回值
Request
会作为参数传入到云函数中,Request
上的属性包括:params: object
:客户端发送的参数对象,当使用rpc
调用时,也可能是AV.Object
。currentUser?: AV.User
:客户端所关联的用户(根据客户端发送的X-LC-Session
头)。sessionToken?: string
:客户端发来的sessionToken
(X-LC-Session
头)。meta: object
:有关客户端的更多信息,目前只有一个remoteAddress
属性表示客户端的 IP。另外,
AV.Cloud.define
还接受一个可选参数options
(位置在函数名称和调用函数之间)。 这个options
对象上的属性包括:fetchUser: boolean
:是否自动抓取客户端的用户信息,默认为真。设置为假时,Request
将不会有currentUser
属性。internal: boolean
:是否只允许在云引擎内(使用AV.Cloud.run
且未开启remote
选项)或使用 Master Key (使用AV.Cloud.run
时传入useMasterKey
)调用,不允许客户端直接调用。默认为假。例如,假设我们不希望客户端直接调用上述函数,也不关心客户端用户信息,那么上述函数的定义可以改写为:
如果云函数返回了一个 Promise,那么云函数会使用 Promise 成功结束后的结果作为成功响应;如果 Promise 中发生了错误,云函数会使用这个错误作为错误响应,对于使用
AV.Cloud.Error
构造的异常对象,我们认为是客户端错误,不会在标准输出打印消息,对于其他异常则会在标准输出打印调用栈,以便排查错误。我们推荐大家使用链式的 Promise 写法来完成业务逻辑,这样会极大地方便异步任务的处理和异常处理,请注意一定要将 Promise 串联起来并在云函数中 return 以保证上述逻辑正确工作,推荐阅读 JavaScript Promise 迷你书 来深入地了解 Promise。
在 2.0 之前的早期版本中,云函数接受
request
和response
两个参数,我们会继续兼容这种用法到下一个大版本,希望开发者尽快迁移到 Promise 风格的云函数上。之前版本的文档见《Node SDK v1 API 文档》。SDK 调用云函数
LeanCloud 各个语言版本的 SDK 都提供了调用云函数的接口:
RPC 调用云函数
在这种调用方式下,云引擎会自动为 HTTP Response Body 做序列化, 而 SDK 调用之后拿回的返回结果就是一个完整的 LCObject 或包含这样的对象的数据结构:
云函数错误响应码
可以根据 HTTP status codes 自定义错误响应码。
客户端收到的响应:
{ "code": 123, "error": "自定义错误信息。" }
。云函数超时
云函数超时时间为 15 秒,如果超过阈值,客户端将收到 HTTP status code 为
503
的响应,body 为The request timed out on the server
。 注意即使已经响应,此时云函数可能仍在执行,但执行完毕后的响应是无意义的(不会发给客户端,会在日志中打印一个Can't set headers after they are sent
的异常)。 除了503
错误外,有些情况下客户端也可能收到其他报错,如524
或141
。超时的处理方案
我们建议将代码中的任务转化为异步队列处理,以优化运行时间,避免云函数或定时任务发生超时。
例如:
status
列;status
值设置为处理中
,然后将请求结束掉,将队列对象的id
发给客户端。status
字段设置为完成
或者失败
;id
可以获取某个任务的执行结果,判断任务状态。不过,对于 before 类 hook 函数,改为异步处理通常没有意义。 虽然改为异步后能保证 before 类函数能够运行完成,不会因超时而报错。 但是,只要 before 类 hook 函数不能及时抛出异常,就无法起到中断操作执行的作用。 对于超时的 before 类 hook 函数,如果无法优化代码,压缩执行时间的话,那只能改为 after 类函数。 例如,假设某个 beforeSave 函数需要调用耗时较久的第三方的自然语言处理接口判断用户提交的评论是否来自真实用户,导致超时, 那么可以改为 afterSave 函数,在保存评论后再调用第三方接口,如果判断是水军评论,那么再行删除。
Hook 函数
Hook 函数本质上是云函数,但它有固定的名称,定义之后会 由系统 在特定事件或操作(如数据保存前、保存后,数据更新前、更新后等等)发生时 自动触发,而不是由开发者来控制其触发时机。需要注意:
_Installation
表暂不支持 Hook 函数。对于
before
类的 Hook,如果返回了一个错误的话,这个操作就会被中断,因此你可以在这些 Hook 中主动抛出一个错误来拒绝掉某些操作。对于after
类的 Hook,返回错误并不会影响操作的执行(因为其实操作已经执行完了)。为了认证 Hook 调用者的身份,我们的 SDK 内部会确认请求确实是从云引擎内网的云存储组件发出的,如果认证失败,可能会出现
Hook key check failed
的提示,如果在本地调试时出现这样的错误,请确保是通过命令行工具启动调试的。BeforeSave
在创建新对象之前,可以对数据做一些清理或验证。例如,一条电影评论不能过长,否则界面上显示不开,需要将其截断至 140 个字符:
上面的代码示例中,
request.object
是被操作的AV.Object
。除了object
之外,request
上还有一个属性:currentUser?: AV.User
:发起操作的用户。类似地,其他 hook 的
request
参数上也包括object
和currentUser
这两个属性。AfterSave
在创建新对象后触发指定操作,比如当一条留言创建后再更新一下所属帖子的评论总数:
再如,在用户注册成功之后,给用户增加一个新的属性
from
并保存:虽然对于
after
类的 Hook 我们并不关心返回值,但我们仍建议你返回一个 Promise,这样如果发生了非预期的错误,会自动在标准输出中打印异常信息和调用栈。BeforeUpdate
在更新已存在的对象前执行操作,这时你可以知道哪些字段已被修改,还可以在特定情况下拒绝本次修改:
对传入对象直接进行的修改不会被保存。如需拒绝修改,可以让函数返回一个错误。
传入的对象是一个尚未保存到数据库的临时对象,并不保证与最终储存到数据库的对象完全相同,这是因为修改中可能包含自增、数组增改、关系增改等原子操作。
AfterUpdate
本 Hook 使用不当可能会造成死循环,导致数据存储 API 的调用次数暴涨,甚至产生更多的费用。因此请仔细阅读 防止死循环调用 部分,做出必要的调整和预防措施。
在更新已存在的对象后执行特定的动作。和 BeforeUpdate 一样,你可以知道哪些字段已被修改。
防止死循环调用
你也许会好奇为什么可以在 AfterUpdate 中保存
post
而不会再次触发该 hook。 这是因为云引擎对所有传入对象做了处理,以阻止死循环调用的产生。不过请注意,以下情况还需要开发者自行处理:
fetch
操作。对于使用上述方式产生的对象,请根据需要自行调用禁用 hook 的接口:
BeforeDelete
在删除一个对象之前做一些检查工作,比如在删除一个相册
Album
前,先检查一下该相册中还有没有照片Photo
:AfterDelete
在一个对象后被删执行操作,例如递减计数、删除关联对象等等。同样以相册为例,这次我们不在删除相册前检查是否还有照片,而是在删除后,同时删除相册中的照片:
OnVerified
当用户通过邮箱或者短信验证时,对该用户执行特定操作。比如:
上面的代码示例中的
object
换成currentUser
也可以。因为这里被操作的对象正好是发起操作的用户。 下面的onLogin
函数同理。数据库中相关的验证字段,如
emailVerified
不需要修改,系统会自动更新。该 hook 属于 after 类 hook.
OnLogin
在用户登录之时执行指定操作,比如禁止在黑名单上的用户登录:
该 hook 属于 before 类 hook.
OnAuthData
在云存储处理第三方登录的
authData
时触发,开发者可以在这个 Hook 中进行对authData
的校验或转换。比如:该 hook 属于 before 类 hook.
即时通讯 Hook 函数
参见《即时通讯概览第四篇》的《万能的 Hook 机制》章节。
Hook 函数错误响应码
为
BeforeSave
这类的 hook 函数定义错误码,需要这样:客户端收到的响应为
Cloud Code validation failed. Error detail: { "code": 123, "message": "An error occurred." }
。可通过 截取字符串 的方式取出错误信息,再转换成需要的对象。Hook 函数超时
Before 类 Hook 函数的超时时间为 10 秒,其他类 Hook 函数的超时时间为 3 秒。如果 Hook 函数被其他的云函数调用(比如因为保存对象而触发
BeforeSave
和AeforeSave
),那么它们的超时时间会进一步被其他云函数调用的剩余时间限制。例如,如果一个
BeforeSave
函数是被一个已经运行了 13 秒的云函数触发,那么它就只剩下 2 秒的时间来运行。同时请参考 云函数超时及处理方案。在线编写云函数
很多人使用云引擎是为了在服务端提供一些个性化的方法供各终端调用,而不希望关心诸如代码托管、npm 依赖管理等问题。为此我们提供了在线维护云函数的功能。使用此功能需要注意:
在 云服务控制台 > 云引擎 > 云引擎分组 > 部署 > 在线编辑 标签页,可以:
cloud.js
覆盖项目模板的同名文件,即可快速的转换为使用项目部署。云函数编辑之后需要点击 部署 才能生效。
在线编写的 SDK 版本
目前在线编辑仅支持 Node.js,提供了 4 种 SDK 版本:
从 v0 升级到 v1:
request.currentUser
获取用户,而不是AV.User.current
。AV.Cloud.run
时需要手动传递 user 对象。从 v1 升级到 v2:
AV.Cloud.httpRequest
。AV.Cloud.Error
来表示错误。从 v2 升级到 v3:
AV.Object.toJSON
的行为变化等)。查看和运行云函数
云服务控制台 > 云引擎 > 云函数页面以表格的形式展示了应用所属各分组上定义的云函数(包括 Hook)的基本信息,包括云函数名称、所属分组、QPM(每分钟请求数)。 在云函数表格中,点击运行按钮可以通过控制台调用云函数。 通过左上角的控制按钮则可以刷新页面、切换预备环境和生产环境。
定时任务
定时任务可以按照设定,以一定间隔自动完成指定动作,比如半夜清理过期数据,每周一向所有用户发送推送消息等等。定时任务的最小时间单位是 秒,正常情况下时间误差都可以控制在秒级别。
定时任务是普通的云函数,也会遇到 超时问题,具体请参考 超时处理方案。
一个定时任务如果在 24 小时内收到了超过 30 次的
400
(Bad Request)或502
(Bad Gateway)的应答,它将会被云引擎禁用,同时系统会向开发者发出相关的禁用通知邮件。在控制台的日志中,对应的错误信息为timerAction short-circuited and no fallback available
。部署云引擎之后,进入 云服务控制台 > 云引擎 > 定时任务,点击 创建定时任务,然后设定执行的函数名称、执行环境等等。例如定义一个打印循环打印日志的任务
logTimer
:定时任务分为两类:
以 Cron 表达式为例,比如每周一早上 8 点打印日志(运行之前定义的
logTimer
函数),创建定时任务的时候,选择 Cron 表达式 并填入0 0 8 ? * MON
。Cron 表达式的语法可以参考《云队列使用指南》的《Cron 表达式》一节。
点击「非必填」会展开更多选项:
定时任务创建后,会显示「最近一次执行时间」和「下次执行时间」。 「最近一次执行时间」除了显示时间外,还会显示执行结果,点击「查看详情」还可以查看细节:
status
任务的状态,包括success
(成功)、failed
(失败)uniqueId
任务的唯一 IDfinishedAt
执行完成的精确时间(仅限成功任务)statusCode
云函数响应的 HTTP 状态码(仅限成功任务)result
来自云函数的响应(仅限成功任务)error
错误提示(仅限失败任务)retryAt
下次重试时间(仅限失败任务)具体的执行日志可以在云服务控制台 > 云引擎 > 云引擎分组 > 日志 查看。 例如:
如果希望暂停定时任务(比如遇到报错需要排查),可以点击状态栏的「暂停」按钮。 相应地,点击「启用」按钮可以重新启用暂停的任务。 点击操作栏的「编辑」、「删除」按钮则可以修改、删除定时任务。
Master Key 和超级权限
因为云引擎运行在可信的服务器端环境中,所以你可以全局开启超级权限(Master Key),这样云端会跳过包括 ACL 和 Class 权限在内的检查,让你自由地操作所有云存储中的数据,当然这种方式也允许调用一些仅供 Master Key 使用的 API。开启 Master Key 的方法如下:
如果没有添加这些代码,默认是没有超级权限的,这意味着在云引擎中你也不能修改被 ACL 保护的数据,你需要在进行操作时手动指定
sessionToken
,让操作以这个用户的权限来执行:或者你也可单独对某一个操作使用 Master Key,跳过权限检查:
当然你也可以在启用了超级权限的情况下使用
useMasterKey: false
来对单个操作关掉超级权限。那么究竟是否应该使用超级权限呢,我们的建议如下:
sessionToken
传入数据修改的相关操作。关于云引擎上的权限问题,还可以参考《ACL 权限管理开发指南》和《在云引擎中使用 ACL》。