前端代码规范(TS + React)
背景:为使团队代码风格尽量统一和规范,方便cr和查找问题,结合过去一段时间的经验,针对react + typescript提出代码规范,后续在实践过程中可持续优化此规范。
javascript规范
使用单引号或es6反引号,使用两个空格作为缩进
句末使用添加分号(buchongzidongtinajia)
前端代码是否只使用camelCase?(建议)
若转换前后端字段,可添加拦截器:
import camelcaseKeys from 'camelcase-keys';
import snakeCaseKeys from 'snakecase-keys';
const handleResponse = (response: AxiosResponse): AxiosResponse => {
return camelcaseKeys(response.data, { deep: true }) as unknown as AxiosResponse;
}
export const handleRequest = (request: AxiosRequestConfig): AxiosRequestConfig => {
if (request.data) {
request.data = snakeCaseKeys(request.data , { deep: true });
}
if (request.params) {
request.params = snakeCaseKeys(request.params , { deep: true });
}
return request;
}
export const productFeedsBaseURL = '/api';
export const productFeedsRequest: AxiosInstance = Axios.create({
// baseURL: `http://www.freehard.top${productFeedsBaseURL}`,
baseURL: '/api',
withCredentials: true
});
productFeedsRequest.interceptors.request.use(handleRequest);
productFeedsRequest.interceptors.response.use(handleResponse, handleErr);
空格
二元和三元运算符两侧必须有一个空格,一元运算符与操作对象之间不允许有空格。
用作代码块起始的左花括号 { 前必须有一个空格。
if / else / for / while / function / switch / do / try / catch / finally 关键字后,必须有一个空格。
在对象创建时,属性中的 : 之后必须有空格,: 之前不允许有空格。
圆括号内侧不留空格;圆括号内的大括号前后,冒号和分号后空一格
单个组件文件代码不超过400行,超过时考虑拆分组件;单函数代码不超过50行
代码注释
单行注释//与注释内容之间空一格 //bad commment // good comment
TODO FIXME: 大写并在冒号后与内容之间空一格,建议配置VSCode插件TODO Highlight使用。(建议)
避免拼写错误,建议使用VSCode 插件 Code Spell Checker(建议)
模块导入
优先导入第三方模块(node_modules 中的),再引入当前项目组件,;第三方模块导入代码段和业务模块导入代码段之间空一行,import 代码段与之后的业务代码之间空一行。
import React, { PureComponent } from 'react'
import { Checkbox, Button, Card, Spin } from 'antd'
import VoiceCollect from './VoiceCollect'
import { AudioPlayerProvider } from '../../components/AudioPlayer'
import * as API from '../../api'
import styles from './index.scss'
import { tips } from '../../common/tools'
import { autoCatch } from '../../decorators'
const CheckboxGroup = Checkbox.Group
导入路径过长时使用webpack配置别名
没有使用到的模块和变量应该删除
优先使用const声明,const > let > var
换行操作符前置(建议)
React规范
基本规则
每个文件只包含一个React组件 无状态组件允许一个文件包含多个组件
命名
文件名:使用PascalCase命名。eg: ReservationCard.jsx, ReservationCard.tsx
引用命名:React组件使用PascalCase命名,实例使用camelCase命名
组件命名:组件名称应该和文件名一致。如果在目录中的组件应该使用index.jsx/index.tsx作为文件名,并使用文件夹名称作为组件名。
其他非组件的小写命名(redux, service)
// bad
import reservationCard from './ReservationCard';
// good
import ReservationCard from './ReservationCard';
// bad
const ReservationItem = <ReservationCard />;
// good
const reservationItem = <ReservationCard />;
// bad
import Footer from './Footer/Footer';
// bad
import Footer from './Footer/index';
// good
import Footer from './Footer';
对齐
多行属性采用缩进,属性可以放在一行就保持在一行中
// bad
<Foo superLongParam="bar"
anotherSuperLongParam="baz" />
// good
<Foo
superLongParam="bar"
anotherSuperLongParam="baz"
/>
// 如果组件的属性可以放在一行就保持在当前一行中
<Foo bar="bar" />
引号
JSX字符串属性采用双引号,其他采用单引号
// bad
<Foo bar='bar' />
// good
<Foo bar="bar" />
// bad
<Foo style={{ left: "20px" }} />
// good
<Foo style={{ left: '20px' }} />
空格
始终在自闭合标签前面添加一个空格
// bad
<Foo/>
// very bad
<Foo />
// bad
<Foo
/>
// good
<Foo />
属性
属性名称始终使用camelCase
属性值为true时,省略改属性的赋值
属性前后不要空格
// bad
<Foo
UserName="hello"
phone_number={12345678}
hidden={true}
loading={ loading }
/>
// good
<Foo
userName="hello"
phoneNumber={12345678}
hidden
loading={loading}
/>
括号
使用括号包裹多行JSX标签
// bad
render() {
return <MyComponent className="long body" foo="bar">
<MyChild />
</MyComponent>;
}
// good
render() {
return (
<MyComponent className="long body" foo="bar">
<MyChild />
</MyComponent>
);
}
// good, when single line
render() {
const body = <div>hello</div>;
return <MyComponent>{body}</MyComponent>;
}
标签
当标签没有子元素时,始终使用自闭合标签
多行属性时,关闭标签应另起一行
// bad
<Foo className="stuff"></Foo>
// good
<Foo className="stuff" />
// bad
<Foo
bar="bar"
baz="baz" />
// good
<Foo
bar="bar"
baz="baz"
/>
class组件成员排布顺序
按照 普通成员, state,生命周期函数,自定义函数,render函数的顺序编写.
class SameCheck extends PureComponent<Prop, State> {
algorithms: Algorithm = []
readonly state: State = {
sampleA: '',
sampleB: '',
algorithms: [],
submiting: false,
result: [],
audioBlob: null,
formIniting: false
}
componentDidMount() {
this.getInfo()
}
handleVoiceAChange = (voiceKey: string) => {
this.setState({ sampleA: voiceKey })
}
render() {}
}
使用React.Fragment
// bad
<React.Fragment>
<div></div>
<h3></h3>
</React.Fragment>
// good
<>
<div></div>
<h3></h3>
</>
使用箭头函数,不使用bind
// good
handleClick = () => { }
// bad
handleClick() { }
<div onClick={this.handleClick.bind(this)}></div>
不允许直接修改state, state应该只读,使用setState修改
// bad
this.state.arr.push(xxx)
this.forceUpdate()
// good
this.setState({ arr: this.state.arr.concat([xxx]) })
避免内存泄漏,组件销毁时清除注册的事件,定时器。
尽量使用函数式组件配合hooks,减少使用class组件(建议)
只在最顶层使用hook,不要在循环,条件或嵌套函数中调用Hook
推荐使用eslint插件 eslint-plugin-react-hooks
{
"plugins": [
// ...
"react-hooks"
],
"rules": {
// ...
"react-hooks/rules-of-hooks": "error", // Checks rules of Hooks
"react-hooks/exhaustive-deps": "warn" // Checks effect dependencies
}
}
useEffect的使用注意事项
可参考资料:
useEffect完全指南
使用hooks请求数据
react hooks依赖实践指南
避免陷入死循环
当effect中更新了state时会引起组件重新渲染,组件重新渲染又会出发effect,无限循环。
给useEffect第二个参数传入[],只在组件mount的时候触发effect。
// bad
function App() {
const [data, setData] = useState({ hits: [] });
useEffect(async () => {
const result = await axios(
'https://hn.algolia.com/api/v1/search?query=redux',
);
setData(result.data);
});
return (
...
);
}
// good
function App() {
const [data, setData] = useState({ hits: [] });
useEffect(async () => {
const result = await axios(
'https://hn.algolia.com/api/v1/search?query=redux',
);
setData(result.data);
}, []);
return (
...
);
}
完整的列出useEffect的依赖,不要欺骗react
// bad
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
// good
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, [count]);
return <h1>{count}</h1>;
}
typescript规范
有jsx代码的ts文件后缀名使用.tsx,没有的用ts
类型名使用PascalCase,枚举值使用PascalCase,枚举成员大写,接口前不要加I
在每个文件中,类型定义应该放在最前面
尽量减少使用any作为类型,类型应尽量详细(建议)
interface声明顺序 只读参数放第一位,必选参数第二位,可选参数次之,不确定参数放最后
interface Props {
readonly x: number;
readonly y: number;
name: string;
age: number;
height?: number;
[propName: string]: any;
}
使用ts内置工具泛型(建议)Record
type Record<K extends keyof any, T> = {
[P in K]: T;
};
可用来声明对象结构的类型。
type Foo = Record<'a' | ‘b’, string>
const foo: Foo = {a: '1'} // 正确
const foo: Foo = {b: '1'} // 错误,因为 key 不为 a
const foo: Foo = {a: 1} // 错误,因为 value 的值不是 string 类型
Partial
type Partial<T> = {
[P in keyof T]?: T[P];
};
将传入的接口的必选属性变为可选属性。
interface iPeople {
title: string;
name: string;
}
const people: Partial<iPeople> = {
title: 'Delete inactive users',
};
Required
type Required<T> = {
[P in keyof T]-?: T[P];
};
将传入的接口的可选属性变为必选属性
interface iPeople {
title?: string;
name?: string;
}
const people: Required<iPeople> = { title: 'ts' }; // Error: property 'name' missing
Readonly
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
将传入的接口的属性变为只读属性
interface iPeople {
title: string;
name: string;
}
const people: Readonly<iPeople> = {
title: 'todo list';
name: 'ts';
};
// title name属性就是只读的了
Pick
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
从传入的接口的中提取一部分属性作为新的接口。
interface iPeople {
name: string;
age: number;
gender: number;
}
const pickPerson: Pick<iPeople, 'name' | 'age'> = {name: 'test', age: 22};
Exclude
type Exclude<T, U> = T extends U ? never : T;
排除掉T中可以赋值给U的类型。
type ExcludeType = Exclude<"a" | "b" | "c" | "d", "a" | "c" | "f">;
// ExcludeType = 'b' | 'd'
Extract
type Extract<T, U> = T extends U ? T : never;
提取出T中可以赋值给U的类型。
type ExtractType = Extract<"a" | "b" | "c" | "d", "a" | "c" | "f">;
// ExcludeType = 'a' | 'c'
Omit
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
基于T生成新的type,其中排除掉了在K中的属性。
interface iPeople {
name: string;
age: number;
gender: number;
}
type OmitPeople = Omit<iPeople, 'name'>;
// OmitPeople = { age: number, gender: number}
减少“魔数”
写代码的时候尽量减少一些未知含义的数字或者布尔值,尽量用英文单词。
// bad
if (type !== 0) {
// TODO
}
// good
const STATUS: Record<string, any> = {
READY: 0,
FETCHING: 1,
FAILED: 2
};
if (type === STATUS.READY) {
// TODO
}
// best
enum STATUS {
// 就绪
READY = 0,
// 请求中
FETCHING = 1,
// 请求失败
FAILED = 2,
}
使用as作为类型断言,不要使用<>
interface Foo {
bar: number;
bas: string;
}
// bad
const foo = <Foo>{};
foo.bar = 123;
foo.bas = 'hello';
// good
const foo = {} as Foo;
foo.bar = 123;
foo.bas = 'hello';
S