前言

本文档主要记录自己在进行vue项目开发过程中遇到以及发现的一些解决方案以及解决思路,并整理出自己未曾熟悉的相关知识点,便于后续浏览与查询!

编码方面

编码规范

建议编码习惯

vue生命周期钩子函数

官方针对组件的生命周期钩子函数执行顺序已经讲解得足够透彻了,具体见官方生命周期钩子说明
这里主要是针对此生命周期钩子函数的执行顺序进行一个验证,另外,再结合组件中存在属性的监听时,这个生命周期的执行情况又会变得如何, 👇 的截图是自己在对应的例子中提供的一个结果:
vue3组件生命周期钩子函数执行结果

这里是对应的例子:

组件的生命周期钩子函数

👆 从上面的例子我们可以看出,组件中的变量的监听动作是在组件created之前完成的,然后在组件mounted之前,需要拿到组件的变量来进行渲染,因此,会在mounted之前触发到这个renderTracked动作!

🤔 而当组件中的数据发生变化的时候,数据对应的watch中的onTrigger(数据级别的监听)将第一时间触发,然后收集需要的变化值,触发renderTriggered,然后收集变量,继续触发变量的监听onTrack,接着准备更新组件,进入beforeUpdate,接着渲染页面,进入renderTracked,最后触发组件的updated,整个过程的执行如下图所示:
钩子函数的执行顺序

vue最佳实践

vue3+的api

以下整理关于在vue3中所提供的公共API,可作为日常编码工具!

toValue()

获取refsgetters的值。这与 unref() 类似,不同的是此函数也会规范化 getter 函数。如果参数是一个 getter,它将会被调用并且返回它的返回值。
这可以在组合式函数中使用,用来规范化一个可以是值、ref 或 getter 的参数。
关于此方法的定义如下:

1
2
3
export function toValue<T>(source: MaybeRefOrGetter<T> | ComputedRef<T>): T {
return isFunction(source) ? source() : unref(source)
}

defineSlots()

这个宏定义可用于为ide提哦那个插槽名称和props类型检查的类型提示,也就是告知插槽组件的使用者如何使用并传递插槽属性相关的,也就是将作用域插槽的相关属性给定义出来,而且拥有什么名称的插槽,都有哪些属性
使用方式如下:

1
2
3
4
5
const slots = defineSlots<{
default(props: { msg: string }): any,
footer: { msg: string },
header?(props: { msg1: string, sort: number }): any
}>()

而关于这个宏定义的源代码定义如下:

1
2
3
4
5
6
7
8
export function defineSlots<
S extends Record<string, any> = Record<string, any>
>(): StrictUnwrapSlotsType<SlotsType<S>> {
if(__DEV__){
warnRuntimeUsage('defineSlots')
}
return null as any
}

🌠 从上述的源码定义我们可以看出,这个插槽的类型参数定义是,可以是key为string,然后value为任何类型的定义!

useSlots()与useAttrs

<script setup>中使用slots以及attrs的情况比较少见, 👉 因为可以在模版中使用$slots以及$attrs来直接访问到,但我们还是可以通过useSlots以及useAttrs两个辅助函数来获取
这两个函数是真实运行的函数,其返回值分别对应于setupContext.slots以及setupContext.attrs
这个slots代表的是一个插槽集合对象,该对象中的属性成员都是一个个的插槽函数,而我们知道插槽函数的执行结果都是一个VNode数组,当我们在声明一个内部动态渲染插槽的机制的时候,可以使用此来实现,比如 🈶 👇 的一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<template>
<h1>我是父亲组件</h1>
<!-- 以下是带插槽的自定义组件 -->
<ChildComponent>
<template #default>
我是默认的内容
</template>
<templte #header>
我是头部内容
</templte>
</ChildComponent>
</template>

<script setup lang="ts">
import { defineComponent, useSlots } from 'vue'
const ChildComponent = defineComponent({
setup(props, ctx){
const slots = useSlots()
// 也可以采用 const slots = ctx.slots
return () => (
<div>
{ slots.header? slots.header() : null }
{ slots.default? slots.defualt() : null }
</div>
)
}
})
</script>

这里我们通过结合defineComponent来创建一个可接收插槽内容的组件,上述如果我们将null替换为自定义的内容的话,即可实现当没有传递插槽的时候,将展示默认的视图效果!

defineComponent()

