从实战中提炼的Vue3开发经验与规范要点全解析,愿你我一同进步!
1、Vue3规范
1.1、箭头函数
推荐使用箭头函数(保持this指向不变,避免后期定位问题的发杂度)。
示例:
javascript
//【建议】业务开发中提倡的做法, 箭头函数配合const函数一起使用
const getTableListData = () => { // TODO }
//【反例】尽量不要出现混用,如下:
function getDomeData () {}
const getDome1Data = () => {}
// 混用会导致可读性变差,而开发首要元素的可读性。
1.2、变量提升
在项目或者开发过程中,尽量使用let或者const定义变量,可以有效的规避变量提升的问题,不在赘述,注意const一般用于声明常量或者值不允许改变的变量。
1.3、数据请求
数据请求类、异步操作类需要使用try...catch捕捉异常。尽量避免回调地狱出现。
示例:
javascript
// 推荐写法
/**
* @description 获取列表数据
* @return void
*/
const getTableListData = async () => {
// 自己的业务处理TODO
try {
const res = await getTableListDataApi();
const res1 = await getTableListDataApi1();
// TODO
} catch (error) {
// 异常处理相关
} finally {
// 最终处理
}
};
//【提倡】推荐接口定义带着Api结尾,比如我的方法是getTableListData,
//【提倡】内部逻辑调用的后端接口,那我的接口便可以定位为getTableListDataApi。
当然也可以使用下面的方式:
示例:
javascript
/**
* @description 获取列表数据
* @return void
*/
const getTableListData = () => {
getTableListDataApi({....}).then(() => {
// TODO
}).catch(() => {
// TODO
}).finally(() => {
// TODO
})
}
// 注意使用这种方式避免嵌套层级太深,如下反例:
const getTableListData1 = () => {
getTableListDataApi({....}).then(() => {
getTableListDataApi1({....}).then(() => {
getTableListDataApi2({....}).then(() => {
// TODO 这种就是典型的回调地狱,禁止出现这种
})
})
})
}
合理使用数据并发请求:
示例:
javascript
// 场景描述:表头和表格数据都需要请求接口获取,可以使用并发请求。
/**
* 查询列表数据
*/
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()方法。
合理使用数据竞速请求:
示例:
javascript
// 场景描述:某些业务需要请求多个接口,但是只要一个接口先返回便处理逻辑
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"
});
注意:
数据请求时一定要做好异常的捕获和处理,异常的捕获和处理可以增加程序的健壮性和提升用户使用体验。
下面的反例要禁止:
javascript
/**
* 获取表格数据
*/
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 : [];
})
}
// 上面写法,界面可能没报错,功能也实现了,但是....
1.4、响应性变量
合理的使用响应性变量。数据量很大的对象或者数组,同时属性又是嵌套的对象,你的业务场景只需要第一层属性具有响应性,推荐使用shallowRef和shallowReactive定义响应性变量,这时不在推荐使用ref和reactive了。
1.5、单一职责原则
组件或者方法的编写一定要遵循单一职责原则(概念不在赘述,自行了解)。
1.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.
示例:
javascript
<script setup name='CustomName'> </script>
// 或者
export default defineComponent({
name: 'CustomName',
.......
})
1.7、监听器使用
在Vue3中使用监听器watchEffect和watch时,需要留意使用方式,先看watchEffect:
示例:
javascript
<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无法监听 '执行了更新操作'。
在看下面的示例:
javascript
<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无法监听 '执行了更新操作'。
在继续看下面示例:
javascript
<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的复杂度。
建议如下:
javascript
<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监听到响应性数据到第几层。
1.8、Hooks使用
在Vue3的项目中强烈推荐使用hooks进行功能的拆分和复用,这是Vue官方团队推荐的编写方式,下面来看一个列子,比如说,我要实现一个弹框的功能,下面常见的写法,第一种偏后端思维的写法:
javascript
const editModel = reactive({
isShow: false,
form: {
name: 'ANDROID',
// ......
},
showFunc: () => {
// 显示逻辑
},
cancelFunc: () => {
// 取消逻辑
},
submitFunc: () => {
// 提交逻辑
},
});
或者其他的类似写法,不在赘述。 其实都可以换成hooks的写法:
示例:
javascript
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编写的思想:
在函数作用域内定义、使用响应式\非响应性状态、变量或者从多个函数中得到的状态、变量、方法进行组合,从而处理复杂问题。
1.9、暴露方法
当我们想要暴露第三方组件的所有属性时,我们怎么快速的暴露?
使用expose需要一个一个写,显然太麻烦,可以使用下面的方式:
javascript
expose(
new Proxy(
{},
{
get(target, key) {
// CustomDomRef是定义的模板中的ref dom节点
return CustomDomRef.value?.[key];
},
has(target, key) {
return key in CustomDomRef.value;
},
},
),
);
1.10、挑选属性
某些业务场景下我们需要挑选出,部分属性传递给接口,如何优雅的挑选属性,可以参考如下:
javascript
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方法
1.11、组合式API
组合式API本身是为了灵活,但是项目中使用时出现了五花八门的情况,有的把expose写到了最开始,把组件引入放到最下面,当你不确定setup语法糖下使用顺序时,可以参考下面的顺序:
示例:
javascript
<script setup>
// import语句
// Props(defineProps)
// Emits(defineEmits)
// 响应性变量定义
// Computed
// Watchers
// 函数
// 生命周期
// Expose(defineExpose)
</script>
1.12、逻辑分支
当我们编写业务代码时,经常会遇到下面这种写法,写法没有对错只是有更好的优化方式:
示例:
javascript
// 场景一
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
}
}
}
}
场景一:违背了开闭原则 (对扩展开放、对修改关闭)和单一职责原则。场景一可以进行如下的优化:
javascript
// 优化方式一:字典映射方式
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);
场景二:违背了圈复杂度原则 和单一职责原则,场景二可以进行如下优化:
javascript
// 优化方式一
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
上面只是简单列举的优化的思路,方案有很多,合理即可。
1.13、删除冗余
在业务开发过程中,我们经常会对代码进行注释,有些文件中会出现好多处注释,当然这些注释后边可能会放开,但是官方提倡的做法是尽量删除掉这些注释的代码,真正需要哪些代码,在还原回来即可。
另一个常见的问题是:console打印和debugger之类的,虽然说可以通过插件配置在打包的时候删除掉,但是官方提倡的是在源码层面一旦调试完成就立即删除。
还有单文件不要超过600行代码,当然也可以适当根据实际情况放宽,一般情况下超过这个行数就要进行代码的拆分,拆分的方式包括组件、方法、样式、配置项等。但是过度拆分也会导致碎片化的问题,需要合理把握。
1.14、异步组件
Vue3中提供了异步组件(defineAsyncComponent)的定义,异步组件的优点:
1、在运行时是懒加载的,可以更好的让浏览器渲染其他功能。
2、有利于vite打包时进行代码分割。
示例:
javascript
// 简单示例
<script setup>
import { defineAsyncComponent } from 'vue'
const AdminPage = defineAsyncComponent(() =>
import('./components/AdminPageComponent.vue')
)
</script>
<template>
<AdminPage />
</template>
// 复杂示例
// 异步组件的定义
import { defineAsyncComponent } from "vue";
export const PreferenceItemComs: any = {
Residence: defineAsyncComponent(() => import("./Residence.vue")),
PastHistory: defineAsyncComponent(() => import("./PastHistory.vue")),
AllergyHistory: defineAsyncComponent(() => import("./AllergyHistory.vue")),
Diagnose: defineAsyncComponent(() => import("./Diagnose.vue")),
};
// 异步组件的使用
<keep-alive>
<component
:is="getCurrentComponents()"
></component>
</keep-alive>
/**
* 获取当前需要渲染的组件
*/
const getCurrentComponents = () => {
const projectType = activeName.value;
if (projectType && PreferenceItemComs[projectType]) {
return PreferenceItemComs[projectType];
}
return null;
};
复杂功能的拆分可以考虑使用异步组件。
1.15、路由懒加载
现有框架里面一般不需要我们接触这块,因为菜单和路由已经是封装完善的,但是我们也需要知道路由懒加载的概念:
示例:
javascript
// 将
// 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对不同的菜单功能进行代码分割,降低打包之后的代码体积,从而增加访问速度。 需要注意的是:不要 在路由中使用异步组件。异步组件仍然可以在路由组件中使用,但路由组件本身就是动态导入的。
1.16、运算符
es新特性中有几个新增的运算符你需要了解,因为它可以简化你的编码编写。
?? ( 空值合并运算符)
?. (可选链式运算符)
??= (空值合并赋值操作符)
?= (安全复制运算符)
示例
javascript
// ?? ( 空值合并运算符):这个运算符主要是左侧为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");
2、代码注释
代码的可读性和可迭代性是编写代码时首要考虑因素。
2.1、文件注释
单个文件注释规范,每个独立的VUE文件开头可进行文件注释,表明该文件的描述信息、作者、创建时间等。
示例:
javascript
<!--
* @FileDescription: 该文件的描述信息
* @Author: 作者信息
* @Date: 文件创建时间
* @LastEditors: 最后更新作者
* @LastEditTime: 最后更新时间
-->
2.2、方法注释
功能开发时编写的相关方法要进行方法注释和说明,注释要遵循JSDOC规范。
方法注释格式:
javascript
/**
* @description: 方法描述 (可以不带@description)
* @param {参数类型} 参数名称
* @param {参数类型} 参数名称
* @return 没有返回信息写 void / 有返回信息 {返回类型} 描述信息
*/
示例:
javascript
/**
* @description 获取解析统计相关数据
* @param {Object} userInfo
* @param {Array} lists
* @return void
*/
或者;
/**
* 获取解析统计相关数据
* @param {Object} userInfo 用户信息
* @param {Array} lists 用户列表
* @return void
*/
2.3、变量注释
关键的变量要进行注释说明,变量注释一般包括两种:
示例:
javascript
// 提倡(vscode可以给出提示的写法)
/* 描述信息 */
activeName: 'first';
activeName: 'first'; // 默认激活的Tab页
或者;
// 默认激活的Tab页
activeName: 'first';
2.4、行内注释
关键业务代码必须进行行内注释,行内注释建议按照以下格式进行:
示例:
javascript
// 根据指定的属性对数据进行分类
或者;
// 根据指定的属性对数据进行分类,
// 分类之后按住时间进行降序排序
// ......
或者;
/**
* 根据指定的属性对数据进行分类,
* 分类之后按住时间进行降序排序
* ......
*/
2.5、折叠代码块注释
耦合度非常高的变量或者方法建议进行代码折叠注释
示例:
javascript
// #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
}
......
// #endregion
2.6、其他
日常开发中,常见的问题修改和功能开发建议按下列方式进行注释:
javascript
- 新功能点开发
// FEAT-001: 进行了XXXXX功能开发(LMX-2024-09-24)
- 问题修复
// BUGFIX-001: 进行了XXXXX功能修复(LMX-2024-09-24)
....
说明:
javascript
格式说明:
[${a1}-${a2}]: 相关描述信息(${a3}-${a4})
- a1:类型描述,建议遵循git提交规范,但是使用全驼峰大写。(feat、fix、bugfix、docs、style、refactor、perf、chore)
- a2: 编号,可以使用bug单号、功能特性单号或者自增序号,建议使用bug单号、功能特性单号。
- a3: git账户或者能标识自己的账号即可。
- a4: 新增或者修改时间,建议精确到天。
3、目录结构
针对于项目功能开发,怎样划分一个功能的目录结构?怎么的目录结构可以提高代码的可读性?
下面是一个相对完善业务功能文件目录,可以进行参考:
plain
custom_module # 业务模块
│ ├── api # 业务模块私有接口
│ ├── components/modules # 业务组件(涉及业务处理)
│ ├── composable # 业务组件(不涉及具体业务)
│ ├── functional # 业务函数式组件
│ ├── methods/hooks # 业务hooks
│ ├── config # 业务配置项
│ ├── styles # 业务样式
│ └── utils # 业务私有工具类
|── index.vue # 业务入口文件
|── .pubrc.js # 业务后期模块联邦入口
└── README.md # 业务说明文档
具体的业务功能划分,可以根据自己的具体业务划定,总之合理即可。如果是公用性组件的话,可以不需要按照上面的目录结构进行划分。
4、性能优化
减小代码打包体积
- 减少源代码重复,复用功能抽取共用组件或者方法
- 优化前端依赖,防止新依赖的加入导致包体积的增大,例如lodash-es要优于lodash
- 代码分割(ESM动态导入,路由懒加载)
- 合理的配置vite.config.ts中配置项。例如rollupOptions配置项中的output.manualChunks,sourceMap等
优化资源加载速度
- 部分静态资源或者依赖项可以考虑cdn方式,增加访问速度
- 开启浏览器的gzip压缩,减少带宽请求
- 某些关键性资源是否可以考虑预加载
- 部分图片和视频是否可以考虑延迟加载
业务代码层面优化
- 较少接口请求数量,耗时接口如何优化
- 大数据量的场景处理(分页、虚拟滚动)
- 减少非必要的更新(父子组件之间的更新, key禁止使用index)
- 减少大数量下的响应性开销
- 减少人为的内存泄露和溢出操作
- 优化JS中执行较长时间的任务(比如是否可以考虑异步、requestAnimationFrame、requestIdleCallback)
合理利用缓存
- 浏览器的协商缓存
- 浏览器的强缓存
- 浏览器本地的存储(localStorage、sessionStorage、indexedDB这些是否可以使用)