漂亮的節點 API
這篇文章是關於如何在 Node.js 中構建漂亮的 API。太好了,什麼是 API?定義說應用程序編程接口,但它是什麼意思?根據上下文,它可能意味著幾件事:
- 面向服務的架構 (SOA) 的端點
- 函數簽名
- 類屬性和方法
主要思想是 API 是兩個或多個實體(對象、類、關注點等)之間的一種契約形式。作為 Node 工程師,你的主要目標是構建漂亮的 API,這樣使用你的模塊/類/服務的開發人員就不會詛咒和向你發送討厭的 IM 和郵件。其餘代碼可能很醜陋,但公開的部分(供其他程序和開發人員使用)需要是常規的、可擴展的、易於使用和理解的以及一致的。
讓我們看看如何構建漂亮的 API,讓其他開發者放心
Node 中的美麗端點:馴服 REST 野獸
很可能,您沒有使用核心節點 http
直接模塊,但是像 Express 或 Hapi 這樣的框架。如果沒有,那麼強烈考慮使用框架。它將附帶解析和路由組織等免費贈品。我將使用 Express 作為示例。
這是我們為 /accounts
使用 CRUD 的 API 服務器 以 HTTP 方法和 URL 模式列出的資源(`{} 表示它是一個變量):
- 獲取
/accounts
:獲取帳號列表 - POST
/accounts
:創建一個新帳戶 - 獲取
/accounts/{ID}
:通過 ID 獲取一個帳戶 - 輸入
/accounts/{ID}
:通過 ID 部分更新一個帳戶 - 刪除
/accounts/{ID}
:按 ID 移除一個帳號
您可以立即註意到,我們需要在最後三個端點的 URL 中發送資源(帳戶)ID。通過這樣做,我們實現了明確區分資源集合和單個資源的目標。這反過來又有助於防止來自客戶端的錯誤。例如 DELETE /accounts
更容易出錯 在刪除所有帳戶的請求正文中帶有 ID,如果此錯誤進入生產環境並實際導致刪除所有帳戶,您很容易被解僱。
通過 URL 進行緩存可以帶來額外的好處。如果您使用或計劃使用 Varnish,它會緩存響應並通過 /accounts/{ID}
您將獲得更好的緩存效果。
仍然不服氣?讓我告訴你,Express 將忽略諸如 DELETE 之類的請求的有效負載(請求正文),因此獲取該 ID 的唯一方法是通過 URL。
Express 在定義端點方面非常優雅。對於稱為 URL 參數的 ID,有一個 req.params
只要您在 URL 模式中定義 URL 參數(或多個),就會填充屬性和值的對象,例如,使用 :id
.
app.get('/accounts', (req, res, next) => {
// Query DB for accounts
res.send(accounts)
})
app.put('/accounts/:id', (req, res, next) => {
const accountId = req.params.id
// Query DB to update the account by ID
res.send('ok')
})
現在,關於 PUT 的幾句話。它被誤用了很多,因為根據規範 PUT 是為了完全更新,即替換整個實體,而不是部分更新。但是,即使是大型知名公司的許多 API 也使用 PUT 作為部分更新。我是不是已經把你弄糊塗了?這只是帖子的開始!好吧,我來說明一下部分和完整的區別。
如果您使用{a: 1}
進行更新 一個對象 {b: 2}
,結果為 {a: 1, b: 2}
當更新是部分的並且 {a: 1}
當它完全替代時。
回到端點和 HTTP 方法。更合適的方法是使用 PATCH 進行部分更新而不是 PUT。但是,PATCH 規範在實施中缺乏。也許這就是為什麼許多開發人員選擇 PUT 作為部分更新而不是 PATCH 的原因。
好的,所以我們使用 PUT,因為它成為了新的 PATCH。那麼我們如何獲得實際的 JSON 呢?有 body-parser
它可以從字符串中給我們一個 Node/JavaScript 對象。
const bodyParser = require('body-parser')
// ...
app.use(bodyParser.json())
app.post('/accounts', (req, res, next) => {
const data = req.body
// Validate data
// Query DB to create an account
res.send(account._id)
})
app.put('/accounts/:id', (req, res, next) => {
const accountId = req.params.id
const data = req.body
// Validate data
// Query DB to update the account by ID
res.send('ok')
})
始終、始終、始終驗證傳入(以及傳出)數據。有像 joi 和 express-validator 這樣的模塊可以幫助你優雅地清理數據。
在上面的代碼片段中,您可能已經註意到我正在發回一個新創建的帳戶的 ID。這是最佳實踐,因為客戶需要知道如何引用新資源。另一個最佳實踐是發送適當的 HTTP 狀態代碼,例如 200、401、500 等。它們分為以下幾類:
[旁注]
閱讀博客文章很好,但觀看視頻課程更好,因為它們更具吸引力。
許多開發人員抱怨 Node.js 上缺乏負擔得起的高質量視頻材料。觀看 YouTube 視頻會讓人分心,花 500 美元購買 Node 視頻課程很瘋狂!
去看看 Node University,它有關於 Node 的免費視頻課程:node.university。
[旁注結束]
- 20 倍:一切都很好
- 30 倍:重定向
- 40 倍:客戶端錯誤
- 50 倍:服務器錯誤
通過提供有效的錯誤消息,您可以顯著地幫助客戶端的開發人員 ,因為他們可以知道請求失敗是他們的錯誤(40x)還是服務器錯誤(500)。在 40x 類別中,您至少應該區分授權、有效負載和未找到。
在 Express 中,狀態碼鏈接在 send()
之前 .例如,對於 POST /accounts
/我們正在發送與ID一起創建的201:
res.status(201).send(account._id)
PUT 和 DELETE 的響應不必包含 ID,因為我們知道客戶端知道 ID。畢竟他們在 URL 中使用了。根據要求發回一些好的消息說這一切仍然是一個好主意。響應可能像 {"msg": "ok"}
一樣簡單 或者和
{
"status": "success",
"affectedCount": 3,
"affectedIDs": [
1,
2,
3
]
}
查詢字符串呢?它們可用於附加信息,例如搜索查詢、過濾器、API 密鑰、選項等。當您需要傳遞附加信息時,我建議使用 GET 查詢字符串數據。例如,這是實現分頁的方式(我們不想為只顯示其中 10 個的頁面獲取所有 1000000 個帳戶)。可變page是頁碼,可變limit是一個頁面需要多少個item。
app.get('/accounts', (req, res, next) => {
const {query, page, limit} = req.query
// Query DB for accounts
res.status(200).send(accounts)
})
端點說得夠多了,讓我們看看如何使用函數在較低級別上工作。
美麗的功能:擁抱節點的功能特性
Node 和 JavaScript 是非常(但不完全)函數式的,這意味著我們可以用函數實現很多。我們可以創建帶有函數的對象。一般規則是,通過保持函數純淨,您可以避免將來出現問題。什麼是純函數?這是一個沒有副作用的功能。你不喜歡用另一個更晦澀的術語定義一個晦澀的術語的聰明驢子嗎?副作用是當一個函數“接觸”外部的東西時,通常是一個狀態(如變量或對象)。正確的定義更複雜,但如果你記得有隻修改他們的論點的函數,你會比多數人更好(多數人只有 51%——無論如何這是我的拙劣猜測)。
這是一個漂亮的純函數:
let randomNumber = null
const generateRandomNumber = (limit) => {
let number = null
number = Math.round(Math.random()*limit)
return number
}
randomNumber = generateRandomNumber(7)
console.log(randomNumber)
這是一個非常不純的函數,因為它正在改變 randomNumber
在其範圍之外。訪問 limit
超出範圍也是一個問題,因為這會引入額外的相互依賴(緊密耦合):
let randomNumber = null
let limit = 7
const generateRandomNumber = () => {
randomNumber = Math.floor(Math.random()*limit)
}
generateRandomNumber()
console.log(randomNumber)
第二個片段可以正常工作,但只要您能記住副作用 limit
和 randomNumber
.
有一些特定於 Node 和函數的東西only .它們的存在是因為 Node 是異步的,而且在 201 年 Node 的核心正在形成和快速增長時,我們還沒有時髦的承諾或 async/await。簡而言之,對於異步代碼,我們需要一種方法來安排一些未來的代碼執行。我們需要能夠傳遞回調。最好的方法是將它作為最後一個參數傳遞。如果您有可變數量的參數(假設第二個參數是可選的),那麼仍然保持回調為最後。您可以使用 arity (arguments
) 來實現它。
例如,我們可以通過使用回調作為最後一個參數模式將之前的函數從同步執行重寫為異步執行。我故意離開 randomNumber =
但它將是 undefined
因為現在該值將在稍後的某個時候出現在回調中。
let randomNumber = null
const generateRandomNumber = (limit, callback) => {
let number = null
// Now we are using super slow but super random process, hence it's async
slowButGoodRandomGenerator(limit, (number) => {
callback(number)
})
// number is null but will be defined later in callback
}
randomNumber = generateRandomNumber(7, (number)=>{
console.log(number)
})
// Guess what, randomNumber is undefined, but number in the callback will be defined later
下一個與異步代碼密切相關的模式是錯誤處理。每次我們設置回調時,它都會在未來某個時刻由事件循環處理。當回調代碼被執行時,我們不再引用原始代碼,只有作用域中的變量。因此,我們不能使用 try/catch
我們不能像我知道你們中的一些人喜歡在 Java 和其他同步語言中那樣拋出錯誤。
出於這個原因,要從嵌套代碼(函數、模塊、調用等)傳播錯誤,我們可以將它作為參數傳遞給回調......以及數據(number
)。您可以在此過程中檢查您的自定義規則。使用 return
一旦發現錯誤,終止代碼的進一步執行。使用 null
時 當不存在錯誤(繼承或自定義)時作為錯誤值。
const generateRandomNumber = (limit, callback) => {
if (!limit) return callback(new Error('Limit not provided'))
slowButGoodRandomGenerator(limit, (error, number) => {
if (number > limit) {
callback(new Error('Oooops, something went wrong. Number is higher than the limit. Check slow function.'), null)
} else {
if (error) return callback(error, number)
return callback(null, number)
}
})
}
generateRandomNumber(7, (error, number) => {
if (error) {
console.error(error)
} else {
console.log(number)
}
})
一旦你有了帶有錯誤處理的異步純函數,就把它移到一個模塊中。你有三個選擇:
- 文件:最簡單的方法是創建一個文件並用
require()
導入 - 模塊:你可以用
index.js
創建一個文件夾 並將其移至node_modules
.這樣你就不必擔心討厭的__dirname
和path.sep
)。設置private: true
以避免發布。 - npm 模塊:通過在 npm 註冊表上發布您的模塊更進一步
在任何一種情況下,您都會對模塊使用 CommonJS/Node 語法,因為 ES6 導入遠不及 TC39 或 Node Foundation 路線圖(截至 2016 年 12 月,以及我在 Node Interactive 2016 上聽到的主要貢獻者的演講)。創建模塊時的經驗法則是導出的就是導入的 .在我們的例子中,它的功能是這樣的:
module.exports = (limit, callback) => {
//...
}
在主文件中,您使用 require
導入 .只是不要在文件名中使用大寫或下劃線。真的,不要使用它們:
const generateRandomNumber = require('./generate-random-number.js')
generateRandomNumber(7, (error, number) => {
if (error) {
console.error(error)
} else {
console.log(number)
}
})
generateRandomNumber
你不高興嗎 是純的嗎? :-) 我敢打賭,由於緊密耦合,模塊化不純函數會花費更長的時間。
總而言之,對於漂亮的函數,您通常會進行異步處理,將數據作為第一個參數,將選項作為第二個參數,將回調作為最後一個參數。此外,使選項成為可選參數,因此回調可以是第二個或第三個參數。最後,回調將錯誤傳遞為 first 參數事件,如果它只是 null(沒有錯誤)和數據作為最後一個(第二個)參數。
Node 中的漂亮類:通過類深入 OOP
我不是 ES6/ES2015 類的忠實粉絲。我盡可能多地使用函數工廠(又名函數繼承模式)。但是,我希望更多來自前端或 Java 背景的人會開始使用 Node 進行編碼。對於他們,我們來看看Node中OOP的繼承方式:
class Auto {
constructor({make, year, speed}) {
this.make = make || 'Tesla'
this.year = year || 2015
this.speed = 0
}
start(speed) {
this.speed = speed
}
}
let auto = new Auto({})
auto.start(10)
console.log(auto.speed)
類初始化的方式(new Auto({})
) 類似於上一節中的函數調用,但這裡我們傳遞一個對象而不是三個參數。傳遞一個對象(你可以稱之為 options
) 是更好更漂亮的圖案,因為它更通用。
有趣的是,與函數一樣,我們可以通過將命名函數(上面的示例)以及匿名類存儲在變量中來創建它們(代碼如下):
const Auto = class {
...
}
類似於 start
的方法 在帶有 Auto
的片段中 被稱為原型或實例方法。與其他 OOP 語言一樣,我們可以創建靜態方法。當方法不需要訪問實例時,它們很有用。假設您是一家初創公司的飢餓程序員。你通過吃拉麵從微薄的收入中節省了 15,000 美元。您可以檢查這是否足以調用靜態方法 Auto.canBuy
而且還沒有汽車(沒有實例)。
class Auto {
static canBuy(moneySaved) {
return (this.price<moneySaved)
}
}
Auto.price = 68000
Auto.canBuy(15000)
當然,如果 TC39 包含諸如 Auto.price
等靜態類屬性的標準,這一切都太容易了 所以我們可以在類的主體而不是外部定義它們,但是沒有。他們沒有在 ES6/ES2015 中包含類屬性。也許明年我們會得到它。
要擴展一個類,假設我們的汽車是 Model S Tesla,有 extends
操作數。我們必須調用 super()
如果我們覆蓋 constructor()
.換句話說,如果你擴展了一個類並定義了自己的構造函數/初始化器,那麼請調用 super 從父級獲取所有的東西(在這種情況下是 Auto)。
class Auto {
}
class TeslaS extends Auto {
constructor(options) {
super(options)
}
}
為了使它美觀,定義一個接口,即類的公共方法和屬性/屬性。這樣,其餘代碼可以保持醜陋和/或更頻繁地更改,而不會對使用私有 API 的開發人員造成任何挫敗或憤怒(睡眠和咖啡剝奪的開發人員往往是最憤怒的——在你的背包裡放點零食給他們萬一發生攻擊)。
因為,Node/JavaScript 是鬆散類型的。與使用其他強類型語言創建類時相比,您應該在文檔方面付出更多的努力。好的命名是文檔的一部分。例如,我們可以使用 _
標記私有方法:
class Auto {
constructor({speed}) {
this.speed = this._getSpeedKm(0)
}
_getSpeedKm(miles) {
return miles*1.60934
}
start(speed) {
this.speed = this._getSpeedKm(speed)
}
}
let auto = new Auto({})
auto.start(10)
console.log(auto.speed)
在函數部分中描述的所有與模塊化相關的東西都適用於類。代碼越細化和松耦合越好。
好的。現在這已經足夠了。如果你想了解更多關於 ES6/ES2015 的內容,請查看我的備忘單和博客文章。
您可能想知道,何時使用函數以及何時使用類。它更像是一門藝術而不是一門科學。這也取決於你的背景。如果你做了 15 年的 Java 架構師,創建類對你來說會更自然。您可以使用 Flow 或 TypeScript 來添加類型。如果你更喜歡函數式 Lisp/Clojure/Elixir 程序員,那麼你會傾向於函數式。
總結
那是一篇很長的文章,但這個話題一點也不瑣碎。您的幸福感可能取決於它,即代碼需要多少維護。假設所有的代碼都被寫來改變。將更頻繁(私有)更改的事物與其他事物分開。僅公開接口(公共)並儘可能使它們對更改具有魯棒性。
最後,進行單元測試。它們將用作文檔,並使您的代碼更加健壯。一旦您擁有良好的測試覆蓋率(最好是自動化的 GitHub+CI,例如 CircleCI 或 Travis),您將能夠更有信心地更改代碼。
繼續點頭!