近日,酷壳网陈皓与支付宝前端负责人玉伯在微博上发起了关于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,以及什么时候需要支持批量处理呢?我觉得有以下几个规律:
-
直接面向普通使用者。比如 shell 中的好多命令,以及
seajs.use
、jQuery(selector)
等等。这些 API 一般来说不用再封装,是高级 API。 -
批量处理本身有含义、是常见需求。比如 readFile 支持批量价值就不大,一次读取多个文件的需求不常见,出现了也很容易基于 readFile 自己去实现。
-
批量处理时,顺序无关,不存在依赖性。比如 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 是独立的。那么我们面临的问题是:
- seajs 如何知道依赖信息?如何知道模块 b 是依赖模块 a 的?谁来告知?何时告知?
- 如何实现 a、b、c 三个模块同时并行加载,但执行时是按照依赖顺序来执行的?
涉及异步、涉及依赖,都绕不开以上问题。在 YUI3、Dojo、RequireJS、SeaJS、OzJS 等等类库 / 框架中都需要解决以上问题。
对于依赖信息的获取,典型的处理方式有两种:
-
提前申明依赖信息。比如 YUI 里,对于自带模块,会有一个很大的 json 数据来声明各个模块之间的依赖。非自带模块,则需要在使用前先注册一下,注册时申明好依赖。这样,处理起来就简单了。
-
自我携带依赖信息。各个模块的依赖,在模块自己的代码中申明,比如
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 还需要去做什么。简单能给用户带去欢喜,完备则可以让开发者去挑战新的领域。