代码规范记录(持续更新) 🙂
TIP
记录一些规范,提高代码质量,减少 bug
空函数清理、注释代码清理、console.log清理
尽量都用 === 全等
后面没有代码了就不要多加一个return
公共js方法,公共组件,要多加注释
不是非常有必要,不要使用any,在很多情况下,我们可以使用 unknown 来替代 any,既灵活,又可以继续保证类型安全
provide/inject 是在解决多级透传问题的时候才能使用,而且使用要特别谨慎。因为它会将逻辑提升到组件树的更高层次来处理逻辑,会使高层组件变得更复杂。并且对于某些组件来说,不利于复用。对于全局状态的使用,都要谨慎
可选链在必要的情况下才能使用,禁止滥用;使用可选链简化代码
? 可选链操作符,虽然好用,但也不能滥用。item?.name 会被编译成 item === null || item === void 0 ? void 0 : item.name,滥用会导致编译后的代码size增大。
// bad
const handleData = (data)=> {
const { userList } = data;
const newList = userList.map((item)=> `用户id是${item?.id},用户名字是${item?.name},用户年龄是${item?.age}岁了`);
}
handleData({userList: [null]})
// good
const handleData = (data)=> {
const { userList } = data;
const newList = userList.map((item)=> {
const { id, name, age } = item || {};
return `用户id是${id},用户名字是${name},用户年龄是${age}岁了`
});
}
handleData({userList: [null]})内联样式不超过两个;模版不建议写复杂判断,需要放在逻辑中维护
变量命名需要有具体语义,不能太泛化;单一组件功能变量名允许泛化
变量命名采用小驼峰
ts: interface命名用I开头,type命名用T开头,enum用E开头
import 顺序需要按照:全局vue,UI库,第三方库,公共方法,业务方法;按从广到窄的维度引入(封装的组件放最后):vue、ui、第三方、全局 、私有
vue单文件模块顺序:template script style
新页面路由命名需要规范:一级菜单/二级菜单/页面名称,例子:/user/plateform/user-create。
swich里赋值相同的话,合并case
组件应用时props参数:按照 ref、class、传入、传出 顺序书写
<my-components
ref="myComponents"
class="home-my-components"
:data="data"
@changeHandle="changeHandle"
/>方法命名
can: 判断是否可执行某个动作 函数返回一个布尔值 true 可执行 false 不可执行
has: 判断是否含有某个值 函数返回一个布尔值 true 含有此值 false 不含有此值
is: 判断是否为某个值,函数返回一个布尔值 true 为某个值 false 不为某个值
get: 获取某个值 函数返回一个非布尔值
set: 设置某个值 无返回值或者返回是否加载完成的结果
路由参数:query对象中属性必须是字符串;不建议传递复杂Json数据,传入标识进行查询
// bad
router.push({
path: '/example/path',
query: {
isView: true,
info: JSON.stringify(data),
},
});
// good
router.push({
path: '/example/path',
query: {
isView: '1',
id: infoId,
},
});模板不能有复杂的运算,超过一层运算建议不在模版中处理
Vue官方提供了4-5种class绑定方式,建议统一使用一种,以数组的方式动态绑定类名
<div :class="['title-text', active ? 'active' : '', errorClass]">
<!-- ... -->
</div>不建议开发者大批量的对一个对象执行多次delete操作,原因是连续的delete操作代码显得冗余
使用解构赋值替代对象多个属性的 delete 操作;
使用 loadsh-es 提供的方法 unset/omit 等替代 delete 操作;
// bad
const params = { /** ... */ };
delete params['attr'];
delete params['sku_id'];
delete params['id'];
// good
const params = { /** ... */ };
const { attr, sku_id, id, ...unset } = params;函数注释
/**
* @Description 加入购物车
* @Author luochen_ya
* @Date 2024-03-13
* @param {Number} goodId 商品id
* @param {Array<Number>} specs sku规格
* @param {Number} amount 数量
* @param {String} remarks 备注
* @returns <Promise> 购物车信息
*/
apiProductAddCard = (goodId, specs, amount, remarks) => {
return axios.post("***", { goodId, specs, amount, remarks });
};
/**
* @Description 网络请求
* @param {object} options 配置对象
* @param {string} options.url 请求地址
* @param {'GET'|'POST'} options.method 请求方法
* @param {object} options.body
* @param {object} options.headers
*/
/**
* 获取指定范围内的随机整数
* @param {number} min 随机数的最小值
* @param {number} max 随机数的最大值
* @returns {number} 随机数
* @example
* getRandom(1,10); //获取[1,10]之间的随机整数
*/利用提前返回简化逻辑
// ❌ 错误做法
function doSomething() {
if (user) {
if (user.role === "ADMIN") {
return "Administrator";
} else {
return "User";
}
} else {
return "Anonymous";
}
}
// ✅ 正确做法
function doSomething() {
if (!user) return "Anonymous";
if (user.role === "ADMIN") return "Administrator";
return "User";
}双向数据绑定,双向数据绑定 和 change 函数共同使用可能会导致数据混乱,产生预期外的bug,change事件内会修改双向绑定值的情况下,应当改为单向数据流
<!-- bad -->
<a-input v-model:value="value" @change="value = formatHandle(value)" />
<!-- good -->
<a-input :value="value" @change="formatHandle" />function formatHandle(e: InputEvent) {
// value format
}回调函数代码简化
// bad
articles.map(article => getArticle(article))
// good
articles.map(getArticle)try/catch的空白catch
// bad
try {
const info = await fetch('xxx')
} catch (e) {
// 为了避免报错的空白catch
}
// good
// 报错是个好事,该报错就报错
try {
const info = await fetch('xxx')
} catch (e) {
// 打印错误信息
// 上报异常信息
// 业务逻辑
}函数参数一堆
// bad
// 造成心智负担,不仅需要知道每个参数,还需要知道每个参数的位置
const getUserInfo = (
name,
age,
weight,
...
)=>{}
// good
const getUserInfo = (options)=>{
const {name,age,...} = options
}命名多余
// bad
class User{
userName;
userAge;
userLogin(){}
getUserProfile(){}
}
// good
class User{
name;
age;
login(){}
getProfile(){}
}switch/case分支过多
// bad
// 后面越加越多分支就越来越多
switch (type){
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
// good
const ins = {
INCREMENT: 1,
DECREMENT: -1
}
return state + (ins[type] || 0)无法阅读的条件
// bad
// 一大堆条件,不知道是干嘛的
if(
remaining ===0 ||
(remaining === 1 && remainingPlayers === 1) ||
remainingPlayers === 0
)
{
quitGame()
}
// good
// 将条件提成函数
function isGameOver(){
return (
remaining ===0 ||
(remaining === 1 && remainingPlayers === 1) ||
remainingPlayers === 0
)
}
if(isGameOver()){
quitGame()
}异常处理和成功的处理耦合
// bad
// 错误处理和正确处理耦合在一起
if(isLoggedIn()){
if(post){
if(isPostDoubleChecked()){
doPost(post)
}else{
throw new Error('不要发重复文章')
}
}else{
throw new Error('文章不可为空')
}
}else{
throw new Error('请先登录')
}
// good
// 先处理错误,再处理正确
if(isLoggedIn()){
throw new Error('请先登录')
}
if(!post){
throw new Error('文章不可为空')
}
if(!isPostDoubleChecked()){
throw new Error('不要发重复文章')
}
// 后面全是正常的隐式耦合
// bad
// 写死字符串在里面,当authorization或token变化,两个函数都要改动
function response(res){
const token = res.headers.get('authorization')
if(token){
localStorage.setItem('token',token)
}
}
function request(){
const token = localStorage.getItem('token')
if(token){
res.headers.set('authorization',`Bearer ${token}`)
}
}
// good
// 提取常量
const AUTH_HEADER_KEY = 'authorization'
const AUTH_KEY = 'token'函数/hooks功能单一性
让函数功能只对一件事负责
// bad
// 这是一个formItem的默认配置函数,但是在里面又修改elProps默认配置,项目里又有一个getDefaultElProps的函数
// 导致这个函数功能混乱,应该保持函数功能单一性
/**
* @param formItem
* @returns 包含默认配置的formItem
*/
export function getDefaultFormItem(formItem: FormGroupItem) {
const defaultFormItem = { ...formItem };
// elProps默认配置
defaultFormItem['elProps'] = getDefaultElProps(formItem.fragmentKey, formItem.elProps);
// 其他默认配置
if (defaultFormItem.fragmentKey === 'renderRangePicker') {
if (!isEmpty(defaultFormItem.rangePickerConfig)) {
// 带tab的renderRangePicker组件,默认占两个搜索项的宽度
defaultFormItem['colProps'] = defaultFormItem['colProps'] || {
xs: 24,
sm: 24,
md: 24,
lg: 16,
xl: 12,
xxl: 12,
};
} else {
defaultFormItem['elProps'] = {
style: {
width: '100%',
},
...defaultFormItem['elProps'],
};
}
}
return defaultFormItem;
}
// good
// 在getDefaultElProps修改elprops;抽离一个方法出来
/**
* @param formItem
* @returns 包含默认配置的formItem
*/
export function getDefaultFormItem(formItem: FormGroupItem) {
const defaultFormItem = { ...formItem };
// elProps默认配置
defaultFormItem['elProps'] = getDefaultElProps(defaultFormItem['fragmentKey'], defaultFormItem);
// colProps默认配置
defaultFormItem['colProps'] = getDEfaultColProps(defaultFormItem['fragmentKey'], defaultFormItem);
return defaultFormItem;
}使用纯函数避免副作用
编写函数时,最好避免修改该函数之外的任何变量。
// bad
let items = 5;
function changeNumber(number) {
items = number + 3;
return items;
}
changeNumber(5);
// good
function addThree(number) {
return number + 3;
}
这里我们去掉了外部变量的依赖,让函数返回一个新值。它的功能现在是完全独立的,因此它的行为现在是完全可预测的。zIndex最好不要超过4位数
/* bad */
z-index: 999999;
/* good */
z-index: 1000;展示组件和容器组件(表单抽离降耦)
表单功能涉及到:新增、编辑、查看(大部分是相同的。些许不同,如标题/提交按钮文字等)
// bad
全部合并为一个组件,里面通过一个变量type判断,if/else。
导致耦合,编辑出现问题,需要到处找编辑的代码,改的时候也需要非常小心,因为可能会动到其他的
// good
展示组件和容器组件(经典普遍成熟的开发模式):
展示组件(只有界面逻辑,只管界面样式等):只负责展示,不负责逻辑处理,只负责接收props,抛出emit事件
容器组件(调用“展示组件”):
如:
新增:调用展示组件
编辑:调用展示组件
// 示例:<product-form :formData="formData" text="新增商品" :loading="loading" @submit="onSubmit" />魔法值的问题
魔法值,也叫做魔法数值、魔法数字,通常是指在代码编写时莫名出现的数字, 无法直接判断数值代表的含义,必须通过联系代码上下文分析才可以明白, 严重降低了代码的可读性。除数字之外,代码中作为key值的常量字符串也被认为是魔法值, 尽管其表示含义比数值较为清晰,但是仍然会产生不规范问题。
// bad
if(flag === '5'){
.......
}
if (businessType === 101){
.......
}
// good
const BusinessTypeEnum = {
SYSTEM: 0, // 系统
CRM: 1, // CRM
JXC: 2, // JXC
UNKNOWN: 404, // 未知对象类型
CUSTOMER_MANAGEMENT: 100, // 客户管理
CUSTOMER: 101, // 客户
CUSTOMER_FOCUS: 102, // 重点客户
CUSTOMER_DEAL: 103, // 成交���户
CUSTOMER_FOLLOW: 104, // 跟进客户
CUSTOMER_PUBLIC: 105 // 客户公海池
}
if (businessType === BusinessTypeEnum.CUSTOMER){
.......
}🌟 React 代码规范 🌟
组件可以渲染其他组件,但是 请不要嵌套他们的定义
// bad
export default function Gallery() {
// 🔴 永远不要在组件中定义组件
function Profile() {
// ...
}
// ...
}
// good
// 上面这段代码 非常慢,并且会导致 bug 产生。因此,你应该在顶层定义每个组件:
export default function Gallery() {
// ...
}
// ✅ 在顶层声明组件
function Profile() {
// ...
}如果你的标签和 return 关键字不在同一行,则必须把它包裹在一对括号中;没有括号包裹的话,任何在 return 下一行的代码都 将被忽略
不要创建未命名的组件,比如 export default () => {},因为这样会使得调试变得异常困难
为了减少在默认导出和具名导出之间的混淆,一些团队会选择只使用一种风格(默认或者具名),或者禁止在单个文件内混合使用。这因人而异,选择最适合你的即可
内联 style 属性使用驼峰命名法编写
例如,HTML 中的:
<ul style="background-color: black">在 React 组件里应该写成:
<ul style={{ backgroundColor: 'black' }}>Props 是只读的时间快照:每次渲染都会收到新版本的 props;你可以使用 <Avatar {...props} /> JSX 展开语法转发所有 props,但不要过度使用它
切勿将数字放在 && 左侧
JavaScript 会自动将左侧的值转换成布尔类型以判断条件成立与否。然而,如果左侧是 0,整个表达式将变成左侧的值(0),React 此时则会渲染 0 而不是不进行渲染。
例如,一个常见的错误是 messageCount && <p>New messages</p>。其原本是想当 messageCount 为 0 的时候不进行渲染,但实际上却渲染了 0。
为了更正,可以将左侧的值改成布尔类型:messageCount > 0 && <p>New messages</p>。
key 值不能改变,否则就失去了使用 key 的意义!所以千万不要在渲染时动态地生成 key
请不要在运行过程中动态地产生 key,像是 key={Math.random()} 这种方式。这会导致每次重新渲染后的 key 值都不一样,从而使得所有的组件和 DOM 元素每次都要重新创建。这不仅会造成运行变慢的问题,更有可能导致用户输入的丢失。所以,使用能从给定数据中稳定取得的值才是明智的选择。
有一点需要注意,组件不会把 key 当作 props 的一部分。Key 的存在只对 React 本身起到提示作用。如果你的组件需要一个 ID,那么请把它作为一个单独的 prop 传给组件: <Profile key={id} userId={id} />。
保持组件纯粹
React 便围绕着这个概念进行设计。React 假设你编写的所有组件都是纯函数。也就是说,对于相同的输入,你所编写的 React 组件必须总是返回相同的 JSX。不要多次调用这个组件会产生不同的 JSX!纯函数仅仅执行计算,因此调用它们两次不会改变任何东西。与渲染函数不同,事件处理函数不需要是 纯函数,事件处理函数是执行副作用的最佳位置。
在 “严格模式” 下开发时,React 会调用每个组件的函数两次,这可以帮助发现由不纯函数引起的错误。
传递给事件处理函数的函数应直接传递,而非调用。例如:
传递一个函数(正确)
<button onClick={() => alert('...')}>
调用一个函数(错误)
<button onClick={alert('...')}>事件处理函数 props 应该以 on 开头,后跟一个大写字母。例如: onPlayMovie 和 onUploadImage
Hooks ——以 use 开头的函数——只能在组件或自定义 Hook 的最顶层调用。
你不能在条件语句、循环语句或其他嵌套函数内调用 Hook。Hook 是函数,但将它们视为关于组件需求的无条件声明会很有帮助。在组件顶部 “use” React 特性,类似于在文件顶部“导入”模块。
避免冗余的 state
// bad
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');
function handleFirstNameChange(e) {
setFirstName(e.target.value);
setFullName(e.target.value + ' ' + lastName);
}
function handleLastNameChange(e) {
setLastName(e.target.value);
setFullName(firstName + ' ' + e.target.value);
}
// good
// 这个表单有三个 state 变量:firstName、lastName 和 fullName。然而,fullName 是多余的。在渲染期间,你始终可以从 firstName 和 lastName 中计算出 fullName,因此需要把它从 state 中删除。
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const fullName = firstName + ' ' + lastName;不要在 state 中镜像 props
除非你特别想防止更新,否则不要将 props 放入 state 中。
以下代码是体现 state 冗余的一个常见例子:
function Message({ messageColor }) {
const [color, setColor] = useState(messageColor);
这里,一个 color state 变量被初始化为 messageColor 的 prop 值。这段代码的问题在于,如果父组件稍后传递不同的 messageColor 值(例如,将其从 'blue' 更改为 'red'),则 color state 变量将不会更新! state 仅在第一次渲染期间初始化。这就是为什么在 state 变量中,“镜像”一些 prop 属性会导致混淆的原因。相反,你要在代码中直接使用 messageColor 属性。如果你想给它起一个更短的名称,请使用常量:
function Message({ messageColor }) {
const color = messageColor;
这种写法就不会与从父组件传递的属性失去同步。
只有当你 想要 忽略特定 props 属性的所有更新时,将 props “镜像”到 state 才有意义。按照惯例,prop 名称以 initial 或 default 开头,以阐明该 prop 的新值将被忽略:
function Message({ initialColor }) {
// 这个 `color` state 变量用于保存 `initialColor` 的 **初始值**。
// 对于 `initialColor` 属性的进一步更改将被忽略。
const [color, setColor] = useState(initialColor);避免重复的 state
对于选择类型的 UI 模式,请在 state 中保存 ID 或索引而不是对象本身。
// bad
const [items, setItems] = useState(initialItems);
const [selectedItem, setSelectedItem] = useState(
items[0]
);
// 当前,它将所选元素作为对象存储在 selectedItem state 变量中。然而,这并不好:selectedItem 的内容与 items 列表中的某个项是同一个对象。 这意味着关于该项本身的信息在两个地方产生了重复。
// good
const [items, setItems] = useState(initialItems);
const [selectedId, setSelectedId] = useState(0);
const selectedItem = items.find(item =>
item.id === selectedId
);
// 你不需要在 state 中保存 选定的元素,因为只有 选定的 ID 是必要的。其余的可以在渲染期间计算。避免深度嵌套的 state
你确实可以随心所欲地嵌套 state,但是将其“扁平化”可以解决许多问题。这使得 state 更容易更新,并且有助于确保在嵌套对象的不同部分中没有重复。 如果 state 嵌套太深,难以轻松更新,可以考虑将其“扁平化”。
export const initialTravelPlan = {
0: {
id: 0,
title: '(Root)',
childIds: [1, 42, 46],
},
1: {
id: 1,
title: 'Earth',
childIds: [2, 10, 19, 26, 34]
},
2: {
id: 2,
title: 'Africa',
childIds: [3, 4, 5, 6 , 7, 8, 9]
},
}
现在 state 已经“扁平化”(也称为“规范化”),更新嵌套项会变得更加容易。
现在要删除一个地点,你只需要更新两个 state 级别:
其 父级 地点的更新版本应该从其 childIds 数组中排除已删除的 ID。
其根级“表”对象的更新版本应包括父级地点的更新版本。可以使用 reducer 整合状态逻辑
// 通常
function handleAddTask(text) {
setTasks([
...tasks,
{
id: nextId++,
text: text,
done: false,
},
]);
}
function handleChangeTask(task) {
setTasks(
tasks.map((t) => {
if (t.id === task.id) {
return task;
} else {
return t;
}
})
);
}
function handleDeleteTask(taskId) {
setTasks(tasks.filter((t) => t.id !== taskId));
}
// 使用 reducer
function tasksReducer(tasks, action) {
switch (action.type) {
case 'added': {
return [
...tasks,
{
id: action.id,
text: action.text,
done: false,
},
];
}
case 'changed': {
return tasks.map((t) => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
}
case 'deleted': {
return tasks.filter((t) => t.id !== action.id);
}
default: {
throw Error('未知 action: ' + action.type);
}
}
}
// 建议将每个 case 块包装到 { 和 } 花括号中,这样在不同 case 中声明的变量就不会互相冲突。此外,case 通常应该以 return 结尾。如果你忘了 return,代码就会 进入 到下一个 case,这就会导致错误!
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);