戰(zhàn):用 TODO 示例應(yīng)用掌握 restify 服務(wù)端、客戶端與測(cè)試的完整工程結(jié)構(gòu))
后端【免費(fèi)下載鏈接】node-restifyThe future of Node.js REST development項(xiàng)目地址https://gitcode.com/gh_mirrors/no/node-restify點(diǎn)擊查看免費(fèi)下載導(dǎo)讀本文以 examples/todoapp 示例應(yīng)用為主體系統(tǒng)講解如何使用 restify 搭建一個(gè)結(jié)構(gòu)清晰、可直接運(yùn)行的 REST API從啟動(dòng)入口、命令行參數(shù)、服務(wù)器組裝formatters / pre 插件 / 常用插件鏈 / 路由 / 審計(jì)日志到基于 restify-clients 的高層 SDK 封裝再到基于 UNIX Domain Socket 的單元測(cè)試策略。讀完本文你將掌握 restify 項(xiàng)目的推薦工程組織方式并能在自己的項(xiàng)目中復(fù)刻這套server client wrapper tests的標(biāo)準(zhǔn)骨架。這個(gè)示例應(yīng)用是什么todoapp是一個(gè)刻意保持小巧的 TODO REST API 示例它使用 restify 組件的合理子集來演示如何組織一個(gè) restify 應(yīng)用。它的核心特征如下提供基于 JSON 的 TODO 增刪改查CRUD接口TODO 數(shù)據(jù)以本地文件形式存儲(chǔ)在文件系統(tǒng)上每個(gè) TODO 是一個(gè) JSON 文件代碼中帶有大量注釋便于理解每一部分的作用刻意保持最小化大部分邏輯集中在 examples/todoapp/lib/server.js 中——真實(shí)項(xiàng)目中你通常會(huì)按模塊拆分到多個(gè)文件但作為教學(xué)示例集中在一個(gè)文件更利于閱讀。示例應(yīng)用包含了什么應(yīng)用由三個(gè)層次組成對(duì)應(yīng) restify 項(xiàng)目的典型組織方式一個(gè)服務(wù)端應(yīng)用examples/todoapp/lib/server.js完成所有 API 功能一個(gè)客戶端封裝examples/todoapp/lib/client.js在 restify-clients 之上包一層高層 SDK把 HTTP 細(xì)節(jié)隱藏起來讓使用者面向業(yè)務(wù)方法編程一組單元測(cè)試examples/todoapp/test/todo.test.js使用 Mocha Chai演示如何對(duì) restify 服務(wù)進(jìn)行單元測(cè)試。作者在 README 中特別解釋了客戶端封裝的設(shè)計(jì)理念通常有一個(gè)服務(wù)端應(yīng)用做 API 工作然后把 restify 客戶端包裝成適合用戶編碼的高層 SDK在這個(gè)層面刻意隱藏 HTTP 細(xì)節(jié)讓整個(gè)系統(tǒng)更容易協(xié)作。入口文件與模塊組織examples/todoapp/main.js 是命令行入口examples/todoapp/lib/index.js 統(tǒng)一導(dǎo)出createClient與createServer兩個(gè)工廠函數(shù)業(yè)務(wù)代碼通過它們組裝應(yīng)用module.exports { createClient: require(./client).createClient, createServer: require(./server).createServer };依賴清單見 examples/todoapp/package.json核心依賴為restify、restify-clients、restify-errors、pino、assert-plus、posix-getopt開發(fā)依賴為mocha、chai、pino-pretty。如何運(yùn)行這個(gè)應(yīng)用安裝依賴示例自帶package.json因此需要先安裝依賴$ npm install啟動(dòng)并查看審計(jì)日志啟動(dòng)并讓終端同時(shí)顯示審計(jì)日志使用$ node main.js 21 | npx pino-pretty如果希望看到 restify 內(nèi)置的全部跟蹤日志更詳細(xì)的日志級(jí)別可以追加-vv$ node main.js -vv 21 | npx pino-pretty默認(rèn)情況下程序把數(shù)據(jù)寫入/tmp目錄可以用-d參數(shù)覆蓋。默認(rèn)不要求認(rèn)證可以通過以下命令開啟 Basic Auth$ node main.js -u admin -z secret 21 | npx pino-pretty關(guān)于21 | npx pino-pretty的說明README 特別強(qiáng)調(diào)生產(chǎn)環(huán)境中不應(yīng)該把輸出管道到 pino-pretty CLI而應(yīng)保留審計(jì)記錄的原始形式便于后續(xù)加工和分析。與所有 UNIX 程序的慣例一致這個(gè)示例把信息性消息寫到stderr把a(bǔ)udit記錄寫到stdout具體如何重定向由使用者自行決定。完整命令行參數(shù)解析通過 examples/todoapp/main.js 中的 POSIX getopt 解析邏輯可以梳理出全部參數(shù)參數(shù)含義說明-d dir數(shù)據(jù)存儲(chǔ)目錄默認(rèn)/tmp最終數(shù)據(jù)庫目錄為dir/todos啟動(dòng)時(shí)會(huì)自動(dòng)創(chuàng)建-p port監(jiān)聽端口默認(rèn)8080-u user開啟認(rèn)證時(shí)的用戶名與-z一起傳入才會(huì)啟用認(rèn)證-z password認(rèn)證密碼與-u配合使用-v提高日志級(jí)別可重復(fù)使用如-vv、-vvv級(jí)別會(huì)逐級(jí)提升至 TRACE當(dāng)級(jí)別低于 DEBUG 時(shí)會(huì)為日志追加源碼位置信息src: true-h打印 usage 幫助打印后退出其中-v的實(shí)現(xiàn)比較巧妙每出現(xiàn)一次-v就把日志級(jí)別數(shù)值下調(diào) 10pino 中數(shù)值越小級(jí)別越詳細(xì)同時(shí)用Math.max(trace, ...)保證永遠(yuǎn)不會(huì)低于 TRACE。啟動(dòng)時(shí) main.js 還會(huì)做兩件事解析參數(shù)后創(chuàng)建數(shù)據(jù)庫目錄若已存在則忽略EEXIST錯(cuò)誤然后調(diào)用todo.createServer()組裝服務(wù)并通過server.listen()開始監(jiān)聽監(jiān)聽成功后打印listening at url。服務(wù)端源碼解析restify 應(yīng)用的組裝骨架examples/todoapp/lib/server.js 中的createServer(options)函數(shù)是整個(gè)示例的核心完整展示了 restify 服務(wù)端的最佳組裝順序。自定義錯(cuò)誤類型示例使用restify-errors的makeConstructor定義了三類業(yè)務(wù)錯(cuò)誤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 });這樣既保留了標(biāo)準(zhǔn) HTTP 狀態(tài)碼409 沖突、404 未找到又通過restCode提供了可機(jī)器識(shí)別的業(yè)務(wù)錯(cuò)誤碼是 restify 錯(cuò)誤處理的標(biāo)準(zhǔn)姿勢(shì)。自定義 Formatter擴(kuò)展 content-typerestify 允許為任意 content-type 注冊(cè)自定義 formatter。server.js 中定義了一個(gè)演示性的自定義類型application/todo——實(shí)際上等同于text/plain只是在對(duì)象中優(yōu)先取出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); }注冊(cè)方式是在restify.createServer的formatters選項(xiàng)中給出并帶上 q 值以影響內(nèi)容協(xié)商優(yōu)先級(jí)var server restify.createServer({ formatters: { application/todo; q0.9: formatTodo }, log: options.log, name: todoapp, version: 1.0.0 });這里q0.9表明該類型在協(xié)商中的權(quán)重略低于默認(rèn)的 JSONq1.0。這解釋了后面 curl 示例中出現(xiàn)的現(xiàn)象當(dāng)curl發(fā)送Accept: */*時(shí)服務(wù)器在多個(gè)可協(xié)商類型中按 q 值擇優(yōu)響應(yīng)詳見下文 curl 演示。version: 1.0.0使所有路由默認(rèn)聲明版本 1.0.0。pre 階段請(qǐng)求進(jìn)入路由前的預(yù)處理示例依次注冊(cè)了三個(gè)pre插件在路由前執(zhí)行// 確保上傳數(shù)據(jù)不丟失 server.pre(restify.plugins.pre.pause()); // 清理不規(guī)范的路徑如 //todo//////1// server.pre(restify.plugins.pre.sanitizePath()); // 處理麻煩的 User-Agent如 curl server.pre(restify.plugins.pre.userAgentConnection());pause()暫停請(qǐng)求流直到路由處理就緒防止上傳數(shù)據(jù)在 handler 掛載前被丟棄sanitizePath()將多個(gè)連續(xù)斜杠折疊為單個(gè)避免路由匹配失敗userAgentConnection()為部分不按規(guī)范設(shè)置Connection頭的客戶端典型的如 curl做兼容處理。這些插件的導(dǎo)出見 lib/plugins/index.js 中的pre命名空間。中間件鏈restify 插件全家桶隨后通過server.use()掛載了一串常用插件server.js// 為每個(gè)請(qǐng)求設(shè)置帶 requestid 的 pino logger server.use(restify.plugins.requestLogger()); // 允許每個(gè) IP 每秒 5 個(gè)請(qǐng)求突發(fā)上限 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());各插件職責(zé)如下requestLogger()注入帶請(qǐng)求 ID 的 pino 子日志throttle()限流示例配置為每 IP 每秒 5 次、突發(fā) 10 次acceptParser(server.acceptable)解析Accept頭若請(qǐng)求的媒體類型不在服務(wù)器支持列表內(nèi)則返回 406。從 lib/plugins/accept.js 的源碼可以看到它通過req.accepts()判斷不接受時(shí)直接next(NotAcceptableError)其中server.acceptable正是由已注冊(cè) formatter 推導(dǎo)出的可響應(yīng)類型數(shù)組dateParser()解析日期相關(guān)請(qǐng)求頭authorizationParser()解析Authorization頭HTTP Basic 等解析結(jié)果掛在req.authorization上供后續(xù)認(rèn)證 handler 使用queryParser()解析查詢字符串到req.querygzipResponse()支持 gzip 壓縮響應(yīng)bodyParser()根據(jù) Content-Type 解析請(qǐng)求體到req.body支持 JSON、表單等。認(rèn)證邏輯基于 req 上下文的 Basic Auth自定義中間件setup與authenticate實(shí)現(xiàn)了可選的 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未設(shè)置則直接跳過認(rèn)證否則從req.authorization.basic取出憑據(jù)缺失時(shí)返回401 Unauthorized并附帶WWW-Authenticate: Basic realmtodoapp頭憑據(jù)不匹配時(shí)返回403 Forbidden。這種把期望憑據(jù)放在 req 上下文、由前置 handler 注入的模式讓認(rèn)證邏輯可以被復(fù)用和測(cè)試。業(yè)務(wù)路由一套完整的 CRUD 設(shè)計(jì)路由設(shè)計(jì)體現(xiàn)了 restify 的中間件復(fù)用技巧server.jsserver.use(loadTodos); // 全局加載 TODO 列表 server.post(/todo, createTodo); // 創(chuàng)建 server.get(/todo, listTodos); // 列表 server.head(/todo, listTodos); // HEAD 列表 server.use(ensureTodo); // 此后的路由都要求 TODO 存在 server.get(/todo/:name, getTodo); // 查詢單個(gè) server.head(/todo/:name, getTodo); // HEAD 單個(gè) server.put({ path: /todo/:name, contentType: application/json }, putTodo); // 整體覆蓋 server.del(/todo/:name, deleteTodo); // 刪除單個(gè) server.del(/todo, deleteAll, respond); // 刪除全部 server.get(/, root); // 返回路由清單要點(diǎn)loadTodos在use階段就把目錄下所有 TODO 文件讀入req.todos后續(xù)所有 handler 無需重復(fù)讀取ensureTodo作為守衛(wèi)中間件放在中間保證:name路由之前的 TODO 一定存在不存在則返回 404put路由通過{ path, contentType: application/json }對(duì)象形式聲明強(qiáng)制要求請(qǐng)求體是 JSON否則返回 415根路由/返回一個(gè)硬編碼的路由清單數(shù)組方便快速瀏覽 API 面createTodo演示了req.params的混合來源既可以從 URL 查詢串POST /todo?namefoo也可以從 JSON body{task: get milk}取參數(shù)。getTodo還特意演示了 restify 的流式響應(yīng)能力當(dāng)客戶端AcceptJSON 時(shí)直接用fs.createReadStream把文件管道到res期間手動(dòng)設(shè)置Content-Type與writeHead(200)——這正是 restify可以直接使用原始 Node HTTP 對(duì)象的體現(xiàn)。當(dāng)然使用原始 Node API 時(shí)內(nèi)容協(xié)商需要自己處理示例通過req.accepts(json)判斷。審計(jì)日志通過 after 事件掛載示例在服務(wù)尾部通過after事件掛載審計(jì)日志server.jsif (!options.noAudit) { server.on( after, restify.auditLogger({ body: true, log: pino({ level: info, name: todoapp-audit }) }) ); }注意審計(jì)日志插件不是用.use()注冊(cè)而是監(jiān)聽服務(wù)器的after事件詳見 lib/plugins/audit.js 的 JSDoc 示例body: true表示把請(qǐng)求體也寫入審計(jì)記錄。noAudit選項(xiàng)允許測(cè)試環(huán)境關(guān)閉審計(jì)避免測(cè)試輸出噪音??蛻舳朔庋b從 HTTP 到業(yè)務(wù) SDKexamples/todoapp/lib/client.js 演示了 restify-clients 的高層封裝模式。TodoClient構(gòu)造函數(shù)內(nèi)部創(chuàng)建一個(gè) JSON clientclients.createJSONClient支持通過socketPath或url連接并透?jìng)鱲ersion默認(rèn)~1.0用于版本協(xié)商this.client clients.createJSONClient({ log: options.log, name: TodoClient, socketPath: options.socketPath, url: options.url, version: ver });若傳入username和password會(huì)自動(dòng)調(diào)用this.client.basicAuth()注入 Basic Auth 憑據(jù)。隨后把 HTTP 動(dòng)詞封裝成業(yè)務(wù)方法client.jscreate(task, cb)→POST /todo自動(dòng)把{ task }序列化為 JSONlist(cb)→GET /todoget(name, cb)→GET /todo/:nameupdate(todo, cb)→PUT /todo/:name直接傳對(duì)象del(name, cb)→DELETE /todo/:name不傳 name 時(shí)刪除全部。toString()方法輸出[object TodoClienturl..., username..., version...]便于調(diào)試。這套模式的價(jià)值在于調(diào)用方完全不接觸 HTTP 細(xì)節(jié)只面對(duì)create/list/get/update/del這樣的業(yè)務(wù)動(dòng)詞。單元測(cè)試策略UNIX Domain Socket 方案examples/todoapp/test/todo.test.js 展示了兩種測(cè)試 restify 服務(wù)的思路在單元測(cè)試中把服務(wù)跑在 UNIX Domain Socket 上本示例采用或讓測(cè)試依賴一個(gè)已運(yùn)行的服務(wù)端點(diǎn)通過環(huán)境變量傳入本示例未采用。測(cè)試在before鉤子中完成組裝todo.test.js創(chuàng)建臨時(shí)目錄/tmp/.todo_unit_test用todo.createServer({ directory, log, noAudit: true })組裝服務(wù)server.listen(SOCK)監(jiān)聽 UNIX Socket/tmp/.todo_sock隨后創(chuàng)建客戶端并通過socketPath連接。測(cè)試用例覆蓋完整 CRUD 生命周期初始列表為空創(chuàng)建一個(gè)任務(wù)校驗(yàn)返回的name與task列表長(zhǎng)度為 1并可get取回update修改任務(wù)內(nèi)容after鉤子中刪除全部 TODO、關(guān)閉客戶端與服務(wù)、清理臨時(shí)目錄。用npm test即可運(yùn)行package.json 中test: mocha test。這套服務(wù)器監(jiān)聽 socket 客戶端走 socketPath的測(cè)試模式讓測(cè)試無需占用真實(shí)端口也無需外部服務(wù)依賴。完整 curl 實(shí)戰(zhàn)演練先以開啟認(rèn)證的方式啟動(dòng)全功能實(shí)例$ node main.js -u admin -z secret 21 | npx pino-pretty同時(shí)建議安裝json工具因?yàn)橄旅娴氖纠纪ㄟ^它格式化輸出。列出全部路由$ 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 ]注意這里響應(yīng)Content-Type是application/todo這正是自定義 formatter 生效的結(jié)果。列出 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 []創(chuàng)建 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響應(yīng)體經(jīng)過formatTodo處理只保留了task字段。值得一提的是由于服務(wù)器為application/todo設(shè)置了q0.9的協(xié)商權(quán)重而 curl 默認(rèn)發(fā)送Accept: */*服務(wù)器會(huì)按 q 值優(yōu)先選擇該自定義類型作為響應(yīng)格式——這正是 q 值參與內(nèi)容協(xié)商的直觀體現(xiàn)README 中原文提到的響應(yīng)類型即此自定義類型。列表$ 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 ]獲取單個(gè) TODO本示例服務(wù)使用了流式響應(yīng)并且這里顯式要求 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到響應(yīng)的特征。同時(shí)服務(wù)器仍通過另一種方式支持完整的內(nèi)容協(xié)商——顯式指定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。小結(jié)與工程啟示這個(gè) TODO 示例雖然代碼量不大卻濃縮了 restify 工程化的關(guān)鍵實(shí)踐分層組織main.js進(jìn)程入口→lib/server.js服務(wù)組裝→lib/client.js高層 SDK模塊邊界清晰插件裝配順序prepause / sanitizePath / userAgentConnection→use公共插件鏈 → 業(yè)務(wù)中間件認(rèn)證、數(shù)據(jù)加載→ 路由每一步職責(zé)單一內(nèi)容協(xié)商通過自定義 formatter q 值實(shí)現(xiàn)任意媒體類型響應(yīng)且兼容流式輸出錯(cuò)誤處理用 restify-errors 定義帶restCode的業(yè)務(wù)錯(cuò)誤HTTP 狀態(tài)碼與業(yè)務(wù)碼分離可測(cè)試性借助 UNIX Domain Socket 讓單元測(cè)試無需端口與外部依賴測(cè)試與生產(chǎn)共用同一套createServer/createClient工廠。在真實(shí)項(xiàng)目中你可以把lib/server.js按路由模塊拆分將createClient封裝發(fā)布為 SDK并為每個(gè)業(yè)務(wù)錯(cuò)誤補(bǔ)充更細(xì)粒度的restCode——這套骨架已經(jīng)為規(guī)?;葸M(jìn)預(yù)留了清晰的方向。贊分享后端【免費(fèi)下載鏈接】node-restifyThe future of Node.js REST development項(xiàng)目地址https://gitcode.com/gh_mirrors/no/node-restify點(diǎn)擊查看免費(fèi)下載相關(guān)推薦如何用restify-clients構(gòu)建REST客戶端API消費(fèi)端完整實(shí)戰(zhàn)教程如何用restify clients構(gòu)建REST客戶端API消費(fèi)端完整實(shí)戰(zhàn)教程 在 node restify 生態(tài)中 restify clients 是構(gòu)建后端restify 客戶端指南JsonClient / StringClient / HttpClient 的使用與實(shí)戰(zhàn)restify 客戶端指南JsonClient / StringClient / HttpClient 的使用與實(shí)戰(zhàn) 本篇技術(shù)指南以 docs/guides/后端Overleaf filestore 深度解析面向 S3/GCS/本地盤的對(duì)象存儲(chǔ)代理 API 與 PDF 轉(zhuǎn)換實(shí)現(xiàn)Overleaf filestore 深度解析面向 S3/GCS/本地盤的對(duì)象存儲(chǔ)代理 API 與 PDF 轉(zhuǎn)換實(shí)現(xiàn) Overleaf 的 filestore后端上一篇抖音視頻下載完整教程douyin-downloader 三步搬空一個(gè)作者主頁下一篇【親測(cè)免費(fèi)】 Angular 中文文檔開源項(xiàng)目推薦創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考