iT邦幫忙

2022 iThome 鐵人賽

DAY 24
1
Software Development

Moleculer 家家酒系列 第 24

Day 24 : API 閘道器 - Part 1

  • 分享至 

  • xImage
  •  

API 閘道器 - Part 1

目前為止,我們已經學會了 Moleculer 的主要功能,接著要來介紹相關的模組工具。

首先今天要介紹的是最重要的模組 moleculer-web ,它是針對 Moleculer 框架所開發的官方 API 閘道器服務。你可以透過它來發布 RESTful API 架構風格的服務。

功能

  • 它支援 HTTP 與 HTTPA
  • 靜態檔案
  • 多路由
  • 類連接 middlewares
  • 支援別名
  • 白名單
  • 多種 Body 解析
  • CORS 標頭
  • 限速器
  • 前後處理呼叫 Hooks
  • Buffer 與 串流處理
  • Middleware 模式

安裝

npm install moleculer-web

使用

範例:快速使用

使用預設值啟動 API 閘道器服務之後,你可以在 http://localhost:3000/ 存取所有的服務(包含內部 $node )。

const { ServiceBroker } = require("moleculer");
const ApiService = require("moleculer-web");

const broker = new ServiceBroker();

// 讀取 API 閘道器
broker.createService(ApiService);

// 啟動服務
broker.start();

範例路徑:

  • 呼叫 test.hello Action: http://localhost:3000/test/hello
  • 呼叫 math.add Action 並夾帶參數: http://localhost:3000/math/add?a=25&b=13
  • 取得節點健康資訊: http://localhost:3000/~node/health
  • 列出 Actions 清單: http://localhost:3000/~node/actions

白名單

如果你不想要將所有的 Actions 都公開,你可以使用白名單選項來過濾它。你也可以使用正規表達式來匹配,要允許所有 Actions 請使用 **

broker.createService({
    mixins: [ApiService],

    settings: {
        routes: [{
            path: "/api",

            whitelist: [
                // 訪問 `posts` 服務中所有的 Actions
                "posts.*",
                // 僅訪問 `users.list` Action
                "users.list",
                // 訪問 `math` 服務中所有的 Actions
                /^math\.\w+$/
            ]
        }]
    }
});

別名

你可以使用別名來替代 Action 名稱,然後再指定對應的方法。如果沒有指定方法則會處理所有類型的方法。別名內也可以加入參數,意思就是你可以定義冒號的參數名稱 (:name)。

broker.createService({
    mixins: [ApiService],

    settings: {
        routes: [{
            aliases: {
                // 使用 `GET /login` 或 `POST /login` 呼叫 `auth.login` Action
                "login": "auth.login",

                // 限制請求的方法
                "POST users": "users.create",

                // 加入 `name` 參數
                // 你可以在 Action 中透過 `ctx.params.name` 來取得它
                "GET greeter/:name": "test.greeter",
            }
        }]
    }
});

參數名稱是由 path-to-regexp [2] 套件處理的,所以你也可以使用 optional [3] 與 repeated [4] 參數設定方式。

API 閘道器有實作 listAliases 的 Action ,你可以利用它在 HTTP 端點列出 Actions 映射清單。

範例:你也可以建立 RESTful 風格的 API 。

broker.createService({
    mixins: [ApiService],

    settings: {
        routes: [{
            aliases: {
                "GET users": "users.list",
                "GET users/:id": "users.get",
                "POST users": "users.create",
                "PUT users/:id": "users.update",
                "DELETE users/:id": "users.remove"
            }
        }]
    }
});

範例:如果是 REST 路由風格,你可以使用簡單速記別名。

broker.createService({
    mixins: [ApiService],

    settings: {
        routes: [{
            aliases: {
                "REST users": "users"
            }
        }]
    }
});

如果使用該速記別名,你需要在服務建立對應的 Action,如: listgetcreateupdateremove

範例:你也可以在別名宣告客製化函數。在這種情況下,需要使用 function (req, res) {...}

注意, Moleculer 是使用 Node.js 的 HTTP Server [5]。

broker.createService({
    mixins: [ApiService],

    settings: {
        routes: [{
            aliases: {
                "POST upload"(req, res) {
                    this.parseUploadedFile(req, res);
                },
                "GET custom"(req, res) {
                    res.end('hello from custom handler')
                }
            }
        }]
    }
});

Moleculer 也額外加入了一些內部指標物件可以使用:

  • req.$ctx - 指向到請求的 context 。
  • req.$serviceres.$service - 指向到服務的實例。可以透過它來訪問 Broker req.$service.broker
  • req.$routeres.$route - 指向到解析的路由定義。
  • req.$params - 指向到解析的參數 (包含 query string 與 post body ) 。
  • req.$alias - 指向到解析的別名定義。
  • req.$action - 指向到解析的 Action 。
  • req.$endpoint - 指向到解析的 Action 端點。
  • req.$next - 如果請求是來自 Express.js 則指向到 next() 處理。