vue2.+的开发中,一般我们是使用的export default {}的方式来创建一个组件的,通过在这个导出的对象中进行一系列选项的配置,实现这个组件的开发,而在vue3.+中,则提供了defineComponent()方法,用来帮助我们创建一个具有类型推导功能的组件,该方法可接收一个对象或者一个函数,并返回一个组件选项对象,使用defineComponent可以获得更好的类型推导支持,尤其是在使用TS开发
官方说明
👉 也就是可以简单地认为使用了defineComponent()方法与直接导出的两者没有什么区别,而且是同一个对象, 🤔 因此,我进行了 👇 的一个尝试:

1
2
3
4
5
6
7
8
9
10
11
12
<script>
import { defineComponent } from 'vue'
const Options = {
name: 'Comp'
}
const Comp = defineComponent(Options)
console.info(Options)
console.info(Comp)
console.info(Options === Comp)
export default Comp
</script>
![defineComponent之前与之后都是同一对象.png](/vue_use_skills/defineComponent之前与之后都是同一对象.png)

🌟 结果证明与官方所描述的是一致的,在后续的vue3 + ts项目中,尽量可以使用这个defineComponent()来定义组件

那么,使用defineComponent与使用<script setup lang="ts"></script> 🈶 什么区别呢?

🤔 这两种方式是属于定义组件的不同两种方式,各有特点并适用于不同的使用场景

  1. 两者都具备类型推断支持,减少显示类型注解的需要;
  2. <script setup>提供了一种更简洁的开发体验,但对于引用自身实例时,可能会有一些局限性;
  3. defineComponent则可以为vue2.+的童鞋提供一种过度方案,而且提供了一种更为通用的开发模式,更容易与vue中的其他特性(比如mixins、extends)等集成,虽然官方推荐是使用composition API,而且,在对于开发复杂项目的时候,defineComponent可以提供更好的结构化和模块化,便于维护

🤔 最开始,我们提及到这个defineComponent可以接收一函数来作为参数,那么如果这个函数执行结果返回不同的结构的话,是否可以借此来控制生成不同的组件呢?
👉 答案是肯定的!这种模式可以用来创建可配置的组件或者是高阶组件,高阶组件本质上也就是一个函数,接收一个组件和额外的参数,根据这些参数返回一个新的增强组件!

v自定义指令-动态化时踩坑了

当我们创建自己的v指令的时候,假如这个指令所作用在的某个元素,它是由另外一个接口来动态控制的,这个时候,我们在指令中所获取到的el将不会是被作用的el元素,这个时候,需要尝试另外的方式来实现,而不能直接去使用!

在vue3中声明一个对象数组类型的ref

在vue3中想要声明一个对象数组类型的响应式变量,一般可以通过以下两种方式方式来声明

1
2
3
4
// 方式一 
const xxxRef = ref<T[]>([])
// 方式二
const xxxRef = ref<Array<T>>([])

碎片化管理:fragment

vue3中,Fragment是一个特殊的类型,用于在模版中返回多个元素而不需要将多个元素包裹在一个额外的DOM元素中
vue3中可以在template模版中无需再嵌套一个公共的父容器节点,可以直接在template下嵌套多个孩子节点的情况,这个情况,其实就是在template模版中隐式的使用一个fragment来包裹template下的孩子节点,使用方式如下:

1
2
3
4
<template>
<div>我是元素一</div>
<div>我是元素一</div>
</template>

vue-tool开发工具中查看,结果如下:
隐式嵌套的fragment
🥸template中包含了两个根级别的div元素,它们被渲染为同一级别的兄弟元素,而且没有任何额外的包裹元素,且在vue-tool视图下可见被fragment包裹其中

组件的属性设计,能v-model就v-model

在设计组件的时候,因为vue3中通过v-model可以实现自定义的双向绑定操作,减少过多的属性定义以及数据更新回调操作,减少来回的编写属性已经更新数据动作,通过采用v-model来捆绑从属性中接接收到的对象,再通过v-model捆绑到子组件中,实现数据的绑定与自动更新操作!
具体可见之前的一篇博文 使用v-model提升编码效率

表格input-cell单元格,采用v-model实现数据双向绑定

当在表格中需要渲染input输入框的时候,可以考虑采用给input使用v-model的方式,实现数据的双向绑定,但是 🈶 人可能会说,我不想输入的时候改变,我只想在点击确定的时候改变(在输入框的同级节点中有确定按钮),这种情况下,还是可以使用v-model来实现
通过在表格获取到数据的时候,同时追加多一个input的值,用来代表编辑时的中间状态,然后只有在惦记确定的时候才去执行对应的更新操作即可!!

