昨天介紹了閘道器的路由相關功能,今天要來談談它內建的各種屬性設定。
路由中有一個 callOptions
屬性可以用來設定 broker.call
的屬性。你可以對其設定 timeout
、 retries
或 fallbackResponse
,它與 Actions 中的設定方式相同。
broker.createService({
mixins: [ApiService],
settings: {
routes: [{
callOptions: {
timeout: 500,
retries: 3,
fallbackResponse(ctx, err) {
// ...
}
}
}]
}
});
你可以建立多個路由,它們可以分別設定不同的名稱、白名單、別名、呼叫選項及授權方式等。
broker.createService({
mixins: [ApiService],
settings: {
routes: [
{
path: "/admin",
authorization: true,
whitelist: [
"$node.*",
"users.*",
],
bodyParsers: {
json: true
}
},
{
path: "/",
whitelist: [
"posts.*",
"math.*",
],
bodyParsers: {
json: true
}
}
]
}
});
當 Action 處理完成需要響應時,API 閘道器會偵測響應的類型,並且在 res
標頭設定 Content-Type
。預設的狀態碼是 200
。你也可以在 ctx.meta
覆蓋它來響應客製化的標頭及狀態碼。
可用的 meta 資訊:
ctx.meta.$statusCode
- 設定狀態碼 res.statusCode
ctx.meta.$statusMessage
- 設定狀態訊息 res.statusMessage
ctx.meta.$responseType
- 設定標頭 Content-Type
ctx.meta.$responseHeaders
- 設定標頭全部的 keysctx.meta.$location
- 設定標頭重新導向 Location
範例:
module.exports = {
name: "export",
actions: {
// 在瀏覽器響應檔案下載
downloadCSV(ctx) {
ctx.meta.$responseType = "text/csv";
ctx.meta.$responseHeaders = {
"Content-Disposition": `attachment; filename="data-${ctx.params.id}.csv"`
};
return csvFileStream;
},
// 請求跳轉頁面
redirectSample(ctx) {
ctx.meta.$statusCode = 302;
ctx.meta.$location = "/login";
return;
}
}
}
你可以很簡單的啟用授權功能,首先在路由設定中加入 authorization: true
,然後在服務中建立一個 authorize
函數。
const E = require("moleculer-web").Errors;
broker.createService({
mixins: [ApiService],
settings: {
routes: [{
// 啟用授權
authorization: true
}]
},
methods: {
// 建立授權函數
authorize(ctx, route, req, res) {
// 由標頭讀取 JWT Token
let auth = req.headers["authorization"];
if (auth && auth.startsWith("Bearer")) {
let token = auth.slice(7);
// 檢查 Token
if (token == "123456") {
// 將使用者授權加到 `ctx.meta`
ctx.meta.user = { id: 1, name: "John Doe" };
return Promise.resolve(ctx);
} else {
// 無效的 Token
return Promise.reject(new E.UnAuthorizedError(E.ERR_INVALID_TOKEN));
}
} else {
// Token 不存在
return Promise.reject(new E.UnAuthorizedError(E.ERR_NO_TOKEN));
}
}
}
}
你可以參考官方原始碼中更詳細的 JWT 範例[2] 。
驗證的啟用方法類似於授權,首先在路由設定中加入 authentication: true
,然後在服務中建立一個 authenticate
函數。
返回的值將會被設定到
ctx.meta.user
屬性中,你可以在 Action 中利用它來取得已登入的使用者物件。
範例:
broker.createService({
mixins: ApiGatewayService,
settings: {
routes: [{
// 啟用驗證
authentication: true
}]
},
methods: {
authenticate(ctx, route, req, res) {
let accessToken = req.query["access_token"];
if (accessToken) {
if (accessToken === "12345") {
// 驗證憑證有效,將設定至 `ctx.meta.user`
return Promise.resolve({ id: 1, username: "john.doe", name: "John Doe" });
} else {
// 無效的憑證
return Promise.reject();
}
} else {
// 匿名訪客
return Promise.resolve(null);
}
}
}
});
路由也有前後呼叫的 Hooks 。你可以使用它來做一些像是設定 ctx.meta
、 訪問 req.headers
或是變更響應的 data
。
範例:
broker.createService({
mixins: [ApiService],
settings: {
routes: [
{
path: "/",
onBeforeCall(ctx, route, req, res) {
// 將請求的標頭設到 ctx.meta
ctx.meta.userAgent = req.headers["user-agent"];
},
onAfterCall(ctx, route, req, res, data) {
// 返回一個異步函數
return doSomething(ctx, res, data);
}
}
]
}
});
在新的版本後,你可以在
onAfterCall
操作data
,但是必須永遠返回一個新的或原始的data
。
你可以加入 route-level
、 global-level
兩個不同層級的客製化錯誤處理,並且在處理完畢後呼叫 res.end
,否則請求將不會被處理。
範例:
broker.createService({
mixins: [ApiService],
settings: {
routes: [{
path: "/api",
// 路由錯誤處理
onError(req, res, err) {
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.writeHead(500);
res.end(JSON.stringify(err));
}
}],
// 全域錯誤處理
onError(req, res, err) {
res.setHeader("Content-Type", "text/plain");
res.writeHead(501);
res.end("Global error: " + err.message);
}
}
}
API 閘道器提供了一個格式化輔助函數,可以使用它來過濾不需要輸出的資料。
範例:
broker.createService({
mixins: [ApiService],
methods: {
reformatError(err) {
// 在發送到客戶端之前,先將錯誤資料做過濾
return _.pick(err, ["name", "message", "code", "type", "data"]);
},
}
}
你可以在 API 閘道器中使用 CORS 標頭。
範例:
const svc = broker.createService({
mixins: [ApiService],
settings: {
// 對所有路由使用全域的 CORS 設定
cors: {
// 設置 Access-Control-Allow-Origin CORS 標頭
origin: "*",
// 設置 Access-Control-Allow-Methods CORS 標頭
methods: ["GET", "OPTIONS", "POST", "PUT", "DELETE"],
// 設置 Access-Control-Allow-Headers CORS 標頭
allowedHeaders: [],
// 設置 Access-Control-Expose-Headers CORS 標頭
exposedHeaders: [],
// 設置 Access-Control-Allow-Credentials CORS 標頭
credentials: false,
// 設置 Access-Control-Max-Age CORS 標頭
maxAge: 3600
},
routes: [{
path: "/api",
// 路由層的 CORS 設定 (會覆蓋全域設定)
cors: {
origin: ["http://localhost:3000", "https://localhost:4000"],
methods: ["GET", "OPTIONS", "POST"],
credentials: true
},
}]
}
});
Moleculer-Web 內建了一個採用記憶體儲存方式的限速器。
範例:
const svc = broker.createService({
mixins: [ApiService],
settings: {
rateLimit: {
// 時間視窗,也是記憶體儲存的有效期限(毫秒)
// 預設 60000 (1 分鐘)
window: 60 * 1000,
// 時間視窗中最大的請求數
// 預設為 30 秒
limit: 30,
// 設定響應限速標頭
// 預設為 false
headers: true,
// 密鑰生成函數,預設如下
key: (req) => {
return req.headers["x-forwarded-for"] ||
req.connection.remoteAddress ||
req.socket.remoteAddress ||
req.connection.socket.remoteAddress;
},
// StoreFactory: CustomStore
}
}
});
class CustomStore {
constructor(clearPeriod, opts) {
this.hits = new Map();
this.resetTime = Date.now() + clearPeriod;
setInterval(() => {
this.resetTime = Date.now() + clearPeriod;
this.reset();
}, clearPeriod);
}
/**
* 由 key 增加計數
*
* @param {String} key
* @returns {Number}
*/
inc(key) {
let counter = this.hits.get(key) || 0;
counter++;
this.hits.set(key, counter);
return counter;
}
/**
* 重設所有計數器
*/
reset() {
this.hits.clear();
}
}
etag 是 HTTP 協定的一種快取驗證機制[3]。它可以設為 false
、 true
、 weak
、 strong
或是一個客製化 Function
。更多請參閱 Code[4] 。
範例:
const ApiGateway = require("moleculer-web");
module.exports = {
mixins: [ApiGateway],
settings: {
// 服務層級選項
etag: false,
routes: [
{
path: "/",
// 路由層級選項
etag: true
}
]
}
}
範例:客製化 etag
Function
module.exports = {
mixins: [ApiGateway],
settings: {
// 服務層級選項
etag: (body) => generateHash(body)
}
}
注意,此方法無法用於串流響應,你必須改為自行產生
etag
。
範例:客製化串流用 etag
module.exports = {
name: "export",
actions: {
// 在瀏覽器響應檔案下載
downloadCSV(ctx) {
ctx.meta.$responseType = "text/csv";
ctx.meta.$responseHeaders = {
"Content-Disposition": `attachment; filename="data-${ctx.params.id}.csv"`,
"ETag": '<your etag here>'
};
return csvFileStream;
}
}
}
API 閘道器提供了一個實驗性的 HTTP2 功能。你可以在服務設定中透過 http2: true
來啟用它。
範例:
const ApiGateway = require("moleculer-web");
module.exports = {
mixins: [ApiGateway],
settings: {
port: 8443,
// HTTPS 伺服器憑證
https: {
key: fs.readFileSync("key.pem"),
cert: fs.readFileSync("cert.pem")
},
// 使用 HTTP2 伺服器
http2: true
}
});
你可以將 API 閘道器作為一個 middleware 放到 Express.js
應用中。
const svc = broker.createService({
mixins: [ApiService],
settings: {
server: false // 預設為 "true"
}
});
// 建立 Express 應用程式
const app = express();
// 使用 ApiGateway 作為 middleware
app.use("/api", svc.express());
// 監聽
app.listen(3000);
// 啟動伺服器
broker.start();
列出 API 閘道器的所有設定
settings: {
// 外部連接埠
port: 3000,
// 外部 IP
ip: "0.0.0.0",
// HTTPS 伺服器憑證
https: {
key: fs.readFileSync("ssl/key.pem"),
cert: fs.readFileSync("ssl/cert.pem")
},
// 使用伺服器實例。如果設為 null 它將會建立一個新的 HTTP(s)(2) 伺服器,
// 如果設為 false 則為作為 middleware 不啟動伺服器。
server: true,
// 外部全域基底路徑
path: "/api",
// 全域層級 middlewares
use: [
compression(),
cookieParser()
],
// 使用 'info' 層級來記錄請求的參數
logRequestParams: "info",
// 使用 'debug' 層級來記錄響應的資料
logResponseData: "debug",
// 使用 HTTP2 伺服器 (實驗性功能)
http2: false,
// 覆蓋 HTTP 伺服器的預設逾時
httpServerTimeout: null,
// 最佳化 route 與 別名路徑 (層數最深的優先)
optimizeOrder: true,
// 路由設定
routes: [
{
// 路由的基底路徑 (完整路徑: /api/admin )
path: "/admin",
// Actions 的白名單 (字串遮罩陣列或正規表達式)
whitelist: [
"users.get",
"$node.*"
],
// 在呼叫 Action 前會先呼叫 `this.authorize` 方法
authorization: true,
// 合併 `querystring` 、 `params` 及 `body` 參數
mergeParams: true,
// 路由層級 middlewares
use: [
helmet(),
passport.initialize()
],
// Action 別名
aliases: {
"POST users": "users.create",
"health": "$node.health"
},
// 映射策略
mappingPolicy: "all",
// 使用 bodyparser 模組
bodyParsers: {
json: true,
urlencoded: { extended: true }
}
},
{
// 路由的基底路徑 (完整路徑: /api )
path: "",
// Actions 的白名單 (字串遮罩陣列或正規表達式)
whitelist: [
"posts.*",
"file.*",
/^math\.\w+$/
],
// 無授權
authorization: false,
// Action 別名
aliases: {
"add": "math.add",
"GET sub": "math.sub",
"POST divide": "math.div",
"GET greeter/:name": "test.greeter",
"GET /": "test.hello",
"POST upload"(req, res) {
this.parseUploadedFile(req, res);
}
},
mappingPolicy: "restrict",
// 使用 bodyparser 模組
bodyParsers: {
json: false,
urlencoded: { extended: true }
},
// 呼叫選項
callOptions: {
timeout: 3000,
retries: 3,
fallbackResponse: "Static fallback response"
},
// 呼叫前的 Hook `broker.call`
onBeforeCall(ctx, route, req, res) {
ctx.meta.userAgent = req.headers["user-agent"];
},
// 呼叫後的 Hook `broker.call`
onAfterCall(ctx, route, req, res, data) {
res.setHeader("X-Custom-Header", "123456");
return data;
},
// 路由錯誤處理
onError(req, res, err) {
res.setHeader("Content-Type", "text/plain");
res.writeHead(err.code || 500);
res.end("Route error: " + err.message);
}
}
],
// 靜態檔案設定
assets: {
// assets 靜態檔案的根目錄路徑
folder: "./examples/www/assets",
// `serve-static` 模組的選項
options: {}
},
// 全域錯誤處理
onError(req, res, err) {
res.setHeader("Content-Type", "text/plain");
res.writeHead(err.code || 500);
res.end("Global error: " + err.message);
}
}
addRoute
方法可以用來新增或取代一個路由。例如你可以由混和函數來呼叫它並且定義一個新的路由 (例如: swagger route
、 graphql route
等) 。
removeRoute
方法可以根據路徑來移除路由。(例如: this.removeRoute("/admin");
)
Moleculer Web 官方還建立了許多的情境的範例,提供給開發者作為參考:
https://moleculer.services/docs/0.14/moleculer-web.html#Examples
[1] Moleculer Runner, https://moleculer.services/docs/0.14/runner.html
[2] JWT Example, https://github.com/moleculerjs/moleculer-web/blob/master/examples/full/index.js#L322
[3] HTTP ETag, https://en.wikipedia.org/wiki/HTTP_ETag
[4] Auto generate ETag & freshness check, https://github.com/moleculerjs/moleculer-web/pull/92