Express 實戰(六):構建 API 介面

BigNerdCoding發表於2019-02-16

Cover
Cover

在介紹了那麼多 Express 核心概念之後,接下來的文章將會把注意力放在如何構建一個真實的應用上。這裡我們先從構建應用 API 介面開始。從某種程度上來說幾乎所有的軟體應用其背後都是由一組強大的 API 驅動。

其實 API 就是一種程式碼之間互動的一種方式,它既可以是在程式內部也可以是通過網路的跨機器進行。例如,Express 中的 app.useapp.get 就屬於在內部使用 API 。而通過 HTTP 或者 FTP 等協議傳送 JSON、XML 資料的方式則屬於後者。對於後一種方式需要注意的是,API 的提供者和使用者必須對資料格式做出約定。在本文示例中,我們將會討論如何使用 Express 構建後一型別的 API 介面,同時所有 HTTP 介面返回的資料格式都將使用 JSON。

另外,本章還會討論如何設計一個優雅的 API 用於提升使用者的體驗和效率,讓 API 的含義一目瞭然而不用去閱讀又臭又長的說明文件。就像“好程式碼”與“壞程式碼”一樣,API 是否優雅其實更多的取決於實際情形。盲目遵循 API 設計的最佳實踐有時會顯得很迂腐,因為它有可能與使用者的期望不一致。

接下來的內容包括:

  • 什麼是 API 。
  • Express 中構建 API 的基礎內容。
  • HTTP 方法與應用邏輯的關聯。
  • 多版本 API 的實現和管理。
  • HTTP 狀態碼的正確使用。

簡單的 JSON 格式 API 示例

首先,我們需要明確該示例的功能以及 API 的使用方式,後面再寫程式碼。

假設,現在程式需要在接受到 America/Los_AngelesEurope/London 等代表時區的字串後,返回該時區的當前時間資訊(例如:2015-04-07T20:09:58-07:00 )。該返回資訊與現實中易懂的時間格式是不一樣的,因為它是為計算機設計的。

通過類似下面格式的 URL 的 HTTP 請求來呼叫應用 API:

/timezone?tz=America+Los_Angeles複製程式碼

而服務端 API 返回的 JSON 的資料格式,如下:

{
    "time": "2015-06-09T16:20:00+01:00",
    "zone": "America/Los_Angeles"
}複製程式碼

只要能呼叫 API 並對 JSON 資料進行解析,你就可以在任意平臺構建任意應用程式。如下圖,你可以通過 AJAX 請求該 API 實現一個展示時區資訊的單頁應用。

06_01
06_01

你也可以利用該介面實現下圖所示的移動應用。

06_02
06_02

你甚至可以利用該 API 實現下圖一樣的終端命令列工具:在終端中列印服務端 API 介面返回的資料。

06_03
06_03

像前一章的天氣應用一樣,我們可以利用這些 API 返回的冰冷資料構建更具表達力的 UI 。

Express 驅動的 JSON API 服務

瞭解 API 概念之後,下面我們就動手實現一個 Express 驅動的 API 服務。實現的原理非常簡單:通過中介軟體和內建函式解析網路請求並將 JSON 資料和 HTTP 狀態碼封裝到響應物件並返回給客戶端。

從技術角度上說,API 服務除了使用 JSON 格式外,你還可以是使用 XML 或者純文字。但是 Express 和 JavaScript 對 JSON 的支援是最好的,同時它也是當前最流行的格式,所以後面會一直使用 JSON 作為預設資料格式。

下面我們編寫一個為多平臺提供隨機數生成的服務,該 API 將擁有如下特性:

  • 在請求 API 時必須附帶隨機數最小值和最大值。
  • 解析請求獲取隨機數範圍並將生產的結果以 JSON 格式返回。

你可能認為這裡完全可以使用純文字來替換 JSON 格式。但是傳送 JSON 資料是開發者的必備技能,而且 JSON 格式極易擴充。

該工程的構建步驟如下:

  1. 新建 package.json
  2. 建立工程主入口檔案 app.js
  3. app.js 中建立應用和路由中介軟體。

首先,在新建的 package.json 中,複製下面的內容並按照依賴項:

{
    "name": "random-number-api",
    "private": true,
    "scripts": {
        "start": "node app"
    },
    "dependencies": {
        "express": "^5.0.0" 
    }
}複製程式碼

接下來,將下面的程式碼複製到入口檔案 app.js 中:

var express = require("express");
var app = express();

app.get("/random/:min/:max", function(req, res) {
    var min = parseInt(req.params.min);
    var max = parseInt(req.params.max);
    if (isNaN(min) || isNaN(max)) {
        res.status(400);
        res.json({ error: "Bad request." });
        return;
    }

    var result = Math.round((Math.random() * (max - min)) + min);
    res.json({ result: result });
});