vue3中的provide与inject管理复杂的form表单

关于provideinject的使用,官方已经讲解得比较透彻了,具体可以上官网浏览,本次这里想要share一个实际的例子:
当我们在使用form来编辑数据的时候,如果表单的元素过多的话,我们很容易考虑到将其拆分,但是像使用一些三方的库比如view-designnaive-ui的时候,我们一般时建议将其当作一个整体form来进行管理,然后最后在提交数据的时候,通过form.validate()方法进行统一的校验,那么我们可以将一个个的form-item拆分为一个个的组件,然后仅将属性传递,并实现将相关的规则统一定义到from下,还是保留对form进行统一管理,但是,如果我们需要每个子组件使用父组件的某些字段,那么这个时候可以考虑provide将父组件的属性以及对应的更新操作,暴露给到后代组件,然后在后代组件中通过inject的方式来获取到对应的属性以及更新方法,然后在子组件中进行调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 以下是父组件的相关代码
<template>
<n-form ref="formRef" :model="formModel" :rules="formRules">
<custom-form-item prop="name"></custom-form-item>
<custom-form-item prop="email"></custom-form-item>
</n-form>
</template>
<script>
import { provide, ref } from 'vue';
import CustomFormItem from './CustomFormItem.vue';
export default {
components: { CustomFormItem },
setup() {
const formRef = ref(null);
const formModel = ref({
name: '',
email: ''
});
const formRules = ref({
name: [
{ required: true, message: 'Name is required', trigger: 'blur' }
],
email: [
{ required: true, message: 'Email is required', trigger: 'blur' },
{ type: 'email', message: 'Invalid email', trigger: 'blur' }
]
});
provide('formRef', formRef);
provide('formRules', formRules);
provide('formModel', formModel);
return {
formRef,
formModel,
formRules
};
}
};
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 以下是子组件的相关代码
<template>
<n-form-item :prop="prop">
<n-input v-model="formModel[prop]" />
</n-form-item>
</template>
<script>
import { inject } from 'vue';
export default {
props: {
prop: {
type: String,
required: true
}
},
setup(props) {
const formRef = inject('formRef');
const formModel = inject('formModel');
const formRules = inject('formRules');
return {
formRef,
formModel,
formRules
};
}
};
</script>

injectKey的独立维护

当我们在使用这个provide以及inject的时候,一般需要定义这个key来作为嵌套组件间的数据通讯,但是这个key的维护的话,需要避免写错,因此我们可以使用这个vue所提供的injectKey这个工具,将需要注入的属性进行统一管理,然后将其存储到单独的文件中来进行统一管理,接着就在provide以及inject中来导入这个key来进行使用,这样子可以确保嵌套层级的组件索使用的key的唯一性,而且也不会编写错误!!!

初始loading状态

一般我们在使用页面加载数据的时候,需要定义一个loading变量,然后将该变量的初始值设置为false,接着在调用接口之前,将这个loading设置为true,然后在接口响应之后,将loading重置为false
这种情况将会导致一种现象: 👉 页面加载完成后,接口还没有发器请求之前,页面处于拥有节点元素但数据为 🈳 的状态,因此可以采用一种将该变量loading定义为true的状态,然后其他的都保持不变,这样子就可以保证在页面加载完成,但还没有发起请求之前,都是处于loading的状态!!

系统主题管理

当我们在系统中需要创建主题变化的时候,可以借鉴于naive-ui来创建自己项目的主题配置,主要有 👇 几个步骤:

  1. 整理并罗列在当前应用中可能会出现的颜色尺寸;
  2. 讲上述的元素整理到对象中的一个个变量中,并提供默认的主题色值方案;
  3. 通过provide以及inject的方式,在根容器组件中将这个主题对象暴露给到其后代组件;
  4. 自定义一useXXX组合式API,调用该方法时,在其中使用到vueinject动作,获取到这个主题对象;
  5. 然后在各个组件中需要使用到变量的地方,去使用对应的变量

关于template下并列多节点的组件

