跳到主要内容
CNode

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

精华分享
Lleizongmin发布于 10 年前最后回复 10 年前
32259610
目录 · 7 个章节

原文地址:http://morning.work/page/2016-05/how-to-write-a-nodejs-api-client-package.html 转载请注明出处


说几句无关主题的话

尽管这几年来 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())。

发觉最近有点上火了 ^_^

查看回复

回复 (32)

W
WangCao#3210 年前

学到很多东西,赞

I
IamFive#3110 年前
引用 leizongmin@IamFive 用原生

@leizongmin 选用了 superagent,大小45k左右,能接受。

I
IamFive#2910 年前

request 在 browserify的时候太大了,有撒可用的替代库吗?我看很多都是用原生的 http,但是要封装好多功能啊。

S
songbo#2810 年前

mark

T
tomleader#2610 年前

支持撸主,用Promise完全可以达到要求。ali sdk搞的那个generator让我用的时候很angry!

B
bajian#2510 年前

撸async/await的路过。表示受不了promise,特别是逻辑比较复杂的时候,写起来特别尴尬!

K
kentkwan#2410 年前

别吐槽ssh了 首先 现在已经不会有新项目用stucts 然后 springboot这个项目带来了快速开发的希望 并且 sh的门槛其实不高

H
haiwennnn#2310 年前

马克 From Noder

H
htoooth#2210 年前

@leizongmin 恩,理解了。从你的文章中学到了很多的东西,谢谢了!

L
leizongmin#2110 年前
引用 htoooth@ayiis 恩我知道了,你说的没错。 但是这样写会误导别人,至少我觉得不太合理,因为这样 callback 就直接调用,而不是异步调用。为了证明我的观点,我去找了 parse js sdk ...

@htoooth 关于你提到的这个问题,我也尝试通过网上搜索别人关于callback的看法,比如这篇文章:http://mao.li/javascript/javascript-callback-function/ 其观点是,callback 不一定用于异步,一般同步(阻塞)的场景下也经常用到回调,比如要求执行某些操作后执行回调函数。,与我的想法是相吻合的

L
leizongmin#2010 年前
引用 htoooth@ayiis 恩我知道了,你说的没错。 但是这样写会误导别人,至少我觉得不太合理,因为这样 callback 就直接调用,而不是异步调用。为了证明我的观点,我去找了 parse js sdk ...

@htoooth 如果一定要保证异步,可以这样:

const resolve = ret => {
  _resolve(ret);
  callback && process.nextTick(() => callback(null, ret));
};

const reject = err => {
  _reject(err);
  callback && process.nextTick(() => callback(err));
};

不过在平时使用的时候,我从来不会假设callback就一定是异步的,所以习惯上不会专门处理以保证它一定不能同步调用

B
beyond5959#1810 年前

mark

H
htoooth#1710 年前

@ayiis 恩我知道了,你说的没错。 但是这样写会误导别人,至少我觉得不太合理,因为这样 callback 就直接调用,而不是异步调用。为了证明我的观点,我去找了 parse-js-sdk ,学学他们是怎么做的。看了看,也是使用 promise 的 then 进行异步的调用,没有直接 callback。直接上代码,链接:

find 方法支持回调和promise:

find(options?: FullOptions): ParsePromise {
    options = options || {};

    let findOptions = {};
    if (options.hasOwnProperty('useMasterKey')) {
      findOptions.useMasterKey = options.useMasterKey;
    }
    if (options.hasOwnProperty('sessionToken')) {
      findOptions.sessionToken = options.sessionToken;
    }

    let controller = CoreManager.getQueryController();

    return controller.find(
      this.className,
      this.toJSON(),
      findOptions
    ).then((response) => {
      return response.results.map((data) => {
        // In cases of relations, the server may send back a className
        // on the top level of the payload
        let override = response.className || this.className;
        if (!data.className) {
          data.className = override;
        }
        return ParseObject.fromJSON(data, true);
      });
    })._thenRunCallbacks(options);
  }
