1. 前言
对于前端开发而言, 肯定会和 API 打交道, 大家也都会想过怎么设计自己的 API 优秀的 API 之于代码, 就如良好内涵对于每个人好的 API 不但利于使用者理解, 开发时也会事半功倍, 后期维护更是顺风顺水至于怎么设计 API, 今天就提下我自己的一些建议如果大家有什么好的想法, 欢迎指点
2. 命名
良好的一个命名习惯, 就是效率开发的第一步如果命名规范, 对自己而言, 文件整理有很大的帮助, 后期修改文件可以快速的定位文件, 命名规范, 也显得自己专业对团队而言, 如果有统一的规范命名, 交接时可以减少大量的学习和沟通成本
关于命名, 下面提点几个小建议
2-1. 正确拼写
这个应该说是命名的一个底线了, 经常性出现, 单词拼写错误, 搞得自己或者团队的人都一头雾水的情况不再少数我遇到情况比较深刻的有
中文大意 | 期望 | 实际 |
---|---|---|
表单 | form | from |
报名 | sign-up | sign-in |
采纳 | adopt | adept |
内容 | content | contend |
测试 | test | text |
联系 | contact | contract |
高度 | height | heigth |
宽度 | width | widht |
移动 | mobile | moblie |
标签 | tab | tap |
这些单词, 如果是拼写错误还好, 至少编辑器都会提醒但是如果写错了, 但是单词又是正确的单词就可大可小了(表单, 报名, 采纳, 内容这些例子, 单词写错了, 意思变了, 但是单词是正确的, 编辑器都不会提醒)
试过挖坑比较深的一次就是: 一个活动, 有报名, 有签到的功能! 处理方法如下
获取已报名用户信息: getSignUpUserList,
重置报名的数据: resetSignUpUser,
提交报名操作: signInDo
获取已签到用户信息: getSignInUserList,
重置签到的表单数据: resetSignInUser,
提交签到的操作: signUpDo
修改 bug 的时候, 完全懵逼了, 原因大家懂的
2-2. 注意单复数
所有涉及遍历, 操作对象, 数组, 集合的函数, 建议都采用复数
对于展现复数, 不同公司有不同的习惯, 但是得统一, 比如产品列表 - productList 这里用了 list 表示复数, 再其它地方, 就不建议使用 products 这种方式表示复数了
2-3. 用词准确
这个主要的两方面的内容
2-3-1. 单词意思搞错
比如弹窗上面的信息, 有些时候见到, 使用包含 notice 的字样, 但是实际上, notice 的中文意思, 准确的应该是公告, 告示, 声明之类
一个弹窗这样的会话消息, 建议使用 message 这个字样 notice 应该像公告, 告示, 声明之类的情况使用
2-3-2. 正反词义单词错用
比如关闭弹窗的方法, 的方法是 closeDialog, 然后显示弹窗用的又是 showDialogshow 的意思是显示, 反义词应该是 hide 隐藏而 close 意思是关闭, 反义词应该是 open
附常用反义词组(有些带缩写)
in | out |
on | off |
prev | next |
show | hide |
close | open |
success | fail |
before | after |
begin | end |
2-4. 命名意义
这一块, 本来打算放在 2-2 里面讲的, 因为命名如果有意义也是一个底线但是最后放在这里, 是因为这个情况在函数里面出现得不多, 更多应该出现在普通变量里面 (相信很多人会遇到过这样的命名: var n1,n2,n3;) 关于命名, 还是建议大家要起有意义名称, 不使用没意义的命名
遇到最多的情况, 就是图标的命名方面
比如下面的图标(选自某平台的底部导航栏), 点击不同的图标出发不同的方法
很多人喜欢下面的命名
- // 版本 1
- function handle1(){
- }
- function handle2(){
- }
- // 版本 2
- function handleA(){
- }
- function handleB(){
- }
- // 版本 3
- function handleOne(){
- }
- function handleTwo(){
- }
这样的命名, 别人函数了, 就算是元素的 class 这样的命名在后期维护绝对增加了难度甚至可能导致重构
建议的姿势
- function handleHome(){
- }
- function handleCollect(){
- }
2-5. 命名格式
文章说的 API, 主要针对的是函数, 但是在这一小块里面, 也列举一下其它的目标的建议命名方式
待命名对象 | 推荐名称 |
---|---|
图片 | ‘-’ ‘_’ 分割 |
class,id | ‘-’ 分割 |
文件,变量 | 驼峰命名 |
临时变量 | ‘_’ 开头,驼峰命名 |
2-6. 处理中文拼音
对于中文拼音, 应该说只有一种情况, 被中国人创造出来, 没有英文翻译的
命名 | 含义 |
---|---|
taobao | 淘宝 |
微博 | |
zongzi | 粽子 |
pinyin | 拼音 |
在一年多以前, 遇到一个中二的命名 - dengluDo 当时一直不知道是什么玩意, 后来向那个人打听才知道, 是执行登录的操作, denglu 是中文拼音, do 又是英文, 这样的命名后期如果维护, 他不哭, 算我输
2-7. 命名潜规则
有些情况, 给特定的对象命名, 还要用特定的名字, 可以说是潜规则吧印象最清楚的就是给按钮命名要么全拼, 要么写 btn 很清楚的记得我一个老师说过: 写 but,bto 的程序也能正常运行, 也没人说你错, 但是我做面试官, 就是不录用你, 就说你不专业
待命名对象 | 推荐名称 | 错误示范 |
---|---|---|
按钮 | btn | but bto |
背景 | bg | back background |
模板 | tpl | tem |
提示信息 | msg | mes |
标签栏 | tab | tit |
网站大图(广告宣传图) | banner | ban |
注册 | register | sign-in |
3. 参数
对于函数而言, 参数是用户设置最频繁, 也是最关心的部分, 合理设计函数参数, 这一步很重要, 直接影响函数的使用
3-1.const 入参
这个应该说是一个习惯吧, 不要直接改变入参的值这个规则的初衷是解决函数副作用问题如果参数是一个引用类型的数据, 如果在函数内修改了参数, 到时候将会使得原本的数据发生改变, 往往会发生难以追踪的问题
3-2. 控制参数数量
参数的数量, 个人建议就是, 超过 3 个, 使用对象进行封装因为如果 API 参数越多, 那么使用对于这个 API 的记忆成本就越大, 易用性也很受影响
比如下面的例子:
- encryptStr: function (str, regArr, type, replacement) {
- var regtext = '',
- Reg = null,
- _type=type||0,
- replaceText = replacement || '*';
- //ecDo.encryptStr('18819322663',[3,5,3],0)
- //result:188*****663
- //repeatStr 是在上面定义过的(字符串循环复制), 大家注意哦
- if (regArr.length === 3 && type === 0) {
- regtext = '(\\w{' + regArr[0] + '})\\w{' + regArr[1] + '}(\\w{' + regArr[2] + '})'
- Reg = new RegExp(regtext);
- var replaceCount = this.repeatStr(replaceText, regArr[1]);
- return str.replace(Reg, '$1' + replaceCount + '$2')
- }
- //ecDo.encryptStr('asdasdasdaa',[3,5,3],1)
- //result:***asdas***
- else if (regArr.length === 3 && type === 1) {
- regtext = '\\w{' + regArr[0] + '}(\\w{' + regArr[1] + '})\\w{' + regArr[2] + '}'
- Reg = new RegExp(regtext);
- var replaceCount1 = this.repeatStr(replaceText, regArr[0]);
- var replaceCount2 = this.repeatStr(replaceText, regArr[2]);
- return str.replace(Reg, replaceCount1 + '$1' + replaceCount2)
- }
- //ecDo.encryptStr('1asd88465asdwqe3',[5],0)
- //result:*****8465asdwqe3
- else if (regArr.length === 1 && type === 0) {
- regtext = '(^\\w{' + regArr[0] + '})'
- Reg = new RegExp(regtext);
- var replaceCount = this.repeatStr(replaceText, regArr[0]);
- return str.replace(Reg, replaceCount)
- }
- //ecDo.encryptStr('1asd88465asdwqe3',[5],1,'+')
- //result:"1asd88465as+++++"
- else if (regArr.length === 1 && type === 1) {
- regtext = '(\\w{' + regArr[0] + '}$)'
- Reg = new RegExp(regtext);
- var replaceCount = this.repeatStr(replaceText, regArr[0]);
- return str.replace(Reg, replaceCount)
- }
- }
大家可以看上面的注释, 就知道这段代码的具体作用了, 如果想想就找个参数, 我必须要除了记得 4 个参数的作用, 还要记得参数的顺序
如果使用对象记录参数, 用户只需要记得 4 个参数的作用, 不需要记参数的顺序
- encryptStr: function (obj) {
- var _default={
- type:0,
- replacement:'*'
- };
- for(var key in obj){
- _default[key]=obj[key];
- }
- },
- // 调用方式
- ecDo.encryptStr({str:'18819266335',regArr:[5],type:0,replacement:'-'});
这样还有一个好处就是, 比如像刚才的函数, type 这个参数, 我想保留默认值, 偷懒不传原来的方案, 就得这样传
ecDo.encryptStr('1asd88465asdwqe3',[5],'','+');
这样肯定是会激起不少有代码洁癖的开发者, 比如我如果使用对象, 就很好避免了
ecDo.encryptStr({str:'18819266335',regArr:[5],replacement:'-'});
3-3. 前置相关性高的参数
这个应该没什么可能, 就一个意思: 必填重要的参数前置, 可省略的参数后置
比如下面的例子
/ 格式化处理字符串
- //ecDo.formatText('1234asda567asd890')
- //result:"12,34a,sda,567,asd,890"
- //ecDo.formatText('1234asda567asd890',4,' ')
- //result:"1 234a sda5 67as d890"
- //ecDo.formatText('1234asda567asd890',4,'-')
- //result:"1-234a-sda5-67as-d890"
- formatText: function (str, size, delimiter) {
- var _size = size || 3, _delimiter = delimiter || ',';
- var regText = '\\B(?=(\\w{' + _size + '})+(?!\\w))';
- var reg = new RegExp(regText, 'g');
- return str.replace(reg, _delimiter);
- },
调用大家都看得出来如果 API 这样设计
- formatText: function (size, delimiter, str) {
- var _size = size || 3, _delimiter = delimiter || ',';
- var regText = '\\B(?=(\\w{' + _size + '})+(?!\\w))';
- var reg = new RegExp(regText, 'g');
- return str.replace(reg, _delimiter);
- },
就得这样调用, 如果这样写 API, 被批斗的可能性很大!
ecDo.formatText('','','1234asda567asd890')
4. 作用
4-1. 支持批量处理
比如这个例子, 页面有这样的元素
- <div class="div1"></div>
- <div class="div1"></div>
- <div id="div2"></div>
有一个类似 jQuery 的 css 这个 API 的 API
- css: function (dom, json) {
- for (var attr in json) {
- dom.style[attr] = json[attr];
- }
- }
然后给这些 div 设置样式的时候, 代码如下
- var oDiv1 =document.querySelectorAll(".div1");
- var oDiv2=document.querySelector("#div1");
- ecDo.css(oDiv2,{'height':'100px','width':'100px','background':'#333'});
- ecDo.css(oDiv1,{'height':'100px','width':'100px','background':'#09f'});
当运行到
ecDo.css(oDiv1,{'height':'100px','width':'100px','background':'#09f'});
会提示报错, 原因大家也知道 css 这个 API 里面, 只处理了单个元素, 并没有处理元素的集合
建议的方式是把 css 这个 API 改成可批量处理元素集合的
- css: function (dom, json) {
- if (dom.length) {
- for (var i = 0; i <dom.length; i++) {
- for (var attr in json) {
- dom[i].style[attr] = json[attr];
- }
- }
- }
- else {
- for (var attr in json) {
- dom.style[attr] = json[attr];
- }
- }
- },
4-2. 多态处理
一个类似 jQuery 的 html 这个 API 的 API-html
之前遇到一个开发者的处理方式是: 获取元素的 innerHTML 和设置元素 innerHTML 分开为两个方法 - getHtml,setHtml 这样的问题又在于记忆的成本比原生的 innerHTML 还要高建议的姿势就是, 获取和设置用同一个 API
- html: function (dom) {
- if (arguments.length === 1) {
- return dom.innerHTML;
- } else if (arguments.length === 2) {
- dom.innerHTML = arguments[1];
- }
- }
- ecDo.html(oDiv);// 获取
- ecDo.html(oDiv,'守候');// 设置
4-3. 可扩展性
可扩展性, 就是建议遵守开放 - 封闭原则对扩展开放, 对修改关闭比如 jQuery 的 $.fn 和 $.fn.extend()
说一个简单的例子 - 计算加薪额度
- var addMoney = (function() {
- // 定义策略类
- var strategies = {
- A: function(money) {
- return money + 2000;
- },
- B: function(money) {
- return money + 1000;
- }
- };
- // 暴露接口
- return {
- // 根据等级和现工资, 输入加薪后的工资
- compute: function(lv, money) {
- return strategies[lv](money)
- }
- };
- })();
- // 比如: 等级为 A,5000+2000
- console.log(addMoney.compute('A', 5000)) //7000
- // 比如: 等级为 B,20000+1000
- console.log(addMoney.compute('B', 20000)) //21000
代码看着没有问题, 但是如果以后需求要增加 C 等级呢? 这就不得不修改 strategies 在里面增加方法
如下
- var strategies = {
- A:function(money){
- return money + 2000;
- },
- B:function(money){
- return money + 1000;
- },
- C:function(money){
- return money + 500;
- }
- };
这样实现也简单, 如果以后要增加 S 等级呢? 又得改 strategies 这里还有一个问题就是, 如果增加的 C 等级只有在 A 模块需要用到, 在 B 模块不会出现, 那么在 B 模块引用 addMoney 的时候, 又会把 C 等级的计算方式也引入进去, 造成不必要的资源浪费
建议的方式是, 设置一个接口, 扩展 strategies
- var addMoney = (function() {
- // 定义策略类
- let strategies = {
- A: function(money) {
- return money + 2000;
- },
- B: function(money) {
- return money + 1000;
- }
- };
- // 暴露接口
- return {
- // 根据等级和现工资, 输入加薪后的工资
- compute: function(lv, money) {
- return strategies[lv](money)
- },
- // 扩展等级
- addRule: function(lv, fn) {
- strategies[lv] = fn;
- }
- };
- })();
- // 增加 C 等级的调用
- addMoney.addRule('C',
- function(money) {
- return money + 500;
- });
- console.log(addMoney.compute('C', 20000)) //20500
4-4. 避免副作用
函数的副作用, 相信很多人都会遇到过, 比如在一个函数体内修改一个外部作用域的变量, 或者全局变量, 在函数体内修改引用类型的参数, 这些情况多少都会遇到过
如何避免呢? 主要是以下两个写代码习惯
1. 函数体内可以使用参数, 进行操作, 但是不能修改如果修改, 用一个临时变量记录参数 (如果是引用类型, 需要用深拷贝记录) 这样可以避免直接修改参数
2. 对于函数外的变量, 如全局变量函数体内可以访问, 但是不能修改
3. 如果需要给函数外的变量赋值, 不能在函数体内操作, 把值返回到外部, 在外部进行赋值(感觉这里有点啰嗦, 因为赋值了, 就是修改了外部变量, 就违反了第二点)
- // 不好做法
- var myName='';
- function setName(firstName,lastName){
- myName=firstName+lastName;
- }
- setName('守','侯');
- // 推荐做法
- var myName='';
- function setName(firstName,lastName){
- return firstName+lastName;
- }
- myName=setName('守','侯');
5. 向下兼容
这个建议主要就是为了兼顾以前的写法还是拿上面的那个例子吧!
原本传参方式是这样
encryptStr: function (str, regArr, type, replacement) {};
后来升级改成这样
encryptStr: function (obj){}
这样问题就来了, 一个项目里面, 因为历史的原因难免会使用这个 API, 并且使用了第一种方式传参现在 API 改了, 解决的方案有两个, 要么把整个项目使用的这个 API 的方式, 都改成第二种的传参方式, 要么就是对接口进行向下兼容, 兼容以前的方案
- encryptStr: function (obj) {
- var _default={
- type:0,
- replacement:'*'
- };
- // 如果还是以之前的方式调用函数, 兼容性判断
- if(arguments.length>1){
- _default.str=arguments[0];
- _default.regArr=arguments[1];
- _default.type=arguments[2]||0;
- _default.replacement=arguments[3]||'*';
- }
- else{
- for(var key in obj){
- _default[key]=obj[key];
- }
- }
- // 下面代码略
- },
如果 API 已经准备来一个大版本的更新,(比如从 1.0.0 升级到 2.0.0, 不是 1.0.0 升级到 1.0.1, 或者 1.0.0 升级到 1.1.0)不打算兼容以前的版本了可以忽略这一步, 毕竟兼容性的代码可能也很多
6. 简单
这一步可以说是 API 设计最高级的一步, 也是最难开发的一步, 这就是为什么这篇文章会带有大道至简的字样, 即使 API 的实现很难, 但使用起来简单感觉就是高级的 API 这一步也直接影响 API 的好用与否简单的 API 不但是用起来简单, 试试可以一看就懂的 API 这样的 API 更易理解记忆调试和变更使用方式
原生的 API, 比如 Date,somemapfind 等所有数组遍历操作函数, es6 提供的 Object.assign,Object.keys,Object.values 等
曾经的霸主 jQuery, 现在的王者 react, 黑马 vue 这些项目让人拍手称赞的原因虽然有很多, 但也不可否认的, 那便是它们的 API 设计非常的巧妙如: jQuery 的 $,siblings,toogleClass,animate 等, react 的 cloneElement,replaceProps 等, vue 的 nextTick,set 等
jQuery 对于现在而言, 虽然是过时了, 但里面的知识还是值得学习, 比如使用的淋漓尽致的 js 写作技巧, 设计模式, 以及 API 设计等
自己写的 API, 我也是把 API 写得尽量的简单, 最高境界就是让别人扫一眼文档, 就知道记牢了 API 的使用方式这个是我追求的目标, 只是现在距离还是有点远大家看我 encryptStr 这个 API 就知道(此处尴尬一天)
7. 小结
在我的眼里, 一个好的 API, 会有一个一看就懂的名字, 一个强大的功能, 一个简单的调用方式虽然只有三个条件, 但是这三个条件结合起来, 可不是那么容易做到的一个好的 API, 无论是对自己, 对团队, 对项目开发都是一个很好的帮助
对于设计 API 的一些个人建议, 就到这里了, 如果以后有更好的想法, 会第一时间分享, 和大家交流意见如果大家有什么想法, 欢迎指点迷津
来源: https://juejin.im/post/5aae8b4f5188255588052ffb