javascript的api设计原则(转)

前言

本篇博文来自一次公司内部的前端分享,从多个方面讨论了在设计接口时遵循的原则,总共包含了七个大块。系卤煮自己总结的一些经验和教训。本篇博文同时也参考了其他一些文章,相关地址会在后面贴出来。很难做到详尽充实,如果有好的建议或者不对的地方,还望不吝赐教斧正。

一、接口的流畅性

好的接口是流畅易懂的,他主要体现如下几个方面:

1.简单

操作某个元素的css属性,下面是原生的方法:

1

document.querySelector('#id').style.color = 'red';

封装之后

1

2

3

4

function a(selector, color) {

  document.querySelector(selector).style.color = color

}

a('#a''red');

从几十个字母长长的一行到简简单单的一个函数调用,体现了api设计原则之一:简单易用。

2.可阅读性

a('#a', 'red')是个好函数,帮助我们简单实用地改变某个元素,但问题来了,如果第一次使用该函数的人来说会比较困惑,a函数是啥函数,没有人告诉他。开发接口有必要知道一点,大多数人都是懒惰的(包括卤煮自己),从颜色赋值这个函数来说,虽然少写了代码,但是增加了单词字母的个数,使得它不再好记。每次做这件事情的时候都需要有映射关系: a---->color. 如果是简单的几个api倒是无所谓,但是通常一套框架都有几十甚至上百的api,映射成本增加会使得程序员哥哥崩溃。 我们需要的就是使得接口名称有意义,下面我们改写一下a函数:

1

2

function letSomeElementChangeColor(selector, color) {

  document.querySelectorAll(selector, color).style.color = color; }

letSomeElementChangeColor相对于a来说被赋予了现实语言上的意义,任何人都不需要看说明也能知道它的功能。

3.减少记忆成本

我们刚刚的函数太长了,letSomeElementChangeColor虽然减少了映射成本,有了语言上的意义,但是毫无疑问增加了记忆成本。要知道,包括学霸在内,任何人都不喜欢背单词。不仅仅在此处,原生获取dom的api也同样有这个问题: document.getElementsByClassName; document.getElementsByNamedocument.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 = '12px';

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

19

20

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 = bg;

  return this;

}

getElement.prototype.fontSize = function(size) {

  this.style.fontSize = size;

  return this;

}

 

//调用

var el = new getElement('#id')

el.color('red').background('pink').fontSize('12px');

简单、流畅、易读,它们看起来就像行云流水一样,即在代码性能上得到了提升优化,又在视觉上悦目。后面我们会在参数里面讲到如何继续优化。

所以,大家都比较喜欢用jquery的api,虽然一个$符号并不代表任何现实意义,但简单的符号有利于我们的使用。它体现了以上的多种原则,简单,易读,易记,链式写法,多参处理。

nightmare:

1

2

3

document.getElementById('id').style.color = 'red';

document.getElementById('id').style.fontSize = '12px';

document.getElementById('id').style.backgourdColor = 'pink';

dream:

1

$('id').css({color:'red', fontSize:'12px', backgroundColor:'pink'})

二、一致性

1.接口的一致性

相关的接口保持一致的风格,一整套 API 如果传递一种熟悉和舒适的感觉,会大大减轻开发者对新工具的适应性。 命名这点事:既要短,又要自描述,最重要的是保持一致性 “在计算机科学界只有两件头疼的事:缓存失效和命名问题” — Phil Karlton 选择一个你喜欢的措辞,然后持续使用。选择一种风格,然后保持这种风格。

Nightmare:

1

2

3

4

setColor,

letBackGround

changefontSize

makedisplay

dream:

1

2

3

4

setColor;

setBackground;

setFontSize

set.........

尽量地保持代码风格和命名风格,使别人读你的代码像是阅读同一个人写的文章一样。

三、参数的处理

1.参数的类型

判断参数的类型为你的程序提供稳定的保障

1

2

3

4

5

//我们规定,color接受字符串类型

