如何用 Node.js 编写一个 RESTful API 客户端

老雷 创作于 2016-05-07 , 2016-08-03 第 1 次更新
Node.js Client 全文约 4747 字,预计阅读时间为 12 分钟

说几句无关主题的话

尽管这几年来 Node.js 已经得到越来越多的关注,连市场卖菜的老太婆都能分别得出哪个是写 Node.js 的,哪个是写 PHP 的。然而,终究是不能跟老大哥 Java 比的。我们在使用一些第三方服务时常常会碰到一时半会还没有官方的 Node.js SDK 的问题,所以能自己随手撸一个刚好够用的 RESTful API 客户端来应急成了必备技能。

说到这里,我忍不住要先吐槽一下:

前几天在 CNodeJS 上看到一个帖子《拥抱 ES6——阿里云 OSS 推出 JavaScript SDK》,对其中的滥用generator洋洋自得的行为有点不满,之前也遇到过该厂的 SDK 强行返回generator而放弃使用,我想说我已经忍了很久了。

「我自己写得爽,也希望把这种“爽”带给用户」该 SDK 的维护者如是说

作为一个 SDK(尤其是官方出品的),应该使用最 common 的技术或规范来实现。比如在 Node.js 中的异步问题,应该使用传统的callback或者 ES6 里面的promise,而不是使用比较奇葩的generator来做。generator来做不妥的地方是:

当然,如果这是一个内部项目,使用各种花式姿势都是没问题的,只要定好规范就行。而如果这是要给别人使用的东西,应该照顾其他人的感受。

所以我们要自己动手写一个 SDK 还有另外一种情况就是对官方的 SDK 并不满意

好了,我吐槽完了。

运行环境

最近一年来,Node.js 相继发布了 4.0、5.0、6.0(前几天),7.0 也已经蓄势待发,但目前来看主流还是 4.x 版本。Node.js 4.x 支持一部分的 ES6 语法,比如箭头函数、letconst等,解决异步问题也可以直接使用 ES6 的promise

如果没有特殊情况,新写的程序可以不用考虑在 0.12 或者更早的 0.10 上运行,如果以后确实需要在些版本上执行,可以借用 Babel 来编译成 ES5 语法的程序。

API 接口将同时支持callbackpromise两种回调方式。promise直接使用 ES6 原生的Promise对象而不是使用bluebird模块。尽管使用bluebird会有更多的功能和更好的性能,但在这样一个需要网络 IO 的场景下,那么一点性能差别基本可以忽略不计,而作为一个极简主义者,觉得没太大必要引入这么一个依赖库。

功能设计

本文将以 CNodeJS 提供的 API 为例。CNodeJS 的 API 分两种:

程序的使用方法如下:

'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;

说明:

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.jsrequest(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);
    };

    // 以下部分不变
    // ...
  });
}

说明:

2、将文件test.jsclient.request()部分改为 callback 方式调用:

client.request('GET', 'topics', {page: 1}, (err, ret) => {
  if (err) {
    console.error(err);
  } else {
    console.log(ret);
  }
});

3、重新执行node test.js可以看到结果跟之前是一样的。

通过简单的修改我们就已经实现了同时支持promisecallback两种异步回调方式。

封装 API

前文我们实现的request()方法已经可以调用任意的 API 了,但是为了是方便,一般需要为每个 API 单独封装一个方法,比如:

对于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())。

相关链接