_thenRunCallbacks(optionsOrCallback, model) {
    var options = {};
    if (typeof optionsOrCallback === 'function') {
      options.success = function(result) {
        optionsOrCallback(result, null);
      };
      options.error = function(error) {
        optionsOrCallback(null, error);
      };
    } else if (typeof optionsOrCallback === 'object') {
      if (typeof optionsOrCallback.success === 'function') {
        options.success = optionsOrCallback.success;
      }
      if (typeof optionsOrCallback.error === 'function') {
        options.error = optionsOrCallback.error;
      }
    }

    return this.then(function(...results) {
      if (options.success) {
        options.success.apply(this, results);
      }
      return ParsePromise.as.apply(ParsePromise, arguments);
    }, function(error) {
      if (options.error) {
        if (typeof model !== 'undefined') {
          options.error(model, error);
        } else {
          options.error(error);
        }
      }
      // By explicitly returning a rejected Promise, this will work with
      // either jQuery or Promises/A+ semantics.
      return ParsePromise.error(error);
    });
  }
A
ayiis#1610 年前
引用 htoooth个人觉得 callback 没有考虑到同步还是异步的设计,如果不是 promise 要扑街。

@htoooth 这里的promise.resolve和callback都会被调用,你指的是在哪种情况下这种实现会仆街?

A
ayiis#1510 年前

很精致

H
htoooth#1410 年前
const resolve = ret => {
      _resolve(ret);
      callback && callback(null, ret);
    };

    const reject = err => {
      _reject(err);
      callback && callback(err);
    };

个人觉得 callback 没有考虑到同步还是异步的设计,如果不是 promise 要扑街。

A
alanyang#1310 年前

同意,api设计就只用考虑callback和promise就可以。。在应用层有co和es7 await的存在promise一点问题没有

Z
zhengjun87#1210 年前

楼主不管是回帖还是文章,都很注重新手操作,大赞

G
glj1102#1110 年前
引用 zouzhenxing支持楼主 nodejs像Java那样搞出SSH框架是有必要的,这样会减轻开发负担。但这样一来,也会让nodejs失掉开源精神。 就像现在大学一样,所有学Java的都学SSH框架,但没有几个人真...

@zouzhenxing ssh刚开始感觉高大上,其实不适合互联网开发,尤其是敏捷开发,nodejs简单,开发快捷,express,koa,restify等框架很实用

X
XadillaX#1010 年前

就我自身而言,我写 SDK 甚至连 ES6 特性都不会用上,比如 let、const、箭头函数、class 等等。

虽然我自己的项目都会用上这些特性,但是写 SDK 就不会这么任性——我感觉这才是友好的做法。

F
fangker#710 年前

@leizongmin 如果关于user有很多api操作,一般是把这操作当做类方法放到user类里么?

F
fangker#610 年前

@leizongmin 雷叔,为什么不告诉我怎么使用生成器..

H
Hohill#510 年前

支持楼主 自豪地采用 CNodeJS ionic

I
iyuq#410 年前

确实,generator本来不是用来解决异步问题的,虽然有co之类的以后会让generator像async和await,但是需要依赖额外的包,而且给调试也带来了难度,koa 也在放弃generator,回归promise和async/await

来自酷炫的 CNodeMD

K
klausgao#210 年前

我觉得不应该吐槽生成器,其实你如果习惯用co,就无所谓promise和生成器了,co4.0都能很好的支持,也能在es7时很方便的替换成async和await。

Z
zouzhenxing#110 年前

支持楼主 nodejs像Java那样搞出SSH框架是有必要的,这样会减轻开发负担。但这样一来,也会让nodejs失掉开源精神。

就像现在大学一样,所有学Java的都学SSH框架,但没有几个人真能理解SSH都解决了什么问题。 我希望阿里或其它大公司搞出像SSH的东西,但又害怕这样的东西。真是矛盾。

参与回复

登录后即可参与回复。登录