Lint 不能解决的前端代码规范

1,395 阅读5分钟

导读

笔者将代码规范分为硬性和软性

所谓硬性规范,是可以靠工具强制保证效果的,比如缩进几个字符、每行多少字母、句尾加分号等,可以用 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;
  };
}>

参考文献

  1. clean-code-javascript