小程序最佳实践之『命名』篇 ✍️

2,456 阅读9分钟

前言:接下来会开启一个小程序最佳实践的知识小册,从小程序作为切入点,但绝不限于此。将涵盖几大篇章『命名篇』、『单测篇』、『代码风格篇』、『注释篇』、『安全生产篇』、『重构篇』、『代码设计篇』、『性能篇』、『国际化篇』和『Why 小程序开发最佳实践』。每一篇都会包含正面和反面示例以及详细的解释。

文中示例都是在 CR 过程中发现的一些典型的问题。希望大家能通过该知识小册提前规避此类问题,让我们的 CR 关注点更多聚焦在业务逻辑而非此类琐碎或基本理念的缺失上而最终形成高质量的 CR。

命名篇 ✍🏻

It’s proven that we spend more time reading code than we do writing it, so good naming always pays off in the future.

原则 1:可读性 Readability Counts

简洁原则:英文如果存在多义词,一定要挑选一个最『平易近人』的,而不是生僻到需要查词典。

Bad

某代扣应用中用户想取消代扣,产品为了挽留用户将按钮文案定为『还是想关』而不是『取消订阅』,开发者根据其字面意思将事件回调命名为 onRescission(“取消,废除”之意),该词佶屈聱牙。

<button ... onClick="onRescission">还是想关</button>
Good

命名为一个更常见的单词 onUnsubscribeBtnClick 而且针对对代扣场景用 unsubscribe 更能体现其订阅和取消订阅的业务属性。

<button ... onClick="onUnsubscribeBtnClick">还是想关</button>

原则 2:富有语义 Meaningful

富有语义原则:富有意义具备业务属性的命名优于含糊其辞大而宽泛的命名。宽泛命名相当于没有命名。

Bad

比如 data(数据), process(处理), handle(操作)以及当做 bool 标志的 flag,都是不好的命名。

Good
  • data => bank(银行)
  • process => addBankNamePostfix(增加银行卡后缀)
  • handle => onCardClick(银行卡点击事件)
  • flag => isDebtCard(是否为储蓄卡)

💡 Tips:学习业界著名的库如何命名。

原则 3:对仗原则 Complementary

利用对仗词汇减少理解成本,可以显著降低代码复杂度。常用对仗词:

动词

getset
addremove
createdestroy
startstop
beginend
insertdelete
showhide
suspendresume
lockunlock
openclose
onoff
subscribeunsubscribe

名词

firstlast
incrementdecrement
minmax
nextprevious
sourcedestination
srcdest
souretarget

形容词或副词

oldnew
activeinactive
visibleinvisible
updown

来自代码整洁之道和 C++ Programming Style Guidelines

Not Good

处于点击态之外的另一个 tab 命名为 anotherTab 还是欠缺点意思 🤔。

const anotherTabTaskId = tabs[activeTab === 0 ? 1 : 0]?.taskId;
Good

active 的对仗词是 inactive,理解成本瞬间下降 👍

const inactiveTabTaskId = tabs[activeTab === 0 ? 1 : 0]?.taskId;
Bad
{
  /**
   * 『查看账单』按钮曝光
   */
  onBillListBtnShow() {
    tracert.expo(withdrawResultSpms.viewBill.expo);
  },

  /**
   * 点击『查看账单』
   */
  onOpenUrl() {
    openPage(WITHDRAW_BILL_LIST_URL);
  },
}
<!-- 账单查看 -->
<view onTap="onOpenUrl" onAppear="onBillListBtnShow">
  ...
</view>
Good

onOpenUrl 改成 onBillListBtnClick,和 onBillListBtnShow 对仗,让读者很容易在二者之间建立联系。

{
  onBillListBtnShow() {
    tracert.expo(withdrawResultSpms.viewBill.expo);
  },

  onBillListBtnClick() {
    openPage(WITHDRAW_BILL_LIST_URL);
  },
}
<view onTap="onBillListBtnClick" onAppear="onBillListBtnShow">
  ...
</view>

函数命名

原则 1:动宾结构

函数是用来处理数据或执行一系列流程(procedure),故必须符合 doSth 的动宾结构,如果上下文足够清晰可以省略宾语,只用动词。

Bad

账号类脱敏方法,命名 accountDesensitization,违反动宾原则。

function accountDesensitization() {
  // ...
}
Good

取名 desensitizeAccount 符合动宾结构。

function desensitizeAccount() {
  // ...
}
Better

命名为 mask,更简洁、不生僻,用暗喻更形象更易懂,因脱敏其实就是遮盖掉大部分字符,仅露出部分字符,比如手机号脱敏 18157061234,保留前三后二变成 181******34

