埋头苦干Vue3项目一年半,总结出了16个代码规范

人生需要不断学习、不断进步,只有不断超越自己才能成为更好的自己。 Lv1

从实战中提炼的Vue3开发经验与规范要点全解析,愿你我一同进步!

1、Vue3规范

1.1、箭头函数

推荐使用箭头函数(保持this指向不变,避免后期定位问题的发杂度)。

示例:

//【建议】业务开发中提倡的做法, 箭头函数配合const函数一起使用 const getTableListData = () => { // TODO } //【反例】尽量不要出现混用,如下: function getDomeData () {} const getDome1Data = () => {} // 混用会导致可读性变差,而开发首要元素的可读性。

1.2、变量提升

在项目或者开发过程中,尽量使用let或者const定义变量,可以有效的规避变量提升的问题,不在赘述,注意const一般用于声明常量或者值不允许改变的变量。

1.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 : []; }) } // 上面写法,界面可能没报错,功能也实现了,但是....

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.

示例:

<script setup name='CustomName'> </script> // 或者 export default defineComponent({ name: 'CustomName', ....... })

1.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监听到响应性数据到第几层。

1.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编写的思想:

在函数作用域内定义、使用响应式\非响应性状态、变量或者从多个函数中得到的状态、变量、方法进行组合,从而处理复杂问题。

1.9、暴露方法

当我们想要暴露第三方组件的所有属性时,我们怎么快速的暴露?

使用expose需要一个一个写,显然太麻烦,可以使用下面的方式:

expose( new Proxy( {}, { get(target, key) { // CustomDomRef是定义的模板中的ref dom节点 return CustomDomRef.value?.[key]; }, has(target, key) { return key in CustomDomRef.value; }, }, ), );

1.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方法

1.11、组合式API

组合式API本身是为了灵活,但是项目中使用时出现了五花八门的情况,有的把expose写到了最开始,把组件引入放到最下面,当你不确定setup语法糖下使用顺序时,可以参考下面的顺序:

示例:

<script setup> // import语句 // Props(defineProps) // Emits(defineEmits) // 响应性变量定义 // Computed // Watchers // 函数 // 生命周期 // Expose(defineExpose) </script>

1.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

上面只是简单列举的优化的思路,方案有很多,合理即可。

1.13、删除冗余

在业务开发过程中,我们经常会对代码进行注释,有些文件中会出现好多处注释,当然这些注释后边可能会放开,但是官方提倡的做法是尽量删除掉这些注释的代码,真正需要哪些代码,在还原回来即可

另一个常见的问题是:console打印和debugger之类的,虽然说可以通过插件配置在打包的时候删除掉,但是官方提倡的是在源码层面一旦调试完成就立即删除

还有单文件不要超过600行代码,当然也可以适当根据实际情况放宽,一般情况下超过这个行数就要进行代码的拆分,拆分的方式包括组件、方法、样式、配置项等。但是过度拆分也会导致碎片化的问题,需要合理把握。

1.14、异步组件

Vue3中提供了异步组件(defineAsyncComponent)的定义,异步组件的优点:

1、在运行时是懒加载的,可以更好的让浏览器渲染其他功能。

2、有利于vite打包时进行代码分割。

示例:

// 简单示例 <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、路由懒加载

现有框架里面一般不需要我们接触这块,因为菜单和路由已经是封装完善的,但是我们也需要知道路由懒加载的概念:

示例:

// 将 // 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新特性中有几个新增的运算符你需要了解,因为它可以简化你的编码编写。

?? ( 空值合并运算符)

?. (可选链式运算符)

??= (空值合并赋值操作符)

?= (安全复制运算符)

示例

// ?? ( 空值合并运算符):这个运算符主要是左侧为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文件开头可进行文件注释,表明该文件的描述信息、作者、创建时间等。

示例:

<!-- * @FileDescription: 该文件的描述信息 * @Author: 作者信息 * @Date: 文件创建时间 * @LastEditors: 最后更新作者 * @LastEditTime: 最后更新时间 -->

2.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 */

2.3、变量注释

关键的变量要进行注释说明,变量注释一般包括两种:

示例:

// 提倡(vscode可以给出提示的写法) /* 描述信息 */ activeName: 'first'; activeName: 'first'; // 默认激活的Tab页 或者; // 默认激活的Tab页 activeName: 'first';

2.4、行内注释

关键业务代码必须进行行内注释,行内注释建议按照以下格式进行:

示例:

// 根据指定的属性对数据进行分类 或者; // 根据指定的属性对数据进行分类, // 分类之后按住时间进行降序排序 // ...... 或者; /** * 根据指定的属性对数据进行分类, * 分类之后按住时间进行降序排序 * ...... */

2.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 } ...... // #endregion

2.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: 新增或者修改时间,建议精确到天。

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这些是否可以使用)
  • 标题: 埋头苦干Vue3项目一年半,总结出了16个代码规范
  • 作者: 人生需要不断学习、不断进步,只有不断超越自己才能成为更好的自己。
  • 创建于 : 2025-02-24 14:25:12
  • 更新于 : 2025-02-24 14:40:20
  • 链接: https://moondev.asia/2025/02/24/sum up Vue3/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论