一、接口的流畅性
好的接口是流畅易懂的,他主要体现如下几个方面:
1.简单
操作某个元素的css属性,下面是原生的方法:
1
|
document.querySelectorAll(
'#id'
).style.color =
'red'
;
|
封装之后
1
2
3
4
|
function
a(selector, color) {
document.querySelectorAll(selector)[].style.color = color
}
a(
'#a'
,
'red'
);
|
从几十个字母长长的一行到简简单单的一个函数调用,体现了api简单易用
2.可阅读性
a('#a', 'red')是个好函数,帮助我们简单实用地改变某个元素,但问题来了,如果第一次使用改函数的人来说会比较困惑,a函数是啥函数,没有人告诉他。开发接口有必要知道一点,人都是懒惰的,从颜色赋值这个函数来说,虽然少写了代码,但是增加了记忆成本。每次做这件事情的时候都需要有映射关系。 a---->color. 如果是简单的几个无所谓,但是通常一套框架都有几十甚至上百的api,映射成本增加会使得程序员哥哥崩溃。 我们需要的就是使得接口有意义,下面我们改写一下a函数:
1
2
3
|
function
letSomeElementChangeColor(selector, color) {
document.querySelectorAll(selector, color);
}
|
letSomeElementChangeColor相对于a来说被赋予了语言意义,任何人都会知道它的意义
3.减少记忆成本
我们刚刚的函数也是这样的它太长了letSomeElementChangeColor虽然减少了映射成本,但是增加了记忆成本。要知道,包括学霸在内,任何人都不喜欢被单词。原生获取dom的api也同样有这个问题 document.getElementsByClassName; document.getElementsByName; document.querySelectorAll;这些api给人的感觉就是单词太长了,虽然他给出的意义是很清晰,然而这种做法是建立在牺牲简易性的基础上进行的。于是我们又再次改写这个之前函数
1
2
3
|
function
setColor(selector, color) {
xxxxxxxxxxxx
}
|
在意义不做大的变化前提下,缩减函数名称。使得它易读易记易用;
4.可延伸
所谓延伸就是指函数的使用像流水一样按照书写的顺序执行形成执行链条:
1
2
3
|
document.getElementById(
'id'
).style.color =
'red'
;
document.getElementById(
'id'
).style.fontSize =
'px'
;
document.getElementById(
'id'
).style.backgourdColor =
'pink'
;
|
用我们之前的之前的方法是再次封装两个函数 setFontSize, setbackgroundColor; 然后执行它们 setColor('id', 'red');setFontSiez('id', '12px'); setbackgroundColor('id', 'pink'); 显然,这样的做法没有懒出境界来;id元素每次都需要重新获取,影响性能,失败;每次都需要添加新的方法 失败 每次还要调用这些方法,还是失败。下面我们将其改写为可以延伸的函数 首先将获取id方法封装成对象,然后再对象的每个方法中返回这个对象:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
function
getElement(selector) {
this
.style = document.querySelecotrAll(selector).style;
}
getElement.prototype.color =
function
(color) {
this
.style.color = color;
return
this
;
}
getElement.prototype.background =
function
(bg) {
this
.style.backgroundColor = color;
return
this
;
}
getElement.prototype.fontSize =
function
(size) {
this
.style.fontSize = size;
return
this
;
}
//调用
var
el =
new
getElement(
'#id'
)
el.color(
'red'
).background(
'pink'
).fontSize(
'px'
);
|
简单、流畅、易读后面我们会在参数里面讲到如何继续优化。所以,大家都比较喜欢用jquery的api,虽然一个$符号并不代表任何现实意义,但简单的符号有利于我们的使用。它体现了以上的多种原则,简单,易读,易记,链式写法,多参处理。
nightware:
1
2
3
|
document.getElementById(
'id'
).style.color =
'red'
;
document.getElementById(
'id'
).style.fontSize =
'px'
;
document.getElementById(
'id'
).style.backgourdColor =
'pink'
;
|
dream:
1
|
$(
'id'
).css({color:
'red'
, fontSize:
'12px'
, backgroundColor:
'pink'
})
|
二、一致性
1.接口的一致性
相关的接口保持一致的风格,一整套 API 如果传递一种熟悉和舒适的感觉,会大大减轻开发者对新工具的适应性。 命名这点事:既要短,又要自描述,最重要的是保持一致性 “在计算机科学界只有两件头疼的事:缓存失效和命名问题” — Phil Karlton 选择一个你喜欢的措辞,然后持续使用。选择一种风格,然后保持这种风格。
Nightware:
setColor,
letBackGround
changefontSize
makedisplay
dream:
setColor;
setBackground;
setFontSize
set.........
尽量地保持代码风格和命名风格,使人读你的代码像是阅读同一个人写的文章一样。
三、参数的处理
1.参数的类型
判断参数的类型为你的程序提供稳定的保障
1
2
3
4
5
|
//我们规定,color接受字符串类型
function
setColor(color) {
if
(
typeof
color !==
'string'
)
return
;
dosomething
}
|
2.使用json方式传参
使用json的方式传值很多好处,它可以给参数命名,可以忽略参数的具体位置,可以给参数默认值等等 比如下面这种糟糕的情况:
function fn(param1, param2...............paramN)
你必须对应地把每一个参数按照顺序传入,否则你的方法就会偏离你预期去执行,正确的方法是下面的做法。
1
2
3
4
5
6
7
8
|
function
fn(json) {
//为必须的参数设置默认值
var
default
= extend({
param:
'default'
,
param:
'default'
......
},json)
}
|
这段函数代码,即便你不传任何参数进来,他也会预期运行。因为在声明的时候,你会根据具体的业务决定参数的缺省值。
四、可扩展性
软件设计最重要的原则之一:永远不修改接口,指扩展它!可扩展性同时会要求接口的职责单一,多职责的接口很难扩展。 举个栗子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
//需要同时改变某个元素的字体和背景
// Nightware:
function
set(selector, color) {
document.querySelectroAll(selector).style.color = color;
document.querySelectroAll(selector).style.backgroundColor = color;
}
//无法扩展改函数,如果需要再次改变字体的大小的话,只能修改此函数,在函数后面填加改变字体大小的代码
//Dream
function
set(selector, color) {
var
el = document.querySelectroAll(selector);
el.style.color = color;
el.style.backgroundColor = color;
return
el;
}
//需要设置字体、背景颜色和大小
function
setAgain (selector, color, px) {
var
el = set(selector, color)
el.style.fontSize = px;
return
el;
}
|
以上只是简单的添加颜色,业务复杂而代码又不是你写的时候,你就必须去阅读之前的代码再修改它,显然是不符合开放-封闭原则的。修改后的function是返回了元素对象,使得下次需要改变时再次得到返回值做处理。
2.this的运用
可扩展性还包括对this的以及call和apply方法的灵活运用:
1
2
3
4
5
6
7
8
|
function
sayBonjour() {
alert(
this
.a)
}
obj.a = ;
obj.say = sayBonjour;
obj.say();
//
//or
sayBonjour.call||apply(obj);
//
|
五、对错误的处理
1.预见错误
可以用 类型检测 typeof 或者try...catch。 typeof 会强制检测对象不抛出错误,对于未定义的变量尤其有用。
2.抛出错误
大多数开发者不希望出错了还需要自己去找带对应得代码,最好方式是直接在console中输出,告诉用户发生了什么事情。我们可以用到浏览器的输出api:console.log/warn/error。你还可以为自己的程序留些后路: try...catch。
1
2
3
4
5
6
7
8
9
10
11
12
|
function
error (a) {
if
(
typeof
a !==
'string'
) {
console.error(
'param a must be type of string'
)
}
}
function
error() {
try
{
// some code excucete here maybe throw wrong
}
catch
(ex) {
console.wran(ex);
}
}
|
六、可预见性
可预见性味程序接口提供健壮性,为保证你的代码顺利执行,必须为它考虑到非正常预期的情况。我们看下不可以预见的代码和可预见的代码的区别用之前的setColor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
//nighware
function
set(selector, color) {
document.getElementById(selector).style.color = color;
}
//dream
zepto.init =
function
(selector, context) {
var
dom
// If nothing given, return an empty Zepto collection
if
(!selector)
return
zepto.Z()
// Optimize for string selectors
else
if
(
typeof
selector ==
'string'
) {
selector = selector.trim()
// If it's a html fragment, create nodes from it
// Note: In both Chrome and Firefox , DOM error
// is thrown if the fragment doesn't begin with <
if
(selector[] ==
'<'
&& fragmentRE.test(selector))
dom = zepto.fragment(selector, RegExp.$, context), selector =
null
// If there's a context, create a collection on that context first, and select
// nodes from there
else
if
(context !== undefined)
return
$(context).find(selector)
// If it's a CSS selector, use it to select nodes.
else
dom = zepto.qsa(document, selector)
}
// If a function is given, call it when the DOM is ready
else
if
(isFunction(selector))
return
$(document).ready(selector)
// If a Zepto collection is given, just return it
else
if
(zepto.isZ(selector))
return
selector
else
{
// normalize array if an array of nodes is given
if
(isArray(selector)) dom = compact(selector)
// Wrap DOM nodes.
else
if
(isObject(selector))
dom = [selector], selector =
null
// If it's a html fragment, create nodes from it
else
if
(fragmentRE.test(selector))
dom = zepto.fragment(selector.trim(), RegExp.$, context), selector =
null
// If there's a context, create a collection on that context first, and select
// nodes from there
else
if
(context !== undefined)
return
$(context).find(selector)
// And last but no least, if it's a CSS selector, use it to select nodes.
else
dom = zepto.qsa(document, selector)
}
// create a new Zepto collection from the nodes found
return
zepto.Z(dom, selector)
}
|
以上是zepto的源码,可以看见,作者在预见传入的参数时做了很多的处理。其实可预见性是为程序提供了若干的入口,无非是一些逻辑判断而已。zepto在这里使用了很多的是非判断,同时导致了代码的冗长,不适合阅读。总之,可预见性真正需要你做的事多写一些对位置实物的参数。把外部的检测改为内部检测。是的使用的人用起来舒心放心开心。呐!做人嘛最重要的就是海森啦。
七、注释和文档的可读性
一个最好的接口是不需要文档我们也会使用它,但是往往接口量一多和业务增加,接口使用起来也会有些费劲。所以接口文档和注释是需要认真书写的。注释遵循简单扼要地原则,给多年后的自己也给后来者看:
1
2
3
4
5
6
7
8
9
|
//注释接口,为了演示PPT用
function
commentary() {
//如果你定义一个没有字面意义的变量时,最好为它写上注释:a:没用的变量,可以删除
var
a;
//在关键和有歧义的地方写上注释,犹如画龙点睛:路由到hash界面后将所有的数据清空结束函数
return
go.Navigate(
'hash'
,
function
(){
data.clear();
});
}
|
最后
推荐markdown语法书写API文档,github御用文档编写语法。简单、快速,代码高亮、话不多说上图
以上所述是小编给大家介绍的JavaScript的API设计原则的全部叙述,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对脚本之家网站的支持!
设计良好的 API ,开发者可以快速上手,没必要经常抱着手册和文档,也没必要频繁光顾技术支持社区。
流畅的接口
方法链:流畅易读,更易理解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// 常见的 API 调用方式:改变一些颜色,添加事件监听
var
elem
=
document
.
getElementById
(
"foobar"
)
;
elem
.
style
.
background
=
"red"
;
elem
.
style
.
color
=
"green"
;
elem
.
addEventListener
(
'click'
,
function
(
event
)
{
alert
(
"hello world!"
)
;
}
,
true
)
;
//(设想的)方法链 API
DOMHelper
.
getElementById
(
'foobar'
)
.
setStyle
(
"background"
,
"red"
)
.
setStyle
(
"color"
,
"green"
)
.
addEvent
(
"click"
,
function
(
event
)
{
alert
(
"hello world"
)
;
}
)
;
|
设置和获取操作,可以合二为一;方法越多,文档可能越难写
1
2
3
4
5
6
7
8
9
10
|
var
$
elem
=
jQuery
(
"#foobar"
)
;
//setter
$
elem
.
setCss
(
"background"
,
"green"
)
;
//getter
$
elem
.
getCss
(
"color"
)
===
"red"
;
//getter, setter 合二为一
$
elem
.
css
(
"background"
,
"green"
)
;
$
elem
.
css
(
"color"
)
===
"red"
;
|
一致性
相关的接口保持一致的风格,一整套 API 如果传递一种熟悉和舒适的感觉,会大大减轻开发者对新工具的适应性。
命名这点事:既要短,又要自描述,最重要的是保持一致性
“There are only two hard problems in computer science: cache-invalidation and naming things.”
“在计算机科学界只有两件头疼的事:缓存失效和命名问题”
— Phil Karlton
选择一个你喜欢的措辞,然后持续使用。选择一种风格,然后保持这种风格。
处理参数
需要考虑大家如何使用你提供的方法,是否会重复调用?为何会重复调用?你的 API 如何帮助开发者减少重复的调用?
接收 map 映射参数,回调或者序列化的属性名,不仅让你的 API 更干净,而且使用起来更舒服、高效。
jQuery 的 css() 方法可以给 DOM 元素设置样式:
1
2
3
4
5
|
jQuery
(
"#some-selector"
)
.
css
(
"background"
,
"red"
)
.
css
(
"color"
,
"white"
)
.
css
(
"font-weight"
,
"bold"
)
.
css
(
"padding"
,
10
)
;
|
这个方法可以接受一个 JSON 对象:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
jQuery
(
"#some-selector"
)
.
css
(
{
"background"
:
"red"
,
"color"
:
"white"
,
"font-weight"
:
"bold"
,
"padding"
:
10
}
)
;
// 通过传一个 map 映射绑定事件
jQuery
(
"#some-selector"
)
.
on
(
{
"click"
:
myClickHandler
,
"keyup"
:
myKeyupHandler
,
"change"
:
myChangeHandler
}
)
;
// 为多个事件绑定同一个处理函数
jQuery
(
"#some-selector"
)
.
on
(
"click keyup change"
,
myEventHandler
)
;
|
处理类型
定义方法的时候,需要决定它可以接收什么样的参数。我们不清楚人们如何使用我们的代码,但可以更有远见,考虑支持哪些参数类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// 原来的代码
DateInterval
.
prototype
.
days
=
function
(
start
,
end
)
{
return
Math
.
floor
(
(
end
-
start
)
/
86400000
)
;
}
;
// 修改后的代码
DateInterval
.
prototype
.
days
=
function
(
start
,
end
)
{
if
(
!
(
start
instanceof
Date
)
)
{
start
=
new
Date
(
start
)
;
}
if
(
!
(
end
instanceof
Date
)
)
{
end
=
new
Date
(
end
)
;
}
return
Math
.
floor
(
(
end
.
getTime
(
)
-
start
.
getTime
(
)
)
/
86400000
)
;
}
;
|
加了短短的 6 行代码,我们的方法强大到可以接收 Date 对象,数字的时间戳,甚至像Sat Sep08 201215:34:35GMT+0200(CEST) 这样的字符串。
如果你需要确保传入的参数类型(字符串,数字,布尔),可以这样转换:
1
2
3
4
5
|
function
castaway
(
some_string
,
some_integer
,
some_boolean
)
{
some_string
+=
""
;
some_integer
+=
0
;
// parseInt(some_integer, 10) 更安全些
some_boolean
=
!
!
some_boolean
;
}
|
处理 undefined
为了使你的 API 更健壮,需要鉴别是否真正的 undefined 值被传递进来,可以检查 arguments 对象:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
function
testUndefined
(
expecting
,
someArgument
)
{
if
(
someArgument
===
undefined
)
{
console
.
log
(
"someArgument 是 undefined"
)
;
}
if
(
arguments
.
length
>
1
)
{
console
.
log
(
" 然而它实际是传进来的 "
)
;
}
}
testUndefined
(
"foo"
)
;
// 结果: someArgument 是 undefined
testUndefined
(
"foo"
,
undefined
)
;
// 结果: someArgument 是 undefined , 然而它实际是传进来的
|
给参数命名
1
2
3
4
5
|
event
.
initMouseEvent
(
"click"
,
true
,
true
,
window
,
123
,
101
,
202
,
101
,
202
,
true
,
false
,
false
,
false
,
1
,
null
)
;
|
Event.initMouseEvent 这个方法简直丧心病狂,不看文档的话,谁能说出每个参数是什么意思?
给每个参数起个名字,赋个默认值,可好
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
event
.
initMouseEvent
(
type
=
"click"
,
canBubble
=
true
,
cancelable
=
true
,
view
=
window
,
detail
=
123
,
screenX
=
101
,
screenY
=
202
,
clientX
=
101
,
clientY
=
202
,
ctrlKey
=
true
,
altKey
=
false
,
shiftKey
=
false
,
metaKey
=
false
,
button
=
1
,
relatedTarget
=
null
)
;
|
ES6, 或者 Harmony 就有 默认参数值 和 rest 参数 了。
参数接收 JSON 对象
与其接收一堆参数,不如接收一个 JSON 对象:
1
2
3
4
5
6
7
8
9
10
11
12
|
function
nightmare
(
accepts
,
async
,
beforeSend
,
cache
,
complete
,
/* 等 28 个参数 */
)
{
if
(
accepts
===
"text"
)
{
// 准备接收纯文本
}
}
function
dream
(
options
)
{
options
=
options
||
{
}
;
if
(
options
.
accepts
===
"text"
)
{
// 准备接收纯文本
}
}
|
调用起来也更简单了:
1
2
3
4
5
6
7
|
nightmare
(
"text"
,
true
,
undefined
,
false
,
undefined
,
/* 等 28 个参数 */
)
;
dream
(
{
accepts
:
"text"
,
async
:
true
,
cache
:
false
}
)
;
|
参数默认值
参数最好有默认值,通过 jQuery.extend() , _.extend() 和 Protoype 的 Object.extend,可以覆盖预设的默认值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
var
default_options
=
{
accepts
:
"text"
,
async
:
true
,
beforeSend
:
null
,
cache
:
false
,
complete
:
null
,
// …
}
;
function
dream
(
options
)
{
var
o
=
jQuery
.
extend
(
{
}
,
default_options
,
options
||
{
}
)
;
console
.
log
(
o
.
accepts
)
;
}
dream
(
{
async
:
false
}
)
;
// prints: "text"
|
扩展性
回调(callbacks)
通过回调, API 用户可以覆盖你的某一部分代码。把一些需要自定义的功能开放成可配置的回调函数,允许 API 用户轻松覆盖你的默认代码。
API 接口一旦接收回调,确保在文档中加以说明,并提供代码示例。
事件(events)
事件接口最好见名知意,可以自由选择事件名字,避免与原生事件 重名。
处理错误
不是所有的错误都对开发者调试代码有用:
1
2
3
4
5
6
|
// jQuery 允许这么写
$
(
document
.
body
)
.
on
(
'click'
,
{
}
)
;
// 点击时报错
// TypeError: ((p.event.special[l.origType] || {}).handle || l.handler).apply is not a function
// in jQuery.min.js on Line 3
|
这样的错误调试起来很痛苦,不要浪费开发者的时间,直接告诉他们犯了什么错:
1
2
3
|
if
(
Object
.
prototype
.
toString
.
call
(
callback
)
!==
'[object Function]'
)
{
// 看备注
throw
new
TypeError
(
"callback is not a function!"
)
;
}
|
备注:typeofcallback ==="function" 在老的浏览器上会有问题,object 会当成个 function 。
可预测性
好的 API 具有可预测性,开发者可以根据例子推断它的用法。
Modernizr’s 特性检测 是个例子:
a) 它使用的属性名完全与 HTML5、CSS 概念和 API 相匹配
b) 每一个单独的检测一致地返回 true 或 false 值
1
2
3
4
5
6
7
8
|
// 所有这些属性都返回 'true' 或 'false'
Modernizr
.
geolocation
Modernizr
.
localstorage
Modernizr
.
webworkers
Modernizr
.
canvas
Modernizr
.
borderradius
Modernizr
.
boxshadow
Modernizr
.
flexbox
|
依赖于开发者已熟悉的概念也可以达到可预测的目的。
jQuery’s 选择器语法 就是一个显著的例子,CSS1-CSS3 的选择器可直接用于它的 DOM 选择器引擎。
1
2
3
|
$
(
"#grid"
)
// Selects by ID
$
(
"ul.nav > li"
)
// All LIs for the UL with class "nav"
$
(
"ul li:nth-child(2)"
)
// Second item in each list
|
比例协调
好的 API 并不一定是小的 API,API 的体积大小要跟它的功能相称。
比如 Moment.js,著名的日期解析和格式化的库,可以称之为均衡,它的 API 既简洁又功能明确。
像 Moment.js 这样特定功能的库,确保 API 的专注和小巧非常重要。
编写 API 文档
软件开发最艰难的任务之一是写文档,实际上每个人都恨写文档,怨声载道的是没有一个好用的文档工具。
以下是一些文档自动生成工具:
- YUIDoc (requires Node.js, npm)
- JsDoc Toolkit (requires Node.js, npm)
- Markdox (requires Node.js, npm)
- Dox (requires Node.js, npm)
- Docco (requires Node.js, Python, CoffeeScript)
- JSDuck (reqires Ruby, gem)
- JSDoc 3 (requires Java)
最重要的是:确保文档跟代码同步更新。