app.listen(3000, function() {
    console.log("App started on port 3000");
});複製程式碼

現在啟動應用並訪問 http://localhost:3000/random/10/100 的話,你將看到一個附帶 10 ~ 100 範圍內隨機數的 JSON 資料。

06_04
06_04

接下來,我們來分析上面的程式碼。

與之前一樣,前兩行程式碼引入了 Express 並建立了一個 Express 應用例項。

然後,我們建立了一個路由中介軟體用於處理類似 /random/10/100 這樣的 API 請求。當然,這裡還存在一些 bug ,例如,沒有過濾掉 /random/foo/bar 請求。所以,在呼叫 API 的時候請確保使用的引數是整型變數。

在然後,我們使用內建的 parseInt 解析範圍引數,而該函式的返回值只可能是整形數字或者 NaN。如果傳入的引數有一個為 NaN 的話就會給客戶端返回一個錯誤資訊。下面這部分程式碼對於整個程式來說是非常重要的:

if (isNaN(min) || isNaN(max)) {
  res.status(400);
  res.json({ error: "Bad request." });
  return;
}複製程式碼

如果上面的引數檢查的結果是最少有一個為 NaN ,程式就會進行如下處理:

  1. 設定 HTTP 狀態碼為 400。常見的 404 錯誤就是它的一個具體變種,表示的含義是:使用者請求的出現了問題。
  2. 傳送包含錯誤資訊的 JSON 資料。
  3. 結束請求處理並跳出中介軟體執行。

在程式碼的最後,我們會在合法的引數返回內生成隨機數並將結果返回給客戶端。

雖然示例很簡單,但是它已經包含了使用 Express 構建 API 的基本流程:解析請求,設定 HTTP 狀態碼,返回響應資料。你可以在這個基礎之上構建更為複雜優雅的 API 。

CURD 操作 API

CURD 是對程式中 Create、Read、Update、Delete 四種業務動作的一個簡稱。

大多數的應用都會涉及到 CURD 操作。例如,對於一個圖片分享應用來說,其中涉及圖片的所有操作就是典型的 CRUD:

  • 使用者上傳照片的行為對應就是 create 操作。
  • 使用者瀏覽照片的行為就是 read 操作。
  • 使用者更新照片的行為就是 update 操作。
  • 使用者刪除照片的行為就是 delete 操作。

無論是分享照片的社交應用還是檔案儲存服務,你生活中的使用的很多服務中都使用了這種模式。不過在開始討論構建 CRUD 功能的 API 之前,我們先來看看被稱為 HTTP 方法的內容。

HTTP 方法

HTTP 的規範中是這樣定義其方法的:

HTTP 方法明確了對請求 URI 所標識資源進行的操作,而且方法是區分大小寫的。

一個更易理解的解釋是:客戶端在傳送 HTTP 請求時需要指定一個 HTTP 方法,然後服務端回依據不同的 HTTP 方法做出不同的響應。雖然,可用的 HTTP 方法有很多,但是常用的其實並不多。其中在 Web 應用中常用是下面 4 個:

  1. GET 是最常用的一個 HTTP 方法,它表示請求服務端資源。例如,載入網站首頁、請求圖片資源都使用的是 GET。雖然服務端的響應可能不同,但是GET 請求並不會改變伺服器的資源。例如,對某圖片資源的一次或者多次請求並不會導致圖片本身出現任何差別。
  2. POST 是另一個常用的 HTTP 方法。例如,建立新部落格、上傳照片、註冊使用者、清空購物車等業務都是使用 POST 。與 GET 不同的是:每次 POST 請求都會導致服務端發生修改。
  3. PUT 方法用於對已有記錄的修改,所有我覺得它應該被稱為 "UPDATE" 更為合適。例如,修改部落格標題、修改使用者暱稱等操作都是 PUT 操作。另外,PUT 還具備 POST 的功能:就是當要修改的記錄不存在時可以進行新建操作(非必需)。其次 PUT 還具有 GET 方法的特點:對同一 URL 的一次或多次 PUT 請求後的結果是一致的。
  4. DELETE 方法用於記錄刪除。例如,刪除使用者文章、刪除網路照片。另外,與 PUT 一樣同一刪除請求無論是執行一次還是多次最終結果是一致的。

雖然 HTTP 還有很多其他的方法,但是它們在現實開發過程中並不常見。理論上你甚至可以只使用 GET 和 POST 請求完成所有業務,但是這是錯誤實踐畢竟它違反了 HTTP 規範也會給開發者造成困惑。另外,很多瀏覽器也是根據 HTTP 方法來明確所執行的操作型別。所以,即使並沒有強制你也應該參照該規範來約束自己的行為。

前面你已經見過 Express 中對部分方法的處理,不過下面的程式碼將一次涵蓋上面所有的四個方法:

var express = express("express");
var app = express();