function setColor(color) {

  if(typeof color !== 'string'return;

dosomething

}

2.使用json方式传参

使用json的方式传值很多好处,它可以给参数命名,可以忽略参数的具体位置,可以给参数默认值等等 比如下面这种糟糕的情况:

1

function fn(param1, param2...............paramN)

你必须对应地把每一个参数按照顺序传入,否则你的方法就会偏离你预期去执行,正确的方法是下面的做法。

1

2

3

4

5

6

7

8

function fn(json) {

//为必须的参数设置默认值

   var default = extend({

    param: 'default',

    param1: 'default'

    ......

   },json)

}

这段函数代码,即便你不传任何参数进来,他也会预期运行。因为在声明的时候,你会根据具体的业务预先决定参数的缺省值。

四、可扩展性

软件设计最重要的原则之一:永远不修改接口,而是去扩展它!可扩展性同时会要求接口的职责单一,多职责的接口很难扩展。 举个栗子:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

//需要同时改变某个元素的字体和背景

// Nightmare:

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

9

function sayBonjour() {

  alert(this.a)

}

 

obj.a = 1;

obj.say = sayBonjour;

obj.say();//1

//or

sayBonjour.call||apply(obj);//1

五、对错误的处理

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

13

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

46

//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 21 and Firefox 15, DOM error 12

    // is thrown if the fragment doesn't begin with <

    if (selector[0] == '<' && fragmentRE.test(selector))

      dom = zepto.fragment(selector, RegExp.$1, 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.$1, 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

10

//注释接口,为了演示PPT用

function commentary() {

  //如果你定义一个没有字面意义的变量时,最好为它写上注释:a:没用的变量,可以删除

  var a;

 

  //在关键和有歧义的地方写上注释,犹如画龙点睛:路由到hash界面后将所有的数据清空结束函数

  return go.Navigate('hash'function(){

    data.clear();

  });

}

最后

推荐markdown语法书写API文档,github御用文档编写语法。简单、快速,代码高亮、话不多说上图

卤煮在此也推荐几个在线编辑的网站。诸君可自行前往练习使用。

https://www.zybuluo.com/mdeditor

http://mahua.jser.me/

参考博文

前端头条-javascript的api设计原则

 

 

前段时间组织优化我们的原生模块 API(iOS、Android 模块封装成 JavaScript 接口),于是学习了几篇 JavaScript API 设计的文章,尽管是旧文,但受益匪浅,这里记录一下。

好的 API 设计:在自描述的同时,达到抽象的目标。

设计良好的 API ,开发者可以快速上手,没必要经常抱着手册和文档,也没必要频繁光顾技术支持社区。

流畅的接口

方法链:流畅易读,更易理解

//常见的 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");
  });

设置和获取操作,可以合二为一;方法越多,文档可能越难写

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 元素设置样式:

jQuery("#some-selector")
  .css("background", "red")
  .css("color", "white")
  .css("font-weight", "bold")
  .css("padding", 10);

这个方法可以接受一个 JSON 对象:

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);

处理类型

定义方法的时候,需要决定它可以接收什么样的参数。我们不清楚人们如何使用我们的代码,但可以更有远见,考虑支持哪些参数类型。

//原来的代码
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 Sep 08 2012 15:34:35 GMT+0200 (CEST) 这样的字符串

如果你需要确保传入的参数类型(字符串,数字,布尔),可以这样转换:

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 对象:

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 , 然而它实际是传进来的

给参数命名

event.initMouseEvent(
  "click", true, true, window,
  123, 101, 202, 101, 202,
  true, false, false, false,
  1, null);

Event.initMouseEvent 这个方法简直丧心病狂,不看文档的话,谁能说出每个参数是什么意思?

给每个参数起个名字,赋个默认值,可好

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 对象:

function nightmare(accepts, async, beforeSend, cache, complete, /* 等28个参数 */) {
  if (accepts === "text") {
    // 准备接收纯文本
  }
}

function dream(options) {
  options = options || {};
  if (options.accepts === "text") {
    // 准备接收纯文本
  }
}

调用起来也更简单了:

nightmare("text", true, undefined, false, undefined, /* 等28个参数 */);

dream({
  accepts: "text",
  async: true,
  cache: false
});

参数默认值

参数最好有默认值,通过 jQuery.extend() http://underscorejs.org/#extend) 和 Protoype 的 Object.extend ,可以覆盖预设的默认值。

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)

事件接口最好见名知意,可以自由选择事件名字,避免与原生事件 重名。

处理错误

不是所有的错误都对开发者调试代码有用:

// 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

这样的错误调试起来很痛苦,不要浪费开发者的时间,直接告诉他们犯了什么错:

if (Object.prototype.toString.call(callback) !== '[object Function]') { // 看备注
  throw new TypeError("callback is not a function!");
}

备注:typeof callback === "function" 在老的浏览器上会有问题,object 会当成个 function 。

可预测性

好的 API 具有可预测性,开发者可以根据例子推断它的用法。

Modernizr’s 特性检测 是个例子:

a) 它使用的属性名完全与 HTML5、CSS 概念和 API 相匹配

b) 每一个单独的检测一致地返回 true 或 false 值

// 所有这些属性都返回 'true' 或 'false'
Modernizr.geolocation
Modernizr.localstorage
Modernizr.webworkers
Modernizr.canvas
Modernizr.borderradius
Modernizr.boxshadow
Modernizr.flexbox

依赖于开发者已熟悉的概念也可以达到可预测的目的。

jQuery’s 选择器语法 就是一个显著的例子,CSS1-CSS3 的选择器可直接用于它的 DOM 选择器引擎。

$("#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 文档

软件开发最艰难的任务之一是写文档,实际上每个人都恨写文档,怨声载道的是没有一个好用的文档工具。

以下是一些文档自动生成工具:

最重要的是:确保文档跟代码同步更新。

参考资料:

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值