如何使用NodeJS構建基於RPC的API系統
如何使用 NodeJS 構建基於 RPC 的 API 系統
API 在它存在的很長時間內都不斷地侵蝕著我們的開發工作。無論是構建僅供其他微服務訪問的微服務還是構建對外暴露的服務,你都需要開發 API。
目前,大多數 API 都基於 REST 規範,REST 規範通俗易懂,並且建立在 HTTP 協議之上。 但是在很大程度上,REST 可能並不適合你。許多公司比如 Uber,facebook,Google,netflix 等都構建了自己的服務間內部通訊協議,這裡的關鍵問題在於何時做,而不是應不應該做。
假設你想使用傳統的 RPC 方式,但是你仍然想通過 http 格式傳遞 json 資料,這時要怎麼通過 node.js 來實現呢?請繼續閱讀本文。
閱讀本教程前應確保以下兩點
- 你至少應該具備 Node.js 的實戰經驗
- 為了獲得 ES6 支援,需要安裝 Node.js
v4.0.0
以上版本。
設計原則
在本教程中,我們將為 API 設定如下兩個約束:
- 保持簡單(沒有外部包裝和複雜的操作)
- API 和介面文件,應該一同編寫
現在開始
本教程的完整原始碼可以在 Github 上找到,因此你可以 clone 下來方便檢視。
首先,我們需要首先定義型別以及將對它們進行操作的方法(這些將是通過 API 呼叫的相同方法)。
建立一個新目錄,並在新目錄中建立兩個檔案,types.js
和 methods.js
。 如果你正在使用 linux 或 mac 終端,可以鍵入以下命令。
mkdir noderpc && cd noderpc
touch types.js methods.js
在 types.js
檔案中,輸入以下內容。
`use strict`;
let types = {
user: {
description:`the details of the user`,
props: {
name:[`string`, `required`],
age: [`number`],
email: [`string`, `required`],
password: [`string`, `required`]
}
},
task: {
description:`a task entered by the user to do at a later time`,
props: {
userid: [`number`, `required`],
content: [`string`, `require`],
expire: [`date`, `required`]
}
}
}
module.exports = types;
乍一看很簡單,用一個 key-value
物件來儲存我們的型別,key
是型別的名稱,value
是它的定義。該定義包括描述(是一段可讀文字,主要用於生成文件),在 props 中描述了各個屬性,這樣設計主要用於文件生成和驗證,最後通過 module.exports
暴露出來。
在 methods.js
有以下內容。
`use strict`;
let db = require(`./db`);
let methods = {
createUser: {
description: `creates a new user, and returns the details of the new user`,
params: [`user:the user object`],
returns: [`user`],
exec(userObj) {
return new Promise((resolve) => {
if (typeof (userObj) !== `object`) {
throw new Error(`was expecting an object!`);
}
// you would usually do some validations here
// and check for required fields
// attach an id the save to db
let _userObj = JSON.parse(JSON.stringify(userObj));
_userObj.id = (Math.random() * 10000000) | 0; // binary or, converts the number into a 32 bit integer
resolve(db.users.save(userObj));
});
}
},
fetchUser: {
description: `fetches the user of the given id`,
params: [`id:the id of the user were looking for`],
returns: [`user`],
exec(userObj) {
return new Promise((resolve) => {
if (typeof (userObj) !== `object`) {
throw new Error(`was expecting an object!`);
}
// you would usually do some validations here
// and check for required fields
// fetch
resolve(db.users.fetch(userObj.id) || {});
});
}
},
fetchAllUsers: {
released:false;
description: `fetches the entire list of users`,
params: [],
returns: [`userscollection`],
exec() {
return new Promise((resolve) => {
// fetch
resolve(db.users.fetchAll() || {});
});
}
},
};
module.exports = methods;
可以看到,它和型別模組的設計非常類似,但主要區別在於每個方法定義中都包含一個名為 exec
的函式,它返回一個 Promise
。 這個函式暴露了這個方法的功能,雖然其他屬性也暴露給了使用者,但這必須通過 API 抽象。
db.js
我們的 API 需要在某處儲存資料,但是在本教程中,我們不希望通過不必要的 npm install
使教程複雜化,我們建立一個非常簡單、原生的記憶體中鍵值儲存,因為它的資料結構由你自己設計,所以你可以隨時改變資料的儲存方式。
在 db.js
中包含以下內容。
`use strict`;
let users = {};
let tasks = {};
// we are saving everything inmemory for now
let db = {
users: proc(users),
tasks: proc(tasks)
}
function clone(obj) {
// a simple way to deep clone an object in javascript
return JSON.parse(JSON.stringify(obj));
}
// a generalised function to handle CRUD operations
function proc(container) {
return {
save(obj) {
// in JS, objects are passed by reference
// so to avoid interfering with the original data
// we deep clone the object, to get our own reference
let _obj = clone(obj);
if (!_obj.id) {
// assign a random number as ID if none exists
_obj.id = (Math.random() * 10000000) | 0;
}
container[_obj.id.toString()] = _obj;
return clone(_obj);
},
fetch(id) {
// deep clone this so that nobody modifies the db by mistake from outside
return clone(container[id.toString()]);
},
fetchAll() {
let _bunch = [];
for (let item in container) {
_bunch.push(clone(container[item]));
}
return _bunch;
},
unset(id) {
delete container[id];
}
}
}
module.exports = db;
其中比較重要是 proc
函式。通過獲取一個物件,並將其包裝在一個帶有一組函式的閉包中,方便在該物件上新增,編輯和刪除值。如果你對閉包不夠了解,應該提前閱讀關於 JavaScript
閉包的內容。
所以,我們現在基本上已經完成了程式功能,我們可以儲存和檢索資料,並且可以實現對這些資料進行操作,我們現在需要做的是通過網路公開這個功能。 因此,最後一部分是實現 HTTP 服務。
這是我們大多數人希望使用express的地方,但我們不希望這樣,所以我們將使用隨節點一起提供的http模組,並圍繞它實現一個非常簡單的路由表。
正如預期的那樣,我們繼續建立 server.js
檔案。在這個檔案中我們把所有內容關聯在一起,如下所示。
`use strict`;
let http = require(`http`);
let url = require(`url`);
let methods = require(`./methods`);
let types = require(`./types`);
let server = http.createServer(requestListener);
const PORT = process.env.PORT || 9090;
檔案的開頭部分引入我們所需要的內容,使用 http.createServer
來建立一個 HTTP 服務。requestListener
是一個回撥函式,我們稍後定義它。 並且我們確定下來伺服器將偵聽的埠。
在這段程式碼之後我們來定義路由表,它規定了我們的應用程式將響應的不同 URL 路徑。
// we`ll use a very very very simple routing mechanism
// don`t do something like this in production, ok technically you can...
// probably could even be faster than using a routing library :-D
let routes = {
// this is the rpc endpoint
// every operation request will come through here
`/rpc`: function (body) {
return new Promise((resolve, reject) => {
if (!body) {
throw new (`rpc request was expecting some data...!`);
}
let _json = JSON.parse(body); // might throw error
let keys = Object.keys(_json);
let promiseArr = [];
for (let key of keys) {
if (methods[key] && typeof (methods[key].exec) === `function`) {
let execPromise = methods[key].exec.call(null, _json[key]);
if (!(execPromise instanceof Promise)) {
throw new Error(`exec on ${key} did not return a promise`);
}
promiseArr.push(execPromise);
} else {
let execPromise = Promise.resolve({
error: `method not defined`
})
promiseArr.push(execPromise);
}
}
Promise.all(promiseArr).then(iter => {
console.log(iter);
let response = {};
iter.forEach((val, index) => {
response[keys[index]] = val;
});
resolve(response);
}).catch(err => {
reject(err);
});
});
},
// this is our docs endpoint
// through this the clients should know
// what methods and datatypes are available
`/describe`: function () {
// load the type descriptions
return new Promise(resolve => {
let type = {};
let method = {};
// set types
type = types;
//set methods
for(let m in methods) {
let _m = JSON.parse(JSON.stringify(methods[m]));
method[m] = _m;
}
resolve({
types: type,
methods: method
});
});
}
};
這是整個程式中非常重要的一部分,因為它提供了實際的介面。 我們有一組 endpoint,每個 endpoint 都對應一個處理函式,在路徑匹配時被呼叫。根據設計原則每個處理函式都必須返回一個 Promise。
RPC endpoint 獲取一個包含請求內容的 json 物件,然後將每個請求解析為 methods.js
檔案中的對應方法,呼叫該方法的 exec
函式,並將結果返回,或者丟擲錯誤。
describe endpoint 掃描方法和型別的描述,並將該資訊返回給呼叫者。讓使用 API 的開發者能夠輕鬆地知道如何使用它。
現在讓我們新增我們之前討論過的函式 requestListener
,然後就可以啟動服務。
// request Listener
// this is what we`ll feed into http.createServer
function requestListener(request, response) {
let reqUrl = `http://${request.headers.host}${request.url}`;
let parseUrl = url.parse(reqUrl, true);
let pathname = parseUrl.pathname;
// we`re doing everything json
response.setHeader(`Content-Type`, `application/json`);
// buffer for incoming data
let buf = null;
// listen for incoming data
request.on(`data`, data => {
if (buf === null) {
buf = data;
} else {
buf = buf + data;
}
});
// on end proceed with compute
request.on(`end`, () => {
let body = buf !== null ? buf.toString() : null;
if (routes[pathname]) {
let compute = routes[pathname].call(null, body);
if (!(compute instanceof Promise)) {
// we`re kinda expecting compute to be a promise
// so if it isn`t, just avoid it
response.statusCode = 500;
response.end(`oops! server error!`);
console.warn(`whatever I got from rpc wasn`t a Promise!`);
} else {
compute.then(res => {
response.end(JSON.stringify(res))
}).catch(err => {
console.error(err);
response.statusCode = 500;
response.end(`oops! server error!`);
});
}
} else {
response.statusCode = 404;
response.end(`oops! ${pathname} not found here`)
}
})
}
// now we can start up the server
server.listen(PORT);
每當有新請求時呼叫此函式並等待拿到資料,之後檢視路徑,並根據路徑匹配到路由表上的對應處理方法。然後使用 server.listen
啟動服務。
現在我們可以在目錄下執行 node server.js
來啟動服務,然後使用 postman 或你熟悉的 API 除錯工具,向 http://localhost{PORT}/rpc
傳送請求,請求體中包含以下 JSON 內容。
{
"createUser": {
"name":"alloys mila",
"age":24
}
}
server 將會根據你提交的請求建立一個新使用者並返回響應結果。一個基於 RPC、文件完善的 API 系統已經搭建完成了。
注意,我們尚未對本教程介面進行任何引數驗證,你在呼叫測試的時候必須手動保證資料正確性。
相關文章
- 「譯」如何使用 NodeJS 構建基於 RPC 的 API 系統NodeJSRPCAPI
- 如何基於 Notadd 構建 API (Laravel 寫 API)APILaravel
- QuillCMS – 基於Nodejs、Nuxtjs、MongoDB構建內容管理系統UINodeJSUXMongoDB
- 基於Hyperf + Vue + Element 構建的後臺管理系統(內建聊天系統)Vue
- 構建基於Spring4的Rest APISpringRESTAPI
- 基於多雲構建監控告警系統
- 基於使用者的協同過濾來構建推薦系統
- 基於MRS-ClickHouse構建使用者畫像系統方案介紹
- 如何使用bloomfilter構建大型Java快取系統OOMFilterJava快取
- 如何構建推薦系統
- 如何構建一個系統?
- 如何構建自己的筆記系統?筆記
- 如何構建基於 docker 的開發環境Docker開發環境
- 如何構建基於docker的開發環境Docker開發環境
- 問題解決:構建基於深度學習架構的推薦系統!深度學習架構
- 使用 JavaFX 構建 Reactive 系統JavaReact
- 基於模擬的數字孿生系統構建與應用
- moell/mojito - 基於 Laravel、Vue、ELement 構建的基礎後臺系統擴充套件LaravelVue套件
- 如何基於 Redis 構建應用程式元件Redis元件
- beego + jwt + vue + element-ui 構建的基於多家 API 的圖床GoJWTVueUIAPI圖床
- [beego新手入門]基於web框架-beego的RESTful API的構建之旅GoWeb框架RESTAPI
- 基於 Docker 構建統一的開發環境Docker開發環境
- 如何構建分散式系統的知識體系分散式
- 基於 K8S 構建資料中心作業系統K8S作業系統
- 基於 Laravel 框架以及 adb 指令構建群控系統 | phoneGCSLaravel框架GC
- 基於Jenkins搭建自動化構建系統採坑記Jenkins
- 使用ASP.NET Web Api構建基於REST風格的服務實戰系列教程ASP.NETWebAPIREST
- 網易基於 Iceberg 的實時湖倉一體系統構建經驗
- 使用cordova構建基於vue的Android專案VueAndroid
- [Kails] 一個基於 Koa2 構建的類似於 Rails 的 nodejs 開源專案AINodeJS
- 如何構建設計語言系統
- 基於 Twirp RPC 的簡易 JSON Api Gateway 實現RPCJSONAPIGateway
- 使用Netty構建Rpc中介軟體(一)NettyRPC
- websocketd | 基於 docker 構建WebDocker
- 基於dropwizard/metrics ,kafka,zabbix構建應用統計資料收集展示系統Kafka
- 使用nodejs構建Docker image最佳實踐NodeJSDocker
- 使用ASP.NET Web API構建RESTful APIASP.NETWebAPIREST
- 基於雲服務MRS構建DolphinScheduler2排程系統