Vue3中,我们可以直接在template标签下并列使用多个不同的节点,这是源于Vue3Fragment的工作机制来实现的,但是 🤔 🈶 那么的 1⃣ 一个场景,如果我们给这个自定义组件传递attrs,然后想要让其中的一个标签节点来接收这个attrs,这个时候vue并不知道应该把这个attrs属性分配给哪一个标签,只能是分配到根节点之下
👉 那么这个时候,我们就需要自行指定需要传递的attrs,可以在template节点中使用$attrs来引用到传递给大的自定义组件的attrs,然后赋值给到对应的某个节点(或者自定义组件),这个时候还需要告知vue,不要使用默认的attrs传递给大组件的机制,通过inheritAttrs: false来告知组件不要默认继承接收到attrs!!

vue-router最佳实践

404错误捕获

🌝vue-router4.x版本中,不再接受使用*通配符的方式来处理项目中的404错误了,而是必须使用自定义的regex参数来定义所有的路由,一般是通过 👇 的方式来配置的

1
2
3
4
5
6
7
8
import { createRouter } from 'vue-router'
const router = createRouter({
routes: [
{ path: '/', component: Home },
// ...这里省略其他路由的定义
{ path: '/:catchAll(.*)', component: NotFound }
]
})

🤔 这里的:catchAll(.*)代表的是什么呢?

👉 首先,这个:catchAll(.*)是一个正则表达式,它是一个路由参数配合正则表达式使用的,主要用来匹配所有无法被之前定义的路由规则捕获的路径,一般用来定义404页面,也就是用户尝试访问不存在的页面时所显示的页面, 关于这个表达式, 👇 将拆分为几个部分来分析一下:

  1. :: 表示接下来的部分是一个动态路由参数;
  2. catchAll: 是参数的名称,可以随意命名,但应该清晰地表明这个参数的用途;
  3. (.*): 是一个正则表达式,()表示一个捕获组,.表示匹配除换行符之外的任意单个字符,*表示匹配前面的那个字符0次或者多次,因此,这个表达式所含义就是匹配任意长度、任意内容的字符串

🥸 的组合起来,整个表达式/:catchAll(.*)就是匹配除了之前定义的所有路由之外的任何路径,因此,在路由配置的最后加上这个规则,可以确保所有未被识别的路由都会导向到我们所指定的component!!

对于动态路由的处理

🧑🎓 在实际的项目中,对于一些基于权限控制的项目,需要根据角色加载对应的路由,因此需要动态的创建路由, 😵💫 但是上面又提及到需要将这个404的路由定义放在所有的路由的最后处,因此, 👉 需要在加载完成所有其他的路由之后,才将这个404路由给push进去!
而且,我们在push最后的这个404路由的时候,可以先检测是否已经存在,那么, 🤔 可以通过什么方式来判断呢?

1
2
3
4
5
6
7
// 这里是动态路由的添加
dynamicRoutes.forEach(route => {
router.addRoute(route)
})
// 然后添加最后的404,在添加之前,可以先移除,然后再添加
router.removeRoute('not-found')
router.addRoute({ path: '/:catchAll(.*)', name: 'not-found', component: NotFound })

离开已编辑数据页面的二次弹窗确认

在项目的coding过程中,难免会需要涉及到表单类的数据编辑,特别是当编辑的很多的信息的时候, 😵 所维护的信息并没有及时地保存下来,如果这个时候,我们一不小心按了其他的按钮进入或者退出当前页面,那么已维护的数据没有被保存下来,这个时候需要重新去编辑才可以,为了解决这种场景,可以利用组件级别的beforeRouteLeave守卫来处理!!
onBeforeRouteLeave是一个导航守卫,不论当前组件何时离开时,都会触发的一个函数,当组件被卸载时,该守卫会被移除!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { ref, computed } from 'vue'
import { onBeforeRouteLeave } from 'vue-router'
const unsavedChange = ref(false)
onBeforeRouteLeave((to, from, next) => {
if(unsavedChange.value){
const answer = window.confirm('您有未保存的更改,确定要离开吗?')
if(answer){
// 继续导航,放弃已编辑的数据
next()
}else{
// 中断当前路由导航
next(false)
}
}else{
// 并没有未保存的更改,正常进行路由导航
next()
}
})

使用push-name来取代push-name

在项目中使用路由进行跳转是一个很常见的事情,一般是通过this.$router.push({path/name})的方式来进行路由挑战的,
但是在实际项目过程中,建议采用push-name的方式来替换push-path,因为我们在定义路由的时候,一般需要同时定义这个路由的名称name

1
2
3
4
5
6
7
export default [
{
path: '/constant',
name: 'constant-view'
// ...
}
]

