小程序最佳实践之『注释』篇
前言:接下来会开启一个小程序最佳实践的知识小册,从小程序作为切入点,但绝不限于此。将涵盖几大篇章『命名篇』、『单测篇』、『代码风格篇』、『注释篇』、『安全生产篇』、『重构篇』、『代码设计篇』、『性能篇』、『国际化篇』和『Why 小程序开发最佳实践』。每一篇都会包含正面和反面示例以及详细的解释。
文中示例都是在 CR 过程中发现的一些典型的问题。希望大家能通过该知识小册提前规避此类问题,让我们的 CR 关注点更多聚焦在业务逻辑而非此类琐碎或基本理念的缺失上而最终形成高质量的 CR。
注释篇
克制的注释
注释需克制,不是越多越好,每次写注释之前都要反问自己是不是我的代码写得不够清晰,如果足够清晰就无需注释。多余的注释只会增加阅读成本;其次注释也是代码的一部分,『Code is Liability』代码是负债,只要存在就会有阅读理解和修改的成本。『好的代码噪音很少,SNR 信噪比高,阅读起来没有负担,组织合理就如一篇优美的文章』不克制的注释即增加了代码的『噪音』。其次注释和代码并不能同步更新,导致维护困难。
也不是越少越好(The count of comments should be as small as possible, but no smaller)。注释应精简,注释应该是补充代码没法快速回答的问题,迅速给阅读者一个总结或一个坑的提醒,『为什么要这么写,当时的考虑的是什么,如果 xx 框架支持了,此处可以用什么 api 重构掉』。
Bad
代码已经够清晰,以下大部分流水账似的注释均冗余。
if (bankList.length > 0) {
// 首次进入出提醒弹窗 - 冗余注释
firstEnterDialog && showFirstEnterDialog(firstEnterDialog);
// 处理银行卡到账时效。后端已按照时效从快到慢排序,前端只管取第一项即可
const currentArriveDate = get(bankList[0], ['arriveTimeInfos', 0]);
const newArriveDatelist = bankList.map(({ arriveTimeInfos }) => arriveTimeInfos[0]);
// 账号进入正常处理流程 - 冗余注释
this.setData({
hidePage: false,
// 处理默认选中第一个银行卡
withDrawCardInfo: bankList[0],
// 处理默认选中第一个银行卡预计到账日期
currentArriveDate,
newArriveDatelist,
// 判读当前支付宝版本是否可用cdp组件 - 冗余注释
canIUseCdp: canIUseCdp() || false,
...res,
});
} else {
// 银行卡为空 - 冗余注释
handleIsNullBankList();
}
文档注释
注释分两种,普通注释和文档注释,后者有特殊格式,需遵守,详见 jsdoc.app。
// xxx
普通注释,一般用作代码逻辑的注解/** xxx */
是文档注释,一般用来做 API 的注释,方便生成公开文档
二者除了格式区别之外,本质上文档注释是能通过文档工具(jsdoc 等)产出文档给用户看的,代码逻辑注释是内部逻辑是黑盒不该公开给到用户,故采用普通注释。其次采用文档注释的好处是,IDE 智能提示会显示文档注释,而不会显示普通注释。
Good
给返回值添加文档注释,IDE 可自动提示字段或函数的含义,代码在引用处即可知其含义、入参和返回值是什么,无需切换到源文件,也能够避免低效率的和后端反复确认,或查看后端接口文档,尤其是当项目后期维护时,其重要性尤其显著。
export interface IMockItem {
/**
* 类型
*/
type: ETypes | string;
/**
* 子类型
* POST / GET 等。只有 HTTP 有,其他接口空
*/
subType: HttpMethodsEnum | string;
/**
* 匹配
*/
matching: string;
/**
* request 名称
*/
cname: string;
/**
* 接口 id
*/
interfaceId: number;
/**
* 这条消息的记录,requestId
*/
id: number;
/**
* mock id
*/
dataId?: number,
/**
* 这是 mock 返回的数据
*/
data: string;
}
例如,想要准确获取 request id
,命名不够友好的情况下,很难区分到底哪一个才是?通过 VS Code 提示的字段注释,一眼看出是 id
,无需猜测和反复确认文档迅速定位字段 ?。
函数注释
每一个方法或函数应当具备自我描述,参数注释,返回值注释,且工具函数至少附带一个示例注释 @example。
基础示例
摘选自 lodash。
/**
* Creates an array of the own enumerable string keyed property values of `object`.
*
* **Note:** Non-object values are coerced to objects.
*
* @param {Object} object The object to query.
* @returns {Array} Returns the array of property values.
* @example
*
* function Foo() {
* this.a = 1;
* this.b = 2;
* }
*
* Foo.prototype.c = 3;
*
* _.values(new Foo);
* // => [1, 2] (iteration order is not guaranteed)
*
* _.values('hi');
* // => ['h', 'i']
*/
function values(object) {
return object == null ? [] : baseValues(object, keys(object));
}
Bad
毫无注释
function repeat(str = '', count = 1): string {
return new Array(str).fill(formatStr).join('') +
}
Good
注释齐全,可读性大大增加 ?。
/**
* 重复一个字符串 N 次
* @param str
* @param count
* @return {string} string
*
* @example
* repeat('abc', 2)
* // => 'abcabc'
*/
function repeat(str = '', count = 1) {
return new Array(str).fill(formatStr).join('');
}
参数注释
结合 TS 强大的泛型,参数注释能爆发出更大的能量。
Good
export const i18nData = {
/** 简体中文 */
'zh-Hans': {
loadingMsg: '加载中...',
// ...
},
/** 繁体中文(台湾) */
'zh-Hant': {
loadingMsg: '加載中...',
// ...
},
/** 繁体中文(香港) */
'zh-HK': {
loadingMsg: '加載中...',
// ...
},
/** 英文 */
en: {
loadingMsg: 'Loading...',
// ...
},
};
/**
* @param {keyof(i18nData['zh-Hans'])} key
* @param {string} lang
*/
export function getI18nData(key, lang = DEFAULT_LANG) {
// ...
}
通过注释 key
为 keyof(i18nData['zh-Hans'])
类型, getI18nData
在调用时能提示所有的 key
。
仅一行注释就能够减少编码过程中的不确定性,防止需跨文件复制粘贴或某个字母手动拼写错误这种低级问题。培养代码注释的意识多么重要,同时感叹 TS 的泛型多么强大!
私有方法注释
等级:【推荐】
“不会被页面直接调用的 methods、getters”等私有方法应当增加私有访问权限 @private
注释。好处是删除 private
方法,只需将当前文件的调用处删除即可,无需关注页面等其他地方是否会有影响。减少代码重构的成本。『好的代码是易于删除的代码』。
Bad
未区分私有和公有。小程序内的公有是指 view 中会调用的方法,反之则为私有。
Page({
/**
* 点击 tab 切换
*/
onTabClick(index) {
this.clickSpm(this.getTabSpm(index));
},
/**
* 点击单个埋点
*/
clickSpm(spm) {
// ...
},
/**
* 获取 tab 埋点
*/
getTabSpm(index) {
// ...
},
})
Good
从方法访问权限注释明确看出 onTabClick
将会被页面调用,而 clickSpm
和 getTabSpm
只是内部逻辑的封装,不会被页面调用。以后修改该方法的名字或删除都无需“胆战心惊”是否页面还有调用之处没有被修改到。
Page({
/**
* @public
* 点击 tab 切换
*/
onTabClick(index) {
this.clickSpm(this.getTabSpm(index));
},
/**
* 点击单个埋点
* @private
*/
clickSpm(spm) {
// ...
},
/**
* @private
* 获取 tab 埋点
*/
getTabSpm(index) {
// ...
},
})
字段注释
字段声明时用于描述字段含义、类型的注释。
Bad
和普通注释混为一谈。
// 请求状态枚举
const RPC_STATUS = {
SUCCESS: 0, // 请求成功
FAILED: 1, // 请求失败
PENDING: 2 // 请求中
};
Good
/** 请求状态枚举 */
const RPC_STATUS = {
/** 请求成功 */
SUCCESS: 0,
/** 请求失败 */
FAILED: 1,
/** 请求中 */
PENDING: 2,
};
特殊注释
某种情况下为了快速测试等目的会将一些代码提交到迭代分支,这些临时代码需要增加 TODO 注释。格式 TODO: xxx
,采用统一的书写规范,好处是方便上线之前全局搜索解决掉。其次建议安装 Todo Highlight,这样 TODO 会有高亮效果,更显目。其他特殊注释:
// FIXME: 说明问题是什么
// TODO: 说明还要做什么或者问题的解决方案
? Tips: 一个我们不愿看到却很普遍的情况是,我们给代码标记 FIXME 或 TODO 后却一直没找到时间处理。所以当你做了特殊标记,你应该为它负责,在某个时间把它解决。
Bad
TODO 注释格式不规范。
my.alert({
title: '',
// TODO 具体文案待PD确定
content: '业务信息异常,请联系业务方',
buttonText: '退出',
success: () => {
my.call("exitApp")
}
})
Good
注释规范的 TODO
将有高亮效果。
my.alert({
title: '',
// TODO: 具体文案待PD确定
content: '业务信息异常,请联系业务方',
buttonText: '退出',
success: () => {
my.call("exitApp")
}
})