function mask() {
  // ...
}

注意:不建议客户端脱敏,如果前端接受到这样的需求可以借安全理由让后端或 BFF 层脱敏,且请使用公司统一的脱敏 SDK。

原则 2:事件回调统一前缀

事件回调以 onhandle 开头,项目内统一风格即可,如: onSthClick handleSthClick 开头。

Bad

targetToCouponAll 违反该项规范。

<button onClick="targetToCouponAll">
  ...
</button>
Good

targetToCouponAll => onCouponCenterClick

<button onClick="onCouponCenterClick">
  ...
</button>

原则 3:返回布尔值的函数命名规范

通常以 is / has / can / should 开头,例如 React 的 shouldComponentUpdate

// 返回布尔值的函数

function isVisible(): boolean;
function hasLicense(): boolean;
function canEvaluate(): boolean;
function shouldSort(): boolean;

原则 4:函数命名抽象原则

函数命名应该抽象,即需要隐藏实现细节。函数名即接口(API),接口是黑盒故命名需要抽象。

Bad

该函数本意是排序某某集合,group 只是排序的手段或实现细节,应该隐藏底层实现细节,而且以后重构后不用 group 改成更好的算法,是否函数名称还得修改?第二 data 该词很宽泛,没有任何具体含义,也并未体现其集合的属性,而该函数并不是一个通用排序函数,建议使用更具业务属性的命名。

function sortAndGroupData(data) {
  // ...
}
Good

sortPromotionTasks 只说明其排序的能力,并未暴露也无需暴露排序细节,符合『least knowledge principle』原则。

/**
 * 将营销任务按照 xx 规则排序
 * @param tasks 
 */
function sortPromotionTasks(tasks) {
  
}

变量命名

常规变量命名

常规变量应该是一个名词,不应该携带类型,抽象而非具体,否则会导致“抽象泄露”,换言之修改变量类型,所有使用处均需改变。其他命名规则见下方。

规则一:变量应该是一个名词

Bad
/** 目标链接 */
const goPage = 'https://www.example.com/'

goPage 违反命名规范,doSth 是函数命名规则。

Good
/** 目标链接 */
const targetUrl = 'https://www.example.com/'

规则二:命名不应该携带类型

Bad

IResultObj 无需类型后缀 Obj

interface IResultObj {
  success: boolean;
};
Good
interface IResult {
  success: boolean;
};

布尔变量

2020 年发生过一起大量 App 使用 Facebook sdk 导致应用崩溃的案例,其中提到了代码命名问题“为什么一个字典结构要用 is_health_app 这样明显是布尔值的名字?”,详见 Facebook SDK 导致欧美主流 App 奔溃

布尔变量命名,善用形容词和各种时态:able / ible / ing / ed。比如:visible maintaining finished found

Bad
let imgDisable
let isTipsVisible // isXXX 一般用作函数命名。
let showTips // showTips 是动宾结构,也是函数命名,不建议当做布尔值命名
let canCollectTaskCard // 领取领取任务
Good
let imgDisabled = false;
let tipsVisible;

// 可领取的 collectible,不可领取的 uncollectible, ible 结尾表示能力“可xx”
let collectable;

枚举命名

【参考】枚举类名带上 Enum 后缀,枚举成员名称需要全大写,单词间用下划线隔开。

不做强制,遵循公司规范即可。

说明:枚举其实就是特殊的常量类,且构造方法被默认强制是私有。

Good

进程状态命名:ProcessStatusEnum,其成员名称:SUCCESS / UNKNOWN_REASON

const enum ProcessStatusEnum {
  SUCCESS = 100,
  UNKNOWN_REASON = 200,
}

接口命名

一、TS 接口必须 I 前缀

接口(interface)或类型(type)比如 ILbs,目的是和 class 区分。

export interface ILbs {
  city: string;
  cityAdcode: string;
  longitude: string;
  latitude: string;
}

二、接口命名必须具备 biz specificity

即具备业务特性。

Bad

入参 IObjParamInterface 过于宽泛,但其实只被一个函数使用而已。

export function mask(objParam: IObjParamInterface): string {
  
}

interface IObjParamInterface {
  str: string;
  startNum?: number;
  lastNum?: number;
  formatStr: string;
}
Good

入参增加函数名 IMaskOptions,让其更加 specific

export function mask(options: IMaskOptions): string {
  
}

interface IMaskOptions {
  str: string;
  startNum?: number;
  lastNum?: number;
  formatStr: string;
}