👆 根据上述的例子,我们可以通过两种方式来跳转到这个路由:

1
2
3
4
// 方式一:通过path的方式拼接参数直接跳转
this.$router.push('/constant')
// 方式二:通过name的方式传递参数跳转
this.$router.push({ name: 'constant-view' })

💯 使用push-name的方式来跳转主要有 👇 几个好处吧:

  1. 无需关心这个path的层级,只要定义的name是唯一的name即可通过name来跳转到目标路由;
  2. 当路由所在的路径path发生改变的时候,也可以直接通过name来跳转到路由,无需更改原先的通过push-path的方式来跳转;
  3. 代码清晰,无需自定拼接参数,只需要在query或者params中传递参数即可!

当需要在h渲染函数中使用到这个RouterLink组件时(vue-router 3.x)

在实际的项目coding过程中,假如我们需要在一h渲染函数中来渲染出一个RouterLink组件的时候,发现这个RouterLink并没有找到,因为这个组件并没有被export出来,它仅仅是在插件中通过Vue.component(RouterLink)的方式来注册的
这个时候原本认为可以直接通过h('router-link')的方式来渲染这个路由组件,发现根本找不到这个组件, 👉 因此这个得出了一个猜想:
vue的上下文与h函数的上下文可能并不是同一个,因此才无法在h()函数中直接使用到这个router-link组件!

🌟 h()函数在vue中用于创建虚拟DOM节点的函数,当在函数式组件或者渲染函数中使用h()函数的时候,并不直接操作组件的实例,在大多数情况下,h()函数是作为一个纯函数存在,不依赖或者操作数组的实例上下文!!!

vuex最佳实践

在不同的模块中应该如何来访问这个state

🤔 有人可能会直接从模块A从导入模块B,然后使用其state
但是, 👉vuex中本来就提供了一种方式让我们很方便地访问到state,如下代码所示:

1
2
3
4
5
6
7
8
// modules/user.js
export default {
actions: {
login({commit, state, rootState}) {
// 这里的rootState也就是整个vuex中的state,按照模块引用,可以直接访问到其他模块中的state
}
}
}

pinia最佳实践

全局项目配置响应式

一般情况下,如果项目需要前端的系统配置的自动化响应式处理操作的话,可考虑将相关的配置与本次存储、以及pinia结合,实现配置的本地化管理操作,并通过自定义的composition API来对外提供API!关于此功能,需要准备以下相关的文件来实现:

三方UI库使用技巧一览

本章节整理了关于在使用一些三方UI的时候的一些小技巧,便于后续工作/学习查询

view-design

关于this.$Modal.config({…})

对于在view-design中使用this.$Modal.config()的方式来展示一个modal对话框的时候,假如需要在对应的点击事件(onOk)方法中,将当前正在显示的modal对话框给隐藏掉的话,可以直接使用this.$Modal.remove()
该方法虽然在官方文档中并没有明确提出来,但可以通过阅读源码而知,将获取当前正在展示的modal,并隐藏掉它!

naive-ui

upload组件的坑

当我们在使用naive-ui的uploa组件来上传资源的时候,如果在默认的upload组件包含其他任何标签的话,都将导致upload组件无法实现拖动文件上传的目的,要确保该元素中不包含任何的其他元素,包括注释都不行!

vant

vue提供的类型辅助工具

vue3中,TypeScript类型辅助工具提供了丰富的类型定义和实用工具来帮助开发者编写类型安全的代码, 👇vue提供的一些常用的TypeScript类型辅助工具

ProType<T>

用于定义组件props类型的工具,一般在选项式写法组件中使用,可以帮助开发者更加精确地指定组件props的类型,从而获得更好的类型检查和代码提示,通过使用PropType<T>,可以利用TypeScript的类型系统来确保传递给组件的props符合预期的类型,一般在defineComponent()或者是export default {}这种选项式的组件中使用, 👉 因为<script setup lang="ts">已经提供了类型检测足够强大的环境支持了!
一般使用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { defineComponent, type PropType } from 'vue'
type User = {
name: string,
age: number
}
export default defineComponent({
props: {
user: Object as PropType<User>,
required: true
}
setup(props) {
console.info(props.user.name) // ts会检查user.name是否为string
}
})

Ref<T>

ComputedRef<T>

ShallowUnwrapRef<T>

InjectionKey<T>

VNode

SetupContext

ComponentPublicInstance