使用 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 代码)
- 为了能够使用它进行版本控制
一些代码的解释
完整的项目可以在这里找到。 下面我们可以看到项目文件夹结构是如何设置的:
所以我们有一个“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
这是我们定义主要方法的迷你框架的核心——get、post、put、delete 和 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,以便我们的应用程序和服务利用它们之间更快的交互。
相关文章
构建 MongoDB REST API
发布时间:2023/04/20 浏览次数:153 分类:MongoDB
-
MongoDB 没有成熟的 REST 接口,因为服务器使用本机二进制协议来提高效率。 但是,各种应用程序都允许创建 MongoDB REST API。
Node.js 中的 HTTP 发送 POST 请求
发布时间:2023/03/27 浏览次数:187 分类:Node.js
-
在本文中,我们将学习如何使用 Node.js 使用第三方包发出发送 post 请求。