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

来源:互联网 发布:淘宝新浪博客 编辑:程序博客网 时间:2024/04/30 01:30

近日,酷壳网陈皓与支付宝前端负责人玉伯在微博上发起了关于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 还需要去做什么。简单能给用户带去欢喜,完备则可以让开发者去挑战新的领域。

原创粉丝点击