app.get("/", function(req, res) {
  res.send("you just sent a GET request, friend");
});

app.post("/", function(req, res) {
  res.send("a POST request? nice");
});

app.put("/", function(req, res) {
  res.send("i don't see a lot of PUT requests anymore");
});

app.delete("/", function(req, res) {
  res.send("oh my, a DELETE??");
});

app.listen(3000, function() {
  console.log("App is listening on port 3000");
});複製程式碼

將程式碼複製到入口檔案 app.js 中並啟動服務,然後你就可以使用 cURL 命令測試不同的 HTTP 方法了。預設情況下 cURL 使用 GET 傳送請求,但是你可以使用 -X 選項來指定其他的方法。例如,curl -X PUT http://localhost:3000

06_05
06_05

通過 HTTP 方法構建 CRUD 介面

回想以下之前的照片分享應用,下面是其中可能的 CRUD 操作:

  1. 使用者上傳圖片,此為 Create
  2. 使用者瀏覽圖片,此為 Read
  3. 使用者更新圖片備註等資訊,此為 Update
  4. 使用者從站點刪除圖片,此為 Delete

不難看出 CRUD 操作與之前四種 HTTP 方法存在對應關係:

  • Create = POST
  • Read = GET
  • Update = PUT
  • Delete = DELETE

因此通過這四個 HTTP 方法我們可以很好的實現最常見 CRUD 風格的 web 應用程式。

實際上對於更新和建立動作與 HTTP 方法的對應關係,一些人有著自己的看法。它們認為 PUT 更應該對應建立動作而非 POST。另外,新的 PATCH 方法則對應更新操作。雖然本文將會使用上面那種更規範的對應關係,但是你完全可以按照自己的意願選擇。

API 版本控制

為了應對未來可能的 API 更新,對 API 進行版本控制是一件非常高效的方法。例如,前面獲取指定時區當前時間的 API 在推出後就被很多的廠商和開發者使用。但是,幾年幾後由於某些原因必須對該 API 進行更新而與此同時你又不能影響之前的使用者。此時,我們就可以通過新增新版本來解決這個問題。其中原有的 API 請求可以通過:

/v1/timezone

而新版本 API 請求則可以使用:

/v2/timezone

這樣不僅在進行 API 更新時防止了程式碼的破壞性更改。而且介面使用者也有了更靈活的選擇,他們可以在必要的時候進行 API 切換。

在 Express 中可以使用 Router 中介軟體來實現 API 版本管理。拷貝下面程式碼到檔案 app1.js 中,並講其作為第一個版本 API 的實現:

var express = require("express");
var api = express.Router();

api.get("/timezone", function(req, res) {
    res.send("Sample response for /timezone");
});
api.get("/all_timezones", function(req, res) {
    res.send("Sample response for /all_timezones");
});

module.exports = api;複製程式碼

請注意,上面的中介軟體程式碼在處理的 URL 並沒有包含 /v1 。下面在入口檔案中引入這個 Router 中介軟體並進行路由對映。

var express = require("express");
var apiVersion1 = require("./api1.js");
var app = express();
app.use("/v1", apiVersion1);
app.listen(3000, function() {
    console.log("App started on port 3000");
});複製程式碼

然後,你將最新版本的 API 實現放在 api2.js 檔案中:

var express = require("express");
var api = express.Router();

api.get("/timezone", function(req, res) {
    res.send("API 2: super cool new response for /timezone");
});
module.exports = api;複製程式碼

最後,通過 Router 將這兩個版本的 API 同時新增到主入口中:

var express = require("express");

var apiVersion1 = require("./api1.js");
var apiVersion2 = require("./api2.js");
var app = express();

app.use("/v1", apiVersion1);
app.use("/v2", apiVersion2);
app.listen(3000, function() {
    console.log("App started on port 3000");
});複製程式碼

你可以通過瀏覽器驗證這些版本化後的 API 是否正確工作,另外你也可以使用 cURL 命令進行測試。

06_07
06_07

就像前面章節介紹的那樣,Router 可以讓你將不同的路由存放在不同檔案中進行管理。而版本化 API 就是最典型的應用例項。

設定 HTTP 狀態碼

每一個 HTTP 響應都應該附帶一個 HTTP 狀態碼,其中最有名的就是 404 Not Found

雖然 404 是最出名的,但是 200 狀態碼確是最常見的。與 404 不同的是,雖然當網頁成功載入或 JSON 資料成功返回後都會包含狀態碼 200,但它並不會被展示出來。

當然,除了 404 和 200 之外,HTTP 中還定義了很多其他的狀態碼,包括 100、200、300、400 以及 500 系列。需要注意的是並不是每個系列中所有 100 個數字都有明確定義,例如,100 系列只有 100,101,102 三個有效碼,緊跟其後就是 200 。

