云函数和 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)为当前的调用请求传入sessionTokenWithUser(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。