出處:https://segmentfault.com/a/1190000038504061
隨著 NodeJS 的流行,JavaScript 在伺服器端的開發中也逐漸佔有了一席之地。相對於 Java 或 C#等傳統後端程式語言,JavaScript 的優勢在於語法靈活,淺顯易懂,上手簡單,用較少的程式碼就可以完成很多複雜的任務。NodeJS 平臺上也有很多優質的第三方庫。在伺服器端應用中,API 有著很重要的地位,是與前端進行互動的基礎。本文介紹的 restify 是一個開發 API 的流行框架,被 npm 和 Netflix 等公司廣泛使用。
restify 入門restify 是一個 NodeJS 模組,可以讓你建立正確的 REST Web Services。它借鑑了很多 express 的設計,restify比起express更專注於REST服務,去掉了express中的template, render等功能,同時強化了REST協議使用,並且提供了版本化支援,HTTP的異常處理等。此外 restify 還提供了 DTrace 功能,為程式調式帶來新的便利!
restify的github地址安裝 restify安裝restify,先建立目錄,然後使用npm安裝即可:
mkdir restify-restfulcd restify-restfulnpm init -y npm install restify
Hello World 程式碼
每學習一個新的框架或者模組總有一個Hello World作為體驗,下面的程式碼就是restify的入門程式:
const restify = require('restify');const server = restify.createServer()server.get('/', (req, res, next)=>{ res.send("hello world"); return next();})server.listen(3001, '127.0.0.1', function () { console.log('%s listening at %s', server.name, server.url)});
響應處理鏈
對於每個 HTTP 請求,restify 透過一個響應處理鏈來對請求進行處理。restify 中有三種不同的處理鏈。
pre:在確定路由之前執行的處理器鏈。use:在確定路由之後執行的處理器鏈。{httpVerb}:一個路由獨有的處理器鏈。透過 restify 伺服器的 pre()方法可以註冊處理器到 pre 處理器鏈中。那麼對所有接收的 HTTP 請求,都會預先呼叫該處理器鏈中的處理器。處理器鏈的執行發生在確定路由之前,因此即便是沒有路由與請求相對應,也會呼叫該處理器鏈。該處理器鏈適合執行日誌記錄、效能指標資料採集和 HTTP 請求清理等工作。典型的應用場景包括記錄所有請求的資訊,以及新增計數器來記錄訪問相關的效能指標。
下面是一個處理器的例子,該處理器是去除請求網址中多餘的 / 的功能,也就是說假如你在輸入的時候本來應該輸入: http://127.0.0.1:3001/ ,但是不小心輸入了: http://127.0.0.1:3001///// ,該處理器可以去掉多餘的 /。並且要知道這個是在路由函式執行之前發生的,程式碼如下:
const restify = require('restify');const server = restify.createServer()server.pre(restify.pre.dedupeSlashes()) // 去除請求網址中的多個 /server.get('/', (req, res, next) => { res.send("hello world"); return next();})server.listen(3001, '127.0.0.1', function () { console.log('%s listening at %s', server.name, server.url)});
當然處理器我們也可以自己實現,例如下面的例子和上面的程式碼是等價的:
const restify = require('restify');const server = restify.createServer()function dedupeSlashes(req, res, next) { console.log(req.url) req.url = req.url.replace(/(\/)\/+/g, '$1'); return next();};server.pre(dedupeSlashes) // 去除請求網址中的多個 /server.get('/', (req, res, next) => { res.send("hello world"); return next();})server.listen(3001, '127.0.0.1', function () { console.log('%s listening at %s', server.name, server.url)});
透過 restify 伺服器的 use()方法可以註冊處理器到 use 處理器鏈中。該處理器鏈在選中了一個路由來處理請求之後被呼叫,但是發生在實際的路由處理邏輯之前。也就是說如果你沒有定義的路由但是卻被請求者請求了,那麼pre會處理該請求,但是use處理鏈不會處理該請求。對於所有定義的路由,該處理器鏈中的處理器都會執行,如果沒有定義的路由那麼use不會觸發執行。該處理器鏈適合執行使用者認證、應用相關的請求清理和響應格式化等工作。典型的應用場景包括檢查使用者是否完成認證,對請求和響應的 HTTP 頭進行修改等。
const restify = require('restify');const server = restify.createServer()function dedupeSlashes(req, res, next) { console.log('pre', req.url) req.url = req.url.replace(/(/)/+/g, '$1'); return next();};server.pre(dedupeSlashes) // 去除請求網址中的多個 /server.use(function (req, res, next) { console.log('use', req.url); return next()})server.get('/', (req, res, next) => { res.send("hello world"); return next();})server.listen(3001, '127.0.0.1', function () { console.log('%s listening at %s', server.name, server.url)});
在每個處理器的實現中,應該在合適的時機呼叫 next()方法來把處理流程轉交給處理器鏈中的下一個處理器。具體的時機由每個處理器實現根據需要來決定。這給處理 HTTP 響應帶來了極大的靈活性,也使得處理器可以被有效複用。每個處理器的實現邏輯也變得更加簡單,只需要專注於完成所設計應有的功能就可以了。在處理完成之後,呼叫 next()方法即可。在某些情況下,可能不需要由處理器鏈中的後續處理器來繼續進行處理,比如 HTTP 請求的格式是非法的。這個時候可以透過 next(false)來直接終止整個處理器鏈。在呼叫 next()方法的時候,也可以傳入一個 Error 物件,使得 restify 直接把錯誤資訊傳送給客戶端並終止處理鏈。在這種情況下,HTTP 響應的狀態碼由 Error 物件的屬性 statusCode 來確定,預設使用 500。呼叫 next.ifError(err)並傳入一個 Error 物件可以使得 restify 丟擲異常並終止程序,可以用來在出現無法恢復的錯誤時終止程式。
響應處理器鏈示例下面的程式碼中,透過 pre()方法註冊的處理器會記錄請求的完整路徑。第一個 use()方法註冊了 restify 的外掛 queryParser,其作用是把請求的查詢字串解析成 JavaScript 物件。第二個 use()方法註冊的處理器把請求的 HTTP 頭 Accept 設定為 application/json,也就是 API 只接受 JSON 格式的請求。最後透過 get()方法註冊了兩個對於 GET 請求的處理器,第一個設定了響應的額外 HTTP 頭 X-Test,第二個設定響應的內容。當請求包含了查詢引數 boom 時,伺服器會直接返回 500 錯誤。
const restify = require('restify');const server = restify.createServer()server.pre((req, res, next) => { console.log('req: %s', req.href()); return next();});server.use(restify.plugins.queryParser());server.use((req, res, next) => { req.headers.accept = 'application/json'; return next();});server.get('/', [(req, res, next) => { res.header('X-Test', 'test'); return next();}, (req, res, next) => { if (req.query.boom) { return next(new Error('boom!')); } res.send({ msg: 'handled!' }); return next();}])server.listen(3001, '127.0.0.1', function () { console.log('%s listening at %s', server.name, server.url)});
注意:如果需要在use或者pre中使用多個處理鏈的程式,可以一次把他們放進陣列中,restify會按照陣列中的先後順序進行處理。
路由restify 的路由表示的是對 HTTP 請求的處理邏輯。一個路由有 3 個部分:分別是 HTTP 動詞、匹配條件和處理方法。restify 伺服器物件可以使用方法 get、put、post、del、head、patch 和 opts,分別與名稱相同的 HTTP 請求動詞相對應。這些方法用來建立路由。這些方法的第一個引數定義了路由的匹配條件。該引數的值可以是字串或正則表示式 Regex 物件,也可以是包含了屬性 name、path 和 version 的 JavaScript 物件。方法的第二個引數是進行實際處理的函式,接受 req、res 和 next 三個引數,分別表示 HTTP 請求、HTTP 響應和處理鏈的下一個處理器。
在下面的程式碼中,logHandler 是通用的路由處理方法,其作用是記錄請求的路徑和引數,並把解析之後的引數物件作為響應返回。程式碼中一共定義了 4 個使用 get 方法的路由。第一個路由使用的是完全匹配的路徑。第二個路由中包含了引數 id,在路徑中以":id"來表示。當訪問路徑/route/user/1 時,返回的結果是{"id":"1"}。第三個路由使用正則表示式。只有在/route/order/後全部為數字的路徑才能滿足匹配。當訪問路徑/route/order/123 時,返回的結果為{"0":"123"}。其中 0 表示是對應於正則表示式中的第一個匹配分組。當嘗試訪問路徑/route/order/xyz 時,伺服器會返回 404 錯誤,因為 xyz 不匹配正則表示式。最後一個路由使用一個 JavaScript 物件聲明瞭路徑和版本號。關於版本號的使用,在下一節會提到。
const restify = require('restify'); const server = restify.createServer(); const logHandler = (req, res, next) => { console.log('req: %s, params: %s', req.href(), JSON.stringify(req.params)); res.send(req.params); return next();}; server.get('/route/simple', logHandler);server.get('/route/user/:id', logHandler);server.get(/^\/route\/order\/(\d+)/, logHandler);server.get({ path: '/route/versioned', version: '1.0.0'}, logHandler); server.listen(8000, () => console.log('%s listening at %s', server.name, server.url));
多版本路由
REST API 通常有同時執行多個版本的要求,以支援 API 的演化。restify 內建提供了基於語義化版本號(semver)規範的多版本支援。在 HTTP 請求中可以使用 HTTP 請求頭 Accept-Version 來指定版本號。每個路由可以按照下面的程式碼中的方式,在屬性 version 中指定該路由的一個或多個版本號。如果請求中不包含 HTTP 頭 Accept-Version,那麼會匹配同一路由中版本最高的那一個。否則,就按照由 Accept-Version 指定的版本號來進行匹配,並呼叫匹配版本的路由。透過請求的 version()方法可以獲取到 Accept-Version 頭的值,matchedVersion()方法可以獲取到匹配到的版本號。
同一個路由 /hello/:name 有多個版本。版本 1.0.0 的處理方法返回sendV1對應的值。第二個路由同時支援 2.0.0、2.1.0 等 2 個版本,返回的是請求的版本和實際匹配的版本。如果直接訪問/hello/:name,返回的結果是{"sendV2":"mark"},因為預設匹配最高版本。如果執行"curl -s -H 'accept-version: ~1' http://localhost :8000/hello/mark",由於請求中 Accept-Version 頭的值為~1,會匹配到 1.0.0 版本的路由,返回結果為 {"sendV1":"mark"}。如果請求中 Accept-Version 頭的值為~3, 則返回結果為{"code":"InvalidVersion","message":"~3 is not supported by GET /hello/mark"},因為並沒有與~3 匹配的版本的路由。
var restify = require('restify');var server = restify.createServer();function sendV1(req, res, next) { console.log('sendV1', req.params.name); res.send({sendV1: req.params.name}); return next();}function sendV2(req, res, next) { console.log('sendV2', req.params.name); res.send({sendV2: req.params.name}); return next();}server.get('/hello/:name', restify.plugins.conditionalHandler([ {version: '1.1.3', handler: sendV1}, {version: ['2.0.0', '2.1.0'], handler: sendV2}, // 預設返還最高版本]));server.listen(3001, '127.0.0.1', () => console.log('%s listening at %s', server.name, server.url));
請求版本:1:
請求版本2:
請求版本3(不存在):
WebSocket在 restify 中也可以使用 WebSocket,不過需要第三方庫的支援。本文使用 Socket.IO 來展示 WebSocket 的使用。下面程式碼中給出了使用 Socket.IO 的伺服器端實現。在建立了 restify 的伺服器物件之後,可以直接把底層的 server 物件直接由 Socket.IO 來使用。路徑為"/"的路由的作用是傳送 HTML 頁面。接下來的程式碼使用 Socket.IO 進行資料傳送和接收。
const fs = require('fs');const restify = require('restify');const server = restify.createServer();const io = require('socket.io')(server.server); server.get('/', (req, res, next) => { fs.readFile(__dirname + '/index.html', function (err, data) { if (err) { next(err); return; } res.setHeader('Content-Type', 'text/html'); res.writeHead(200); res.end(data); next(); });}); io.on('connection',(socket) => { socket.emit('news', { hello: 'world' }); socket.on('my other event', console.log);}); server.listen(8000, () => console.log('socket.io server listening at %s', server.url))
對應的 index.html 的內容如下:
<script src="/socket.io/socket.io.js"></script><script> var socket = io.connect('http://localhost:8000'); socket.on('news', function (data) { console.log(data); socket.emit('my other event', { my: 'data' }); });</script>
內容協商
在之前的示例中,我們都是使用 send() 方法來直接傳送響應內容。如果傳入的是 JavaScript 物件,restify 會自動轉換成 JSON 格式。這是由於 restify 內建提供了對於不同響應內容型別的格式化實現。內建支援的響應內容型別包括 application/json、text/plain 和 application/octet-stream。restify 會根據請求的 Accept 頭來確定響應的內容型別。如果無法確定,則預設使用 application/octet-stream。可以在建立 restify 伺服器時,新增額外的響應內容型別的支援。
下面的程式碼中,我們建立了一個對於內容型別 application/base64 的格式化實現。在該實現中,我們會把 String 型別的內容轉換成 Base64 編碼之後的格式。
const util = require('util');const restify = require('restify'); const server = restify.createServer({ formatters: { 'application/base64': (req, res, body) => { if (body instanceof Error) { return body.stack; } if (Buffer.isBuffer(body)) { return body.toString('base64'); } if (typeof body === 'string') { return new Buffer(body).toString('base64'); } return util.inspect(body); } }}); server.get('/content', (req, res, next) => { res.send('Hello World'); next();}); server.listen(8000, () => console.log('%s listening at %s', server.name, server.url));
如果直接訪問/content,返回的結果是 SGVsbG8gV29ybGQ=,是 Base64 編碼之後的結果。這是因為預設的內容型別變成了 application/base64。如果指定: accept: text/plain 來訪問,則返回的結果是 Hello World(注意沒有引號),內容型別變為純文字。如果使用 accept: application/json 來訪問,則返回的結果是"Hello World"(注意帶有引號)。
錯誤處理在 REST API 的實現中,錯誤處理是很重要的一部分。在前面的示例中,我們使用 send() 方法傳送 Error 物件來使得 restify 產生錯誤響應。由於 HTTP 的狀態碼是標準的,restify 提供了一個專門的模組 restify-errors 來建立對應不同狀態碼的 Error 物件的處理,可以直接在 send() 方法中使用。restify 中產生的錯誤會作為事件來發送,可以使用 Node.js 標準的事件處理機制來進行處理。 需要注意的是,只有使用 next()方法傳送的錯誤會被作為事件來發送,使用響應物件的 send()方法傳送的則不會。
在下面的程式碼中路由 /error/500 使用 send() 傳送了一個 InternalServerError 錯誤物件,而 /error/400 和 /error/404 使用 next()分別傳送了 BadRequestError 和 NotFoundError 錯誤物件。可以使用 server.on()來新增對於 NotFound 錯誤的處理邏輯,但是對於 InternalServer 錯誤的處理邏輯不會被觸發,因為該錯誤是透過 send() 方法來發送的。
const restify = require('restify');const errors = require('restify-errors'); const server = restify.createServer(); server.get('/error/500', (req, res, next) => { res.send(new errors.InternalServerError('boom!')); return next();});server.get('/error/400', (req, res, next) => next(new errors.BadRequestError('bad request')));server.get('/error/404', (req, res, next) => next(new errors.NotFoundError('not found'))); server.on('NotFound', (req, res, err, cb) => { console.error('404 %s', req.href()); return cb();}); server.on('InternalServer', (req, res, err, cb) => { console.error('should not appear'); return cb();}); server.listen(8000, () => console.log('%s listening at %s', server.name, server.url));
外掛在 REST API 的開發中,某些任務是比較常見的。restify 提供了一系列外掛來滿足這些通用的需求。這些外掛可以透過 restify.plugins 來訪問,並使用 use()方法來註冊。例如在下面的程式碼中,我們使用了 restify 中的若干個常用外掛:
acceptParser 用來解析請求的 Accept 頭,以確保是伺服器端可以處理的型別。如果是伺服器端不支援的型別,該外掛會返回 406 錯誤。authorizationParser 用來解析請求中的 Authorization 頭,並把解析的結果儲存在請求物件的屬性 authorization 中。queryParser 之前已經介紹過,用來解析請求中的查詢字串。gzipResponse 用來發送 GZIP 壓縮之後的響應。bodyParser 用來解析請求的內容,並把結果儲存在請求物件的屬性 body 中。目前支援的請求內容型別包括 application/json、application/x-www-form-urlencoded 和 multipart/form-data。const restify = require('restify'); const server = restify.createServer();// 載入外掛server.use(restify.plugins.acceptParser(server.acceptable));server.use(restify.plugins.authorizationParser());server.use(restify.plugins.queryParser());server.use(restify.plugins.gzipResponse());server.use(restify.plugins.bodyParser()); server.post('/plugins', (req, res, next) => { console.log(req.body); res.send({a: 1}); return next();}); server.listen(8000, () => console.log('%s listening at %s', server.name, server.url));
總結當需要在 NodeJS 上開發 REST API 時,restify 是一個很好的選擇。restify 的響應處理鏈使得處理 HTTP 請求變得非常簡單。可以使用不同的方式來定義路由,也提供了對多版本的支援。本文對 restify 進行了詳細介紹,包括 WebSocket 支援,內容協商、錯誤處理和外掛等。
出處:https://segmentfault.com/a/1190000038504061