每個狀態碼系列其實都有特定的含義和主題,總結就是:

1xx: 成功接收到請求。
2xx: 成功
3xx: 重定向
4xx: 客戶端錯誤
5xx: 服務端錯誤

規範中只定義的大約 60 個狀態碼。你可以在此基礎上擴充自己的狀態碼,但是通常並不會這麼做。因為優秀的 API 的首要設計原則就是確保不會對使用者造成任何歧義,所以應該最大程度遵循官方規範的指導。後面我們會對上面的每個區間的狀態碼進行講解,但是在此之前先來看看如何在 Express 中設定狀態碼。

少部分應用還在使用 HTTP 1.0 版本的協議,而大部分以及切換到了 1.1 版本。作為下一個版本的 HTTP 2.0 標準現在也逐漸在推廣過程中。幸運的是,2.0 版本的協議大部分更新都在底層所以切換時並不會涉及太大的工作量。另外,2.0 版本還新增了一個 421 的狀態碼。

設定 HTTP 狀態碼

預設情況下,HTTP 狀態碼是 200。如果使用者訪問的 URL 對應資源不存在的話,Express 會傳送 404 錯誤。如果訪問的伺服器出現問題的話,Express 就會傳送 500 錯誤。

但是這些都是 Express 的預設行為,某些情形下可能會需要自行設定狀態碼。為此,Express 的 response 物件提供了一個 status 方法,你需要在呼叫是傳入對應狀態碼就能完成設定。

// ...
res.status(404);
// ...複製程式碼

該方法可以進行鏈式呼叫,所以你可以緊跟其後使用 json 設定返回的資料。

res.status(404).json({ error: "Resource not found!" });
// 它等價於:
res.status(404);
res.json({ error: "Resource not found!" });複製程式碼

雖然 Express 對原生 Node 的 response 物件進行了擴充,並且在使用 Express 時也應遵循 Express 風格,但是你依舊可以使用原生方法來完成設定。

res.statusCode = 404;複製程式碼

100 區間

100 區間的官方狀態碼只有兩個:100(繼續) 和 101 (切換協議),而且它們很少會被用到。如果你必須處理的話,可以去官網或者維基上檢視。

200 區間

200 區間狀態碼錶示請求成功。雖然該區間狀態碼不少,但是常用的也就下面 4 個:

  • 200:作為最常見的狀態碼,它也被稱為 "OK"。這意味著請求和響應都正確執行期間並沒有出現任何錯誤或者重定向操作。
  • 201:與 200 十分類似,但是使用情形略有不同。它通常用於 POST 或者 PUT 請求成功建立記錄後。例如,建立博文、上傳圖片等操作成功後就會傳送 201。
  • 202:202 是 201 的一個變種。因為,資源的建立大多是非同步進行的,而這些操作也是費時的。所以,你可以在此時給客戶端響應 202 。它表示已經成功接收資料正在等待建立。
  • 204:它表示使用者刪除請求所對應的資源並不存在已經被刪除過了。

300區間

同樣,在 300 區間,我們只介紹其中常用的三個,並且它們全都涉及重定向。

  • 301:它表示所訪問資源位置已經發生修改,請訪問最新的 URL 。通常它還會附帶一個 Location 的頭部資訊指明重定向的位置。
  • 303:它表示請求的資源已經建立完成,現在你就會被重定位到一個新頁面。
  • 307:與 301 類似都是提示當前 URL 不存在。不過區別是,301 的重定向是永久的而 307 可能重定向的只是一個臨時性 URL 。

400 區間

400 區間的狀態碼是最多的,而它通常都是表示由於客戶端的錯誤導致請求失敗。

  • 401 和 403:這兩個狀態碼分別表示“未授權”和“禁止”。字面上看兩者很類似,但是前者可能表示使用者未登入而後者則可能是使用者登入了但是許可權不夠。
  • 404:它表示使用者 URL 請求的資源並不存在。

至於該區間其他狀態碼,讀者可以去維基上自行檢視,這裡就不一一介紹了。另外,當你不確定應該使用哪種客戶端錯誤狀態碼時,你可以直接使用 400 。

500 區間

作為 HTTP 規範裡的最後一個區間,500 區間狀態碼錶示的是服務內部出現錯誤。例如,請求過載或者資料庫連線中斷。另外,理論上該區間的錯誤只能有服務內部自己觸發。最後,為了防止黑客窺探太多內部資訊,你可以對所有的內部錯誤僅僅返回一個抽象的“內部伺服器錯誤”這樣的資訊。

總結

本章包含的內容有:

  • 使用 Express 構建 API 服務。
  • HTTP 方法以及與 CRUD 操作之間的關係。
  • 如果對 API 進行版本控制,提示服務的相容性和穩定性。
  • HTTP 狀態碼的使用和其意義。

原文地址

相關文章