本指南旨在统一业务前端系统的编码风格与最佳实践,提升代码可读性、可维护性与团队协作效率,降低潜在缺陷风险。
1、文档说明
1.1 、编制说明
软件行业的高速发展,对软件开发者的综合素质要求越来越高,不仅仅是编程知识点,其他维度知识点也会影响最后的交付质量,本文档以开发前端项目角度,详细描写了前端的代码规范,分别从 HTML、CSS、JavaScript、TypeScript、Vue、代码注释、性能优化等几个方面入手,并且每个章节进行了详细划分,方便读者能快速定位,规范自己的代码,提高项目代码质量。
1.2、名词解释
-
圈复杂度:软件源码某部分的圈复杂度就是这部分代码中线性无关路径的数量 eg:如果一段源码中不包含控制流语句(条件或决策点),那么这段代码的圈复杂度为 1,因为这段代码中只会有一条路径;如果一段代码中仅包含一个 if 语句,且 if 语句仅有一个条件,那么这段代码的圈复杂度为 2;包含两个嵌套的 if 语句,或是一个 if 语句有两个条件的代码块的圈复杂度为 3。
-
认知复杂度:认知复杂度分数根据三个基本规则进行评估:忽略允许将多个语句易于理解地简写成一个的情况在代码线性流程中的每一次中断都增加(+1), 断流结构嵌套时增加(复杂度)。
2、HTML
2.1、文档类型
- 【强制】 使用 HTML5 DOCTYPE。
<!-- good -->
<!DOCTYPE html>
<html>
</html>2.2、文档语言
- 【推荐】指定 html 标签上的 lang 属性。
<html lang="zh-CN"></html>2.3、资源加载
- 【推荐】引入 CSS 和 JavaScript 时无需指定 type。根据 HTML5 规范,引入 CSS 和 JavaScript 时通常不需要指明 type,因为 text/css 和 text/javascript 分别是他们的默认值。
- 【推荐】 在 head 标签内引入 CSS,在 body 结束标签前引入 JS。在 中指定外部样式表和嵌入式样式块可能会导致页面的重排和重绘,对页面的渲染造成影响。因此,一般情况下,CSS 应在 标签里引入。
2.4、文档标签
- 【强制】标签名统一使用小写。
- 【强制】HTML 标签的自闭合标签应使用自闭合语法
<br /><img />等,不应该写成<br>或<img>。 - 【推荐】在 Vue3 中引用自定义组件时,推荐使用完整的闭合标签形式(不是自闭合)。
示例:
<!-- 不推荐:自闭合组件标签 -->
<CustomComponent />
<!-- 推荐:完整闭合标签 -->
<CustomComponent></CustomComponent>
<!-- HTML自闭合标签 -->
<br />
<img src="path" alt="description" />
<input type="text" />2.5、项目补充
Vue3 单页面应用的项目中,如果需要引用第三方资源,其他功能页面中使用三方资源,如下,项目中使用打印控件的资源,需要在 index.html 中引入:
<head>
<!-- 下面是打印控件资源 -->
<script type="text/javascript" src="/js/jquery.min.js"></script>
<script type="text/javascript" src="/js/jquery.watermark.js"></script>
<script type="text/javascript" src="/js/socket.io.js"></script>
<script type="text/javascript" src="/js/finereport.js"></script>
</head>这种方式没有问题,但是可能会影响打包之后首页渲染的速度,造成白屏时间的增加,我们可以尝试放在 body 下方,用以优化渲染速度。(注意:具体示例具体分析)
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
<script type="text/javascript" src="/js/jquery.min.js"></script>
<script type="text/javascript" src="/js/jquery.watermark.js"></script>
<script type="text/javascript" src="/js/socket.io.js"></script>
<!-- 下面是打印控件资源 -->
<script type="text/javascript" src="/js/finereport.js"></script>
</body>3、CSS
3.1、文件引用
- 【强制】 一律使用 link 的方式调用外部样式。
- 【推荐】 不要在 style 中使用@import;不要在页面中使用 style 块;
3.2、组成元素
- 【强制】 命名必须由字母、中划线或数字组成且不能以数字或中划线开头。
- 【强制】不允许使用拼音与英文的混合命名,更不允许直接使用中文的方式;禁止同一个含义的内容,在同一个应用中出现多种不同的单词与翻译。
3.3、词汇规范
- 【参考】 不依据表现形式来命名。
- 【参考】 可根据内容来命名,可根据功能来命名。
3.4、命名缩写
- 【强制】 保证缩写后还能较为清晰保持原单词所能表述的意思。
- 【推荐】 使用业界熟知的或者约定俗成的来定义 CSS 类名。
扩展
在
Vue/React/Angular等框架流行前,样式有一种BEM命名规范,这种规范普遍被接受和认可。但是随着
tailwind css和unocss的流行,这种规范也在淡化, 项目可自行判断是否使用BEM规范
3.5、编码风格
- 【强制】 所有声明都应该以分号结尾,不能省略。
- 【推荐】使用 2 个空格缩进,不要使用 4 个空格或 tab 缩进。
- 【推荐】选择器和 { 之间保留一个空格。
- 【推荐】属性名和 : 之前无空格,: 和属性值之间保留一个空格。
- 【推荐】 >、+、~ 、|| 等组合器前后各保留一个空格。
- 【推荐】 在使用 , 分隔的属性值中,, 之后保留一个空格。
- 【推荐】 注释内容和注释符之间留有一个空格。
- 【推荐】 声明块的右大括号 } 应单独成行。
- 【推荐】 属性声明应单独成行。
- 【推荐】 单行代码最多不要超过 100 个字符。
- 【参考】 使用多个选择器时,每个选择器应该单独成行。
- 【参考】 声明块内只有一条语句时,也应该写成多行。
- 【参考】 注释行上方需留有一行空行,除非上一行是注释或块的顶部。
3.6、选择器
- 【参考】 不要使用 id 选择器。id 会带来过高的选择器优先级,使得后续很难进行样式覆盖(继而引起样式污染)。
- 【推荐】 防止使用 !important 覆盖样式的恶性循环。
- 【参考】 属性选择器的值始终用双引号包裹。
3.7、项目补充
在使用 sass 进行样式开发的过程中需要主要区分一个地方:@import 和@import url();
@import 是文件内容的引用,需要 sass loader 的预处理;@import url()是路径引用,css 本身支持这种语法。
一般项目开发中使用@import 引入文件内容,但是可以尝试@import url()进行式样复用。
4、JavaScript
-
建议遵照 airbnb javascript 规范执行。
-
也可以遵照 Google JavaScript Style Guide。
自己根据实际业务开发灵活选用即可。
5、TypeScript
-
建议遵照 typescript 官方规范执行。
-
也可以遵照 Google TypeScript Style Guide 。
项目根据实际情况选择即可。
6、Vue
Vue3 中项目开发过程中注意事项和规范提倡。刚开始从事前端开发工作或者后端开发人员编写前端页面的需要重点学习。
6.1、箭头函数
推荐使用箭头函数(保持 this 指向不变), 提这个主要是为了代码风格的一致性 ,目的是尽可能的防止两个人三种代码风格的出现。切记工程开发中代码可读性是首要因素。
示例:
//【建议】业务开发中提倡的做法, 箭头函数配合const函数一起使用
const getTableListData = () => { // TODO }
//【反例】尽量不要出现混用,如下:
function getDomeData () {}
const getDome1Data = () => {}
// 【反例】this乱指向,乱用,如下:
const obj = {
name: 'Alice',
greet: function() {
// 普通函数,this指向obj
setTimeout(() => {
// 箭头函数内 this 已经指向 obj
// 这里却画蛇添足地再次保存 this
const that = this;
console.log(that.name); // 输出 Alice,但直接 this.name 即可
}, 100);
}
};
// 混用会导致可读性变差,而开发首要元素的可读性。6.2、变量提升
在项目或者开发过程中,尽量使用 let 或者 const 定义变量,可以有效的规避变量提升的问题,不在赘述,注意 const 一般用于声明常量或者值不允许改变的变量。
6.3、数据请求
数据请求类、异步操作类需要使用 try…catch 捕捉异常。尽量避免回调地狱出现。
示例:
// 推荐写法
/**
* @description 获取列表数据
* @return void
*/
const getTableListData = async () => {
// 自己的业务处理TODO
try {
const res = await getTableListDataApi();
const res1 = await getTableListDataApi1();
// TODO
} catch (error) {
// 异常处理相关
} finally {
// 最终处理
}
};
//【提倡】推荐接口定义带着Api结尾,比如我的方法是getTableListData,
//【提倡】内部逻辑调用的后端接口,那我的接口便可以定位为getTableListDataApi。当然也可以使用下面的方式: 示例:
/**
* @description 获取列表数据
* @return void
*/
const getTableListData = () => {
getTableListDataApi({....}).then(() => {
// TODO
}).catch(() => {
// TODO
}).finally(() => {
// TODO
})
}
// 注意使用这种方式避免嵌套层级太深,如下反例:
const getTableListData1 = () => {
getTableListDataApi({....}).then(() => {
getTableListDataApi1({....}).then(() => {
getTableListDataApi2({....}).then(() => {
// TODO 这种就是典型的回调地狱,禁止出现这种
})
})
})
}合理使用数据并发请求: 示例:
// 场景描述:表头和表格数据都需要请求接口获取,可以使用并发请求。
/**
* 查询列表数据
*/
const getTableList = async () => {
// TODO
try {
// 并行获取表格列数据和列表数据
const [resColumns, resData] = await Promise.all([
getTableColumnsApi({....}),
getTableListApi({...}),
]);
// TODO
} catch (error) {
// TODO
} finally {
// TODO
}
};
// Promise.all的一些执行细节不在赘述,但是注意区分和Promise.allSettled用法
//
// Promise.all()方法会在任何一个输入的 Promise 被拒绝时立即拒绝。
// 相比之下,Promise.allSettled() 方法返回的 Promise 会等待所有
// 输入的 Promise 完成,不管其中是否有 Promise 被拒绝。如果你需
// 要获取输入可迭代对象中每个 Promise 的最终结果,则应使用allSettled()方法。合理使用数据竞速请求: 示例:
// 场景描述:某些业务需要请求多个接口,但是只要一个接口先返回便处理逻辑
let promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("数据请求1");
}, 1000);
});
let promise2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("数据请求2");
}, 500);
});
Promise.race([promise1, promise2]).then((result) => {
console.log(result); // 输出 "数据请求2"
});注意:
数据请求时一定要做好异常的捕获和处理,异常的捕获和处理可以增加程序的健壮性和提升用户使用体验。
下面的反例要禁止:
/**
* 获取表格数据
*/
function getTableListData () {
getTableListData({
pageNum: 1,
pageSize: 10, // pageSize: 100000
}).then((res) => {
tableList.value = res.rows;
tableTotal.value = res.total;
//【提倡】
tableList.value = res?.code === 200 ? res.rows : [];
})
}
// 上面写法,界面可能没报错,功能也实现了,但是....6.4、响应性变量
合理的使用响应性变量。数据量很大的对象或者数组,同时属性又是嵌套的对象,你的业务场景只需要第一层属性具有响应性,推荐使用 shallowRef 和 shallowReactive 定义响应性变量,这时不在推荐使用 ref 和 reactive 了。
6.5、单一职责
组件或者方法的编写一定要遵循单一职责原则(概念不在赘述,自行了解)。
这个地方扩展一下,编码中有四条原则需要遵循:对扩展开放、对修改关闭、高内聚、低耦合。
6.6、文件命名
功能菜单的入口文件一定要带着 name,同时其他编写的业务组件也推荐带着 name,同时 name 的命名规则大写驼峰,且尽量要全局唯一(避免后期定位问题增加复杂度)。
文件名命名中,Vue 中没有强制的规则,这里借鉴 React 的规则,大写驼峰。
React component names must start with a capital letter, like StatusBar and SaveButton. React components also need to return something that React knows how to display, like a piece of JSX.
示例:
<script setup name='CustomName'> </script>
// 或者
export default defineComponent({
name: 'CustomName',
.......
})6.7、监听器使用
在 Vue3 中使用监听器 watchEffect 和 watch 时,需要留意使用方式,先看 watchEffect:
示例:
<script setup>
import { ref, watchEffect } from "vue"
const a = ref(true)
const b = ref(false)
watchEffect(() => {
if (a.value || b.value) {
console.log('执行了更新操作');
}
})
const test = () => b.value = !b.value;
</script>
<template>
<button @click="test">改变b的值</button>
<h2>当前b的值:{{ b }}</h2>
</template>答案:当模板中改变 b 的值时,watchEffect 无法监听 执行了更新操作。
在看下面的示例:
<script setup>
import { ref, watchEffect } from "vue"
const getInfo = async () => {
await new Promise((resolve, reject) => {
setTimeout(() => {
resolve(111)
}, 2000)
})
}
watchEffect(async () => {
// 请求信息
await getInfo()
if (b.value) console.log('执行了更新操作');
})
const test = () => b.value = !b.value;
</script>
<template>
<button @click="test">改变b的值</button>
<h2>当前b的值:{{ b }}</h2>
</template>答案:当模板中改变 b 的值时,watchEffect 无法监听 执行了更新操作。
在继续看下面示例:
<script setup>
import { ref, watchEffect } from "vue"
const a = ref(true)
const b = ref(true)
setTimeout(() => {
watchEffect(() => {
if (a.value) {
console.log('执行了更新操作');
}
})
}, 2000)
const test = () => b.value = !b.value;
</script>
<template>
<button @click="test">改变b的值</button>
<h2>当前b的值:{{ b }}</h2>
</template>答案:当模板中改变 b 的值时,watchEffect 无法监听 '执行了更新操作'。 使用 watchEffect 一定要注意两点:
1、要使 watchEffect 可以第一时间捕捉到响应性变量;
2、异步操作触发微任务会影响 watchEffect 第一时间捕捉响应性变量。 当你 watchEffect 使用不是很熟悉的话,建议尽量使用 watch。
watch 注意点:当你的组件内部使用 watch 较多或者你想手动消除 watch 的复杂度,建议如下:
<script setup>
....
const currentScope = effectScope();
currentScope.run(() => {
watch(
() => props.currentRow,
(newVal, oldVal) => {
// TODO
},
{ deep: true }
);
watchEffect(() => {
if (queryObj.visitId) {
// TODO
}
});
});
onBeforeUnmount(() => {
currentScope.stop();
});
</script>需要留意的是 Vue3.5+中新增了 deep 属性可以直接传入数字,告诉 wacth 监听到响应性数据到第几层。
6.8、Hooks 使用
在 Vue3 的项目中强烈推荐使用 hooks 进行功能的拆分和复用,这是 Vue 官方团队推荐的编写方式,下面来看一个列子,比如说,我要实现一个弹框的功能,下面常见的写法,第一种偏后端思维的写法:
const editModel = reactive({
isShow: false,
form: {
name: "ANDROID",
// ......
},
showFunc: () => {
// 显示逻辑
},
cancelFunc: () => {
// 取消逻辑
},
submitFunc: () => {
// 提交逻辑
},
});或者其他的类似写法,不在赘述。 其实都可以换成 hooks 的写法:
示例:
const useEditModel = () => {
const isShow = ref(false);
/**
* 显示弹框
*/
const showModal = () => {};
/**
* 关闭弹框
*/
const cancelModal = () => {};
/**
* 提交操作
*/
const submitModal = () => {};
onBeforeMount(() => {
// TODO
});
return {
isShow,
showModal,
cancelModal,
submitModal,
};
};
// 其他地方使用
const { isShow, showModal, cancelModal, submitModal } = useEditModel();简单总结一下 hooks 编写的思想:
在函数作用域内定义、使用响应式\非响应性状态、变量或者从多个函数中得到的状态、变量、方法进行组合,从而处理复杂问题。
6.9、暴露方法
当我们想要暴露第三方组件的所有属性时,我们怎么快速的暴露?
使用 expose 需要一个一个写,显然太麻烦,可以使用下面的方式:
expose(
new Proxy(
{},
{
get(target, key) {
// CustomDomRef是定义的模板中的ref dom节点
return CustomDomRef.value?.[key];
},
has(target, key) {
return key in CustomDomRef.value;
},
},
),
);6.10、挑选属性
某些业务场景下我们需要挑选出,部分属性传递给接口,如何优雅的挑选属性,可以参考如下:
const obj = {
name: "张三",
age: 20,
sex: "男",
name1: "张三1",
};
// 当不需要name1传递时,怎么做呢?
// 方式1
delete obj.name1;
// 方式2
const newObj = {
name: obj.name,
age: obj.age,
sex: obj.sex,
};
// 方式3
const newObj = {
...obj,
name1: undefined,
};
// 其实可以使用一种更优雅的方式
const { name1, ...newObj } = obj;
// 或者使用lodash的omit或者pick方法6.11、组合式 API
组合式 API 本身是为了灵活,但是项目中使用时出现了五花八门的情况,有的把 expose 写到了最开始,把组件引入放到最下面,当你不确定 setup 语法糖下使用顺序时,可以参考下面的顺序:
示例:
<script setup>
// import语句
// Props(defineProps)
// Emits(defineEmits)
// 响应性变量定义
// Computed
// Watchers
// 函数
// 生命周期
// Expose(defineExpose)
</script>6.12、逻辑分支
当我们编写业务代码时,经常会遇到下面这种写法,写法没有对错只是有更好的优化方式:
示例:
// 场景一
if (type === 1) {
// TODO
} else if (type === 2) {
// TODO
} else if (type === 3) {
// TODO
} else if (type === 4) {
// TODO
} else if (type === 5) {
// TODO
} else {
// TODO
}
// 场景二
if (type === 1) {
if (type1 === 1) {
if (type2 === 1) {
if (type3 === 1) {
// TODO
}
}
}
}场景一:违背了开闭原则(对扩展开放、对修改关闭)和单一职责原则。场景一可以进行如下的优化:
// 优化方式一:字典映射方式
const typeHandlers = {
1: handleType1,
2: handleType2,
3: handleType3,
4: handleType4,
5: handleType5,
default: handleDefault,
};
const handler = typeHandlers[type] || typeHandlers.default;
handler();
// 优化方式二:高阶函数方式
const handleType1 = () => {
/* TODO for type 1 */
};
const handleType2 = () => {
/* TODO for type 2 */
};
// 其他处理函数...
const handlers = [handleType1, handleType2 /*...*/];
const processType = (type) => {
if (handlers[type - 1]) handlers[type - 1]();
};
processType(type);场景二:违背了圈复杂度原则和单一职责原则,场景二可以进行如下优化:
// 优化方式一
const isValidType = () => {
return type === 1 && type1 === 1 && type2 === 1 && type3 === 1;
};
if (isValidType()) {
}
// 优化方式二:使用"早返回原则"或者叫"错误前置原则"进行优化
if (type !== 1) return;
if (type1 !== 1) return;
if (type2 !== 1) return;
if (type3 !== 1) return;
// TODO上面只是简单列举的优化的思路,方案有很多,合理即可。
6.13、删除冗余
在业务开发过程中,我们经常会对代码进行注释,有些文件中会出现好多处注释,当然这些注释后边可能会放开,但是官方提倡的做法是尽量删除掉这些注释的代码,真正需要哪些代码,在还原回来即可。
另一个常见的问题是:console 打印和 debugger 之类的,虽然说可以通过插件配置在打包的时候删除掉,但是官方提倡的是在源码层面一旦调试完成就立即删除。
还有单文件不要超过 600行代码 ,当然也可以适当根据实际情况放宽,一般情况下超过这个行数就要进行代码的拆分,拆分的方式包括组件、方法、样式、配置项等。但是过度拆分也会导致碎片化的问题,需要合理把握。
6.14、异步组件
Vue3 中提供了异步组件(defineAsyncComponent)的定义,异步组件的优点:
1、在运行时是懒加载的,可以更好的让浏览器渲染其他功能。
2、有利于 vite 打包时进行代码分割。
简单示例:
<template>
<AdminPage />
</template>
<script setup>
import { defineAsyncComponent } from "vue";
const AdminPage = defineAsyncComponent(() =>
import("./components/AdminPageComponent.vue")
);
</script>复杂示例:
<template>
<keep-alive>
<component :is="getCurrentComponents()"></component>
</keep-alive>
</template>
<script setup>
import { defineAsyncComponent } from "vue";
export const PreferenceItemComponents: any = {
Residence: defineAsyncComponent(() => import("./Residence.vue")),
PastHistory: defineAsyncComponent(() => import("./PastHistory.vue")),
AllergyHistory: defineAsyncComponent(() => import("./AllergyHistory.vue")),
Diagnose: defineAsyncComponent(() => import("./Diagnose.vue")),
};
/**
* 获取当前需要渲染的组件
*/
const getCurrentComponents = () => {
const projectType = activeName.value;
if (projectType && PreferenceItemComponents[projectType]) {
return PreferenceItemComponents[projectType];
}
return null;
};
</script>复杂功能的拆分可以考虑使用异步组件,特别是包含非常多的组件的功能
6.15、路由懒加载
现有框架里面一般不需要我们接触这块,因为菜单和路由已经是封装完善的,但是我们也需要知道路由懒加载的概念:
示例:
// 将
// import UserDetails from './views/UserDetails.vue'
// 替换成
const UserDetails = () => import('./views/UserDetails.vue')
const router = createRouter({
// ...
routes: [
{ path: '/users/:id', component: UserDetails }
// 或在路由定义里直接使用它
{ path: '/users/:id', component: () => import('./views/UserDetails.vue') },
],
})路由懒加载有利于 vite 对不同的菜单功能进行代码分割,降低打包之后的代码体积,从而增加访问速度。 需要注意的是:不要在路由中使用异步组件。异步组件仍然可以在路由组件中使用,但路由组件本身就是动态导入的。
6.16、运算符
ES 新特性中有几个新增的运算符你需要了解,因为它可以简化你的编码编写。
?? ( 空值合并运算符)
?. (可选链式运算符)
??= (空值合并赋值操作符)
?= (安全复制运算符)
示例
// ?? ( 空值合并运算符):这个运算符主要是左侧为null和undefined,直接返回右侧值
// 请在开发过程中合理使用||和??
let result = value ?? '默认值';
console.log('result', result);
// ?.(可选链运算符): 用于对可能为 null 或 undefined 的对象进行安全访问。
// 建议这个属性要用起来,防止数据不规范时控制台直接报错
const obj = null;
let prop = obj?.property;
console.log('prop', prop);
// ??= (空值合并赋值操作符): 用于在变量已有非空值,避免重复赋值。
let x = null;
x ??= 5; // 如果 x 为 null 或 undefined,则赋值为 5
// ?= (安全复制运算符):旨在简化错误处理。改运算符与 Promise、async 函数以及任何实现了 Symbol.result 方法的对象兼容,简化了常见的错误处理流程。
// 注意:任何实现了 Symbol.result 方法的对象都可以与 ?= 运算符一起使用,Symbol.result 方法返回一个数组,第一个元素为错误,第二个元素为结果。
const [error, response] ?= await fetch("https://blog.conardli.top");7、代码注释
代码的可读性和可迭代性是编写代码时首要考虑因素。
7.1、文件注释
单个文件注释规范,每个独立的 VUE 文件开头可进行文件注释,表明该文件的描述信息、作者、创建时间等。
示例:
<!--
* @FileDescription: 该文件的描述信息
* @Author: 作者信息
* @Date: 文件创建时间
* @LastEditors: 最后更新作者
* @LastEditTime: 最后更新时间
-->7.2、方法注释
功能开发时编写的相关方法要进行方法注释和说明,注释要遵循 JSDOC 规范。
方法注释格式:
/**
* @description: 方法描述 (可以不带@description)
* @param {参数类型} 参数名称
* @param {参数类型} 参数名称
* @return 没有返回信息写 void / 有返回信息 {返回类型} 描述信息
*/示例:
/**
* @description 获取解析统计相关数据
* @param {Object} userInfo
* @param {Array} lists
* @return void
*/
或者;
/**
* 获取解析统计相关数据
* @param {Object} userInfo 用户信息
* @param {Array} lists 用户列表
* @return void
*/7.3、变量注释
关键的变量要进行注释说明,变量注释一般包括两种:
示例:
// 提倡(vscode可以给出提示的写法)
/* 描述信息 */
activeName: "first";
activeName: "first"; // 默认激活的Tab页
或者;
// 默认激活的Tab页
activeName: "first";7.4、行内注释
关键业务代码必须进行行内注释,行内注释建议按照以下格式进行:
示例:
// 根据指定的属性对数据进行分类
或者;
// 根据指定的属性对数据进行分类,
// 分类之后按住时间进行降序排序
// ......
或者;
/**
* 根据指定的属性对数据进行分类,
* 分类之后按住时间进行降序排序
* ......
*/7.5、折叠代码
耦合度非常高的变量或者方法建议进行代码折叠注释
示例:
// #region 升序、降序处理逻辑
/**
* 升序、降序处理逻辑说明:
*
* 根据指定的属性对数据进行分类,
* 分类之后按住时间进行降序排序
* ......
*/
const asceOrderLists = []; // 升序数组
const descOrderLists = []; // 降序数组
/**
* @description 升序操作
* @param {Array} lists
* @return {Array} arrs
*/
const handleAsceOrder = (lists) => {
// .........
return arrs
}
/**
* @description 降序操作
* @param {Array} lists
* @return {Array} arrs
*/
const handleDescOrder = (lists) => {
// .........
return arrs
}
......
// #endregion7.6、其他说明
日常开发中,常见的问题修改和功能开发建议按下列方式进行注释:
- 新功能点开发
// FEAT-001: 进行了XXXXX功能开发(LMX-2024-09-24)
- 问题修复
// BUGFIX-001: 进行了XXXXX功能修复(LMX-2024-09-24)
....说明:
格式说明:
[${a1}-${a2}]: 相关描述信息(${a3}-${a4})
- a1:类型描述,建议遵循git提交规范,但是使用全驼峰大写。(feat、fix、bugfix、docs、style、refactor、perf、chore)
- a2: 编号,可以使用bug单号、功能特性单号或者自增序号,建议使用bug单号、功能特性单号。
- a3: git账户或者能标识自己的账号即可。
- a4: 新增或者修改时间,建议精确到天。8、目录结构
针对于项目功能开发,怎样划分一个功能的目录结构?怎么的目录结构可以提高代码的可读性?
下面是一个相对完善业务功能文件目录,可以进行参考:
custom_module # 业务模块
│ ├── api # 业务模块私有接口
│ ├── components/modules # 业务组件(涉及业务处理)
│ ├── composable # 业务组件(不涉及具体业务)
│ ├── functional # 业务函数式组件
│ ├── methods/hooks # 业务hooks
│ ├── config # 业务配置项
│ ├── styles # 业务样式
│ └── utils # 业务私有工具类
|── index.vue # 业务入口文件
|── .pubrc.js # 业务后期模块联邦入口
└── README.md # 业务说明文档具体的业务功能划分,可以根据自己的具体业务划定,总之合理即可。如果是公用性组件的话,可以不需要按照上面的目录结构进行划分。
8.1、状态管理规范
推荐使用 Pinia 进行状态管理,需要注意以下规范:
// stores/user.ts
import { defineStore } from "pinia";
import type { UserInfo } from "@/types";
/**
* @description 用户信息store
*/
export const useUserStore = defineStore("user", () => {
// 【强制】使用 ref 定义响应式状态
const userInfo = (ref < UserInfo) | (null > null);
const isLoading = ref(false);
// 【推荐】使用 computed 定义派生状态
const isAdmin = computed(() => userInfo.value?.role === "admin");
// 【强制】使用函数定义 action,并使用 async/await 处理异步操作
/**
* @description 获取用户信息
*/
const fetchUserInfo = async () => {
isLoading.value = true;
try {
const res = await getUserInfoApi();
userInfo.value = res;
} catch (error) {
console.error("获取用户信息失败", error);
} finally {
isLoading.value = false;
}
};
// 【推荐】重置状态方法
const resetUserInfo = () => {
userInfo.value = null;
};
return {
userInfo,
isLoading,
isAdmin,
fetchUserInfo,
resetUserInfo,
};
});规范要点:
- Store 模块名应该清晰描述功能
- 【强制】Store 建立在模块化的基础上,不应该过于庞大
- 【推荐】关键的 action 应该进行错误捕获和处理
- 【推荐】避免在 Store 中进行复杂的业务逻辑,应该在 hooks 或服务层处理
8.2、正则表达式
规范化正则表达式的定义和使用:
// ✓ 提倡做法:使用常量存储正则,并添加注释说明用途
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; // 邮箱地址
const PHONE_REGEX = /^1[3-9]\d{9}$/; // 中国手机号码
const ID_CARD_REGEX =
/^[1-9]\d{5}(18|19|20|21)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/; // 身份证号
// 【推荐】复杂的正则表达式应该使用 new RegExp 并添加详细注释
const complexRegex = new RegExp(
// 匹配 URL
"^(https?:\\/\\/)?" + // protocol
"((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|" + // domain
"((\\d{1,3}\\.){3}\\d{1,3}))" + // OR IP
"(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*" + // port and path
"(\\?[;&a-z\\d%_.~+=-]*)?" + // query string
"(\\#[-a-z\\d_]*)?$",
"i", // flags
);
// 【反例】避免动态生成正则表达式
const userInput = getUserInput();
const dynamicRegex = new RegExp(userInput); // 不安全,易被利用8.3、请求管理规范
建立统一的 API 请求层,确保请求的一致性和可维护性:
// api/index.ts
import axios, { type AxiosInstance, type AxiosRequestConfig } from "axios";
/**
* @description API 响应通用格式
*/
interface ApiResponse<T = any> {
code: number;
message: string;
data: T;
timestamp?: number;
}
/**
* @description 创建 axios 实例
*/
const instance: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000,
});
// 【推荐】请求拦截器:统一处理请求前置逻辑
instance.interceptors.request.use(
(config) => {
// 添加认证 token
const token = localStorage.getItem("token");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error),
);
// 【推荐】响应拦截器:统一处理响应和错误
instance.interceptors.response.use(
(response) => {
const { code, message, data } = response.data as ApiResponse;
if (code !== 200) {
throw new Error(message);
}
return data;
},
(error) => {
// 统一错误处理
if (error.response?.status === 401) {
// 处理未授权
}
return Promise.reject(error);
},
);
export default instance;API 命名规范:
// api/user.ts
/**
* @description 获取用户完整信息
* @param {string} userId 用户ID
* @returns {Promise<UserInfo>}
*/
export const getUserInfoApi = (userId: string) => {
return request.get(`/user/${userId}`);
};
/**
* @description 创建新用户
* @param {CreateUserPayload} payload 创建用户请求体
* @returns {Promise<UserInfo>}
*/
export const createUserApi = (payload: CreateUserPayload) => {
return request.post("/user", payload);
};
/**
* @description 批量获取用户列表
* @param {QueryParams} params 查询参数
* @returns {Promise<PaginatedResponse<UserInfo[]>>}
*/
export const getUserListApi = (params: QueryParams) => {
return request.get("/users", { params });
};8.4、单元测试规范
编写单元测试和集成测试时应遵循以下规范:
// src/utils/__tests__/formatter.test.ts
import { describe, it, expect, beforeEach } from "vitest";
import { formatDate, formatPrice } from "../formatter";
describe("Formatter Utils", () => {
/**
* 【强制】每个测试用例应该有清晰的描述
*/
describe("formatDate", () => {
it("should format date correctly", () => {
const result = formatDate(new Date("2024-01-01"));
expect(result).toBe("2024-01-01");
});
it("should handle invalid date", () => {
expect(() => formatDate(null as any)).toThrow();
});
});
describe("formatPrice", () => {
/**
* 【推荐】使用专业的测试用例描述,不要使用"test 1"、"test 2"
*/
it("should format price with 2 decimal places", () => {
expect(formatPrice(99.9)).toBe("99.90");
});
it("should add thousand separator", () => {
expect(formatPrice(10000)).toBe("10,000.00");
});
it("should handle negative price", () => {
expect(formatPrice(-50.5)).toBe("-50.50");
});
});
});
/**
* @description 【强制】类型定义测试
*/
describe("Type Safety", () => {
it("should have correct return type", () => {
const result: string = formatDate(new Date());
expect(typeof result).toBe("string");
});
});测试覆盖率要求:
- 【强制】关键业务逻辑覆盖率不低于 80%
- 【推荐】公用工具函数覆盖率不低于 90%
- 【推荐】使用 vitest 进行单元测试,使用 Playwright 进行 E2E 测试
8.5、国际化规范
Vue3 项目中的国际化实现规范:
// locales/en.ts
export default {
'common.save': 'Save',
'common.delete': 'Delete',
'common.cancel': 'Cancel',
'user.login': 'User Login',
'user.logout': 'User Logout',
'user.profile': 'User Profile',
'error.networkFailed': 'Network request failed',
'error.unauthorized': 'Unauthorized access',
};
// locales/zh-CN.ts
export default {
'common.save': '保存',
'common.delete': '删除',
'common.cancel': '取消',
'user.login': '用户登录',
'user.logout': '用户登出',
'user.profile': '用户资料',
'error.networkFailed': '网络请求失败',
'error.unauthorized': '未授权访问',
};
// 在组件中使用
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
const { t, locale } = useI18n();
</script>
<template>
<button>{{ t('common.save') }}</button>
<div>{{ t('error.networkFailed') }}</div>
</template>规范要点:
- 【强制】国际化 key 使用点号分隔的命名空间
module.feature.key - 【推荐】文案翻译应存放在单独的 locale 文件中
- 【推荐】避免在代码中硬编码文案,应统一使用 i18n
8.6、无障碍访问
编写满足 WCAG 2.1 标准的无障碍组件:
<!-- ✓ 提倡做法 -->
<form>
<label for="username">Username</label>
<input id="username" type="text" required />
</form>
<!-- ✓ 提倡做法:使用 aria-label 和 aria-describedby -->
<button aria-label="Close dialog" @click="handleClose">×</button>
<input aria-describedby="password-hint" type="password" />
<p id="password-hint">Password must be at least 8 characters</p>
<!-- ✓ 提倡做法:使用 role 属性明确 DOM 角色 -->
<div role="status" aria-live="polite">{{ message }}</div>
<!-- ✗ 反例:跳过标题级别 -->
<h1>Main Title</h1>
<h3>Subtitle</h3>
<!-- 应该是 h2 -->
<!-- ✗ 反例:没有标签的表单输入 -->
<input type="text" placeholder="Username" />9、性能优化
9.1、减小代码打包体积
- 减少源代码重复,复用功能抽取共用组件或者方法
- 优化前端依赖,防止新依赖的加入导致包体积的增大,例如 lodash-es 要优于 lodash
- 代码分割(ESM 动态导入,路由懒加载)
- 合理的配置 vite.config.ts 中配置项。例如 rollupOptions 配置项中的 output.manualChunks,sourceMap 等
- 移除未使用的 polyfill 和 shim
- 使用动态导入替代静态导入以实现代码分割
9.2、优化资源加载速度
- 部分静态资源或者依赖项可以考虑 cdn 方式,增加访问速度
- 开启浏览器的 gzip 压缩,减少带宽请求
- 某些关键性资源是否可以考虑预加载
- 部分图片和视频是否可以考虑延迟加载
- 使用 HTTP/2 服务器推送关键资源
- 对常用的第三方库进行缓存策略配置
9.3、业务代码层面优化
- 较少接口请求数量,耗时接口如何优化
- 大数据量的场景处理(分页、虚拟滚动)
- 减少非必要的更新(父子组件之间的更新, key 禁止使用 index)
- 减少大数量下的响应性开销
- 减少人为的内存泄露和溢出操作
- 优化 JS 中执行较长时间的任务(比如是否可以考虑异步、requestAnimationFrame、requestIdleCallback)
- 使用 Web Worker 处理 CPU 密集型任务
- 虚拟滚动技术处理大列表
9.4、合理利用缓存
- 浏览器的协商缓存
- 浏览器的强缓存
- 浏览器本地的存储(localStorage、sessionStorage、indexedDB 这些是否可以使用)
10、代码风格与工具
使用统一的代码格式化和代码校验等插件工具进行规范。
10.1、Prettier
使用统一的代码格式化工具,进行代码格式化,保持项目的统一。后续项目将增加统一的prettierrc 和 prettierignore文件。
.prettierrc
{
// 最大的行长度
"printWidth": 150,
// 每个缩进级别的空格数
"tabWidth": 2,
// 使用制表符还是空格缩进行
"useTabs": false,
// 是否使用分号
"semi": true,
// 是否使用单引号
"singleQuote": true,
// 遵循对象属性中引号的输入使用
"quoteProps": "as-needed",
// 在jsx中是否使用单引号替代双引号
"jsxSingleQuote": false,
// > 是否单独一行,false默认单独一行
"bracketSameLine": false,
// "all" "es5" "none" --》更换为all
"trailingComma": "all",
// 在对象,数组括号与属性之间加空格 "{ foo: bar }"
"bracketSpacing": true,
// 是否进行嵌入代码的格式化
"embeddedLanguageFormatting": "auto",
// 箭头函数只有一个参数时是否要"()"
"arrowParens": "always",
// 是否需要Pragma
"requirePragma": false,
// 是否需要插入Pragma
"insertPragma": false,
// 什么都不做保持散文原格式
"proseWrap": "preserve",
// 标签周围的空格处理方式
"htmlWhitespaceSensitivity": "css",
// Vue 文件中的 <script> 和 <style> 标签内缩进代码。
"vueIndentScriptAndStyle": false,
// 结尾是 \n \r \n\r auto lf...
"endOfLine": "auto"
}.prettierignore
/dist/*
.local
.output.js
/node_modules/**
**/*.svg
**/*.png
**/*.jpg
**/*.jpeg
**/*.sh
/public/*10.2、ESLint
使用统一的代码校验工具,保持项目的代码的正确性。后续项目将增加统一的eslintrc.cjs和eslintignore文件。
.eslintrc.cjs
module.exports = {
env: {
browser: true,
node: true,
es6: true,
},
parser: "vue-eslint-parser",
extends: [
"plugin:vue/vue3-recommended",
"plugin:@typescript-eslint/recommended",
"./.eslintrc-auto-import.json",
"prettier",
"plugin:prettier/recommended",
],
parserOptions: {
ecmaVersion: "2020",
sourceType: "module",
project: "./tsconfig.*?.json",
parser: "@typescript-eslint/parser",
},
plugins: [
"vue",
"@typescript-eslint",
"import",
"promise",
"node",
"prettier",
],
rules: {
// 关闭空方法校验
"@typescript-eslint/no-empty-function": "off",
// 关闭类型限制不允许使用any
"@typescript-eslint/no-explicit-any": "off",
// 关闭没有使用变量校验
"@typescript-eslint/no-unused-vars": "off",
// 关闭不允许复制this给变量校验
"@typescript-eslint/no-this-alias": "off",
// 关闭不允许使用ts的关闭注释等
"@typescript-eslint/ban-ts-comment": "off",
// 关闭要求组件名称始终为多个单词
"vue/multi-word-component-names": "off",
// 关闭强制执行有效的defineProps编译器宏
"vue/valid-define-props": "off",
// 关闭禁止在自定义组件中使用带有参数的v-model
"vue/no-v-model-argument": "off",
// 关闭禁止使用 v-html
"vue/no-v-html": "off",
// 关闭标记arguments变量提示通知
"prefer-rest-params": "off",
// 把Prettier的规则设为错误级别
"prettier/prettier": "error",
// 开启圈复杂度检查, 最大圈复杂度为10
complexity: ["warn", { max: 10 }],
},
globals: {
DialogOption: "readonly",
OptionType: "readonly",
defineEmits: "readonly",
defineProps: "readonly",
},
};.eslintignore
*.sh
node_modules
*.md
*.woff
*.ttf
.vscode
.idea
dist
/public
/docs
.husky
.local
/bin
.eslintrc.cjs
prettier.config.js
src/assets
tailwind.config.js10.3、Stylelint
使用统一的样式校验限制,保持项目样式的正确和统一,后续项目将统一增加stylelint.config.js
stylelint.config.js
export default {
extends: ["stylelint-config-standard"],
ignoreFiles: [
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.tsx",
"src/**/*.ts",
"src/**/*.json",
"src/**/*.md",
"src/**/*.{jpg,png,gif,jpge,svg,webp}",
],
overrides: [
{
customSyntax: "postcss-html",
files: ["*.(html|vue)", "**/*.(html|vue)"],
rules: {
"selector-pseudo-class-no-unknown": [
true,
{
ignorePseudoClasses: ["deep"],
},
],
"selector-pseudo-element-no-unknown": [
true,
{
ignorePseudoElements: ["v-deep", "v-slotted", ":deep", ":slotted"],
},
],
},
},
{
customSyntax: "postcss-scss",
extends: [
"stylelint-config-recommended-scss",
"stylelint-config-recommended-vue/scss",
],
files: ["*.scss", "**/*.scss"],
},
],
plugins: ["stylelint-prettier"],
rules: {
"prettier/prettier": true,
// 自定义未知规则
"at-rule-no-unknown": [
true,
{
ignoreAtRules: [
"extends",
"ignores",
"include",
"mixin",
"if",
"else",
"media",
"for",
"at-root",
"tailwind",
"apply",
"variants",
"responsive",
"screen",
"function",
"each",
"use",
"forward",
"return",
],
},
],
// 不校验兜底字体
"font-family-no-missing-generic-family-keyword": null,
// 不校验自定义函数
"function-no-unknown": null,
// 不校验import引入方式
"import-notation": null,
// 不校验媒体查询范围
"media-feature-range-notation": null,
// 不校验网格布局
"named-grid-areas-no-invalid": null,
// 不校验同类选择器的优先级
"no-descending-specificity": null,
// 不校验空文件
"no-empty-source": null,
// 校验类规则之间的空行
"rule-empty-line-before": [
"always",
{
ignore: ["after-comment", "first-nested"],
},
],
// 校验类命名
"selector-class-pattern":
"^(?:(?:o|c|u|t|s|is|has|_|js|qa)-)?[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*(?:__[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*)?(?:--[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*)?(?:[.+])?$",
// 不校验 not 语法
"selector-not-notation": null,
// 不校验自定义属性
"custom-property-pattern": null,
// 不校验伪类未知属性
"selector-pseudo-class-no-unknown": null,
// 不校验自定义export属性
"property-no-unknown": null,
// 不校验浏览器前缀
"property-no-vendor-prefix": null,
// 不校验动画命名规则
"keyframes-name-pattern": null,
},
};10.4、Commitlint
使用 commitlint 做 git 提交的规范限制。
commitlint.config.js
export default {
extends: ["@commitlint/config-conventional"],
rules: {
"type-case": [0],
"type-enum": [
2,
"always",
[
"feat",
"fix",
"docs",
"style",
"refactor",
"perf",
"test",
"build",
"ci",
"revert",
"other",
],
],
"scope-case": [0],
"scope-empty": [2, "never"],
"subject-case": [0, "never"],
"subject-full-stop": [0, "never"],
"header-max-length": [0, "always", 120],
},
};10.5、Code-spell-checker
使用 code-spell-checker 做代码拼写检查。
cspell.json
{
// 版本
"version": "0.2",
// 检查的语言
"language": "en,en-US",
// 允许使用复合词
"allowCompoundWords": true,
// 被认为是正确的单词列表
"words": [
"axios",
"paratera",
"blsc",
"bscc",
"iconfont",
"pinia",
"wechat",
"nprogress",
"istio",
"pkce",
"echarts",
"weixin",
"vite",
"cros",
"jsencrypt",
"dompurify",
"mockjs",
"remixicon",
"vuex",
"exceljs",
"matomo",
"Piwik",
"brotli",
"npmmirror",
"monoplasty",
"rushstack",
"argb",
"cascader",
"backtop",
"popconfirm",
"msgbox",
"acmr",
"antd",
"antdv",
"astro",
"brotli",
"clsx",
"defu",
"demi",
"echarts",
"ependencies",
"esno",
"etag",
"execa",
"iconify",
"iconoir",
"intlify",
"ipaddr",
"lockb",
"logininfor",
"lucide",
"mkdist",
"vitejs",
"noopener",
"noreferrer",
"nprogress",
"nuxt",
"oper",
"operlog",
"prefixs",
"publint",
"Qqchat",
"qrcode",
"shadcn",
"sonner",
"sortablejs",
"styl",
"taze",
"ui-kit",
"uicons",
"unref",
"vitepress",
"vnode",
"vueuse",
"yxxx",
"unocss",
"attributify",
"svgicon",
"amfe"
],
// 要忽略检测的单词列表
"ignoreWords": [
"userinfo",
"userid",
"readed",
"unplugin",
"mcode",
"persistedstate",
"selfservice",
"accountapply",
"sess",
"jobname",
"jobid",
"preinstall",
"cusers",
"pcas",
"invoicable",
"supercomputing",
"cmscp",
"headerbtn",
"qrcode-mpdefault",
"realname",
"resultv2",
"VVIP",
"hasPermi",
"checkPermi",
"clientid",
"Relogin",
"ctyk",
"yjk",
"unmatch",
"strs",
"gitee",
"auths",
"jpge",
"webp",
"pxtorem"
],
// 总是被认为是不正确单词的列表
"flagWords": [],
// 忽略iconfont相关目录的拼写检测
"ignorePaths": ["**/node_modules/**", "**/dist/**", "**/icons/**", "npm-lock.yaml", "**/*.log", "src/assets/**"]
}10.6、Husky
后续考虑使用 husky 做代码提交前的校验修复和提交描述信息规范化校验。
husky和lint-staged(lint-staged 调用 prettier 和 eslint),做代码校验修复。 husky和commitlint,做提交描写信息的校验。
评论交流
支持作者