云函数和 Hook 开发指南
这篇文档专注在「云函数和 Hook」这种云引擎上的特殊的应用场景,如需部署通用的后端应用,或需要了解云引擎平台提供的更多功能,请看 云引擎平台功能。
云函数是云引擎提供的一种经过高度封装的函数计算功能,在我们的各个客户端 SDK 中也有对应的支持,可以自动地序列化 数据存储 服务中的各种数据类型。
适合使用云函数和 Hook 的场景包括:
- 将跨平台应用(同时有 Android、iOS、浏览器客户端)中复杂的计算逻辑集中到一处,而不必每个客户端单独实现一遍。
- 需要在服务器端对一些逻辑进行灵活调整,而不必更新客户端。
- 需要越过 ACL 或表权限的限制,对数据进行查询或修改。
- 需要使用 Hook 在数据存储中的对象被创建、更新、删除,或用户登录、认证时,触发自定义的逻辑、进行额外的权限检查。
- 需要运行定时任务,如每小时关闭未支付的订单、每天凌晨运行过期数据的清理任务等。
你可以使用云引擎支持的所有语言(运行环境)来编写云函数,包括 Node.js、Python、Java、PHP、.NET 和 Go。其中 Node.js 支持在控制台上在线编辑,其他语言需基于我们的示例项目部署到云引擎。
云函数
现在让我们看一个更复杂的例子,在一个应用中我们允许用户对电影进行评分,一个评分对象(Review
)大概是这样:
{
"movie": "夏洛特烦恼",
"stars": 5,
"comment": "夏洛一梦,笑成麻花"
}
stars
表示评分,是 1 至 5 的数字。通过云引擎,我们可以简单地传入电影名称,然后返回电影的平均分。
云函数接收 JSON 格式的请求对象,我们可以用它来传入电影名称。云函数中可以直接使用对应语言的数据存储 SDK,所以我们可以使用它来查询所有的评分。结合在一起,我们可以实现一个 averageStars
函数:
- Node.js
- Python
- PHP
- Java
- .NET (C#)
- Go
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;
});
});
AV.Cloud.define
还接受一个可选参数 options
(位置在函数名称和调用函数之间),这个 options
对象上的属性包括:
fetchUser?: boolean
:是否自动抓取客户端的用户信息,默认为true
。设置为假时,Request
将不会有currentUser
属性。internal?: boolean
:是否只允许在云引擎内(使用AV.Cloud.run
且未开启remote
选项)或使用Master Key
(使用AV.Cloud.run
时传入useMasterKey
)调用,不允许客户端直接调用。默认为false
。
例如,假设我们不希望客户端直接调用上述函数,也不关心客户端用户信息,那么上述函数的定义可以改写为:
AV.Cloud.define(
"averageStars",
{ fetchUser: false, internal: true },
function (request) {
// 内容同上
}
);
@engine.define
def averageStars(movie, **params):
reviews = leancloud.Query(Review).equal_to('movie', movie).find()
result = sum(x.get('stars') for x in reviews)
return result
客户端 SDK 调用时,云函数的名称默认为 Python 代码中函数的名称。有时需要设置云函数的名称与 Python 代码中的函数名称不相同,可以在 engine.define
后面指定云函数名称,比如:
@engine.define('averageStars')
def my_custom_average_start(movie, **params):
pass
use \LeanCloud\Engine\Cloud;
use \LeanCloud\Query;
use \LeanCloud\CloudException;
Cloud::define("averageStars", function($params, $user) {
$query = new Query("Review");
$query->equalTo("movie", $params["movie"]);
try {
$reviews = $query->find();
} catch (CloudException $ex) {
// 查询失败,将错误输出到日志
error_log($ex->getMessage());
return 0;
}
$sum = 0;
forEach($reviews as $review) {
$sum += $review->get("stars");
}
if (count($reviews) > 0) {
return $sum / count($reviews);
} else {
return 0;
}
});
@EngineFunction("averageStars")
public static float getAverageStars(@EngineFunctionParam("movie") String movie) throws LCException {
LCQuery<LCObject> query = new LCQuery("Review");
query.whereEqualTo("movie", movie);
List<LCObject> reviews = query.find();
int sum = 0;
if (reviews == null && reviews.isEmpty()) {
return 0;
}
for (LCObject review : reviews) {
sum += review.getInt("star");
}
return sum / reviews.size();
}
[LCEngineFunction("averageStars")]
public static float AverageStars([LCEngineFunctionParam("movie")] string movie) {
if (movie == "夏洛特烦恼") {
return 3.8f;
}
return 0;
}
type Review struct {
leancloud.Object
Movie string `json:"movie"`
Stars int `json:"stars"`
Comment string `json:"comment"`
}
leancloud.Engine.Define("averageStars", func(req *leancloud.FunctionRequest) (interface{}, error) {
reviews := make([]Review, 10) // 预留一小部分空间
if err := client.Class("Review").NewQuery().EqualTo("movie", req.Params["movie"].(string)).Find(&reviews); err != nil {
return nil, err
}
sum := 0
for _, v := range reviews {
sum += v.Stars
}
return sum / len(reviews), nil
})
参数和返回值
- Node.js
- Python
- PHP
- Java
- .NET (C#)
- Go
Request
会作为参数传入到云函数中,Request
上的属性包括:
params: object
:客户端发送的参数对象,当使用rpc
调用时,也可能是AV.Object
。currentUser?: AV.User
:客户端所关联的用户(根据客户端发送的X-LC-Session
头)。sessionToken?: string
:客户端发来的sessionToken
(X-LC-Session
头)。meta: object
:有关客户端的更多信息,目前只有一个remoteAddress
属性表示客户端的 IP。
如果云函数返回了一个 Promise,那么云函数会使用 Promise 成功结束后的结果作为成功响应;如果 Promise 中发生了错误,云函数会使用这个错误作为错误响应,对于使用 AV.Cloud.Error
构造的异常对象,我们认为是客户端错误,不会在标准输出打印消息,对于其他异常则会在标准输出打印调用栈,以便排查错误。
我们推荐大家使用链式的 Promise 写法来完成业务逻辑,这样会极大地方便异步任务的处理和异常处理,请注意一定要将 Promise 串联起来并在云函数中 return 以保证上述逻辑正确工作,推荐阅读 JavaScript Promise 迷你书 来深入地了解 Promise。
点击展开 Node.js SDK 早期版本详情
在 2.0 之前的 Node.js 中,云函数接受 request
和 response
两个参数,我们会继续兼容这种用法到下一个大版本,希望开发者尽快迁移到 Promise 风格的云函数上。之前版本的文档见 Node SDK v1 API 文档。
调用云函数时的参数会直接传递给云函数,因此直接声明这些参数即可。另外调用云函数时可能会根据不同情况传递不同的参数,这时如果定义云函数时没有声明这些参数,会触发 Python 异常,因此建议声明一个额外的关键字参数(关于关键字参数,请参考 此篇文档 中「关键字参数」一节)来保存多余的参数。
@engine.define
def my_cloud_func(foo, bar, baz, **params):
pass
除了调用云函数的参数之外,还可以通过 engine.current
对象,来获取到调用此云函数的客户端的其他信息。engine.current
对象上的属性包括:
engine.current.user: leancloud.User
:客户端所关联的用户(根据客户端发送的X-LC-Session
头)。engine.current.session_token: str
:客户端发来的sessionToken
(X-LC-Session
头)。engine.current.meta: dict
:有关客户端的更多信息,目前只有一个remote_address
属性表示客户端的 IP。
传递给云函数的参数依次为:
$params: array
:客户端发送的参数。$user: User
:客户端所关联的用户(根据客户端发送的X-LC-Session
头)。$meta: array
:有关客户端的更多信息,目前只有一个$meta['remoteAddress']
属性表示客户端的 IP。
云函数中可以获取的参数和上下文信息有:
@EngineFunctionParam
:客户端发送的参数。EngineRequestContext
:有关客户端的更多信息,其中EngineRequestContext.getSessionToken()
会返回客户端所关联用户的 sessionToken(根据客户端发送的X-LC-Session
头),EngineRequestContext.getRemoteAddress()
会返回客户端的实际地址。
云函数中可以获取的参数和上下文信息有:
LCEngineFunctionParam
:客户端发送的参数。LCEngineRequestContext
:有关客户端的更多信息,其中LCEngineRequestContext.SessionToken
会返回客户端所关联用户的 sessionToken(根据客户端发送的X-LC-Session
头),LCEngineRequestContext.RemoteAddress
会返回客户端的实际地址。
leancloud.FunctionRequest
会作为参数传入到云函数中,leancloud.FunctionRequest
上的属性包括:
Params
包含客户端发送的参数。CurrentUser
包含客户端所关联的用户(根据客户端发送的X-LC-Session
头)。可以在Define
定义云函数时,在最后传入可选参数WithoutFetchUser()
禁止获取当前调用的用户。SessionToken
包含客户端发来的sessionToken
(根据客户端发送的X-LC-Session
头)。可以在Define
定义云函数时,在最后传入可选参数WithoutFetchUser()
禁止获取当前调用的sessionToken
。Meta
包含有关客户端的更多信息,目前只有一个remoteAddress
属性表示客户端的 IP。
客户端 SDK 调用云函数
各个客户端 SDK 都提供了调用云函数的功能:
- Unity
- Android
- iOS
try {
Dictionary<string, object> response = await LCCloud.Run("averageStars", parameters: new Dictionary<string, object> {
{ "movie", "夏洛特烦恼" }
});
// 处理结果
} catch (LCException e) {
// 处理异常
}
// 构建传递给服务端的参数字典
Map<String, String> dicParameters = new HashMap<String, String>();
dicParameters.put("movie", "夏洛特烦恼");
// 调用指定名称的云函数 averageStars,并且传递参数
LCCloud.callFunctionInBackground("averageStars", dicParameters).subscribe(new Observer<Object>() {
@Override
public void onSubscribe(Disposable disposable) {
}
@Override
public void onNext(Object object) {
// succeed.
}
@Override
public void onError(Throwable throwable) {
// failed.
}
@Override
public void onComplete() {
}
});
Java SDK 还提供了一个支持缓存的 callFunctionWithCacheInBackground
,和 LCQuery
一样,开发者在请求的时候,可以指定 CachePolicy
以及缓存的最长期限,这样可以避免短时间一直直接请求云端服务器。
// 构建传递给服务端的参数字典
NSDictionary *dicParameters = [NSDictionary dictionaryWithObject:@"夏洛特烦恼"
forKey:@"movie"];
// 调用指定名称的云函数 averageStars,并且传递参数
[LCCloud callFunctionInBackground:@"averageStars"
withParameters:dicParameters
block:^(id object, NSError *error) {
if(error == nil){
// 处理结果
} else {
// 处理报错
}
}];
云函数调用(Run)默认将请求参数和响应结果作为 JSON 对象来处理,如果需要在请求或响应中传递 LCObject 对象,则可以使用 RPC 方式来调用云函数,SDK 将会完成 LCObject 类型的序列化和反序列化,在云函数和客户端代码中都可以直接获取到 LCObject 对象:
- Unity
- Android
- iOS
try {
LCObject response = await LCCloud.RPC("averageStars", parameters: new Dictionary<string, object> {
{ "movie", "夏洛特烦恼" }
});
// 处理结果
} catch (LCException e) {
// 处理异常
}
// 构建参数
Map<String, Object> dicParameters = new HashMap<>();
dicParameters.put("movie", "夏洛特烦恼");
LCCloud.<LCObject>callRPCInBackground("averageStars", dicParameters).subscribe(new Observer<LCObject>() {
@Override
public void onSubscribe(Disposable disposable) {
}
@Override
public void onNext(LCObject avObject) {
// succeed.
}
@Override
public void onError(Throwable throwable) {
// failed
}
@Override
public void onComplete() {
}
});
Java SDK 还提供了一个支持缓存的 callRPCWithCacheInBackground
,和 LCQuery
一样,开发者在请求的时候,可以指定 CachePolicy
以及缓存的最长期限,这样可以避免短时间一直直接请求云端服务器。
NSDictionary *dicParameters = [NSDictionary dictionaryWithObject:@"夏洛特烦恼"
forKey:@"movie"];
[LCCloud rpcFunctionInBackground:@"averageStars"
withParameters:parameters
block:^(id object, NSError *error) {
if(error == nil){
// 处理结果
}
else {
// 处理报错
}
}];
RPC 会处理以下形式的请求和响应:
- 单个 LCObject
- 包含 LCObject 的散列表(HashMap)
- 包含 LCObject 的数组(Array)
其他形式的数据 SDK 会保持原样,不进行处理。
云函数内部调用云函数
- Node.js
- Python
- PHP
- Java
- .NET (C#)
- Go
云引擎 Node.js 环境下,默认会直接进行一次本地的函数调用,而不会像客户端一样发起一个 HTTP 请求。
AV.Cloud.run("averageStars", {
movie: "夏洛特烦恼",
}).then(
function (data) {
// 调用成功,得到成功的应答 data
},
function (error) {
// 处理调用失败
}
);
如果你希望发起 HTTP 请求来调用云函数,可以传入一个 remote: true
的选项。当你在云引擎之外运行 Node.js SDK(包括调用位于其他分组上的云函数)时这个选项非常有用:
AV.Cloud.run("averageStars", { movie: "夏洛特烦恼" }, { remote: true }).then(
function (data) {
// 成功
},
function (error) {
// 处理调用失败
}
);
上面的 remote
选项实际上是作为 AV.Cloud.run
的可选参数 options 对象的属性传入的。这个 options
对象包括以下参数:
remote?: boolean
:上面的例子用到的remote
选项,默认为假。user?: AV.User
:以特定的用户运行云函数(建议在remote
为假时使用)。sessionToken?: string
:以特定的sessionToken
调用云函数(建议在remote
为真时使用)。req?: http.ClientRequest | express.Request
:为被调用的云函数提供remoteAddress
等属性。
云引擎 Python 环境下,默认会进行远程调用。 例如,以下代码会发起一次 HTTP 请求,去请求部署在云引擎上的云函数。
from leancloud import cloud
cloud.run('averageStars', movie='夏洛特烦恼')
如果想要直接调用本地(当前进程)中的云函数,或者发起调用就是在云引擎中,想要省去一次 HTTP 调用的开销,可以使用 leancloud.cloud.run.local
来取代 leanengine.cloud.run
,这样会直接在当前进程中执行一个函数调用,而不会发起 HTTP 请求来调用此云函数。
云引擎中默认会直接进行一次本地的函数调用,而不是像客户端一样发起一个 HTTP 请求。
try {
$result = Cloud::run("averageStars", array("movie" => "夏洛特烦恼"));
} catch (\Exception $ex) {
// 云函数错误
}
如果想要通过 HTTP 调用,可以使用 runRemote
方法:
try {
$token = User::getCurrentSessionToken(); // 以特定的 `sessionToken` 调用云函数,可选
$result = Cloud::runRemote("averageStars", array("movie" => "夏洛特烦恼"), $token);
} catch (\Exception $ex) {
// 云函数错误
}
Java SDK 不支持本地调用云函数。 如有代码复用需求,建议将公共逻辑提取成普通函数(Java 方法),在多个云函数中调用。
.NET SDK 不支持本地调用云函数。 如有代码复用需求,建议将公共逻辑提取成普通函数,在多个云函数中调用。
使用 Engine.Run
即是本地调用:
averageStars, err := leancloud.Engine.Run("averageStars", Review{Movie: "夏洛特烦恼"})
if err != nil {
panic(err)
}
如果希望发起 HTTP 请求来调用云函数,请使用 Client.Run
。
Run
的可选参数如下:
WithSessionToken(token)
为当前的调用请求传入sessionToken
WithUser(user)
为当前的调用请求传入对应的用户对象
云函数错误响应码
可以根据 HTTP status codes 自定义错误响应码。
- Node.js
- Python
- PHP
- Java
- .NET (C#)
- Go
AV.Cloud.define("customErrorCode", function (request) {
throw new AV.Cloud.Error("自定义错误信息。", { code: 123 });
});
from leancloud import LeanEngineError
@engine.define
def custom_error_code(**params):
raise LeanEngineError(123, '自定义错误信息。')
Cloud::define("customErrorCode", function($params, $user) {
throw new FunctionError("自定义错误信息。", 123);
});
@EngineFunction()
public static void customErrorCode() throws Exception {
throw new LCException(123, "自定义错误信息。");
}
[LCEngineFunction("throwLCException")]
public static void ThrowLCException() {
throw new LCException(123, "自定义错误信息。");
}
leancloud.Engine.Define("customErrorCode", func(req *leancloud.FunctionRequest) (interface{}, error) {
return nil, leancloud.CloudError{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 函数,在保存评论后再调用第三方接口,如果判断是水军评论,那么再进行删除。
数据存储 Hook
Hook 函数本质上是云函数,但它有固定的名称,定义之后会 由系统 在特定事件或操作(如数据保存前、保存后,数据更新前、更新后等等)发生时 自动触发,而不是由开发者来控制其触发时机。需要注意:
- 通过控制台进行数据导入时不会触发任何 hook 函数。
- 使用 Hook 函数需要 防止死循环调用。
_Installation
表暂不支持 Hook 函数。- Hook 函数只对当前应用的 Class 生效,对绑定后的目标 Class 无效。
对于 before
类的 Hook,如果返回了一个错误的话,这个操作就会被中断,因此你可以在这些 Hook 中主动抛出一个错误来拒绝掉某些操作。对于 after
类的 Hook,返回错误并不会影响操作的执行(因为其实操作已经执行完了)。
为了认证 Hook 调用者的身份,我们的 SDK 内部会确认请求确实是从云引擎内网的云存储组件发出的,如果认证失败,可能会出现 Hook key check failed
的提示,如果在本地调试时出现这样的错误,请确保是通过命令行工具启动调试的。
BeforeSave
在创建新对象之前,可以对数据做一些清理或验证。例如,一条电影评论不能过长,否则界面上显示不开,需要将其截断至 140 个字符:
- Node.js
- Python
- PHP
- Java
- .NET (C#)
- Go
AV.Cloud.beforeSave("Review", function (request) {
var comment = request.object.get("comment");
if (comment) {
if (comment.length > 140) {
// 截断并添加 '…'
request.object.set("comment", comment.substring(0, 140) + "…");
}
} else {
// 不保存数据,并返回错误
throw new AV.Cloud.Error("No comment provided!");
}
});
上面的代码示例中,request.object
是被操作的 AV.Object
。除了 object
之外,request
上还有一个属性:
currentUser?: AV.User
:发起操作的用户。
类似地,其他 hook 的 request
参数上也包括 object
和 currentUser
这两个属性。
@engine.before_save('Review') # Review 为需要 hook 的 class 的名称
def before_review_save(review):
comment = review.get('comment')
if not comment:
raise leancloud.LeanEngineError(message='No comment provided!')
if len(comment) > 140:
review.comment.set('comment', comment[:140] + '…')
Cloud::beforeSave("Review", function($review, $user) {
$comment = $review->get("comment");
if ($comment) {
if (strlen($comment) > 140) {
// 截断并添加 '…'
$review->set("comment", substr($comment, 0, 140) . "…");
}
} else {
// 不保存数据,并返回错误
throw new FunctionError("No comment provided!", 101);
}
});
@EngineHook(className = "Review", type = EngineHookType.beforeSave)
public static LCObject reviewBeforeSaveHook(LCObject review) throws Exception {
if (StringUtil.isEmpty(review.getString("comment"))) {
throw new Exception("No comment provided!");
} else if (review.getString("comment").length() > 140) {
review.put("comment", review.getString("comment").substring(0, 140) + "…");
}
return review;
}
[LCEngineClassHook("Review", LCEngineObjectHookType.BeforeSave)]
public static LCObject ReviewBeforeSave(LCObject review) {
if (string.IsNullOrEmpty(review["comment"])) {
throw new Exception("No comment provided!");
}
string comment = review["comment"] as string;
if (comment.Length > 140) {
review["comment"] = string.Format($"{comment.Substring(0, 140)}...");
}
return review;
}
leancloud.Engine.BeforeSave("Review", func(req *ClassHookRequest) (interface{}, error) {
review := new(Review)
if err := req.Object.Clone(review); err != nil {
return nil, err
}
if len(review.Comment) > 140 {
review.Comment = review.Comment[:140]
}
return review, nil
})
AfterSave
在创建新对象后触发指定操作,比如当一条留言创建后再更新一下所属帖子的评论总数:
- Node.js
- Python
- PHP
- Java
- .NET (C#)
- Go
AV.Cloud.afterSave("Comment", function (request) {
var query = new AV.Query("Post");
return query.get(request.object.get("post").id).then(function (post) {
post.increment("comments");
return post.save();
});
});
import leancloud
@engine.after_save('Comment') # Comment 为需要 hook 的 class 的名称
def after_comment_save(comment):
post = leancloud.Query('Post').get(comment.id)
post.increment('commentCount')
try:
post.save()
except leancloud.LeanCloudError:
raise leancloud.LeanEngineError(message='An error occurred while trying to save the post.')
Cloud::afterSave("Comment", function($comment, $user) {
$query = new Query("Post");
$post = $query->get($comment->get("post")->getObjectId());
$post->increment("commentCount");
try {
$post->save();
} catch (CloudException $ex) {
throw new FunctionError("An error occurred while trying to save the post: " . $ex->getMessage());
}
});
@EngineHook(className = "Review", type = EngineHookType.afterSave)
public static void reviewAfterSaveHook(LCObject review) throws Exception {
LCObject post = review.getLCObject("post");
post.fetch();
post.increment("comments");
post.save();
}
[LCEngineClassHook("Review", LCEngineObjectHookType.AfterSave)]
public static async Task ReviewAfterSave(LCObject review) {
LCObject post = review["post"] as LCObject;
await post.Fetch();
post.Increment("comments", 1);
await post.Save();
}
leancloud.Engine.AfterSave("Review", func(req *ClassHookRequest) error {
review := new(Review)
if err := req.Object.Clone(review); err != nil {
return err
}
if err := client.Object(review.Post).Update(map[string]interface{}{
"comment": leancloud.OpIncrement(1),
}); err != nil {
return leancloud.CloudError{Code: 500, Message: err.Error()}
}
return nil
})
再如,在用户注册成功之后,给用户增加一个新的属性 from
并保存:
- Node.js
- Python
- PHP
- Java
- .NET (C#)
- Go
AV.Cloud.afterSave("_User", function (request) {
console.log(request.object);
request.object.set("from", "LeanCloud");
return request.object.save().then(function (user) {
console.log("Success!");
});
});
虽然对于 after
类的 Hook 我们并不关心返回值,但我们仍建议你返回一个 Promise,这样如果发生了非预期的错误,会自动在标准输出中打印异常信息和调用栈。
@engine.after_save('_User')
def after_user_save(user):
print(user)
user.set('from', 'LeanCloud')
try:
user.save()
except LeanCloudError, e:
print('Error: ', e)
Cloud::afterSave("_User", function($userObj, $currentUser) {
$userObj->set("from", "LeanCloud");
try {
$userObj->save();
} catch (CloudException $ex) {
throw new FunctionError("An error occurred while trying to save the user: " . $ex->getMessage());
}
});
@EngineHook(className = "_User", type = EngineHookType.afterSave)
public static void userAfterSaveHook(LCUser user) throws Exception {
user.put("from", "LeanCloud");
user.save();
}
[LCEngineClassHook("_User", LCEngineObjectHookType.AfterSave)]
public static async Task UserAfterSave(LCObject user) {
user["from"] = "LeanCloud";
await user.Save();
}
leancloud.Engine.AfterSave("_User", func(req *ClassHookRequest) error{
if req.User != nil {
if err := client.User(req.User).Set("from", "LeanCloud"); err != nil {
return err
}
}
return nil
})
BeforeUpdate
在更新已存在的对象前执行操作,这时你可以知道哪些字段已被修改,还可以在特定情况下拒绝本次修改:
- Node.js
- Python
- PHP
- Java
- .NET (C#)
- Go
AV.Cloud.beforeUpdate("Review", function (request) {
// 如果 comment 字段被修改了,检查该字段的长度
if (request.object.updatedKeys.indexOf("comment") != -1) {
if (request.object.get("comment").length > 140) {
// 拒绝过长的修改
throw new AV.Cloud.Error("comment 长度不得超过 140 字符。");
}
}
});
@engine.before_update('Review')
def before_hook_object_update(obj):
# 如果 comment 字段被修改了,检查该字段的长度
if 'comment' in obj.updated_keys and len(obj.get('comment')) > 140:
# 拒绝过长的修改
raise leancloud.LeanEngineError(message='comment 长度不得超过 140 字符。')
Cloud::beforeUpdate("Review", function($review, $user) {
// 如果 comment 字段被修改了,检查该字段的长度
if (in_array("comment", $review->updatedKeys) &&
strlen($review->get("comment")) > 140) {
throw new FunctionError("comment 长度不得超过 140 字符。");
}
});
@EngineHook(className = "Review", type = EngineHookType.beforeUpdate)
public static LCObject reviewBeforeUpdateHook(LCObject review) throws Exception {
List<String> updateKeys = EngineRequestContext.getUpdateKeys();
for (String key : updateKeys) {
// 如果 comment 字段被修改了,检查该字段的长度
if ("comment".equals(key) && review.getString("comment").length()>140) {
throw new Exception("comment 长度不得超过 140 字符。");
}
}
return review;
}
[LCEngineClassHook("Review", LCEngineObjectHookType.BeforeUpdate)]
public static LCObject ReviewBeforeUpdate(LCObject review) {
ReadOnlyCollection<string> updatedKeys = review.GetUpdatedKeys();
if (updatedKeys.Contains("comment")) {
string comment = review["comment"] as string;
if (comment.Length > 140) {
throw new Exception("comment 长度不得超过 140 字符。");
}
}
return review;
}
leancloud.Engine.BeforeUpdate("Review", func(req *ClassHookRequest) (interface{}, error) {
updatedKeys = req.UpdatedKeys()
for _, v := range updatedKeys {
if v == "comment" {
comment, ok := req.Object.Raw()["comment"].(string)
if !ok {
return nil, leancloud.CloudError{Code: 400, Message: "Bad Request"}
}
if len(comment) > 140 {
return nil, leancloud.CloudError{Code: 400, Message: "Bad Request"}
}
}
}
return nil, nil
})
对传入对象直接进行的修改不会被保存。如需拒绝修改,可以让函数返回一个错误。
传入的对象是一个尚未保存到数据库的临时对象,并不保证与最终储存到数据库的对象完全相同,这是因为修改中可能包含自增、数组增改、关系增改等原子操作。
AfterUpdate
本 Hook 使用不当可能会造成死循环,导致数据存储 API 的调用次数暴涨,甚至产生更多的费用。因此请仔细阅读 防止死循环调用 部分,做出必要的调整和预防措施。
在更新已存在的对象后执行特定的动作。和 BeforeUpdate 一样,你可以知道哪些字段已被修改。
- Node.js
- Python
- PHP
- Java
- .NET (C#)
- Go
AV.Cloud.afterUpdate("Review", function (request) {
if (request.object.updatedKeys.indexOf("comment") != -1) {
if (request.object.get("comment").length < 5) {
console.log(review.ObjectId + " 看起来像灌水评论:" + comment);
}
}
});
@engine.after_update('Review')
def after_review_update(article):
if 'comment' in obj.updated_keys and len(obj.get('comment')) < 5:
print(review.ObjectId + " 看起来像灌水评论:" + comment)
Cloud::afterUpdate("Review", function($review, $user) {
if (in_array("comment", $review->updatedKeys) &&
strlen($review->get("comment")) < 5) {
error_log(review.ObjectId . " 看起来像灌水评论:" . comment);
}
});
@EngineHook(className = "Review", type = EngineHookType.afterUpdate)
public static void reviewAfterUpdateHook(LCObject review) throws Exception {
List<String> updateKeys = EngineRequestContext.getUpdateKeys();
for (String key : updateKeys) {
if ("comment".equals(key) && review.getString("comment").length()<5) {
LOGGER.d(review.ObjectId + " 看起来像灌水评论:" + comment);
}
}
}
[LCEngineClassHook("Review", LCEngineObjectHookType.AfterUpdate)]
public static void ReviewAfterUpdate(LCObject review) {
ReadOnlyCollection<string> updatedKeys = review.GetUpdatedKeys();
if (updatedKeys.Contains("comment")) {
string comment = review["comment"] as string;
if (comment.Length < 5) {
Console.WriteLine($"{review.ObjectId} 看起来像灌水评论:{comment}");
}
}
}
leancloud.Engine.AfterUpdate("Review", func(req *ClassHookRequest) error {
updatedKeys := req.UpdatedKeys()
for _, v := range updatedKeys {
if v == "comment" {
comment, ok := req.Object.Raw()["comment"].(string)
if !ok {
return nil, leancloud.CloudError{Code: 400, Message: "Bad Request"}
}
if len(comment) < 5 {
fmt.Println(req.Object.ID, " 看起来像灌水评论:", comment))
}
}
}
return nil
})
BeforeDelete
在删除一个对象之前做一些检查工作,比如在删除一个相册 Album
前,先检查一下该相册中还有没有照片 Photo
:
- Node.js
- Python
- PHP
- Java
- .NET (C#)
- Go
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
);
}
);
});
import leancloud
@engine.before_delete('Album') # Album 为需要 hook 的 class 的名称
def before_album_delete(album):
query = leancloud.Query('Photo')
query.equal_to('album', album)
try:
matched_count = query.count()
except leancloud.LeanCloudError:
raise engine.LeanEngineError(message='An error occurred with LeanEngine.')
if count > 0:
# delete 操作会被丢弃
raise engine.LeanEngineError(message='Cannot delete an album if it still has photos in it.')
Cloud::beforeDelete("Album", function($album, $user) {
$query = new Query("Photo");
$query->equalTo("album", $album);
try {
$count = $query->count();
} catch (CloudException $ex) {
throw new FunctionError("An error occurred when getting photo count: {$ex->getMessage()}");
}
if ($count > 0) {
// delete 操作会被丢弃
throw new FunctionError("Cannot delete an album if it still has photos in it.");
}
});
@EngineHook(className = "Album", type = EngineHookType.beforeDelete)
public static LCObject albumBeforeDeleteHook(LCObject album) throws Exception {
LCQuery query = new LCQuery("Photo");
query.whereEqualTo("album", album);
int count = query.count();
if (count > 0) {
// delete 操作会被丢弃
throw new Exception("Cannot delete an album if it still has photos in it.");
} else {
return album;
}
}
[LCEngineClassHook("Album", LCEngineObjectHookType.BeforeDelete)]
public static async Task<LCObject> AlbumBeforeDelete(LCObject album) {
LCQuery<LCObject> query = new LCQuery<LCObject>("Photo");
query.WhereEqualTo("album", album);
int count = await query.Count();
if (count > 0) {
throw new Exception("Cannot delete an album if it still has photos in it.");
}
return album;
}
leancloud.Engine.BeforeDelete("Album", func(req *ClassHookRequest) (interface{}, error) {
photo := new(Photo)
if err := req.Object.Clone(photo); err != nil {
return nil, err
}
count, err := client.Class("Photo").NewQuery().EqualTo("album", photo.Album).Count()
if err != nil {
return nil, err
}
if count > 0 {
return nil, leancloud.CloudError{Code: 500, Message: "Cannot delete an album if it still has photos in it."}
}
fmt.Println("Deleted.")
return nil, nil
})
AfterDelete
在一个对象被删除后执行操作,例如递减计数、删除关联对象等等。同样以相册为例,这次我们不在删除相册前检查是否还有照片,而是在删除后,同时删除相册中的照片:
- Node.js
- Python
- PHP
- Java
- .NET (C#)
- Go
AV.Cloud.afterDelete("Album", function (request) {
var query = new AV.Query("Photo");
var album = AV.Object.createWithoutData("Album", request.object.id);
query.equalTo("album", album);
return query
.find()
.then(function (posts) {
return AV.Object.destroyAll(posts);
})
.catch(function (error) {
console.error(
"Error " +
error.code +
" occurred when finding photos: " +
error.message
);
});
});
import leancloud
@engine.after_delete('Album') # Album 为需要 hook 的 class 的名称
def after_album_delete(album):
query = leancloud.Query('Photo')
query.equal_to('album', album)
try:
query.destroy_all()
except leancloud.LeanCloudError:
raise leancloud.LeanEngineError(message='An error occurred with LeanEngine.')
Cloud::afterDelete("Album", function($album, $user) {
$query = new Query("Photo");
$query->equalTo("album", $album);
try {
$photos = $query->find();
LeanObject::destroyAll($photos);
} catch (CloudException $ex) {
throw new FunctionError("An error occurred when getting photo count: {$ex->getMessage()}");
}
});
@EngineHook(className = "Album", type = EngineHookType.afterDelete)
public static void albumAfterDeleteHook(LCObject album) throws Exception {
LCQuery query = new LCQuery("Photo");
query.whereEqualTo("album", album);
List<LCObject> result = query.find();
if (result != null && !result.isEmpty()) {
LCObject.deleteAll(result);
}
}
[LCEngineClassHook("Album", LCEngineObjectHookType.AfterDelete)]
public static async Task AlbumAfterDelete(LCObject album) {
LCQuery<LCObject> query = new LCQuery<LCObject>("Photo");
query.WhereEqualTo("album", album);
ReadOnlyCollection<LCObject> result = await query.Find();
if (result != null && result.Count > 0) {
await LCObject.DeleteAll(result);
}
}
leancloud.Engine.AfterDelete("Album", func(req *ClassHookRequest) error {
photo := new(Photo)
if err := req.Object.Clone(photo); err != nil {
return nil, err
}
count, err := client.Class("Photo").NewQuery().EqualTo("album", photo.Album).Count()
if err != nil {
return nil, err
}
if count > 0 {
return nil, leancloud.CloudError{Code: 500, Message: "An error occurred with LeanEngine."}
}
fmt.Println("Deleted.")
return nil, nil
})
OnVerified
当用户通过邮箱或者短信验证时,对该用户执行特定操作。比如:
- Node.js
- Python
- PHP
- Java
- .NET (C#)
- Go
AV.Cloud.onVerified("sms", function (request) {
console.log("User " + request.object + " is verified by SMS.");
});
上面的代码示例中的 object
换成 currentUser
也可以。因为这里被操作的对象正好是发起操作的用户。
下面的 onLogin
函数同理。
@engine.on_verified('sms')
def on_sms_verified(user):
print(user)
Cloud::onVerifed("sms", function($user, $meta) {
error_log("User {$user->getUsername()} is verified by SMS.");
});
@EngineHook(className = "_User", type = EngineHookType.onVerifiedSMS)
public static void userOnVerifiedHook(LCUser user) throws Exception {
LOGGER.d("用户 " + user.getObjectId() + " 已通过短信验证。");
}
@EngineHook(className = "_User", type = EngineHookType.onVerifiedEmail)
public static void userOnVerifiedHook(LCUser user) throws Exception {
LOGGER.d("用户 " + user.getObjectId() + " 已通过邮箱验证。");
}
[LCEngineUserHook(LCEngineUserHookType.OnSMSVerified)]
public static void OnVerifiedSMS(LCUser user) {
Console.WriteLine($"用户 {user.ObjectId} 已通过短信验证。");
}
[LCEngineUserHook(LCEngineUserHookType.OnEmailVerified)]
public static void OnVerifiedEmail(LCUser user) {
Console.WriteLine($"用户 {user.ObjectId} 已通过邮箱验证。");
}
leancloud.Engine.OnVerified("sms", func(req *ClassHookRequest) error {
fmt.Println("用户 ", req.User.ID, " 已通过短信验证。")
})
leancloud.Engine.OnVerified("email", func(req *ClassHookRequest) error {
fmt.Println("用户 ", req.User.ID, " 已通过邮箱验证。")
})
数据库中相关的验证字段,如 emailVerified
不需要修改,系统会自动更新。
该 hook 属于 after 类 hook。
OnLogin
在用户登录之时执行指定操作,比如禁止在黑名单上的用户登录:
- Node.js
- Python
- PHP
- Java
- .NET (C#)
- Go
AV.Cloud.onLogin(function (request) {
// 因为此时用户还没有登录,所以用户信息是保存在 request.object 对象中
console.log("User " + request.object + " is trying to log in.");
if (request.object.get("username") === "noLogin") {
// 如果是 error 回调,则用户无法登录(收到 401 响应)
throw new AV.Cloud.Error("Forbidden");
}
});
@engine.on_login
def on_login(user):
print(user)
if user.get('username') == 'noLogin':
# 如果抛出 LeanEngineError,则用户无法登录(收到 401 响应)
raise LeanEngineError('Forbidden')
# 没有抛出异常,函数正常执行完毕的话,用户可以登录
Cloud::onLogin(function($user) {
error_log("User {$user->getUsername()} is trying to log in.");
if ($user->get("blocked")) {
// 如果抛出异常,则用户无法登录(收到 401 响应)
throw new FunctionError("Forbidden");
}
// 没有抛出异常,函数正常执行完毕的话,用户可以登录
});
@EngineHook(className = "_User", type = EngineHookType.onLogin)
public static LCUser userOnLoginHook(LCUser user) throws Exception {
if ("noLogin".equals(user.getUsername())) {
throw new Exception("Forbidden");
} else {
return user;
}
}
[LCEngineUserHook(LCEngineUserHookType.OnLogin)]
public static LCUser OnLogin(LCUser user) {
if (user.Username == "noLogin") {
throw new Exception("Forbidden");
}
return user;
}
leancloud.Engine.OnLogin(func(req *ClassHookRequest) error {
fmt.Println("用户 ", req.User.ID, " 已登录。")
})
该 hook 属于 before 类 hook。
OnAuthData
在云存储处理第三方登录的 authData
时触发,开发者可以在这个 Hook 中进行对 authData
的校验或转换。比如:
- Node.js
- Python
- .NET (C#)
AV.Cloud.onAuthData(function (request) {
let authData = request.authData;
console.log(authData);
if (authData.weixin.code === "12345") {
authData.weixin.accessToken = "45678";
} else {
// 校验失败,抛出异常,则用户无法登录
throw new AV.Cloud.Error("invalid code");
}
// 校验成功,返回校验或转换之后的 authData,用户继续登录流程
return authData;
});
@engine.on_auth_data
def on_auth_data(auth_data):
if auth_data['weixin']['code'] == '12345':
# 校验成功,返回校验或转换之后的 auth_data,用户继续登录流程
auth_data['weixin']['code'] = '45678'
return auth_data
else:
# 校验失败,抛出异常,则用户无法登录
raise LeanEngineError('invalid code')
[LCEngineUserHook(LCEngineUserHookType.OnAuthData)]
public static Dictionary<string, object> OnAuthData(Dictionary<string, object> authData) {
if (authData.TryGetValue("fake_platform", out object tokenObj)) {
if (tokenObj is Dictionary<string, object> token) {
// 模拟校验
if (token["openid"] as string == "123" && token["access_token"] as string == "haha") {
LCLogger.Debug("Auth data Verified OK.");
} else {
throw new Exception("Invalid auth data.");
}
} else {
throw new Exception("Invalid auth data");
}
}
return authData;
该 hook 属于 before 类 hook。
防止死循环调用
你也许会好奇为什么可以在 AfterUpdate 中保存 post
而不会再次触发该 hook。
这是因为云引擎对所有传入对象做了处理,以阻止死循环调用的产生。
不过请注意,以下情况还需要开发者自行处理:
- 对传入对象进行
fetch
操作。 - 重新构造传入的对象。
对于使用上述方式产生的对象,请根据需要自行调用禁用 hook 的接口:
- Node.js
- Python
- PHP
- Java
- .NET (C#)
// 直接修改并保存对象不会再次触发 afterUpdate Hook 函数
request.object.set("foo", "bar");
request.object.save().then(function (obj) {
// 你的业务逻辑
});
// 如果有 fetch 操作,则需要在新获得的对象上调用 disableAfterHook 来确保不会再次触发 Hook 函数
request.object
.fetch()
.then(function (obj) {
obj.disableAfterHook();
obj.set("foo", "bar");
return obj.save();
})
.then(function (obj) {
// 你的业务逻辑
});
// 如果是其他方式构建对象,则需要在新构建的对象上调用 disableAfterHook 来确保不会再次触发 Hook 函数
var obj = AV.Object.createWithoutData("Post", request.object.id);
obj.disableAfterHook();
obj.set("foo", "bar");
obj.save().then(function (obj) {
// 你的业务逻辑
});
@engine.after_update('Post')
def after_post_update(post):
# 直接修改并保存对象不会再次触发 after_update Hook 函数
post.set('foo', 'bar')
post.save()
# 如果有 fetch 操作,则需要在新获得的对象上调用 disable_after_hook 来确保不会再次触发 Hook 函数
post.fetch()
post.disable_after_hook()
post.set('foo', 'bar')
# 如果是其他方式构建对象,则需要在新构建的对象上调用 disable_after_hook 来确保不会再次触发 Hook 函数
post = leancloud.Object.extend('Post').create_without_data(post.id)
post.disable_after_hook()
post.save()
Cloud::afterUpdate("Post", function($post, $user) {
// 直接修改并保存对象不会再次触发 afterUpdate Hook 函数
$post->set('foo', 'bar');
$post->save();
// 如果有 fetch 操作,则需要在新获得的对象上调用 disableAfterHook 来确保不会再次触发 Hook 函数
$post->fetch();
$post->disableAfterHook();
$post->set('foo', 'bar');
$post->save();
// 如果是其他方式构建对象,则需要在新构建的对象上调用 disableAfterHook 来确保不会再次触发 Hook 函数
$post = LeanObject::create("Post", $post->getObjectId());
$post->disableAfterHook();
$post->save();
});
@EngineHook(className="Post", type = EngineHookType.afterUpdate)
public static void afterUpdatePost(LCObject post) throws LCException {
// 直接修改并保存对象不会再次触发 afterUpdate Hook 函数
post.put("foo", "bar");
post.save();
// 如果有 fetch 操作,则需要在新获得的对象上调用 disableAfterHook 来确保不会再次触发 Hook 函数
post.fetch();
post.disableAfterHook();
post.put("foo", "bar");
// 如果是其他方式构建对象,则需要在新构建的对象上调用 disableAfterHook 来确保不会再次触发 Hook 函数
post = LCObject.createWithoutData("Post", post.getObjectId());
post.disableAfterHook();
post.save();
}
// 直接修改并保存对象不会再次触发 afterUpdate Hook 函数
post["foo"] = "bar";
await post.Save();
// 如果有 fetch 操作,则需要在新获得的对象上调用 DisableAfterHook 来确保不会再次触发 Hook 函数
await post.Fetch();
post.DisableAfterHook();
post["foo"] = "bar";
// 如果是其他方式构建对象,则需要在新构建的对象上调用 DisableAfterHook 来确保不会再次触发 Hook 函数
post = LCObject.CreateWithoutData("Post", post.ObjectId);
post.DisableAfterHook();
await post.Save();
Hook 错误响应码
为 BeforeSave
这类的 hook 函数定义错误码,需要这样:
- Node.js
- Python
- PHP
- Java
- .NET (C#)
- Go
AV.Cloud.beforeSave("Review", function (request) {
// 使用 JSON.stringify() 将 object 变为字符串
throw new AV.Cloud.Error(
JSON.stringify({
code: 123,
message: "An error occurred.",
})
);
});
@engine.before_save('Review') # Review 为需要 hook 的 class 的名称
def before_review_save(review):
comment = review.get('comment')
if not comment:
raise leancloud.LeanEngineError(
code=123,
message='An error occurred.'
)
Cloud::beforeSave("Review", function($review, $user) {
$comment = $review->get("comment");
if (!$comment) {
throw new FunctionError(json_encode(array(
"code" => 123,
"message" => "An error occurred.",
)));
}
});
@EngineHook(className = "Review", type = EngineHookType.beforeSave)
public static LCObject reviewBeforeSaveHook(LCObject review) throws Exception {
throw new LCException(123, "An error occurred.");
}
[LCEngineClassHook("Review", LCEngineObjectHookType.BeforeDelete)]
public static void ReviewBeforeDelete(LCObject review) {
throw new LCException(123, "An error occurred.");
}
leancloud.Engine.BeforeSave("Review", func(req *ClassHookRequest) (interface{}, error) {
return nil, leancloud.CloudError{Code: 123, Message: "An error occurred."}
})
客户端收到的响应为 Cloud Code validation failed. Error detail: { "code": 123, "message": "An error occurred." }
。可通过 截取字符串 的方式取出错误信息,再转换成需要的对象。
Hook 超时
Before 类 Hook 函数的超时时间为 10 秒,其他类 Hook 函数的超时时间为 3 秒。如果 Hook 函数被其他的云函数调用(比如因为保存对象而触发 BeforeSave
和 AfterSave
),那么它们的超时时间会进一步被其他云函数调用的剩余时间限制。
例如,如果一个 BeforeSave
函数是被一个已经运行了 13 秒的云函数触发,那么它就只剩下 2 秒的时间来运行。同时请参考 云函数超时及处理方案。
即时通讯 Hook
参见即时通讯指南中万能的 Hook 机制章节。
在线编写云函数
很多人使用云引擎是为了在服务端提供一些个性化的方法供各终端调用,而不希望关心诸如代码托管、npm 依赖管理等问题。为此我们提供了在线维护云函数的功能。使用此功能需要注意:
- 在线定义的函数会覆盖你之前用 Git 或命令行部署的项目。
- 目前只能在线编写云函数和 Hook,不支持托管静态网页、编写动态路由。
- 只能使用 JavaScript SDK 和一些内置的 Node.js 模块(详见下节表格),无法引入其他模块作为依赖。
功能地址
在 开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 云引擎 > 管理部署 > 云引擎分组 > 部署 > 在线编辑 页,可以:
- 创建函数:指定函数类型、函数名称、函数体的具体代码、注释等信息,点击「创建」即可创建一个云函数。函数类型包括 Function(普通云函数)、Hook、Global(这里可以定义多个云函数的公共逻辑)。
- 部署:选择要部署的环境,点击「部署」即可看到部署过程和结果。
- 预览:会将所有函数汇总并生成一个完整的代码段,可以确认代码,或者将其保存为
cloud.js
覆盖项目模板的同名文件,即可快速地转换为使用项目部署。 - 维护云函数:可以编辑已有云函数,查看保存历史,以及删除云函数。
云函数编辑之后需要点击 部署 才能生效。
目前在线编辑仅支持 Node.js,最新的 v3
版本使用 Node.js 8.x 和 3.x 的 Node.js SDK,使用 Promise 写法,默认提供的依赖包有:async, bluebird, crypto, debug, ejs, jade, lodash, moment, nodemailer, qiniu, redis, request, request-promise, superagent, underscore, uuid, wechat-api, xml2js。
点击展开在线编辑 SDK 版本详情
在线编辑版本 | Node.js SDK | JS SDK | Node.js | 备注 | 可用依赖 |
---|---|---|---|---|---|
v0 | 0.x | 0.x | 0.12 | 已不推荐使用 | moment, request, underscore |
v1 | 1.x | 1.x | 4 | async, bluebird, co, ejs, handlebars, joi, lodash, marked, moment, q, request, superagent, underscore | |
v2 | 2.x | 2.x | 6 | 需要使用 Promise 写法 | async, bluebird, crypto, debug, ejs, jade, lodash, moment, nodemailer, qiniu, redis, request, request-promise, superagent, underscore, uuid, wechat-api, xml2js |
v3 | 3.x | 3.x | 8 | 需要使用 Promise 写法 | async, bluebird, crypto, debug, ejs, jade, lodash, moment, nodemailer, qiniu, redis, request, request-promise, superagent, underscore, uuid, wechat-api, xml2js |
从 v0 升级到 v1:
- JS SDK 升级到了 1.0。
- 需要从
request.currentUser
获取用户,而不是AV.User.current
。 - 在调用
AV.Cloud.run
时需要手动传递 user 对象。
从 v1 升级到 v2:
- JS SDK 升级到 2.0(必须使用 Promise,不再支持 callback 风格)。
- 删除了
AV.Cloud.httpRequest
。 - 在云函数中 必须 返回 Promise 作为云函数的值,抛出
AV.Cloud.Error
来表示错误。
从 v2 升级到 v3:
- JS SDK 升级到了 3.0(
AV.Object.toJSON
的行为变化等)。
点击展开在线编辑和项目部署的关系
「在线编辑」的产生是为了方便大家初次体验云引擎,或者只是需要一些简单 hook 方法的应用使用。我们的实现方式就是把定义的函数拼接起来,生成一个云引擎项目然后部署。
所以可以认为「在线编辑」和「项目部署」最终是一样的,都是一个完整的项目。
定义函数是一个单独功能,可以让你不用使用基础包、git 等工具就能快速生成和编辑云引擎项目。
当然,你也可以使用基础包,自己写代码并部署项目。
这两条路是分开的,使用任何一种方式部署都会导致另一种方式失效。
点击展开如何从在线编辑迁移到项目部署
- 按照云引擎命令行工具使用指南安装命令行工具,使用
lean new
初始化项目,模板选择Node.js > Express
(我们的 Node.js 示例项目)。 - 在 开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 云引擎 > 管理部署 > 云引擎分组 > 部署 > 在线编辑 点击 预览,将全部函数的代码拷贝到新建项目中的
cloud.js
(替换掉原有内容)。 - 运行
lean up
,在 http://localhost:3001 的调试界面中测试云函数和 Hook,然后运行lean deploy
部署代码到云引擎(使用标准实例的用户还需要执行lean publish
)。 - 部署后请留意云引擎控制台上是否有错误产生。
如果在线编辑使用的是 0.x 版本的 Node.js SDK,那么还需要修改不兼容的代码。
比如将 AV.User.current()
改为 request.currentUser
。
详见 升级到云引擎 Node.js SDK 1.0。
查看和运行云函数
开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 云引擎 > 管理部署 > 云引擎分组 > 部署 页面以表格的形式展示了应用各分组上定义的云函数(包括 Hook)的信息,包括云函数名称、所属分组、QPM(每分钟请求数)。在云函数表格中,点击 运行 按钮可以通过控制台调用云函数。
这里显示的是应用下所有分组中的云函数,包括在线编辑也包括项目部署。
生产环境和预备环境
云引擎应用有「生产环境」和「预备环境」之分。在云引擎通过 SDK 调用云函数时,包括显式调用以及隐式调用(由于触发 hook 条件导致 hook 函数被调用),SDK 会根据云引擎所属环境(预备、生产)调用相应环境的云函数。例如,假定定义了 beforeDelete
云函数,在预备环境通过 SDK 删除一个对象,会触发预备环境的 beforeDelete
hook 函数。
在云引擎以外的环境通过 SDK 显式或隐式调用云函数时,X-LC-Prod
的默认值一般为 1
,也就是调用生产环境。但由于历史原因,各 SDK 的具体行为有一些差异:
- 在 Node.js、PHP、Java、C# 这四个 SDK 下,默认总是调用生产环境的云函数。
- 在 Python SDK 下,配合 lean-cli 本地调试时,且应用存在预备环境时,默认调用预备环境的云函数,其他情况默认调用生产环境的云函数。
- 云引擎 Java 环境的模板项目 java-war-getting-started 和 spring-boot-getting-started 做了处理,配合 lean-cli 本地调试时,且应用存在预备环境时,默认调用预备环境的云函数,其他情况默认调用生产环境的云函数(与 Python SDK 的行为一致)。
你还可以在 SDK 中指定客户端将请求所发往的环境:
- Unity
- Android
- iOS
LCCloud.IsProduction = true; // production (default)
LCCloud.IsProduction = false; // staging
LCCloud.setProductionMode(true); // production
LCCloud.setProductionMode(false); // staging
[LCCloud setProductionMode:YES]; // production (default)
[LCCloud setProductionMode:NO]; // staging
体验版云引擎应用只有「生产环境」,因此请不要切换到预备环境。
定时任务
定时任务可以按照设定,以一定间隔自动完成指定动作,比如半夜清理过期数据,每周一向所有用户发送推送消息等等。定时任务的最小时间单位是 秒,正常情况下时间误差都可以控制在秒级别。
定时任务是普通的云函数,也会遇到 超时问题,具体请参考 超时处理方案。
一个定时任务如果在 24 小时内收到了超过 30 次的 400
(Bad Request)或 502
(Bad Gateway)的应答,它将会被云引擎禁用,同时系统会向开发者发出相关的禁用通知邮件。在控制台的日志中,对应的错误信息为 timerAction short-circuited and no fallback available
。
部署云引擎之后,进入 开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 云引擎 > 管理部署 > 云引擎分组 > 定时任务,点击 创建定时任务,然后设定执行的函数名称、执行环境等等。例如定义一个打印循环打印日志的任务 logTimer
:
- Node.js
- Python
- PHP
- Java
- .NET (C#)
- Go
AV.Cloud.define("logTimer", function (request) {
console.log("This log is printed by logTimer.");
});
@engine.define
def logTimer(movie, **params):
print('This log is printed by logTimer.')
Cloud::define("logTimer", function($params, $user) {
error_log("This log is printed by logTimer.");
});
@EngineFunction("logTimer")
public static float logTimer throws Exception {
LogUtil.avlog.d("This log is printed by logTimer.");
}
[LCEngineFunction("logTimer")]
public static void LogTimer() {
Console.WriteLine("This log is printed by logTimer.");
}
leancloud.Engine.Define("logTimer", func(req *FunctionRequest) (interface{}, error) {
fmt.Println("This log is printed by logTimer.")
return nil, nil
})
云引擎支持两种定时任务:
- 使用 Cron 表达式安排调度
- 以分钟为单位的简单循环调度
以 Cron 表达式为例,比如每周一早上 8 点打印日志(运行之前定义的 logTimer
函数),创建定时任务的时候,选择 Cron 表达式 并填入 0 0 8 ? * MON
。
Cron 表达式的语法可以参考 云队列(Cloud Queue)开发指南 § CRON 表达式。
在配置定时任务时可以指定一些额外的非必填选项:
- 运行参数:传递给云函数的参数(JSON 对象)。
- 异常策略:任务因云函数超时失败后重试执行还是放弃执行,详见 云队列指南 § 异常处理策略。
「最近一次执行」会显示最近一次执行的时间和详情,但目前这个数据仅保留 5 分钟,在查看详情中:
status
任务的状态,包括success
(成功)、failed
(失败)uniqueId
任务的唯一 IDfinishedAt
执行完成的精确时间(仅限成功任务)statusCode
云函数响应的 HTTP 状态码(仅限成功任务)result
来自云函数的响应(仅限成功任务)error
错误提示(仅限失败任务)retryAt
下次重试时间(仅限失败任务)
定时任务的结果(执行日志)可以在 开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 云引擎 > 管理部署 > 云引擎分组 > 日志 中查看。