导读
笔者将代码规范分为硬性和软性。
所谓硬性规范,是可以靠工具强制保证效果的,比如缩进几个字符、每行多少字母、句尾加分号等,可以用 Lint 工具保证落地效果的规范;
所谓软性规范,是指诸如方法命名、文件命名、套路代码等无法用工具强行约束的规则。这种规则只能算是一种约定,更多的是要靠自觉,除了 CR 之外,暂时也想不到其他有效的方法来保证其落地效果。
显而易见的,好的代码规范能够提升项目的可维护性,降低团队的沟通和切换成本;对于个人而言,好的编码习惯,能够提升自己的专业性,甚至是架构设计的入室法门(毫不夸张)。本文将罗列出一些普适的软性编码规范,是多年来笔者经历的各种团队规范的总结,给各位读者做个参考
“代码是写给人看的,不是写给机器看的,只是顺便计算机可以执行而已。” ——《计算机程序的构造和解释》(简称为SICP)
代码规范
目录文件
文件名
- 文件名全部小写,用「-」连接;只有组件文件(实际上是继承于 class 文件)以大驼峰命名
// GOOD
import MyComponent from './MyComponent';
// BAD
import MyComponent from './myComponent';
import MyComponent from './my-component';
- 常用文件名(也可用作文件夹名,通常用复数较合理)
- index.js - 入口文件;
- constants.js - 常量;
- services.js - 网络请求;
- utils.js - 工具函数;
- routes.js - 路由;
- style.css - 样式;
- types.ts - ts 声明;
目录结构
.
├── config # 工程配置文件
│ ├── webpack.local.js
│ ├── webpack.dev.js
│ ├── webpack.prod.js
│ └── webpack.config.js
├── public # 不需要编译的文件
│ ├── favicon.ico
│ ├── manifest.json
│ └── robots.txt
├── scripts # 工程脚本文件
│ ├── build.js
│ └── deploy.js
├── src
│ ├── assets # 静态文件
│ │ ├── iconfonts
│ │ └── sprite.png
│ ├── components # 组件
│ │ ├── MyComponent1
│ │ │ ├── index.tsx
│ │ │ └── style.css
│ │ └── MyComponent2.tsx
│ ├── locales # 国际化
│ │ ├── en-US.json
│ │ └── zh-CN.json
│ ├── pages # 页面
│ │ └── Home
│ │ ├── components
│ │ ├── index.tsx
│ │ ├── services.ts # API 请求
│ │ ├── style.css
│ │ └── utils.ts
│ ├── constants.ts # 常量
│ ├── index.tsx
│ ├── routes.ts # 路由
│ ├── style.css
│ └── utils.ts # 公共方法
├── .gitignore
├── .npmrc # npm 源控制
├── .yarnrc # yarn 源控制
├── yarn.lock # 通常与 package-lock.json 二存一
├── package.json
└── package-lock.json # lock 文件不要 ignore,保证依赖的稳定性
代码相关
变量
- 常量名全大写并用下划线连接
// GOOD
const DEFAULT_PAGE_SIZE = 10;
// BAD
const defaultPageSize = 10;
- boolean 类型的变量名建议:isXXX、shouldXXX、canXXX、hasXXX、XXXable
// GOOD
const { isModalShow, shouldShowModal, canRun, hasReaded, closeable } = { true, false, true, false, false }
// BAD
const { showModal, modalClose } = { true, false }
函数
- 函数名统一用动词开头;
// GOOD
function showModal() {}
function changeStatus() {}
// BAD
function modalShow() {}
function statusChange() {}
- 参数控制在 3 个(含)以内
// GOOD
function createMenu({ title, body, buttonText, cancellable }) {
// ...
}
createMenu({
title: "Foo",
body: "Bar",
buttonText: "Baz",
cancellable: true
});
// BAD
function createMenu(title, body, buttonText, cancellable) {
// ...
}
createMenu("Foo", "Bar", "Baz", true);
- 使用纯函数,避免副作用
// GOOD
function splitIntoFirstAndLastName(name) {
return name.split(" ");
}
const name = "Ryan McDermott";
const newName = splitIntoFirstAndLastName(name);
console.log(name); // 'Ryan McDermott';
console.log(newName); // ['Ryan', 'McDermott'];
// BAD
let name = "Ryan McDermott";
function splitIntoFirstAndLastName() {
name = name.split(" ");
}
splitIntoFirstAndLastName();
console.log(name); // ['Ryan', 'McDermott'];
- 注意引用类型数据的处理,通常返回 cloneDeep 的新数据
// GOOD
const addItemToCart = (cart, item) => {
// return [...cart, { item, date: Date.now() }]; OR below:
const copyCart = _.cloneDeep(cart);
copyCart.push({ item, date: Date.now() });
return copyCart;
};
// BAD
const addItemToCart = (cart, item) => {
cart.push({ item, date: Date.now() });
};
- API 请求函数建议根据请求 Method 统一前缀,避免与外层函数混淆。(注:get 请求建议用 fetch 做前缀,避免其他常用冲突)
// GOOD
function submitForm() {
postSubmitForm(values); // API 请求
}
function getUserInfo() {
fetchUser(); // API 请求
}
// BAD
function submit() {
submitForm(values); // API 请求
}
function getUserInfo() {
getUser(); // API 请求
}
- 组件内声明的事件回调函数,推荐以 handleXXX 格式命名,而不是 onXXX。避免与 props 传入的回调函数混淆
// GOOD
function handleSubmit() {
props.onSubmit();
}
<button onClick={handleSubmit}>Submit</button>
// BAD
function onSubmit() {
props.onSubmit();
}
<button onClick={onSubmit}>Submit</button>
可读性
- 类型转换推荐用显示声明
// GOOD
Boolean(a); Number(b); String(c);
// BAD
!!a; +b; `${c}`; '' + c;
- 码表保证语义化,禁止出现 Magic Number
// GOOD
enum YN { no, yes } // ts
const STATUS = { disabled: 0; active: 1; running: 2; }
if (status === STATUS.active) // code
// BAD
if (status === 2) // code
- 如果判断条件太长,需要进行封装
// GOOD
function shouldShowSpinner(fsm, listNode) {
return fsm.state === "fetching" && isEmpty(listNode);
}
if (shouldShowSpinner(fsmInstance, listNodeInstance)) {
// ...
}
// BAD
if (fsm.state === "fetching" && isEmpty(listNode)) {
// ...
}
- 使用 async/await、Promise,代替 callback
// GOOD
import { get } from "request-promise";
import { writeFile } from "fs-extra";
get("https://en.wikipedia.org/wiki/Robert_Cecil_Martin")
.then(body => {
return writeFile("article.html", body);
})
.then(() => {
console.log("File written");
})
.catch(err => {
console.error(err);
});
// BAD
import { get } from "request";
import { writeFile } from "fs";
get(
"https://en.wikipedia.org/wiki/Robert_Cecil_Martin",
(requestErr, response, body) => {
if (requestErr) {
console.error(requestErr);
} else {
writeFile("article.html", body, writeErr => {
if (writeErr) {
console.error(writeErr);
} else {
console.log("File written");
}
});
}
}
);
- 如果可能,用 switch 代替 if else
- 删除不必要的 console、debugger、注释、废弃代码,想查看代码历史,请使用 GIT
常见业务逻辑
表单
- 字符类输入项要有长度校验,且要明确是否需要 trim 操作
- textarea 输入的内容,「展示时」要保留格式,如缩进、换行等
- 提交操作要做频控,用 loading 或 disabled 控制按钮的二次点击
表格
- 数据更新时,要有 loading 效果,避免用户误操作
- 页码、页展示数、查询条件等获取数据的条件项,本质相同,要统一收敛管理
- 如需分享 URL,上述条件项数据维护在 URL 的 query 里较合适,否则一般维护在 sessionStorage 中,谨慎使用 localStorage
- 单页展示数、查询条件变化时,页面要初始化为 1
- 增删改数据时,建议直接重新获取分页列表数据,可以避免很多麻烦,注意维护 query
- 单选、多选在触发分页、查询条件等变化时,记得清空
接口规范
- ID 类,推荐用 string 类型,不推荐 number,超过 17 位的整数 js 会有精度丢失;
- 时间类,推荐用毫秒时间戳(1609459200000),不推荐 string('2021-01-01 08:00:00'),避免国际化时区造成的麻烦;
- API URL 都以
/api/xxx
开头,其他地址留给前端页面,也方便设置代理; - Response 格式最外层要统一,方便封装与维护,如:
// TS
type Response<T = any> = Promise<{
code: number;
msg: string;
data: T;
}>
- List 类请求的参数和返回值格式也要尽量统一,方便封装与维护,如:
// TS
interface ListParams<U = any> {
pageNo: number;
pageSize: number;
filter?: U;
}
type ListResponse<T = any> = Promise<{
code: number;
msg: string;
data: {
records: T[];
total: number;
};
}>