如何用 Node.js 編寫一個 API 客戶端
說幾句無關主題的話
盡管這幾年來 Node.js 已經得到越來越多的關注,連市場賣菜的老太婆都能分別得出哪個是寫 Node.js 的,哪個是寫 PHP 的。然而,終究是不能跟老大哥 Java 比的。我們在使用一些第三方服務時常常會 碰到一時半會還沒有官方的 Node.js SDK 的問題,所以能自己隨手擼一個剛好夠用的 API 客戶端來應急 成了必備技能。
說到這里,我忍不住要先吐槽一下:
前幾天在 CNodeJS 上看到一個帖子, 擁抱 ES6——阿里云 OSS 推出 JavaScript SDK 對其中的濫用 generator 還 洋洋自得 的行為有點不滿,之前也遇到過該廠的 SDK 強行返回 generator 而放棄使用,我想說我 已經忍了很久 了。
「我自己寫得爽,也希望把這種“爽”帶給用戶」-- 該 SDK 的維護者如是說
作為一個 SDK(尤其是官方出品的),應該使用最 common 的技術或規范來實現。比如在 Node.js 中的異步問題,應該使用傳統的 callback 或者 ES6 里面的 promise ,而不是使用 比較奇葩的 generator 來做。 generator 來做不妥的地方是:
- generator 的出現不是為了解決異步問題
- 使用 generator 是會傳染的,當你嘗試 yield 一下的時候,它要求你也必須在一個 generator function 內
當然,如果這是一個內部項目,使用各種花式姿勢都是沒問題的,只要定好規范就行。而如果這是要給別人 使用的東西,應該照顧其他人的感受。
所以我們要自己動手寫一個 SDK 還有另外一種情況就是 對官方的 SDK 并不滿意 。
好了,我吐槽完了。
運行環境
最近一年來,Node.js 相繼發布了 4.0、5.0、6.0(前幾天),7.0 也已經蓄勢待發,但目前來看 主流還是 4.x 版本 。Node.js 4.x 支持一部分的 ES6 語法,比如箭頭函數、 let 和 const 等,解決異步問題也可以直接使用 ES6 的 promise 。
如果沒有特殊情況,新寫的程序可以不用考慮在 0.12 或者更早的 0.10 上運行,如果以后確實需要在這些 版本上執行,可以借用 Babel 來編譯成 ES5 語法的程序。
API 接口將同時支持 callback 和 promise 兩種回調方式。 promise 直接使用 ES6 原生的 Promise 對象而不是使用 bluebird 模塊。盡管使用 bluebird 會有更多的功能和更好的性能, 但在這樣一個需要網絡 IO 的場景下,那么一點性能差別基本可以忽略不計,而作為一個極簡主義者,覺得 沒太大必要引入這么一個依賴庫。
功能設計
本文將以 CNodeJS 提供的 API 為例。CNodeJS 的 API 分兩種:
- 公共接口,比如獲取主題列表和詳情等
- 用戶接口,需要提供 accesstoken 參數來驗證用戶權限( accessToken 可以在個人設置界面中 得到)
程序的使用方法如下:
'use strict';
const client = new CNodeJS({
token: 'xxxxxxx', // accessToken,可為空
});
// promise 方式調用
client.getTopics({page: 1})
.then(list => console.log(list))
.catch(err => console.error(err));
// callback 方式調用
client.getTopics({page: 1}, (err, list) => {
if (err) {
console.error(err);
} else {
console.log(list);
}
});
初始化項目
1、首先新建項目目錄:
$ mkdir cnodejs_api_client
$ cd cnodejs_api_client
$ git init
2、初始化 package.json :
$ npm init
3、新建文件 index.js :
'use strict';
const rawRequest = require('request');
class CNodeJS {
constructor(options) {
this.options = options = options || {};
options.token = options.token || null;
options.url = options.url || 'https://cnodejs.org/api/v1/';
}
baseParams(params) {
params = Object.assign({}, params || {});
if (this.options.token) {
params.accesstoken = this.options.token;
}
return params;
}
request(method, path, params, callback) {
return new Promise((resolve, reject) => {
const opts = {
method: method.toUpperCase(),
url: this.options.url + path,
json: true,
};
if (opts.method === 'GET' || opts.method === 'HEAD') {
opts.qs = this.baseParams(params);
} else {
opts.body = this.baseParams(params);
}
rawRequest(opts, (err, res, body) => {
if (err) return reject(err);
if (body.success) {
resolve(body);
} else {
reject(new Error(body.error_msg));
}
});
});
}
}
module.exports = CNodeJS;
說明:
- 使用 request 模塊來發送 HTTP 請求,需要執行命令來安裝該模塊: npm install request --save
- 我們實現了一個帶有 request 方法的 CNodeJS 類,可以通過該方法來發送任意 API 請求, 比如請求主題首頁是 request('GET', 'topics', {page: 1})
- 如果初始化 CNodeJS 實例時傳入了 token ,則每次請求都會自動帶上 accesstoken 參數
- 返回的結果 success=true 表示 API 請求成功,則直接回調該結果;如果失敗則 error_msg 表示出錯信息
4、新建測試文件 test.js :
'use strict';
const CNodeJS = require('./');
const client = new CNodeJS();
client.request('GET', 'topics', {page: 1})
.then(ret => console.log(ret))
.catch(err => console.error(err));
5、執行命令 node test.js 即可看到類似以下的結果:
{ success: true,
data:
[ { id: '572afb6b15c24e592c16e1e6',
author_id: '504c28a2e2b845157708cb61',
tab: 'share',
content: '.......'
...
至此我們已經完成了一個 API 客戶端最基本的功能,接下來根據不同的 API 封裝一下 request 方法 即可。
支持 callback
前文已經提到, 「作為一個 SDK,應該使用最 common 的技術或規范來實現」 ,所以除了 promise 之外還需要提供 callback 的支持。
1、修改文件 index.js 中 request(method, path, params) { } 定義部分:
request(method, path, params, callback) {
return new Promise((_resolve, _reject) => {
const resolve = ret => {
_resolve(ret);
callback && callback(null, ret);
};
const reject = err => {
_reject(err);
callback && callback(err);
};
// 以下部分不變
// ...
});
}
說明:
- 將 new Promise() 中的 resolve 和 reject 分別改名為 _resolve 和 _reject
- 在函數開頭新建 resolve 和 reject ,其作用是調用原來的 _resolve 和 _reject ,同時 判斷如果有 callback 參數,則也調用該函數
2、將文件 test.js 中 client.request() 部分改為 callback 方式調用:
client.request('GET', 'topics', {page: 1}, (err, ret) => {
if (err) {
console.error(err);
} else {
console.log(ret);
}
});
3、重新執行 node test.js 可以看到結果跟之前是一樣的。
通過簡單的修改我們就已經實現了同時支持 promise 和 callback 兩種異步回調方式。
封裝 API
前文我們實現的 request() 方法已經可以調用任意的 API 了,但是為了是方便,一般需要為每個 API 單獨封裝一個方法,比如:
- getTopics() - 獲取主題首頁
- getTopicDetail() - 獲取主題詳情
- testToken() - 測試 token 是否正確
對于 getTopics() 可以這樣簡單地實現:
getTopics(params, callback) {
return this.request('GET', 'topics', params, callback);
}
但其返回的結果是這樣結構的:
{ success: true,
data: []
}
要取得結果還要讀取里面的 data ,針對這種情況我們可以改成這樣:
getTopics(params, callback) {
return this.request('GET', 'topics', params, callback)
.then(ret => Promise.resolve(ret.data));
}
getTopicDetail() 和 testToken() 可以這樣實現:
getTopicDetail(params, callback) {
return this.request('GET', `topic/${params.id}`, params, callback)
.then(ret => Promise.resolve(ret.data));
}
testToken(callback) {
return this.request('POST', `accesstoken`, {}, callback);
}
對于其他的 API 也可以采用類似的方法一一實現。
由此看來編寫一個簡單的 API 客戶端也不是一件很難的事情,本文介紹的方法已經能適用大多數的情況了。 當然還有些問題是沒提到的,比如阿里云 OSS 這種 SDK 還要考慮 stream 上傳問題,還有斷點續傳。 對于安全性要求較高的 SDK 可能還需要做數據簽名等等。
在編寫本文的時候,通過閱讀 request 的 API 文檔我才發現原來可以通過 json=true 選項來讓 它自動解析返回的結果,這樣確實能少寫好幾行代碼了。
另外我還是忍不住再吐槽一下,CNodeJS 的 API 接口設計得并不一致,響應成功時并不是所有數據都放在 data 里面(比如 testToken() )。
發覺最近有點上火了 ^_^
來自: http://morning.work/page/2016-05/how-to-write-a-nodejs-api-client-package.html