🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
[TOC] 云函数是云引擎(LeanEngine)的一个子模块,请确保阅读本文档之前,你已经阅读了 [云引擎服务概览](https://leancloud.cn/docs/leanengine_overview.html)。 当你开发移动端应用时,可能会有下列需求: * 应用在多平台客户端(Android、iOS、Windows Phone、浏览器等)中很多逻辑都是一样的,希望将这部分逻辑抽取出来只维护一份。 * 有些逻辑希望能够较灵活的调整(比如某些个性化列表的排序规则),但又不希望频繁的更新和发布移动客户端。 * 有些逻辑需要的数据量很大,或者运算成本高(比如某些统计汇总需求),不希望在移动客户端进行运算,因为这样会消耗大量的网络流量和手机运算能力。 * 当应用执行特定操作时,由云端系统自动触发一段逻辑(称为 [Hook 函数](#Hook_函数)),比如:用户注册后对该用户增加一些信息记录用于统计;或某业务数据发生变化后希望做一些别的业务操作。这些代码不适合放在移动客户端(比如因为上面提到的几个原因)。 * 需要定时任务,比如每天凌晨清理垃圾注册账号等。 这时,你可以使用云引擎的云函数。云函数是一段部署在服务端的代码,编写 JavaScript 或者 Python 代码,并部署到我们的平台上,可以很好的完成上述需求。 如果还不知道如何创建云引擎项目,本地调试并部署到云端,请阅读 [云引擎快速入门](https://leancloud.cn/docs/leanengine_quickstart.html)。 ## [多语言支持](#多语言支持) 云引擎支持多种语言的运行环境,你可以选择自己熟悉的语言开发应用: * [Node.js]() * [Python](https://leancloud.cn/docs/leanengine_cloudfunction_guide-python.html) * [PHP](https://leancloud.cn/docs/leanengine_cloudfunction_guide-php.html) * [Java](https://leancloud.cn/docs/leanengine_cloudfunction_guide-java.html) ## [云函数](#云函数) 示例项目中 `$PROJECT_DIR/cloud.js` 文件定义了一个很简单的 `hello` 云函数。现在让我们看一个明显较复杂的例子来展示云引擎的用途。在云端进行计算的一个重要理由是,你不需要将大量的数据发送到设备上做计算,而是将这些计算放到服务端,并返回结果这一点点信息就好。 例如,你写了一个应用,让用户对电影评分,一个评分对象大概是这样: ~~~ { "movie": "夏洛特烦恼", "stars": 5, "comment": "夏洛一梦,笑成麻花" } ~~~ `stars` 表示评分,1-5。如果你想查找《夏洛特烦恼》这部电影的平均分,你可以找出这部电影的所有评分,并在设备上根据这个查询结果计算平均分。但是这样一来,尽管你只是需要平均分这样一个数字,却不得不耗费大量的带宽来传输所有的评分。通过云引擎,我们可以简单地传入电影名称,然后返回电影的平均分。 Cloud 函数接收 JSON 格式的请求对象,我们可以用它来传入电影名称。整个 [JavaScript SDK](https://leancloud.cn/docs/leanstorage_guide-js.html) 都在云引擎运行环境上有效,可以直接使用,所以我们可以使用它来查询所有的评分。结合在一起,实现 `averageStars` 函数的代码如下: ~~~ AV.Cloud.define('averageStars', function(request, response) { var query = new AV.Query('Review'); query.equalTo('movie', request.params.movie); query.find().then(function(results) { var sum = 0; for (var i = 0; i < results.length; i++ ) { sum += results[i].get('stars'); } response.success(sum / results.length); }).catch(function(error) { response.error('查询失败'); }); }); ~~~ ### [参数信息](#参数信息) Request 和 Response 会作为两个参数传入到云函数中: `Request` 上的属性包括: * `params: object`:客户端发送的参数对象,当使用 `rpc` 调用时,也可能是 `AV.Object`。 * `currentUser?: AV.User`:客户端所关联的用户(根据客户端发送的 `LC-Session` 头)。 * `meta: object`:有关客户端的更多信息,目前只有一个 `remoteAddress` 属性表示客户端的 IP。 * `sessionToken?: string`:客户端发来的 sessionToken(`X-LC-Session` 头)。 `Response` 上的属性包括: * `success: function(result?)`:向客户端发送结果,可以是包括 AV.Object 在内的各种数据类型或数组,客户端解析方式见各 SDK 文档。 * `error: function(err?: string)`:向客户端返回一个错误,目前仅支持字符串,`Error` 等类型也会被转换成字符串。 ### [SDK 调用云函数](#SDK_调用云函数) LeanCloud 各个语言版本的 SDK 都提供了调用云函数的接口。 ~~~ // 在 iOS SDK 中,AVCloud 提供了一系列静态方法来实现客户端调用云函数 // 构建传递给服务端的参数字典 NSDictionary *dicParameters = [NSDictionary dictionaryWithObject:@"夏洛特烦恼" forKey:@"movie"]; // 调用指定名称的云函数 averageStars,并且传递参数 [AVCloud callFunctionInBackground:@"averageStars" withParameters:dicParameters block:^(id object, NSError *error) { if(error == nil){ // 处理结果 } else { // 处理报错 } }]; ~~~ [Objective-C](#) [Java](#) [JavaScript](#) [PHP](#) [Python](#) ### [通过 REST API 调用云函数](#通过_REST_API_调用云函数) [REST API 调用云函数](https://leancloud.cn/docs/rest_api.html#云函数-1) 是 LeanCloud 云端提供的统一的访问云函数的接口,所有的客户端 SDK 也都是封装了这个接口从而实现对云函数的调用。 关于调试工具,我们推荐的工具有:[Postman](http://www.getpostman.com/) 以及 [Paw](https://luckymarmot.com/paw) ,它们可以帮助开发者更方便地调试 Web API。 假设没有以上工具,也可以使用命令行进行调试: ~~~ curl -X POST -H "Content-Type: application/json; charset=utf-8" \ -H "X-LC-Id: csXFgnEzBkodigdDUARBrEse-gzGzoHsz" \ -H "X-LC-Key: K2CE4ChmGnUwI8mMBgTRHw7y" \ -H "X-LC-Prod: 0" \ -d '{"movie":"夏洛特烦恼"}' \ https://leancloud.cn/1.1/functions/averageStars ~~~ 上述命令行实际上就是向云端发送一个 JSON 对象作为参数,参数的内容是要查询的电影的名字。 ### [云引擎调用云函数](#云引擎调用云函数) 在云引擎中可以使用 `AV.Cloud.run` 调用 `AV.Cloud.define` 定义的云函数: ~~~ var paramsJson = { movie: '夏洛特烦恼', }; AV.Cloud.run('averageStars', paramsJson).then(function(data) { // 调用成功,得到成功的应答 data }, function(error) { // 处理调用失败 }); ~~~ 云引擎中默认会直接进行一次本地的函数调用,而不是像客户端一样发起一个 HTTP 请求。如果你希望发起 HTTP 请求来调用云函数,可以传入一个 `remote: true` 的选项(与 success 和 error 回调同级),当你在云引擎之外运行 Node SDK 时这个选项非常有用: ~~~ AV.Cloud.run('averageStars', paramsJson).then(function(data) { // 成功 }, function(error) { // 失败 }); ~~~ ### [RPC 调用云函数](#RPC_调用云函数) RPC 调用云函数是指:云引擎会在这种调用方式下自动为 Http Response Body 做序列化,而 SDK 调用之后拿回的返回结果就是一个完整的 `AVObject`。 ~~~ NSDictionary *dicParameters = [NSDictionary dictionaryWithObject:@"夏洛特烦恼" forKey:@"movie"]; [AVCloud rpcFunctionInBackground:@"averageStars" withParameters:parameters block:^(id object, NSError *error) { if(error == nil){ // 处理结果 } else { // 处理报错 } }]; ~~~ [Objective-C](#) [Java](#) [PHP](#) [JavaScript](#) [Python](#) ### [切换云引擎环境](#切换云引擎环境) 专业版云引擎应用有「生产环境」和「预备环境」,切换方法为: ~~~ [AVCloud setProductionMode:NO]; // 调用预备环境 ~~~ [Objective-C](#) [Java](#) [PHP](#) [JavaScript](#) [Python](#) [免费版云引擎应用只有「生产环境」](https://leancloud.cn/docs/leanengine_plan.html#免费版) ,因此以上切换方法不适用。 ### [云函数错误响应码](#云函数错误响应码) 错误响应码允许自定义。云引擎方法最终的错误对象如果有 `code` 和 `message` 属性,则响应的 body 以这两个属性为准,否则 `code` 为 1, `message` 为错误对象的字符串形式。比如: ~~~ AV.Cloud.define('errorCode', function(req, res) { AV.User.logIn('NoThisUser', 'lalala').catch(function(err) { res.error(err); }); }); ~~~ 客户端收到的响应:`{"code":211,"error":"Could not find user"}` ~~~ AV.Cloud.define('customErrorCode', function(request, response) { response.error({code: 123, message: 'custom error message'}); }); ~~~ 客户端收到的响应: `{"code":123,"error":"自定义错误信息"}` ### [云函数超时](#云函数超时) 云函数超时时间为 15 秒,如果超过阈值,[LeanEngine Node.js SDK](https://github.com/leancloud/leanengine-node-sdk) 将强制响应: * 客户端收到 HTTP status code 为 503 响应,body 为 `The request timed out on the server.`。 * 服务端会出现类似这样的日志:`LeanEngine function timeout, url=/1.1/functions/<cloudFunc>, timeout=15000`。 另外还需要注意:虽然 [LeanEngine Node.js SDK](https://github.com/leancloud/leanengine-node-sdk) 已经响应,但此时云函数可能仍在执行,但执行完毕后的响应是无意义的(不会发给客户端,会在日志中打印一个 `Can't set headers after they are sent` 的异常)。 #### [超时的处理方案](#超时的处理方案) 我们建议将代码中的任务转化为异步队列处理,以优化运行时间,避免云函数或 [定时任务](#定时任务) 发生超时。比如: * 在存储服务中创建一个队列表,包含 `status` 列; * 接到任务后,向队列表保存一条记录,`status` 值设置为「处理中」,然后直接 response,也可以把队列对象 id 返回,如 `response.success(id);`; * 当业务处理完毕,根据处理结果更新刚才的队列对象状态,将 `status` 字段设置为「完成」或者「失败」; * 在任何时候,在控制台通过队列 id 可以获取某个任务的执行结果,判断任务状态。 ## [Hook 函数](#Hook_函数) Hook 函数本质上是云函数,但它有固定的名称,定义之后会由系统在特定事件或操作(如数据保存前、保存后,数据更新前、更新后等等)发生时自动触发,而不是由开发者来控制其触发时机。 需要注意: * 通过控制台进行 [数据导入](https://leancloud.cn/docs/dashboard_guide.html#本地数据导入_LeanCloud) 不会触发以下任何 hook 函数。 * 使用 Hook 函数需要 [防止死循环调用](#防止死循环调用)。 * `_Installation` 表暂不支持 Hook 函数。 * Hook 函数只对当前应用的 Class 生效,[对绑定后的目标 Class 无效](https://leancloud.cn/docs/app_data_share.html#云引擎_Hook_函数)。 ### [beforeSave](#beforeSave) 在创建新对象之前,可以对数据做一些清理或验证。例如,一条电影评论不能过长,否则界面上显示不开,需要将其截断至 140 个字符: ~~~ AV.Cloud.beforeSave('Review', function(request, response) { var comment = request.object.get('comment'); if (comment) { if (comment.length > 140) { // 截断并添加... request.object.set('comment', comment.substring(0, 137) + '...'); } // 保存到数据库中 response.success(); } else { // 不保存数据,并返回错误 response.error('No comment!'); } }); ~~~ ### [afterSave](#afterSave) 在创建新对象后触发指定操作,比如当一条留言创建后再更新一下所属帖子的评论总数: ~~~ AV.Cloud.afterSave('Comment', function(request) { var query = new AV.Query('Post'); query.get(request.object.get('post').id).then(function(post) { post.increment('comments'); post.save(); }); }); ~~~ 再如,在用户注册成功之后,给用户增加一个新的属性 from 并保存: ~~~ AV.Cloud.afterSave('_User', function(request) { console.log(request.object); request.object.set('from','LeanCloud'); request.object.save().then(function(user) { console.log('ok!'); }); }); ~~~ 如果 `afterSave` 函数调用失败,save 请求仍然会返回成功应答给客户端。`afterSave` 发生的任何错误,都将记录到云引擎日志里,可以到 [控制台 > 存储 > 云引擎 > 日志](https://leancloud.cn/cloud.html?appid=csXFgnEzBkodigdDUARBrEse-gzGzoHsz#/log) 中查看。 ### [beforeUpdate](#beforeUpdate) 在更新已存在的对象前执行操作,这时你可以知道哪些字段已被修改,还可以在特定情况下拒绝本次修改: ~~~ AV.Cloud.beforeUpdate('Review', function(request, response) { // 如果 comment 字段被修改了,检查该字段的长度 if (request.object.updatedKeys.indexOf('comment') != -1) { if (request.object.get('comment').length <= 140) { response.success(); } else { // 拒绝过长的修改 response.error('comment 长度不得超过 140 字符'); } } else { response.success(); } }); ~~~ 注意: 不要修改 `request.object`,因为对它的改动并不会保存到数据库,但可以用 `response.error` 返回一个错误,拒绝这次修改。 注意:传入的对象是一个尚未保存到数据库的临时对象,并不保证与最终储存到数据库的对象完全相同,这是因为修改中可能包含自增、数组增改、关系增改等原子操作。 ### [afterUpdate](#afterUpdate) 在更新已存在的对象后执行特定的动作,比如每次修改文章后记录下日志: ~~~ AV.Cloud.afterUpdate('Article', function(request) { console.log('Updated article,the id is :' + request.object.id); }); ~~~ ### [beforeDelete](#beforeDelete) 在删除一个对象之前做一些检查工作,比如在删除一个相册 Album 前,先检查一下该相册中还有没有照片 Photo: ~~~ AV.Cloud.beforeDelete('Album', function(request, response) { //查询Photo中还有没有属于这个相册的照片 var query = new AV.Query('Photo'); var album = AV.Object.createWithoutData('Album', request.object.id); query.equalTo('album', album); query.count().then(function(count) { if (count > 0) { //还有照片,不能删除,调用error方法 response.error('Can\'t delete album if it still has photos.'); } else { //没有照片,可以删除,调用success方法 response.success(); } }, function(error) { response.error('Error ' + error.code + ' : ' + error.message + ' when getting photo count.'); }); }); ~~~ ### [afterDelete](#afterDelete) 在被删一个对象后执行操作,例如递减计数、删除关联对象等等。同样以相册为例,这次我们不在删除相册前检查是否还有照片,而是在删除后,同时删除相册中的照片: ~~~ 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); query.find().then(function(posts) { //查询本相册的照片,遍历删除 AV.Object.destroyAll(posts); }).then(function(error) { console.error('Error finding related comments ' + error.code + ': ' + error.message); }); }); ~~~ ### [onVerified](#onVerified) 当用户通过邮箱或者短信验证时,对该用户执行特定操作。比如: ~~~ AV.Cloud.onVerified('sms', function(request, response) { console.log('onVerified: sms, user: ' + request.object); response.success(); }); ~~~ 函数的第一个参数是验证类型。短信验证为 `sms`,邮箱验证为 `email`。另外,数据库中相关的验证字段,如 `emailVerified` 不需要修改,系统会自动更新。 ### [onLogin](#onLogin) 在用户登录之时执行指定操作,比如禁止在黑名单上的用户登录: ~~~ AV.Cloud.onLogin(function(request, response) { // 因为此时用户还没有登录,所以用户信息是保存在 request.object 对象中 console.log("on login:", request.object); if (request.object.get('username') == 'noLogin') { // 如果是 error 回调,则用户无法登录(收到 401 响应) response.error('Forbidden'); } else { // 如果是 success 回调,则用户可以登录 response.success(); } }); ~~~ ### [实时通信 Hook 函数](#实时通信_Hook_函数) 请阅读 [实时通信概览 · 云引擎 Hook](https://leancloud.cn/docs/realtime_v2.html#云引擎_Hook) 来了解以下函数的相关参数和用法。 #### [_messageReceived](#_messageReceived) 在消息达到服务器、群组成员已解析完成、发送给收件人之前触发。例如,提前过滤掉聊天内容中的一些广告类的关键词: ~~~ AV.Cloud.define("_messageReceived", (request, response) => { // request.params = { // fromPeer: 'Tom', // receipt: false, // groupId: null, // system: null, // content: '{"_lctext":"耗子,起床!","_lctype":-1}', // convId: '5789a33a1b8694ad267d8040', // toPeers: ['Jerry'], // __sign: '1472200796787,a0e99be208c6bce92d516c10ff3f598de8f650b9', // bin: false, // transient: false, // sourceIP: '121.239.62.103', // timestamp: 1472200796764 // }; console.log('_messageReceived start'); let content = JSON.parse(request.params.content); let text = content._lctext; console.log('text', text); let processedContent = text.replace('XX中介', '**'); // 必须含有以下语句给服务端一个正确的返回,否则会引起异常 response.success({ content: processedContent }); console.log('_messageReceived end'); }); ~~~ #### [_receiversOffline](#_receiversOffline) 在消息发送完成时触发、对话中某些用户却已经下线,此时可以根据发送的消息来生成离线消息推送的标题等等。例如截取所发送消息的前 6 个字符作为推送的标题: ~~~ AV.Cloud.define('_receiversOffline', (request, response) => { console.log('_receiversOffline start'); let params = request.params; let content = params.content; let shortContent = content; // params.content 为消息的内容 if (shortContent.length > 6) { shortContent = content.slice(0, 6); } console.log('shortContent', shortContent); let json = { // 自增未读消息的数目,不想自增就设为数字 badge: "Increment", sound: "default", // 使用开发证书 _profile: "dev", alert: shortContent }; let pushMessage = JSON.stringify(json); response.success({ "pushMessage": pushMessage }); console.log('_receiversOffline end'); }); ~~~ #### [_messageSent](#_messageSent) 消息发送完成之后触发,例如消息发送完后,在云引擎中打印一下日志: ~~~ AV.Cloud.define('_messageSent', (request, response) => { console.log('_messageSent start'); let params = request.params; console.log('params', params); response.success({}); console.log('_messageSent end'); // 在云引擎中打印的日志如下: // _messageSent start // params { fromPeer: 'Tom', // receipt: false, // onlinePeers: [], // content: '12345678', // convId: '5789a33a1b8694ad267d8040', // msgId: 'fptKnuYYQMGdiSt_Zs7zDA', // __sign: '1472703266575,30e1c9b325410f96c804f737035a0f6a2d86d711', // bin: false, // transient: false, // sourceIP: '114.219.127.186', // offlinePeers: [ 'Jerry' ], // timestamp: 1472703266522 } // _messageSent end }); ~~~ #### [_conversationStart](#_conversationStart) 创建对话,在签名校验(如果开启)之后、实际创建之前触发。例如对话创建时,在云引擎中打印一下日志: ~~~ AV.Cloud.define('_conversationStart', (request, response) => { console.log('_conversationStart start'); let params = request.params; console.log('params', params); response.success({}); console.log('_conversationStart end'); // 在云引擎中打印的日志如下: //_conversationStart start // params { // initBy: 'Tom', // members: ['Tom', 'Jerry'], // attr: { // name: 'Tom & Jerry' // }, // __sign: '1472703266397,b57285517a95028f8b7c34c68f419847a049ef26' // } //_conversationStart end }); ~~~ #### [_conversationStarted](#_conversationStarted) 创建对话完成触发。例如对话创建之后,在云引擎打印一下日志: ~~~ AV.Cloud.define('_conversationStarted', (request, response) => { console.log('_conversationStarted start'); let params = request.params; console.log('params', params); response.success({}); console.log('_conversationStarted end'); // 在云引擎中打印的日志如下: // _conversationStarted start // params { // convId: '5789a33a1b8694ad267d8040', // __sign: '1472723167361,f5ceedde159408002fc4edb96b72aafa14bc60bb' // } // _conversationStarted end }); ~~~ #### [_conversationAdd](#_conversationAdd) 向对话添加成员,在签名校验(如果开启)之后、实际加入之前,包括主动加入和被其他用户加入两种情况都会触发,注意在创建对话时传入了其他用户的 Client Id 作为 Member 参数,不会触发 _conversationAdd 。例如在云引擎中打印成员加入时的日志: ~~~ AV.Cloud.define('_conversationAdd', (request, response) => { console.log('_conversationAdd start'); let params = request.params; console.log('params', params); response.success({}); console.log('_conversationAdd end'); // 在云引擎中打印的日志如下: // _conversationAdd start // params { // initBy: 'Tom', // members: ['Mary'], // convId: '5789a33a1b8694ad267d8040', // __sign: '1472786231813,a262494c252e82cb7a342a3c62c6d15fffbed5a0' // } // _conversationAdd end }); ~~~ #### [_conversationRemove](#_conversationRemove) 从对话中踢出成员,在签名校验(如果开启)之后、实际踢出之前触发,用户自己退出对话不会触发。例如在踢出某一个成员时,在云引擎日志中打印出该成员的 Client Id: ~~~ AV.Cloud.define('_conversationRemove', (request, response) => { console.log('_conversationRemove start'); let params = request.params; console.log('params', params); response.success({}); console.log('removed client Id:', params.members[0]); console.log('_conversationRemove end'); // 在云引擎中打印的日志如下: // _conversationRemove start // params { // initBy: 'Tom', // members: ['Jerry'], // convId: '57c8f3ac92509726c3dadaba', // __sign: '1472787372605,abdf92b1c2fc4c9820bc02304f192dab6473cd38' // } //removed client Id: Jerry // _conversationRemove end }); ~~~ #### [_conversationUpdate](#_conversationUpdate) 修改对话属性、设置或取消对话消息提醒,在实际修改之前触发。例如在更新发生时,在云引擎日志中打印出对话的名称: ~~~ AV.Cloud.define('_conversationUpdate', (request, response) => { console.log('_conversationUpdate start'); let params = request.params; console.log('params', params); console.log('name', params.attr.name); response.success({}); console.log('_conversationUpdate end'); // 在云引擎中打印的日志如下: // _conversationUpdate start // params { // convId: '57c9208292509726c3dadb4b', // initBy: 'Tom', // attr: { // name: '聪明的喵星人', // type: 'public' // }, // name 聪明的喵星人 // _conversationUpdate end }); ~~~ ### [防止死循环调用](#防止死循环调用) 在实际使用中有这样一种场景:在 `Post` 类的 `afterUpdate` Hook 函数中,对传入的 `Post` 对象做了修改并且保存,而这个保存动作又会再次触发 `afterUpdate`,由此形成死循环。针对这种情况,我们为所有 Hook 函数传入的 `request.object` 对象做了处理,以阻止死循环调用的产生。 不过请注意,以下情况还需要开发者自行处理: * 对传入的 `request.object` 对象进行 `fetch` 操作。 * 重新构造传入的 `request.object` 对象,如使用 `AV.Object.createWithoutData()` 方法。 对于使用上述方式产生的对象,请根据需要自行调用以下 API: * `object.disableBeforeHook()` 或 * `object.disableAfterHook()` 这样,对象的保存或删除动作就不会再次触发相关的 Hook 函数。 ~~~ AV.Cloud.afterUpdate('Post', function(request) { // 直接修改并保存对象不会再次触发 afterUpdate Hook 函数 request.object.set('foo', 'bar'); request.object.save().then(function(obj) { // 你的业务逻辑 }).catch(console.error); // 如果有 fetch 操作,则需要在新获得的对象上调用相关的 disable 方法 // 来确保不会再次触发 Hook 函数 request.object.fetch().then(function(obj) { obj.disableAfterHook(); obj.set('foo', 'bar'); return obj.save(); }).then(function(obj) { // 你的业务逻辑 }).catch(console.error); // 如果是其他方式构建对象,则需要在新构建的对象上调用相关的 disable 方法 // 来确保不会再次触发 Hook 函数 var obj = AV.Object.createWithoutData('Post', request.object.id); obj.disableAfterHook(); obj.set('foo', 'bar'); obj.save().then(function(obj) { // 你的业务逻辑 }).catch(console.error); }); ~~~ 提示:云引擎 Node.js 环境从 [0.3.0](https://github.com/leancloud/leanengine-node-sdk/blob/master/CHANGELOG.md#v030-20151231) 开始支持 `object.disableBeforeHook()` 和 `object.disableAfterHook()`。 ### [Hook 函数错误响应码](#Hook_函数错误响应码) 为 `beforeSave` 这类的 hook 函数定义错误码,需要这样: ~~~ AV.Cloud.beforeSave('Review', function(request, response) { // 使用 JSON.stringify() 将 object 变为字符串 response.error(JSON.stringify({ code: 123, message: '自定义错误信息' })); }); ~~~ 客户端收到的响应为:`Cloud Code validation failed. Error detail : {"code":123, "message": "自定义错误信息"}`,然后通过截取字符串的方式取出错误信息,再转换成需要的对象。 ### [Hook 函数超时](#Hook_函数超时) Hook 函数的超时时间为 3 秒。如果 Hook 函数被其他的云函数调用(比如因为 save 对象而触发 `beforeSave` 和 `afterSave`),那么它们的超时时间会进一步被其他云函数调用的剩余时间限制。 例如,如果一个 `beforeSave` 函数是被一个已经运行了 13 秒的云函数触发,那么 `beforeSave` 函数就只剩下 2 秒的时间来运行。同时请参考 [云函数超时](#云函数超时)。 ## [在线编写云函数](#在线编写云函数) 很多人使用 LeanEngine 是为了在服务端提供一些个性化的方法供各终端调用,而不希望关心诸如代码托管、npm 依赖管理等问题。为此我们提供了在线维护云函数的功能。 使用此功能需要注意: * 在定义的函数会覆盖你之前用 Git 或命令行部署的项目。 * 目前只能在线编写云函数和 Hook,不支持托管静态网页、编写动态路由。 在 [控制台 > 存储 > 云引擎 > 部署 > 在线编辑](https://leancloud.cn/cloud.html?appid=csXFgnEzBkodigdDUARBrEse-gzGzoHsz#/deploy/online) 标签页,可以: * 创建函数:指定函数类型,函数名称,函数体的具体代码,注释等信息,然后「保存」即可创建一个云函数。 * 部署:选择要部署的环境,点击「部署」即可看到部署过程和结果。 * 预览:会将所有函数汇总并生成一个完整的代码段,可以确认代码,或者将其保存为 `cloud.js` 覆盖项目模板的同名文件,即可快速的转换为使用项目部署。 * 维护云函数:可以编辑已有云函数,查看保存历史,以及删除云函数。 提示:云函数编辑之后需要重新部署才能生效。 ## [定时任务](#定时任务) 定时任务可以按照设定,以一定间隔自动完成指定动作,比如半夜清理过期数据,每周一向所有用户发送推送消息等等。定时任务的最小时间单位是秒,正常情况下时间误差都可以控制在秒级别。 定时任务是普通的云函数,也会遇到 [超时问题](#云函数超时),具体请参考 [超时处理方案](#超时的处理方案)。 部署云引擎之后,进入 [控制台 > 存储 > 云引擎 > 定时任务](https://leancloud.cn/cloud.html?appid=csXFgnEzBkodigdDUARBrEse-gzGzoHsz#/task),点击 创建定时器,然后设定执行的函数名称、执行环境等等。例如定义一个打印循环打印日志的任务 `log_timer`: ~~~ AV.Cloud.define('log_timer', function(request, response){ console.log('Log in timer.'); return response.success(); }); ~~~ 定时器创建后,其状态为未运行,需要点击 启用 来激活。之后其执行日志可以通过 [日志](https://leancloud.cn/cloud.html?appid=csXFgnEzBkodigdDUARBrEse-gzGzoHsz#/log) 查看。 定时任务分为两类: * 使用 Cron 表达式安排调度 * 以秒为单位的简单循环调度 以 Cron 表达式为例,比如每周一早上 8 点准时发送推送消息给用户: ~~~ AV.Cloud.define('push_timer', function(request, response){ AV.Push.send({ channels: ['Public'], data: { alert: 'Public message' } }); return response.success(); }); ~~~ 创建定时器的时候,选择 Cron 表达式 并填入 `0 0 8 ? * MON`。 ### [Cron 表达式](#Cron_表达式) Cron 表达式的基本语法为: ~~~ <秒> <分钟> <小时> <日期 day-of-month> <月份> <星期 day-of-week> <年> ~~~ | 位置 | 字段 | 约束 | 取值 | 可使用的特殊字符 | | --- | --- | --- | --- | --- | | 1 | 秒 | 必须 | 0-59 | `, - * /` | | 2 | 分钟 | 必须 | 0-59 | `, - * /` | | 3 | 小时 | 必须 | 0-23(0 为午夜) | `, - * /` | | 4 | 日期 | 必须 | 1-31 | `, - * ? / L W` | | 5 | 月份 | 必须 | 1-12、JAN-DEC | `, - * /` | | 6 | 星期 | 必须 | 1-7、SUN-SAT | `, - * ? / L #` | | 7 | 年 | 可选 | 空、1970-2099 | `, - * /` | 特殊字符的用法: | 字符 | 含义 | 用法 | | --- | --- | --- | | `*` | 所有值 | 代表一个字段的所有可能取值。如将 `<分钟>` 设为 *,表示每一分钟。 | | `?` | 不指定值 | 用于可以使用该字符的两个字段中的一个,在一个表达式中只能出现一次。如任务执行时间为每月 10 号,星期几无所谓,那么表达式中 `<日期>` 设为 10,`<星期>` 设为 ?。 | | `-` | 范围 | 如 `<小时>` 为 10-12,即10 点、11 点、12 点。 | | `,` | 分隔多个值 | 如 `<星期>` 为 MON,WED,FRI,即周一、周三、周五。 | | `/` | 增量 | 如 `<秒>` 设为 0/15,即从 0 秒开始,以 15 秒为增量,包括 0、15、30、45 秒;5/15 即 5、20、35、50 秒。*/ 与 0/ 等效,如 `<日期>` 设为 1/3,即从每个月的第一天开始,每 3 天(即每隔 2 天)执行一次任务。 | | `L` | 最后 | 其含义随字段的不同而不同。 `<日期>` 中使用 L 代表每月最后一天,如 1 月 31 号、2 月 28 日(非闰年);`<星期>` 中单独使用 L,则与使用 7 或 SAT 等效,若前面搭配其他值使用,如 6L,则表示每月的最后一个星期五。 注意,在 L 之前不要使用多个值或范围,如 1,2L、1-2L,否则会产生错误结果。 | | `W` | weekday | 周一到周五的任意一天,离指定日期最近的非周末的那一天。 `<日期>` 为 15W 即离 15 号最近的非周末的一天;如果 15 号是周六,任务则会在 14 号周五触发,如果 15 号是周日,则在 16 号周一触发,如果 15 号是周二,则周二当天触发。 `<日期>` 为 1W,如果 1 号是周六,任务则会在 3 号周一触发,因为不能向前跨月来计算天数。 在 `<日期>` 中 W 之前只能使用一个数值,不能使用多个值或范围。LW 可在 `<日期>` 中组合使用,表示每月最后一个非周末的一天。 | | `#` | 第 N 次 | 如 `<星期>` 为 6#3 代表每月第三个周五,2#1 为每月头一个周一,4#5 为每月第五个周三;如果当月没有第五周,则 #5 不会产生作用。 | 各字段以空格或空白隔开。JAN-DEC、SUN-SAT 这些值不区分大小写,比如 MON 和 mon 效果一样。更详细的使用方法请参考 [Quartz 文档(英文)](http://www.quartz-scheduler.org/documentation/quartz-1.x/tutorials/crontrigger) 。 举例如下: | 表达式 | 说明 | | --- | --- | | `0 0/5 * * * ?` | 每隔 5 分钟执行一次 | | `10 0/5 * * * ?` | 每隔 5 分钟执行一次,每次执行都在分钟开始的 10 秒,例如 10:00:10、10:05:10 等等。 | | `0 30 10-13 ? * WED,FRI` | 每周三和每周五的 10:30、11:30、12:30、13:30 执行。 | | `0 0/30 8-9 5,20 * ?` | 每个月的 5 号和 20 号的 8 点和 10 点之间每隔 30 分钟执行一次,也就是 8:00、8:30、9:00 和 9:30。 | ### [定时器数量](#定时器数量) 生产环境和预备环境的定时器数量都限制在 5 个以内,也就是说你总共最多可以创建 10 个定时器。 ### [错误信息](#错误信息) 定时器执行后的日志会记录在 [控制台 > 存储 > 云引擎 > 其它 > 日志](https://leancloud.cn/cloud.html?appid=csXFgnEzBkodigdDUARBrEse-gzGzoHsz#/log) 中,以下为常见的错误信息及原因。 * timerAction timed-out and no fallback available. 某个定时器触发的云函数,因 15 秒内没有响应而超时(可参考 [对云函数调用超时的处理](#超时的处理方案))。 * timerAction short-circuited and no fallback available. 某个定时器触发的云函数,因为太多次超时而停止触发。 ## [权限说明](#权限说明) 云引擎可以有超级权限,使用 Master key 调用所有 API,因此会忽略 ACL 和 Class Permission 限制。你只需要使用下列代码来初始化 SDK(在线定义默认就有超级权限): ~~~ //参数依次为 AppId, AppKey, MasterKey AV.init({ appId: 'csXFgnEzBkodigdDUARBrEse-gzGzoHsz', appKey: 'K2CE4ChmGnUwI8mMBgTRHw7y', masterkey: 'l3fwovKapDmHHC6lDHNfJhR5' }) AV.Cloud.useMasterKey(); ~~~ 如果在你的服务端环境里也想做到超级权限,也可以使用该方法初始化。