[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();
~~~
如果在你的服務端環境里也想做到超級權限,也可以使用該方法初始化。