支付宝玉伯:我心目中的优秀API

近日,酷壳网陈皓与支付宝前端负责人玉伯在微博上发起了关于API设计的话题。陈皓表示,有两点是前端工程都需要认真考虑的:1)一些API最好支持批量数据处理,而不是让人一次一次地掉用,2)需要考虑多个API间的关联性,可以降低使用方的工作量。对此支付宝前端负责人玉伯分享了他的看法:

延续大前天的话题,陈皓在微博中提到:

【如何设计JS API?】我觉得有两点各个前端工程需要认真考虑:1)我们的一些 API 最好支持批量数据处理,而不是让人一次一次地调用。2)我们需要考虑多个 API 间的关联性,如果别人有可能在调用 API2 之前需要 API1 的结果,那么我们应该把 API1 和 API2 包一下。这会降低使用方的工作量。

支持批量处理

陈皓提到的这两点非常具体。支持批量处理,是 API 在设计时需要考虑多个输入。比如 shell 中的 cp 命令:

$ cp *.js target_dir

对于 loader 来说也一样:

// 加载一个文件
seajs.use('a', callback)

// 加载多个文件
seajs.use(['a', 'b'], callback)

API 是否支持批量处理,得具体看是什么功能,比如 Node.js 中的读取文件接口:

fs.readFile('/etc/passwd', function (err, data) {
  // do something
});

这个 readFile 就没必要支持批量读取。

什么样的 API,以及什么时候需要支持批量处理呢?我觉得有以下几个规律:

  1. 直接面向普通使用者。比如 shell 中的好多命令,以及 seajs.usejQuery(selector) 等等。这些 API 一般来说不用再封装,是高级 API。

  2. 批量处理本身有含义、是常见需求。比如 readFile 支持批量价值就不大,一次读取多个文件的需求不常见,出现了也很容易基于 readFile 自己去实现。

  3. 批量处理时,顺序无关,不存在依赖性。比如 cp 多个文件时,先处理哪个文件是没关系的。seajs.use 加载多个文件时,先加载同一层级的哪个文件也不应该影响最终结果。

能满足以上需求的 API,经常就需要支持批量处理。

考虑 API 的关联性

这个说的其实是依赖,很大程度上属于 user-land 范畴,API 本身经常很难做什么。比如在 shell 上,可以通过管道来解决依赖:

$ cat sea-debug.js | wc -l

上面通过管道先后执行两个命令,可得到 sea-debug.js 文件的代码行数。

依赖问题最终都是顺序问题,shell 通过管道将依赖转换成单向顺序来解决,很轻巧方便。

但在浏览器端,异步满天飞,问题往往就没那么简单了。

比如

seajs.use(['a', 'b', 'c'], callback)

如果模块 b 依赖模块 a,模块 c 是独立的。那么我们面临的问题是:

  1. seajs 如何知道依赖信息?如何知道模块 b 是依赖模块 a 的?谁来告知?何时告知?
  2. 如何实现 a、b、c 三个模块同时并行加载,但执行时是按照依赖顺序来执行的?

涉及异步、涉及依赖,都绕不开以上问题。在 YUI3、Dojo、RequireJS、SeaJS、OzJS 等等类库 / 框架中都需要解决以上问题。

对于依赖信息的获取,典型的处理方式有两种:

  1. 提前申明依赖信息。比如 YUI 里,对于自带模块,会有一个很大的 json 数据来声明各个模块之间的依赖。非自带模块,则需要在使用前先注册一下,注册时申明好依赖。这样,处理起来就简单了。

  2. 自我携带依赖信息。各个模块的依赖,在模块自己的代码中申明,比如

    define('b', ['a'], factory)
    

    上面的第二个数组参数,表示模块 b 的依赖是模块 a.

有了依赖信息后,就可以转换成顺序问题。依赖先加载,加载并执行后,再加载后续模块。这是最简单的处理方式。

还有一种方式是,因为依赖影响的是执行顺序,因此加载依旧可以并行,通通并行下载好后,在真正执行时,才根据依赖信息按顺序执行。这是 SeaJS 等 loader 的处理方式。

比如对于陈皓的那道面试题,如果用 SeaJS 来解决,可以:

var API_URL = 'http://coolshell.cn/t.php?callback=define&n='
var urls = []

for(var i = 1; i < 31; i++) {
  urls.push(API_URL + i)
}

seajs.use(urls, function() {
  for (var i = 1; i < 31; i++) {
    console.log(i, arguments[i - 1])
  }
})

并发请求和顺序输出都解决了。注意这里的依赖仅仅是最后的顺序输出,与普通的依赖是不同的。普通的模块之间的依赖,可以通过模块之间声明依赖关系来解决。

各种 loader 仅是解决文件加载、文件依赖。如何处理依赖是更宽泛的话题,这里就不多说了。

我心目的优秀 API

以上说的,纯粹是从陈皓的微博引发的一些点上的思考,不具有普适性。对大部分前端 API 设计来说,参考价值也很有限。

下面扯扯更宽层面上,我心目中优秀 API 的标准。

简单

我想了很久,依旧想把“简单”摆在第一位。好的 API 必须是简单的。简单不仅仅是看起来简单,简单还意味着背后的实现逻辑是正常人类思路能理解的。比如

document.getElementById('string')

这个 API 是个前端都能看懂,并且能大概猜出背后是怎么实现的。虽然很可能猜错,但没关系,关键是你不会觉得神秘难懂。类似的,有很多实物 API:

汽车车窗的控制把手。往上提就是关窗,往下摁就是开窗。很符合直觉,大概也能猜出是怎么实现的(当然实际没那么简单,但能让用户感觉很简单)。

简单也意味着一致性。比如 JavaScript 里,forEach、map、filter 等所有数组遍历操作,callback 接收的参数都是 item、index、array. 这种一致性可以让你触类旁通,非常舒适。

完备

完备是指,某个类库或框架,对所解决的问题领域和业务需求,要有彻底的深入理解。提供的 API 是一整套的,能处理该问题领域的各种可能性,各种实际需求。

要达到完备性,首先要解决的是定位问题。任何类库框架都不可能解决所有问题,必须要非常清楚要解决的问题范畴。依旧拿我最爱的 jQuery 来举例。

jQuery 的定位非常清晰: DOM 操作类库,包括 DOM 操作、事件、动画和 Ajax。其他的比如 Cookie 操作、Loader 等功能,即便用户需求很旺盛,jQuery 也会节制欲望,不去涉足。

在这个定位下,jQuery 的设计也非常清晰: 找到 DOM 元素,并操作它。 这样,jQuery 的整套 API 变得很优美:

$(selector).attr(...)
$(selector).css(...)
$(selector).animate(...)
...

优美之处在于,你能想到的常用 DOM 操作等功能,jQuery 都提供了。不怎么常用的,使用 jQuery 的现有 API,也能快速实现。

这就是 API 的完备性,让你不会因为某些功能的实现而抓狂。一切都在那里静静躺着,等着你去发现,等着你去欣赏。

同样,SeaJS 也是抱着这个目的去做。SeaJS 的定位是 Web 端的模块加载器,核心是解决模块定义、依赖管理、模块加载。此外一切问题都不属于 SeaJS 范畴。 SeaJS 的理想是把自己做“死”,“死”意味着完备性,意味着站在 loader 的角度,SeaJS 的功能能增无可增,减无可减。

除了简单、完备这两个关键词,我想不到优秀的 API 还需要去做什么。简单能给用户带去欢喜,完备则可以让开发者去挑战新的领域。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值