ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

node-restify 实战:用 TODO 示例应用掌握 restify 服务端、客户端与测试的完整工程结构

node-restify 实战:用 TODO 示例应用掌握 restify 服务端、客户端与测试的完整工程结构 后端【免费下载链接】node-restifyThe future of Node.js REST development项目地址https://gitcode.com/gh_mirrors/no/node-restify点击查看免费下载导读本文以 examples/todoapp 示例应用为主体系统讲解如何使用 restify 搭建一个结构清晰、可直接运行的 REST API从启动入口、命令行参数、服务器组装formatters / pre 插件 / 常用插件链 / 路由 / 审计日志到基于 restify-clients 的高层 SDK 封装再到基于 UNIX Domain Socket 的单元测试策略。读完本文你将掌握 restify 项目的推荐工程组织方式并能在自己的项目中复刻这套server client wrapper tests的标准骨架。这个示例应用是什么todoapp是一个刻意保持小巧的 TODO REST API 示例它使用 restify 组件的合理子集来演示如何组织一个 restify 应用。它的核心特征如下提供基于 JSON 的 TODO 增删改查CRUD接口TODO 数据以本地文件形式存储在文件系统上每个 TODO 是一个 JSON 文件代码中带有大量注释便于理解每一部分的作用刻意保持最小化大部分逻辑集中在 examples/todoapp/lib/server.js 中——真实项目中你通常会按模块拆分到多个文件但作为教学示例集中在一个文件更利于阅读。示例应用包含了什么应用由三个层次组成对应 restify 项目的典型组织方式一个服务端应用examples/todoapp/lib/server.js完成所有 API 功能一个客户端封装examples/todoapp/lib/client.js在 restify-clients 之上包一层高层 SDK把 HTTP 细节隐藏起来让使用者面向业务方法编程一组单元测试examples/todoapp/test/todo.test.js使用 Mocha Chai演示如何对 restify 服务进行单元测试。作者在 README 中特别解释了客户端封装的设计理念通常有一个服务端应用做 API 工作然后把 restify 客户端包装成适合用户编码的高层 SDK在这个层面刻意隐藏 HTTP 细节让整个系统更容易协作。入口文件与模块组织examples/todoapp/main.js 是命令行入口examples/todoapp/lib/index.js 统一导出createClient与createServer两个工厂函数业务代码通过它们组装应用module.exports { createClient: require(./client).createClient, createServer: require(./server).createServer };依赖清单见 examples/todoapp/package.json核心依赖为restify、restify-clients、restify-errors、pino、assert-plus、posix-getopt开发依赖为mocha、chai、pino-pretty。如何运行这个应用安装依赖示例自带package.json因此需要先安装依赖$ npm install启动并查看审计日志启动并让终端同时显示审计日志使用$ node main.js 21 | npx pino-pretty如果希望看到 restify 内置的全部跟踪日志更详细的日志级别可以追加-vv$ node main.js -vv 21 | npx pino-pretty默认情况下程序把数据写入/tmp目录可以用-d参数覆盖。默认不要求认证可以通过以下命令开启 Basic Auth$ node main.js -u admin -z secret 21 | npx pino-pretty关于21 | npx pino-pretty的说明README 特别强调生产环境中不应该把输出管道到 pino-pretty CLI而应保留审计记录的原始形式便于后续加工和分析。与所有 UNIX 程序的惯例一致这个示例把信息性消息写到stderr把audit记录写到stdout具体如何重定向由使用者自行决定。完整命令行参数解析通过 examples/todoapp/main.js 中的 POSIX getopt 解析逻辑可以梳理出全部参数参数含义说明-d dir数据存储目录默认/tmp最终数据库目录为dir/todos启动时会自动创建-p port监听端口默认8080-u user开启认证时的用户名与-z一起传入才会启用认证-z password认证密码与-u配合使用-v提高日志级别可重复使用如-vv、-vvv级别会逐级提升至 TRACE当级别低于 DEBUG 时会为日志追加源码位置信息src: true-h打印 usage 帮助打印后退出其中-v的实现比较巧妙每出现一次-v就把日志级别数值下调 10pino 中数值越小级别越详细同时用Math.max(trace, ...)保证永远不会低于 TRACE。启动时 main.js 还会做两件事解析参数后创建数据库目录若已存在则忽略EEXIST错误然后调用todo.createServer()组装服务并通过server.listen()开始监听监听成功后打印listening at url。服务端源码解析restify 应用的组装骨架examples/todoapp/lib/server.js 中的createServer(options)函数是整个示例的核心完整展示了 restify 服务端的最佳组装顺序。自定义错误类型示例使用restify-errors的makeConstructor定义了三类业务错误server.jsvar MissingTaskError errors.makeConstructor(MissingTaskError, { statusCode: 409, restCode: MissingTask, message: task is a required parameter }); var TodoExistsError errors.makeConstructor(TodoExistsError, { statusCode: 409, restCode: TodoExists, message: Todo already exists }); var TodoNotFoundError errors.makeConstructor(TodoNotFoundError, { statusCode: 404, restCode: TodoNotFound, message: Todo was not found });这样既保留了标准 HTTP 状态码409 冲突、404 未找到又通过restCode提供了可机器识别的业务错误码是 restify 错误处理的标准姿势。自定义 Formatter扩展 content-typerestify 允许为任意 content-type 注册自定义 formatter。server.js 中定义了一个演示性的自定义类型application/todo——实际上等同于text/plain只是在对象中优先取出task字段function formatTodo(req, res, body, cb) { if (body instanceof Error) { res.statusCode body.statusCode || 500; body body.message; } else if (typeof body object) { body body.task || JSON.stringify(body); } else { body body.toString(); } res.setHeader(Content-Length, Buffer.byteLength(body)); return cb(null, body); }注册方式是在restify.createServer的formatters选项中给出并带上 q 值以影响内容协商优先级var server restify.createServer({ formatters: { application/todo; q0.9: formatTodo }, log: options.log, name: todoapp, version: 1.0.0 });这里q0.9表明该类型在协商中的权重略低于默认的 JSONq1.0。这解释了后面 curl 示例中出现的现象当curl发送Accept: */*时服务器在多个可协商类型中按 q 值择优响应详见下文 curl 演示。version: 1.0.0使所有路由默认声明版本 1.0.0。pre 阶段请求进入路由前的预处理示例依次注册了三个pre插件在路由前执行// 确保上传数据不丢失 server.pre(restify.plugins.pre.pause()); // 清理不规范的路径如 //todo//////1// server.pre(restify.plugins.pre.sanitizePath()); // 处理麻烦的 User-Agent如 curl server.pre(restify.plugins.pre.userAgentConnection());pause()暂停请求流直到路由处理就绪防止上传数据在 handler 挂载前被丢弃sanitizePath()将多个连续斜杠折叠为单个避免路由匹配失败userAgentConnection()为部分不按规范设置Connection头的客户端典型的如 curl做兼容处理。这些插件的导出见 lib/plugins/index.js 中的pre命名空间。中间件链restify 插件全家桶随后通过server.use()挂载了一串常用插件server.js// 为每个请求设置带 requestid 的 pino logger server.use(restify.plugins.requestLogger()); // 允许每个 IP 每秒 5 个请求突发上限 10 server.use( restify.plugins.throttle({ burst: 10, rate: 5, ip: true }) ); // 常用插件组合 server.use(restify.plugins.acceptParser(server.acceptable)); server.use(restify.plugins.dateParser()); server.use(restify.plugins.authorizationParser()); server.use(restify.plugins.queryParser()); server.use(restify.plugins.gzipResponse()); server.use(restify.plugins.bodyParser());各插件职责如下requestLogger()注入带请求 ID 的 pino 子日志throttle()限流示例配置为每 IP 每秒 5 次、突发 10 次acceptParser(server.acceptable)解析Accept头若请求的媒体类型不在服务器支持列表内则返回 406。从 lib/plugins/accept.js 的源码可以看到它通过req.accepts()判断不接受时直接next(NotAcceptableError)其中server.acceptable正是由已注册 formatter 推导出的可响应类型数组dateParser()解析日期相关请求头authorizationParser()解析Authorization头HTTP Basic 等解析结果挂在req.authorization上供后续认证 handler 使用queryParser()解析查询字符串到req.querygzipResponse()支持 gzip 压缩响应bodyParser()根据 Content-Type 解析请求体到req.body支持 JSON、表单等。认证逻辑基于 req 上下文的 Basic Auth自定义中间件setup与authenticate实现了可选的 Basic Authserver.jsserver.use(function setup(req, res, next) { req.dir options.directory; if (options.user options.password) { req.allow { user: options.user, password: options.password }; } next(); }); server.use(authenticate);authenticate的逻辑如果req.allow未设置则直接跳过认证否则从req.authorization.basic取出凭据缺失时返回401 Unauthorized并附带WWW-Authenticate: Basic realmtodoapp头凭据不匹配时返回403 Forbidden。这种把期望凭据放在 req 上下文、由前置 handler 注入的模式让认证逻辑可以被复用和测试。业务路由一套完整的 CRUD 设计路由设计体现了 restify 的中间件复用技巧server.jsserver.use(loadTodos); // 全局加载 TODO 列表 server.post(/todo, createTodo); // 创建 server.get(/todo, listTodos); // 列表 server.head(/todo, listTodos); // HEAD 列表 server.use(ensureTodo); // 此后的路由都要求 TODO 存在 server.get(/todo/:name, getTodo); // 查询单个 server.head(/todo/:name, getTodo); // HEAD 单个 server.put({ path: /todo/:name, contentType: application/json }, putTodo); // 整体覆盖 server.del(/todo/:name, deleteTodo); // 删除单个 server.del(/todo, deleteAll, respond); // 删除全部 server.get(/, root); // 返回路由清单要点loadTodos在use阶段就把目录下所有 TODO 文件读入req.todos后续所有 handler 无需重复读取ensureTodo作为守卫中间件放在中间保证:name路由之前的 TODO 一定存在不存在则返回 404put路由通过{ path, contentType: application/json }对象形式声明强制要求请求体是 JSON否则返回 415根路由/返回一个硬编码的路由清单数组方便快速浏览 API 面createTodo演示了req.params的混合来源既可以从 URL 查询串POST /todo?namefoo也可以从 JSON body{task: get milk}取参数。getTodo还特意演示了 restify 的流式响应能力当客户端AcceptJSON 时直接用fs.createReadStream把文件管道到res期间手动设置Content-Type与writeHead(200)——这正是 restify可以直接使用原始 Node HTTP 对象的体现。当然使用原始 Node API 时内容协商需要自己处理示例通过req.accepts(json)判断。审计日志通过 after 事件挂载示例在服务尾部通过after事件挂载审计日志server.jsif (!options.noAudit) { server.on( after, restify.auditLogger({ body: true, log: pino({ level: info, name: todoapp-audit }) }) ); }注意审计日志插件不是用.use()注册而是监听服务器的after事件详见 lib/plugins/audit.js 的 JSDoc 示例body: true表示把请求体也写入审计记录。noAudit选项允许测试环境关闭审计避免测试输出噪音。客户端封装从 HTTP 到业务 SDKexamples/todoapp/lib/client.js 演示了 restify-clients 的高层封装模式。TodoClient构造函数内部创建一个 JSON clientclients.createJSONClient支持通过socketPath或url连接并透传version默认~1.0用于版本协商this.client clients.createJSONClient({ log: options.log, name: TodoClient, socketPath: options.socketPath, url: options.url, version: ver });若传入username和password会自动调用this.client.basicAuth()注入 Basic Auth 凭据。随后把 HTTP 动词封装成业务方法client.jscreate(task, cb)→POST /todo自动把{ task }序列化为 JSONlist(cb)→GET /todoget(name, cb)→GET /todo/:nameupdate(todo, cb)→PUT /todo/:name直接传对象del(name, cb)→DELETE /todo/:name不传 name 时删除全部。toString()方法输出[object TodoClienturl..., username..., version...]便于调试。这套模式的价值在于调用方完全不接触 HTTP 细节只面对create/list/get/update/del这样的业务动词。单元测试策略UNIX Domain Socket 方案examples/todoapp/test/todo.test.js 展示了两种测试 restify 服务的思路在单元测试中把服务跑在 UNIX Domain Socket 上本示例采用或让测试依赖一个已运行的服务端点通过环境变量传入本示例未采用。测试在before钩子中完成组装todo.test.js创建临时目录/tmp/.todo_unit_test用todo.createServer({ directory, log, noAudit: true })组装服务server.listen(SOCK)监听 UNIX Socket/tmp/.todo_sock随后创建客户端并通过socketPath连接。测试用例覆盖完整 CRUD 生命周期初始列表为空创建一个任务校验返回的name与task列表长度为 1并可get取回update修改任务内容after钩子中删除全部 TODO、关闭客户端与服务、清理临时目录。用npm test即可运行package.json 中test: mocha test。这套服务器监听 socket 客户端走 socketPath的测试模式让测试无需占用真实端口也无需外部服务依赖。完整 curl 实战演练先以开启认证的方式启动全功能实例$ node main.js -u admin -z secret 21 | npx pino-pretty同时建议安装json工具因为下面的示例都通过它格式化输出。列出全部路由$ curl -isS http://127.0.0.1:8080 | json HTTP/1.1 200 OK Content-Type: application/todo Content-Length: 127 Date: Sat, 29 Dec 2012 23:05:05 GMT Connection: keep-alive [ GET /, POST /todo, GET /todo, DELETE /todo, PUT /todo/:name, GET /todo/:name, DELETE /todo/:name ]注意这里响应Content-Type是application/todo这正是自定义 formatter 生效的结果。列出 TODO空列表$ curl -isS http://127.0.0.1:8080/todo | json HTTP/1.1 200 OK Content-Type: application/todo Content-Length: 2 Date: Sat, 29 Dec 2012 23:07:05 GMT Connection: keep-alive []创建 TODO$ curl -isS http://127.0.0.1:8080/todo -X POST -d namedemo -d taskbuy milk HTTP/1.1 201 Created Content-Type: application/todo Content-Length: 8 Date: Sat, 29 Dec 2012 23:08:04 GMT Connection: keep-alive buy milk这里返回的是201 Created响应体经过formatTodo处理只保留了task字段。值得一提的是由于服务器为application/todo设置了q0.9的协商权重而 curl 默认发送Accept: */*服务器会按 q 值优先选择该自定义类型作为响应格式——这正是 q 值参与内容协商的直观体现README 中原文提到的响应类型即此自定义类型。列表$ curl -isS http://127.0.0.1:8080/todo | json HTTP/1.1 200 OK Content-Type: application/todo Content-Length: 8 Date: Sat, 29 Dec 2012 23:09:45 GMT Connection: keep-alive [ demo ]获取单个 TODO本示例服务使用了流式响应并且这里显式要求 JSON$ curl -isS http://127.0.0.1:8080/todo/demo | json HTTP/1.1 200 OK Content-Type: application/json Date: Sat, 29 Dec 2012 23:11:19 GMT Connection: keep-alive Transfer-Encoding: chunked { name: demo, task: buy milk }Transfer-Encoding: chunked正是流式读取文件并pipe到响应的特征。同时服务器仍通过另一种方式支持完整的内容协商——显式指定Accept: application/todo$ curl -isS -H accept:application/todo http://127.0.0.1:8080/todo/demo HTTP/1.1 200 OK Content-Type: application/todo Content-Length: 8 Date: Sat, 29 Dec 2012 23:14:31 GMT Connection: keep-alive buy milk删除 TODO$ curl -isS -X DELETE http://127.0.0.1:8080/todo/demo HTTP/1.1 204 No Content Date: Sat, 29 Dec 2012 23:15:50 GMT Connection: keep-alive删除成功返回204 No Content。此外还可以通过DELETE /todo不带 name一次删除全部 TODO。小结与工程启示这个 TODO 示例虽然代码量不大却浓缩了 restify 工程化的关键实践分层组织main.js进程入口→lib/server.js服务组装→lib/client.js高层 SDK模块边界清晰插件装配顺序prepause / sanitizePath / userAgentConnection→use公共插件链 → 业务中间件认证、数据加载→ 路由每一步职责单一内容协商通过自定义 formatter q 值实现任意媒体类型响应且兼容流式输出错误处理用 restify-errors 定义带restCode的业务错误HTTP 状态码与业务码分离可测试性借助 UNIX Domain Socket 让单元测试无需端口与外部依赖测试与生产共用同一套createServer/createClient工厂。在真实项目中你可以把lib/server.js按路由模块拆分将createClient封装发布为 SDK并为每个业务错误补充更细粒度的restCode——这套骨架已经为规模化演进预留了清晰的方向。赞分享后端【免费下载链接】node-restifyThe future of Node.js REST development项目地址https://gitcode.com/gh_mirrors/no/node-restify点击查看免费下载相关推荐如何用restify-clients构建REST客户端API消费端完整实战教程如何用restify clients构建REST客户端API消费端完整实战教程 在 node restify 生态中 restify clients 是构建后端restify 客户端指南JsonClient / StringClient / HttpClient 的使用与实战restify 客户端指南JsonClient / StringClient / HttpClient 的使用与实战 本篇技术指南以 docs/guides/后端Overleaf filestore 深度解析面向 S3/GCS/本地盘的对象存储代理 API 与 PDF 转换实现Overleaf filestore 深度解析面向 S3/GCS/本地盘的对象存储代理 API 与 PDF 转换实现 Overleaf 的 filestore后端上一篇抖音视频下载完整教程douyin-downloader 三步搬空一个作者主页下一篇【亲测免费】 Angular 中文文档开源项目推荐创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表