昨天我們透過簡單的範例來了解 Services
的基本綱目,而今天要來專注於說明 Actions
的規則與用法。
在服務中可以使用 Actions 來建立可公開呼叫的方法,並且通過遠端程序呼叫 (Remote Procedure Call, RPC) 與另一個程序進行互動。它就像 HTTP 一樣能夠對指定的對象發送參數請求,然後響應相應的內容。
如果你建立了多個實例, Broker 將會依負載平衡規則來請求服務:
Fig. 1. 負載平衡請求服務
若要呼叫一個服務可以使用 broker.call
方法,當你使用這個方法時, Broker 會去找尋符合服務的動作 Action 並且呼叫它,然後回傳一個 Promise 物件。
範例:
const res = await broker.call(actionName[, params[, opts]]);
參數說明:
actionName
<String> 是一個由點分隔的字串。依序為可選的版本號,使用的服務名稱,執行的動作名稱。params
<Object> 選填的物件型態自定義參數。可以透過 ctx.params
來取得參數內容,預設值為空物件 {}
。opts
<Object> 請求的相關內建參數設定。更多參數請參考下表:名稱 | 類型 | 預設值 | 說明 |
---|---|---|---|
timeout |
<Number> | null |
請求逾時設定(單位:毫秒)。 |
retries |
<Number> | null |
請求重試次數。 |
fallbackResponse |
<Any> | null |
請求失敗時的 Fallback 函數。 |
nodeID |
<String> | null |
目標節點。 |
meta |
<Object> | {} |
請求的 Metadata。可以使用 ctx.meta 來取用它。 |
parentCtx |
<Context> | null |
父層上下文,用於串接呼叫。 |
requestID |
<String> | null |
請求或關聯的 ID。用於追蹤。 |
最簡單的方式就是直接呼叫:
const res = await broker.call("user.list");
也可以夾帶參數進行呼叫:
const res = await broker.call("user.get", { id: 3 });
帶有參數設定及 Fallback 函數的呼叫方式:
const res = await broker.call("user.recommendation", { limit: 5 }, {
timeout: 500,
retries: 3,
fallbackResponse: defaultRecommendation
});
含有 Promise 異常處理函數的使用方式:
broker.call("posts.update", { id: 2, title: "Modified post title" })
.then(res => console.log("Post updated!"))
.catch(err => console.error("Unable to update Post!", err));
直接呼叫指定節點的方式:
const res = await broker.call("$node.health", null, { nodeID: "node-21" })
在 Actions 中的 Meta 屬性是用來儲存功能相關的資訊,可以通過 ctx.meta
來取用它。但在函數巢狀呼叫的情況下, meta 資訊是會被合併在一起的,在呼叫後返回也會保持合併過後的資料。
broker.createService({
name: "farm",
actions: {
async greenhouse(ctx) {
console.log(ctx.meta);
// { tool: 'Shovel' }
const result = await ctx.call("farm.container", null, {
meta: {
plant: "Rose"
}
});
console.log(ctx.meta);
// { tool: 'Shovel', plant: 'Rose' }
return result;
},
async container(ctx) {
console.log(ctx.meta);
// { tool: 'Shovel', plant: 'Rose' }
return ctx.meta;
}
}
});
broker.call("farm.greenhouse", null, {
meta: {
tool: "Shovel"
}
});
如果使用的是內部呼叫的方式,記得要加上 parentCtx
來傳遞 meta 資訊。
this.actions.container(ctx.params, { parentCtx: ctx });
逾時也可以在 Actions 設定。逾時設定的優先層級依序為:Service 設定、Actions 設定、call 函數設定,當後者被設定後將會取代前者。
範例:
const brokerNode = new ServiceBroker({
nodeID: "node-1",
requestTimeout: 5000, // 在 Service 設定
});
brokerNode.createService({
name: "greeter",
actions: {
normal: {
async handler() {
return "Normal";
}
},
slow: {
timeout: 3000, // 在 Action 設定
async handler() {
return "Slow";
}
},
}
});
呼叫範例:
// 5 秒
await broker.call("greeter.normal");
// 3 秒
await broker.call("greeter.slow");
// 1 秒
await broker.call("greeter.slow", null, {
timeout: 1000 // 在 call 函數設定
});
除了逐個呼叫 Action 外,可以使用 mcall
一次對多個 Actions 呼叫。
陣列型態的 mcall
呼叫方式:
await broker.mcall(
[
{
action: 'posts.find', params: { author: 1 }, options: {
// 個別選項設定
}
},
{
action: 'users.find', params: { name: 'John' }
}
],
{
// 通用選項設定
meta: { token: '63f20c2d-8902-4d86-ad87-b58c9e2333c2' }
}
);
物件型態的 mcall
呼叫方式:
await broker.mcall(
{
posts: {
action: 'posts.find', params: { author: 1 }, options: {
// 個別選項設定
}
},
users: {
action: 'users.find', params: { name: 'John' }
}
},
{
// 通用選項設定
meta: { token: '63f20c2d-8902-4d86-ad87-b58c9e2333c2' }
}
);
Moleculer 支援 Node.js 的 stream 可作為請求的參數也可以響應。也支援將由閘道器 (Gateway) 傳入的串流檔案進行編碼/解碼或是壓縮/解壓縮。
範例:
接收一個串流檔案。
注意,進行串流傳輸時
ctx.params
將會是一個串流資料。
module.exports = {
name: "storage",
actions: {
save(ctx) {
const stream = fs.createWriteStream(`/tmp/${ctx.meta.filename}`);
ctx.params.pipe(stream);
}
}
};
範例:
返回一個串流檔案
module.exports = {
name: "storage",
actions: {
get: {
params: {
filename: "string"
},
handler(ctx) {
return fs.createReadStream(`/tmp/${ctx.params.filename}`);
}
}
}
};
範例:
上傳一個串流檔案至其它服務。
注意,傳遞串流時請不要對
ctx.params
參雜任何其它的參數,如有需要夾帶其它相關資訊請使用meta
資訊來傳遞,或是直接傳遞parentCtx
。
upload: {
async handler(ctx) {
await this.broker.call(
"storage.save",
ctx.params,
{ meta: { filename: "avatar.jpg" } },
// { parentCtx: ctx },
);
}
},
範例:
取得遠端串流檔案儲存於本地。例如用於下次取得檔案時,可以直接從本地取得檔案。需要提供下載檔案名稱時,可以由 ctx.meta.$responseHeaders
來設定標頭資訊。
get: {
rest: {
method: "GET",
path: "/file"
},
params: {
filename: "string"
},
async handler(ctx) {
const { filename } = ctx.params;
// 檔案名稱
ctx.meta.$responseHeaders = {
"Content-Disposition": `attachment; filename*=utf-8''${encodeURIComponent(filename)}`
};
if (fs.existsSync(`./${filename}`)) {
return fs.createReadStream(`./${filename}`);
} else {
const stream = await this.broker.call("storage.get", { filename });
// 檔案可以在本地儲存
const s = fs.createWriteStream(`./${filename}`);
stream.pipe(s);
s.on("close", () => this.broker.logger.info("File has been received"));
return stream;
}
}
}
範例:
AES 編碼/解碼服務
const crypto = require("crypto");
const password = "moleculer";
module.exports = {
name: "aes",
actions: {
encrypt(ctx) {
const encrypt = crypto.createCipher("aes-256-ctr", password);
return ctx.params.pipe(encrypt);
},
decrypt(ctx) {
const decrypt = crypto.createDecipher("aes-256-ctr", password);
return ctx.params.pipe(decrypt);
}
}
};
[1] Actions, https://moleculer.services/docs/0.14/actions.html
ctx.meta
資料並不會影響 Service 的 this.metadata
,可以同時使用唷。