使用 NodeJS 的 WebSocket REST API — Express.js 风格

正如我们在之前的文章(HTTP REST API 和 WebSocket REST API 之间的性能差异)中看到的,当我们谈论响应时间、服务器资源和 Internet 带宽时,HTTP 和 WebSocket REST API 之间的差异非常大。 结合在客户端和服务器之间建立双向通信通道的能力,WebSockets 成为我们武器库中非常强大的工具。

目标

到目前为止,我们缺少的是一些可以帮助我们使用 WebSockets 组织工作的架构风格或框架。 像 Express.js 这样熟悉的东西将是一个很大的优势,因为我们将能够为我们的新一代应用程序和服务创建快速的 WebSocket REST API,而且还可以维护旧版软件所需的旧 HTTP REST API,两者都来自 相同的 NodeJS 实例。 朝这个方向思考促使我创建了一个非常小的框架,可以帮助我实现 Express.js 为我们提供的一些功能。 所以我为这个迷你项目设定的目标是:

  • 创建路由器类
  • 创建“使用”功能
  • 创建“get”、“post”、“put”和“delete”函数
  • 能够使用 URI 参数
  • 使用相同样式的响应(使用标准 HTTP 代码)
  • 为了能够使用它进行版本控制

一些代码的解释

完整的项目可以在这里找到。 下面我们可以看到项目文件夹结构是如何设置的:

使用 NodeJS 的 WebSocket REST API — Express.js 风格

所以我们有一个“endpoints”文件夹,其中包含 API 端点的“v1”和“v2”。 在它们中的每一个中,我们都有 index.js 文件,它应该包含特定版本的所有文件端点(在这种情况下只有 books.js)。 “endpoints”文件夹还包含一个 versions.js 文件,其中加载了两个版本的索引。 最后,API 端点加载到根目录index.js 文件中

/models/response.model.js

class Response {
    uri;
    method;
    socket;

    constructor(method, uri, socket) {
        this.method = method;
        this.uri = uri;
        this.socket = socket;
    }

    async send(response, status = 200) {
        await this.socket.send(JSON.stringify({
            "method": this.method,
            "uri": this.uri,
            "response": response,
            "status": status, 
            "timestamp": Date.now(),
        }));
    } 
}

module.exports = Response;

在这里,我们定义了我们框架的 Response 对象的外观。 在构造函数中,我们传递了请求的“method”、“uri”和“socket”。 我们还为这个类创建了一个方法,用于生成答案并将其发送给客户端。

/services/web_socket_router.service.js

const Response = require("../models/response.model");

class WSRouter {
    URI_DELIMITER = "/";
    PARAM_DELIMITER = ":";
    
    #endpointMap = {
        "get": {},
        "post": {},
        "put": {},
        "delete": {}
    };

    constructor() {}

    getEndpoints() { return this.#endpointMap; }

    get(uri, callback) {
        this.setEndpoint("get", uri, callback);
    }
    
    post(uri, callback) {
        this.setEndpoint("post", uri, callback);
    }

    put(uri, callback) {
        this.setEndpoint("put", uri, callback);
    }

    delete(uri, callback) {
        this.setEndpoint("delete", uri, callback);
    } 

    setEndpoint(method, uri, callback) {
        var uriArray = uri.split(this.URI_DELIMITER);
        var filteredUriArray = uriArray.filter(function(value, index, array) {
            return value != "" && value != null;
        });
        var pointer = this.#endpointMap[method];
        for(var idx=0; idx<filteredUriArray.length; idx++) {
            if(pointer[filteredUriArray[idx]] == null) {
                pointer[filteredUriArray[idx]] = {};
            }
            pointer = pointer[filteredUriArray[idx]];
        }
        pointer["_callback"] = callback;
    }

    use(path, router) {
        // 拆分路径(/api/ 到 ["", "api"] 之类的东西)
        var pathArray = path.split(this.URI_DELIMITER);
        // 从路径数组中删除空值或 null 值,以便留下 ["api"]
        var filteredPathArray = pathArray.filter(function(value, index, array) {
            return value != "" && value != null;
        });
        // 获取子路由器路径
        var requestTypes = router.getEndpoints();
        for(let type in requestTypes) {
            // 指向当前路由器类型根(例如指向“get”方法 URI)
            let pointer = this.#endpointMap[type];
            // 遍历所有路径元素并将它们添加到地图中; 结果应该是这样的。#endpointMap["get"]["api"]
            for(let idx=0; idx<filteredPathArray.length; idx++) {
                if(pointer[filteredPathArray[idx]] == null) {
                    pointer[filteredPathArray[idx]] = {};
                }
                pointer = pointer[filteredPathArray[idx]];
            }
            // 将子路由器添加到当前类型和路径 
            // ex. requestTypes["get"] = { "v1": { "book": { "paramsNameList": [], "callback": function } } }
            // 将导致 this.#endpointMap["get"] = { "api" : { "v1": { "book": { "paramsNameList": [], "callback": function } } } }
            for(let endpoint in requestTypes[type]) {
                pointer[endpoint] = requestTypes[type][endpoint];
            }
        }
    }

    async execute(request, socket) {
        try {
            var req = JSON.parse(request.toString());
            var uri = req.path;
            var type = req.type.toLowerCase();
            var resObj = new Response(type, uri, socket);
            // Split path
            var pathArray = uri.split(this.URI_DELIMITER);
            // 从路径数组中删除空值或 null 值
            var filteredPathArray = pathArray.filter(function(value, index, array) {
                return value != "" && value != null;
            });
            try {
                // 获取回调和url参数
                var pointerData = this.#helperFunction(this.#endpointMap[type], filteredPathArray, 0);
                // 到达Path的尽头后,我们检查是否有回调。
                if(typeof pointerData.pointer._callback === 'function') {
                    var reqObj = {
                        "oroginal": req,
                        "params": pointerData.params,
                    };
                    await pointerData.pointer._callback(reqObj, resObj);
                    return;
                }
                throw Error();
            } catch(err) {
                resObj.send("Not found, sorry!", 404);
            }
        } catch(err) {
            var res = new Response(null, null, socket);
            res.send(request.toString, 400);
        }
    }

    // 此函数递归地为 URI 找到最合适的匹配项
    #helperFunction(pointer, path, idx) {
        if(path[idx] == null) {
            return null;
        }
        if(pointer[path[idx]] != null) {
            if(path.length-1 == idx) {
                return { "pointer": pointer[path[idx]], "params": {} };
            }
            return this.#helperFunction(pointer[path[idx]], path, idx+1);
        }
        for(let prop in pointer) {
            if(prop[0] == this.PARAM_DELIMITER) {
                if(path.length-1 == idx) {
                    var params = {};
                    params[prop.slice(1)] = path[idx];
                    return { "pointer": pointer[prop], "params": params };
                }
                let newPointer = this.#helperFunction(pointer[prop], path, idx+1);
                if(newPointer != null) {
                    newPointer.params[prop.slice(1)] = path[idx];
                    return newPointer;
                }
            }
        }
        return null;
    }
}