映射策略

除了指定的路由以外,還有一個 mappingPolicy 屬性規則來處理沒有設定別名的路由。

範例:只能請求 POST /add ,不能請求 /math.add/math/add

  • all - 允許所有的路由,無論是否有別名。(預設值)
  • restrict - 僅允許有設定別名的路由。
broker.createService({
    mixins: [ApiService],

    settings: {
        routes: [{
            mappingPolicy: "restrict",
            aliases: {
                "POST add": "math.add"
            }
        }]
    }
});

檔案上傳別名

API 閘道器也支援檔案上傳。你可以使用 multipart form data (由 busboy 套件支援[6] )或是原始的 body 請求來上傳檔案,但無論如何檔案都會被轉換成串流的 Action。在 multipart form data 模式下,你還可以上傳多個檔案。

範例:

api.service.js

const ApiGateway = require("moleculer-web");

module.exports = {
    mixins: [ApiGateway],
    settings: {
        path: "/upload",

        routes: [
            {
                path: "",

                aliases: {
                    // 由 HTML multipart form 上傳檔案
                    "POST /": "multipart:file.save",

                    // 由 AJAX 或 cURL 上傳檔案
                    "PUT /:id": "stream:file.save",

                    // HTML form 上傳檔案並覆蓋 busboy 設定
                    "POST /multi": {
                        type: "multipart",
                        // Action 層的 busboy 設定
                        busboyConfig: {
                            limits: { files: 3 }
                        },
                        action: "file.save"
                    }
                },

                // 路由層的 busboy 設定
                busboyConfig: {
                    limits: { files: 1 }
                    // 可以在這裡設定限制的事件:
                    // `onPartsLimit` 、 `onFilesLimit` 、 `onFieldsLimit`
                },

                mappingPolicy: "restrict"
            }
        ]
    }
});

Multipart 參數

你可以在 Actions 中使用以下的特定欄位來訪問由 multipart-form 傳遞的檔案參數:

  • ctx.params - 終端可讀的檔案串流。
  • ctx.meta.$params - 由網址得到的 query 參數。
  • ctx.meta.$multipart - 由 form-data 附加的文字欄位,必須在其它檔案欄位前發送。

自動別名

自動別名可以讓你在服務中宣告你的路由別名,再由閘道器從服務綱目來動態建構完整的路由。

每當服務加入或離開時,閘道器都會重新生成路由。

範例:使用白名單參數來指定服務,讓閘道器能追蹤並建立路由。

api.service.js

module.exports = {
    mixins: [ApiGateway],

    settings: {
        routes: [
            {
                path: "/api",

                whitelist: [
                    "v2.posts.*",
                    "test.*"
                ],

                aliases: {
                    "GET /hi": "test.hello"
                },

                autoAliases: true
            }
        ]
    }
};

posts.service.js

module.exports = {
    name: "posts",
    version: 2,

    settings: {
        // 基底服務路徑
        // rest: "posts/" // 可以設定需要改變的路徑
        // 原始的路徑為 "/api/v2/posts"
        // 將會被改為 "/api/posts"
    },

    actions: {
        list: {
            // 對外路徑為 "/api/v2/posts/"
            rest: "GET /",
            handler(ctx) { }
        },

        get: {
            // 對外路徑為 "/api/v2/posts/:id"
            rest: "GET /:id",
            handler(ctx) { }
        },

        create: {
            rest: "POST /",
            handler(ctx) { }
        },

        update: {
            rest: "PUT /:id",
            handler(ctx) { }
        },

        remove: {
            rest: "DELETE /:id",
            handler(ctx) { }
        }
    }
};

生成的別名

GET     /api/hi             => test.hello
GET     /api/v2/posts       => v2.posts.list
GET     /api/v2/posts/:id   => v2.posts.get
POST    /api/v2/posts       => v2.posts.create
PUT     /api/v2/posts/:id   => v2.posts.update
DELETE  /api/v2/posts/:id   => v2.posts.remove

範例:設定完整路徑

posts.service.js

module.exports = {
    name: "posts",
    version: 2,

    settings: {
        // 基底路徑
        rest: "posts/"
    },

    actions: {
        tags: {
            // 強制使用 "/tags" 路徑取代原本的 "/api/v2/posts/tags" 路徑
            rest: {
                method: "GET",
                fullPath: "/tags"
            },
            handler(ctx) {}
        }
    }
};

參數