三、请求返回值和参数命名

[建议]: I 开头 + 业务特性 + Resp 结尾,这样一样就能知道这是返回值类型,同理请求参数以 Req 结尾。

Good
interface IParkingFeeResp {}
interface IParkingFeeReq {}

常量命名

对于一系列对等或相关常量,通常最后一个单词来区分其状态或用途,这样可以规范整齐地扩展到各种状态。

举例,应用状态命名:

Not Good
const APP_MAINTENANCE_STATUS = 1; // 维护中
const APP_RUNNING_STATUS = 2; // 正常运行中
Good

视觉上更加整齐,重点放在最后,肉眼更容易抓到不同之处。

const APP_STATUS_MAINTAINING = 1;
const APP_STATUS_IN_SERVICE = 2;

CSS class 命名

  1. class 是描述状态,应该是形容词或名词。请参考业界著名 CSS 组件库,比如 antd、bootstrap 等。
  2. 抽象原则,命名不要暴露底层实现,更利于以后扩展。命名是接口,接口需抽象。
    • 不能含样式,比如定位(margin、left、right、center)或颜色(blue)等,否则以后样式改了类名也要修改。
  3. [建议] 中划线分隔
  4. 组件根节点命名 {组件名}-wrapper,使用处若需要包一层,当未使用 css module,为避免冲突则命名 {组件名}-usage
Bad

命名不规范:coupon-info-centercenter 是样式,命名和样式耦合了,导致缺少可扩展性,假如以后改成不居中了呢?

<view class="coupon-info-center"></view>
Good

coupon-info-center => coupon-title-only 因为样式修改的概率远大于业务逻辑,如果样式变了,只需修改 css 即可,无需『牵一发而动全身』。

<view class="coupon-title-only"></view>

复数命名

集合命名规则。

  1. 可数名词加 s,比如 cards, items
  2. 不可数名词加 List,比如 extra => extraList
  3. 二维数组怎么命名:可数名词 contactsList 不可数名词 extraLists
Bad
const imagelist = JSON.parse(getIn(item, [ 'extInfos', 'imagelist' ], '[]'));
Good

image 是可数名词,改成 images 更简洁。

const images = JSON.parse(getIn(item, [ 'extInfos', 'imagelist' ], '[]'));
Bad

ActivityStatusEnumArr 后缀 Arr 定死了底层的数据结构一定是数据,不具备扩展性。

export const ActivityStatusEnumArr = [
  'ON',
  'OFF',
 ]
Good
export const activityStatusList = [
  'ON',
  'OFF',
 ]

存储字段命名

【强制】存储字段 key 是一类特殊的常量,需要让其从其他常量中区别开来,以防冲突:

  1. 存储 key 命名规范,以 STORAGE_KEY 开头,和普通常量显著区分开,格式:STORAGE_KEY_${BIZ_KEY}
    • 示例:上一次关闭时间提示: STORAGE_KEY_TIPS_LAST_CLOSED_TIME
  2. 所有存储字段统一归纳到一个公共的常量文件管理,防止冲突,并通过 namespace 减少重复前缀
Bad

每个页面都定义自己的 key 容易被覆写或读取到其他页面的存储内容。

//  page/index/index.ts

const TIPS_LAST_CLOSED_TIME = 'TIPS_LAST_CLOSED_TIME';
Good

统一到放到一个常量文件,规范管理。

// src/common/constants.ts

export const STORAGE_KEY_TIPS_LAST_CLOSED_TIME = 'TIPS_LAST_CLOSED_TIME';
export const STORAGE_KEY_UNCERTIFIED_PROMOTION_TIPS_LAST_CLOSED_TIME
  = 'UNCERTIFIED_PROMOTION_TIPS_LAST_CLOSED_TIME'
Better

用 namespace STORAGE_KEYS 上下文节省 key 长度。

// src/common/constants/storage-keys.ts

export const STORAGE_KEYS = {
  tipsLastClosedTime: 'TIPS_LAST_CLOSED_TIME',
  uncertifiedPromotionTipsLastClosedTime: 'UNCERTIFIED_PROMOTION_TIPS_LAST_CLOSED_TIME',
}

页面、组件命名

【建议】页面以动词或名词形式呈现,组件必须是名词形式。

Good

页面以动词或名词命名均可

src/pages
├── car-moving-code-apply // 挪车码申请页。动词形式
└── discovery // 发现页。名词形式

组件以名词命名

src/components
├── auth-tips // 授权组件
├── car-moving-code-explanation-card // 挪车码使用说明卡片
└── tmall-store-list // 天猫门店列表组件