module.exports = WSRouter;
view raw

这是我们定义主要方法的迷你框架的核心——getpostputdelete 和 use。 我们还设置了一个辅助函数(#helperFunction),用于递归搜索我们拥有的路径并为它们设置正确的参数。 这用于异步功能 – “execute”。

/endpoints/v2/books.js

var wsRouter = require('../../services/web_socket_router.service');
var router = new wsRouter();

router.get("/", async function(req,res) {
    res.send(req);
});

router.get("/endpoint/:param", async function(req,res) {
    res.send(req);
});

router.get("/:isbn", async function(req,res) {
    res.send(req);
});

router.get('/:author/:publisher', async function (req,res) {
    res.send(req);
})

router.get('/:author/:publisher/:year/:stock', async function (req,res) {
    res.send(req);
})

router.post("/", async function(req,res) {
    res.send(req);
});

router.put("/:isbn", async function(req,res) {
    res.send(req);
});

router.delete("/:isbn", async function(req,res) {
    res.send(req);
});

module.exports = router;

这是最终端点外观的示例,它与 express.js 的外观完全相同。

/endpoints/v2/index.js

var wsRouter = require('../../services/web_socket_router.service');
var router = new wsRouter();

router.use("/books", require("./books.js"));

router.use("/:userId/books", require("./books.js"));

module.exports = router;

这是一个带有“use”功能的示例,同样类似于 express.js 样式。

/endpoitns/versions.js

var wsRouter = require('../services/web_socket_router.service');
var router = new wsRouter();

router.use("/v1", require("./v1/index.js"));
router.use("/v2", require("./v2/index.js"));

module.exports = router;

这与上述相同。 只是另一层……

/index.js

const wsRouter = require("./services/web_socket_router.service");
const WebSocket = require('ws');
const ws = new WebSocket.Server({ port: 7071 });

var router = new wsRouter();
var wsApi = require('./endpoints/versions.js');
router.use("/api", wsApi);

ws.on('connection', (socket) => {
    // Init recources if needed
    // ...
    socket.on('message', async (request) => {
        await router.execute(request, socket);
    });
    socket.on('close', (msg) => {
        // 如果需要,清除资源
    });
});

根 index.js 正在加载 WebSocket REST API 并打开套接字服务器以侦听传入消息。


总结

通过一些工作,我们可以实现接近甚至等同于 express.js 框架的架构结构。 这将帮助我们更快地以熟悉的方式创建 WebSocket REST API,以便我们的应用程序和服务利用它们之间更快的交互。

 

免责声明:
1.本站所有内容由本站原创、网络转载、消息撰写、网友投稿等几部分组成。
2.本站原创文字内容若未经特别声明,则遵循协议CC3.0共享协议,转载请务必注明原文链接。
3.本站部分来源于网络转载的文章信息是出于传递更多信息之目的,不意味着赞同其观点。
4.本站所有源码与软件均为原作者提供,仅供学习和研究使用。
5.如您对本网站的相关版权有任何异议,或者认为侵犯了您的合法权益,请及时通知我们处理。
火焰兔 » 使用 NodeJS 的 WebSocket REST API — Express.js 风格