API 閘道器會從網址查詢 queryparamsbody 並合併它們,然後將結果放在 req.$params

停用合併

你可以透過 mergeParams: false 設定來停用參數合併。在這種情況下,參數將被拆分開。

範例:

broker.createService({
    mixins: [ApiService],
    settings: {
        routes: [{
            path: "/",
            mergeParams: false
        }]
    }
});

未被合併的參數

{
    // query 參數
    query: {
        category: "general",
    }

    // body 參數
    body: {
        title: "Hello",
        content: "...",
        createdAt: 1530796920203
    },

    // params 參數
    params: {
        id: 5
    }
}

Query string 參數

相關資訊請參閱 qs[7] 。

範例:陣列參數

// GET /api/opt-test?a=1&a=2
a: ["1", "2"]

範例:物件與陣列

// GET /api/opt-test?foo[bar]=a&foo[bar]=b&foo[baz]=c
foo: { 
    bar: ["a", "b"], 
    baz: "c" 
}

Middlewares

支援類連結的 Middlewares ,分別為 global-levelroute-levelalias-level 三個層級。你可以建立 function(req, res, next) {...} 函數來使用它。更多請參閱 Express middlewares[8] 。

範例:

broker.createService({
    mixins: [ApiService],
    settings: {
        // 全域層 middlewares ,套用到所有路由
        use: [
            cookieParser(),
            helmet()
        ],

        routes: [
            {
                path: "/",

                // 路由層 middlewares
                use: [
                    compression(),
                    
                    passport.initialize(),
                    passport.session(),

                    serveStatic(path.join(__dirname, "public"))
                ],
                
                aliases: {
                    "GET /secret": [
                        // 別名層 middlewares
                        auth.isAuthenticated(),
                        auth.hasRole("admin"),
                        "top.secret" // 呼叫 `top.secret` action
                    ]
                }
            }
        ]
    }
});

使用 swagger-stats [9] 介面快速查看 API 的健康度 (使用 TypeScript) 。

import { Service, ServiceSchema } from "moleculer";
import ApiGatewayService from "moleculer-web";
const swStats = require("swagger-stats");

const swMiddleware = swStats.getMiddleware();

broker.createService({
    mixins: [ApiGatewayService],
    name: "gw-main",

    settings: {
        cors: {
            methods: ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"],
            origin: "*",
        },

        routes: [
            // ...
        ],

        use: [swMiddleware],
    },

    async started(this: Service): Promise<void> {
        this.addRoute({
            path: "/",
            use: [swMiddleware],
        });
    },
} as ServiceSchema);

錯誤處理

API 閘道器支援在 Middlewares 中使用錯誤處理。如果你拋出錯誤到 next(err) 函數的話,它將會呼叫帶有 errreqresnext 引數的錯誤處理 Middleware 。

broker.createService({
    mixins: [ApiService],
    settings: {
        use: [
            cookieParser(),
            helmet()
        ],

        routes: [
            {
                path: "/",

                use: [
                    compression(),

                    passport.initialize(),
                    passport.session(),

                    function (err, req, res, next) {
                        this.logger.error("Error is occured in middlewares!");
                        this.sendError(req, res, err);
                    }
                ],
            }
        ]
    }
});

靜態檔案服務

它以 serve-static 模組來提供像是 Express.js 的靜態路由服務。

broker.createService({
    mixins: [ApiService],

    settings: {
        assets: {
            // assets 靜態檔案的根目錄路徑
            folder: "./assets",

            // `serve-static` 模組的選項
            options: {}
        }
    }
});

參考文獻

[1] API Gateway, https://moleculer.services/docs/0.14/moleculer-web.html
[2] Path-to-RegExp, https://github.com/pillarjs/path-to-regexp
[3] Path-to-RegExp Optional, https://github.com/pillarjs/path-to-regexp#optional
[4] Path-to-RegExp Zero or more, https://github.com/pillarjs/path-to-regexp#zero-or-more
[5] Node.js HTTP Server, https://nodejs.org/api/http.html
[6] busboy
, https://github.com/mscdex/busboy
[7] qs, https://github.com/ljharb/qs
[8] Express Using middleware, https://expressjs.com/en/guide/using-middleware.html
[9] swagger-stats, https://swaggerstats.io/
[10] serve-static, https://github.com/expressjs/serve-static

家家酒小劇場

  • Otter - API 閘道器也是一個服務嗎?
  • Boxy - 對唷, API 閘道器本身也是一個微服務,如果你也使用 Runner 的話,就要將它放在 services 目錄下一起跑。
tags: 鐵人賽

上一篇
Day 23 : Moleculer Runner
下一篇
Day 25 : API 閘道器 - Part 2
系列文
Moleculer 家家酒31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言