vue3项目开发中常见知识点记录,持续更新中...
动态路由权限
1. 动态添加路由权限,刷新界面后出现404或空白页
出现此场景可能与两个地方有关
- 动态添加路由权限时,匹配404的路由需要添加到末尾(等动态路由添加完成之后再添加),不然可能会出现404
vue
const addRouter = () => {
return new Promise((resolve) => {
setTimeout(() => {
// 此处调用接口动态获取路由菜单权限,然后处理
const constantRoutes = [xxx]
const roleRouter = deepRoutes(constantRoutes, [])
roleRouter.forEach(item => {
// 添加嵌套路由,主路由名称为main
router.addRoute('main', {
...item,
component: () => import(`../views/${item.url}`)
})
})
// 等权限路由添加完成之后,再添加全局匹配的404路由
router.addRoute('main', {
path: '/:catchAll(.*)',
name: 'error',
component: () => import('../views/Error.vue')
})
resolve()
}, 1500)
})
}
- 在beforeEach钩子中addRouter后跳转路由使用next({...to}), 不能直接使用next(),不然会出现空白页
vue
const isAddRouter = false
router.beforeEach(async(to, form, next) => {
const token = getToken()
if (token) {
if (!isAddRouter) {
// 调用上面动态添加路由的方法
await addRouter()
// 此处需要传递参数,重新指向新的路由导航,一般与动态添加路由结合使用
// 此处会导致vue报找不到路由的警告(不影响功能)
next({ ...to})
} else {
next()
}
} else if(to.path !== '/login') {
next('/login)
}
))
路由缓存
1. 路由name属性定义
路由缓存使用keep-alive组件以及其name属性一起实现,定义组件的name有如下几种方式
第一种:默认以组件的文件名作为name属性,HelloCom.vue路由name默认为HelloCom
第二种:在script结合setup语法糖之外,再新增一个script标签
js
<script setup>
// todo
</script>
<script>
export default {
name: "HelloCom"
}
</script>
第三种:使用unplugin-vue-define-options插件实现
在vite.config.js中配置插件, 如果使用vue3.3+的版本,则不需要使用此插件,可以在组件中直接使用defineOptions
js
import DefineOptions from 'unplugin-vue-define-options/vite'
export default defineConfig({
plugins: [
DefineOptions()
]
})
在路由组件中定义name属性
js
<script setup>
defineOptions({
name: "HelloCom"
})
</script>
第四种:使用vite-plugin-vue-setup-extend插件
在vite.config.js中配置插件
js
import vueSetupExtend from 'vite-plugin-vue-setup-extend'
export default defineConfig({
plugins: [
vueSetupExtend()
]
})
在路由组件中直接定义name属性
js
<script setup name="HelloCom">
</script>
2. 路由缓存属性配置
在vue3中路由的缓存是结合keep-alive以及动态组件component实现,在keep-alive中通过inClude配置其需要缓存的组件name集合
vue
<script setup>
import { ref } from 'vue'
// 缓存路由的配置根据项目实际情况而定
const keepAliveList = ref(['Home', 'TestCom', 'UserList'])
</script>
<router-view v-slot="{Component}">
<keep-alive :inClude="keepAliveList">
<component :is="Component">
</keep-alive>
</router-view>
3. 动态缓存路由
通过keepAlive的inClude属性配置可缓存的路由之后,也可以根据项目的实际情况对所缓存的路由配置进行更改,只需要手动设置inClude对应的配置项
vue
<script setup>
import { ref } from 'vue'
// 缓存路由的配置根据项目实际情况而定
const keepAliveList = ref(['Home', 'TestCom', 'UserList'])
const addAlive = () => {
keepAliveList.value.push('myCom')
}
const delAlive = (name) => {
const index = keepAlive.value.findIndex(item => item === name)
if (index > -1) keepAlive.value.splice(index , 1)
}
</script>
<template>
<el-button type="primary" @click="addAlive">新增路由缓存</el-button>
<el-button type="primary" @click="delAlive('Home')">删除首页缓存</el-button>
<router-view v-slot="{Component}">
<keep-alive :inClude="keepAliveList">
<component :is="Component">
</keep-alive>
</router-view>
</template>
全局状态管理
pinia相比vuex更加的轻量,配置项更加的简洁,使用模块化方案实现
安装配置
定义pinia入口文件pinia/index.js
vue
import { createPinia } from 'pinia'
import persistedState from 'pinia-plugin-persistedstate' // 使用数据缓存插件
const pinia = createPinia()
pinia.use(persistedState)
export default pinia
在main.js中使用
js
import { createApp } from 'vue'
import pinia from '@/pinia' // 引入pinia/index.js文件
import App from './App.vue'
const app = createApp(App)
app.use(pinia).mount('#app')
定义模块
在main.js中引入pinia后,可以直接定义单独的数据模块文件, 比如userStore.js
vue
import { defineStore } from 'pinia'
const useUserStore = defineStore('user', {
state:() => {
userName: 'hello world'
},
getters: {},
actions: {
updateState(name) {
this.userName = name
}
},
// 配置缓存项
persist: {
key: 'myUser', // 表示本地存储数据时的属性值,默认为定义的属性名(对应上面的user)
storage: localStorage,
paths: ['userName'], // 定义state中的属性存储配配置,空数组表示都不存储,null或undefined表示都存储,填写值时表示存储对应的值
}
})
export default useUserStore
数据使用
在路由中直接引入数据模块
vue
<script setup>
import { storeToRefs } from 'pinia'
import useUserStore from '@/pinia/userStore'
const { userName } = storeToRefs(useUserStore()) // 使用pinia提供的storeToRefs进行解构(响应式)
const changeName = () => {
userName.value = 'hehe'
}
</script>
<template>
{{ userName }}
<el-button type="primary" @click="changeName">修改名称</el-button>
</template>
修改pinia状态的数据方式
第一种:通过整个store对象直接修改属性和调用方法
vue
<script setup>
import { storeToRefs } from 'pinia'
import useUserStore from '@/pinia/userStore'
const userStore = useUserStore()
const changeName = () => {
userStore.userName = 'hehe' // 修改属性
userStore.updateState() // 调用方法
}
</script>
第二种:使用storeToRefs或toRefs对数据进行解构
使数据变为ref响应式后,直接修改值, 此方法适合用来绑定数据
vue
<script setup>
import { storeToRefs } from 'pinia'
import useUserStore from '@/pinia/userStore'
const { userName } = storeToRefs(useUserStore())
const changeName = () => {
userName.value = 'hehe' // 修改属性
}
</script>
第三种:通过$patch
方法或$state
属性修改
可用来批量修改state的数据
vue
<script setup>
import useUserStore from '@/pinia/userStore'
const userStore = useUserStore()
const changeName = () => {
userStore.$patch({
userName: 'hihi'
})
userStore.$state = {
userName: 'hehe'
}
}
</script>
获取动态组件实例
单个的组件可以通过ref获取组件实例,组件遍历后也可以通过ref获取到每个组件对应的实例,只是使用方法有所区别
单个组件获取实例
js
<script setup>
import { ref } from 'vue'
import TestCom from './TestCom.vue'
const testRef = ref(null)
</script>
<template>
<test-com ref="testRef"></test-com>
</template>
组件遍历获取
vue
<script setup>
import { ref } from 'vue'
import TestCom from './TestCom.vue'
const refList = []
// 使用时通过refList的索引去匹配
const setRef = (el) => {
if(el) refList.push(el)
}
// 比如修改第二个组件的标题
const changeTitle = () => {
refList[1].changeTitle()
}
</script>
<template>
<template v-for="item in 5">
<test-com :ref="setRef"></test-com>
</template>
</template>
新增内置组件
teleport
此组件可以把元素绑定到界面其他任意的地方(传送门), 使用时需要注意的地方
第一点:确保目标元素在使用teleport的时候已经存在,否则会报找不到元素
第二点:目标元素不能是teleport标签元素所在组件的父组件或子组件
vue
<template>
<teleport to="body">
<div class="absolute z-20 top-0 left-0 w-[100vw] h-[100vh] bg-[red]">我是弹窗</div>
</teleport>
</template>
Suspense
主要用来处理异步组件的加载状态,在等待异步组件加载时提供一个等待加载的效果,提高用户体验
Suspence包含两个插槽:
- #default 默认插槽用来显示异步组件
- #fallback 用来显示等待时的提示...
异步组件AsyncCom.vue
vue
<script setup>
// 模拟异步请求,等待2秒钟...
const getData = async () => {
await new Promise((resolve) => setTimeout(resolve, 2000))
return '我是异步组件'
}
const title = await getData()
</script>
<template>
<div>
{{ title }}
</div>
</template>
<style scoped lang="scss"></style>
父组件
xml
<script setup>
import { defineAsyncComponent } from 'vue'
const AsyncCom = defineAsyncComponent(() => import('./AsyncCom.vue'))
</script>
<template>
<Suspense>
<template #default>
<async-com><async-com>
</template>
<template #fallback>
loading...
</template>
</Suspense>
</template>
<style scoped lang="scss"></style>
fragment
vue3中template模板可以绑定多个根元素,而vue2中只能绑定一个
vue2中为什么只能绑定一个根元素?
主要是由于vue2中编译器和虚拟dom的设计决策决定,这样简化了模板的处理和渲染逻辑,使得vue在编译和运行时更加高效。
vue中使用虚拟dom来提高性能,而虚拟dom需要一种一对一的关系,即一个组件对一个虚拟dom树节点,如果存在多个节点,这里面就会涉及到多个节点的合并和对并,反而会增加性能开销。
vue中编译器主要负责把模块转化为渲染函数,如果存在多个根元素,编译器可能要处理更复杂的逻辑,例如如何去合并多个根元素的渲染函数,以及v-for指令等等。
存在的缺点是可能会造成大量没用的标签元素存在。
vue3中为什么可以有多个根节点?
首先vue3中对底层的编译器和虚拟dom进行了重写(优化升级),其次引入了片段(fragment)的概念, 它容许你在模板中定义多个相邻的元素而无需用容器元素包裹, 这样提升了开发的灵活性和体验。
异步加载组件
与路由懒加载类似,异步加载组件会分包,同步加载组件不会,根据实际场景使用能提高性能以及用户体验。
异步组件一般结合内置组件Suspense一起使用
vue
<script>
import { defineAsyncComponent } from 'vue'
const AsyncCom = defineAsyncComponent(() => import('@/components/AsyncCom.vue'))
</script>
<template>
我是父组件...
<async-com></async-com>
</template>
样式属性scoped
样式隔离
在组件样式style标签中添加scoped后,它会根据当前组件生成一个唯一的data-v-hash值,在编译渲染模板时会把hash值添加到组件内每个标签元素上,生成样式时通过样式名以及属性选择器(hash)去匹配查找,这样每个组件的hash不同,就算样式名相同也不同冲突。
样式属性绑定
父子组件绑定时,子组件的根节点数会影响到父组件中属性和样式的传递
场景一:当子组件只有一个根节点时
Child.vue
<template>
<div class="my-text">我是子组件</div>
</template>
<style scoped lang="scss">
.my-text {
color: red
}
</style>
Parent.vue
<template>
<child class="text-color"></child>
</template>
<style scoped lang="scss">
.font-22px {
font-size: 22px
}
</style>
子组件生成的dom根节点会包含父组件的hash值以及class类名
生成的dom节点
生成的样式
场景二:当子组件有多个根节点时
此时父组件传递的样式以及hash属性不会对子组件造成任何影响(不会绑定到子组件的任何节点上)
样式穿透
vue3中使用:deep()来实现样式穿透,从而修改子组件的样式,原理是在父组件中生成一个以父组件hash + 子组件中类名的样式规则,这样子组件就能访问到样式
Parent.vue
<style lang="scss" scoped>
:deep(.my-text) {
font-size: 22px
}
</style>
指令
vue中提供了大量的内置指令来满足业务场景的需要,但是某些特殊情况下还是需要自定义指令来满足场景需求,自定义指令一共提供了7个钩子函数, 可以在不同阶段通过el操作dom元素。
js
const config = {
created(el, binding) {},
beforeMount(el, binding){},
mounted(el, binding){},
beforeUpdate(el, binding){},
updated(el, binding){},
beforeUnmount(el, binding){},
unmounted(){el, binding}{}
}
全局子定义指令
index.js
const globalDirectives = {
install(app) {
app.directive('foucs', config)
}
}
在入口文件中注入
main.js
import globalDirectives from './directive/index.js
app.use(globalDirectives)
局部自定义指令
index.vue
<script setup>
import { ref } from 'vue'
const vFoucs = config
const userName = ref('')
</script>
<template>
<el-input v-model="userName" v-focus />
</template>
指令使用场景
场景一:按钮权限
传入按钮的唯一标识,跟接口返回的按钮权限列表做对比,如果存在则显示,不存在则隐藏
场景二:图片懒加载
显示图片列表时,默认给一个本地的图片,当滚动页面图片元素在可视区域时在把远程图片地址设置到图片的src属性
场景三:输入框限制
当文本输入不同数字时,文本框的背景颜色发生变化
插槽
插槽在组件封装中基本上是必用的一个功能,它在组件中提供一个或多个区域,在使用组件时可以往这些区域中插入自定义内容
默认插槽
Parent.vue
<template>
<Child>
<template>
我是默认插槽
</template>
</Child>
</template>
Child.vue
<template>
<div>
我是子组件
<slot></slot>
</div>
</template>
具名插槽
Parent.vue
<template>
<Child>
<template #content>
我是默认插槽
</template>
</Child>
</template>
Child.vue
<template>
<div>
我是子组件
<slot name="content"></slot>
</div>
</template>
作用域插槽
作用域插槽可以把子组件中的数据通过属性传递到父组件
Parent.vue
<template>
<Child>
<template #content={userName}>
我是作用域插槽{{ userName }}
</template>
</Child>
</template>
Child.vue
<template>
<div>
我是子组件
<slot name="content" user-name="hello"></slot>
</div>
</template>
组件交互
props和emits
props和emits是最常见的父子组件交互的方式, 子组件通过props接受父组件传递的参数,通过emits触发监听事件
Parent.vue
<script setup>
const setName = () => {
console.log('setName')
}
</script>
<template>
<Child v-bind="$attrs" userName="hello" userAge="30" @setName="setName"></Child>
</template>
Child.vue
<script setup>
const emits = defineEmits(['setName'])
const props = defineProps({
userName: {
type: String,
default: ''
}
})
const setUserName = () => {
emits('setName')
}
</script>
<template>
<div>
我是子组件{{ $attrs.userName }}
<el-button type="primary" @click="setUserName"></el-button>
</div>
</template>
ref属性
ref属性主要用作父组件对子组件的操作以及获取元素的DOM节点, 获取子组件的数据时,需要在子组件中提前通过defineExpose导出
Parent.vue
<script setup>
import { ref } from 'vue'
const childRef = ref(null)
const change = () => {
console.log(childRef.value.title)
childRef.value.setTitle()
}
</script>
<template>
<Child ref="childRef"></Child>
<el-button type="primary" @click="change"></el-button>
</template>
Child.vue
<script setup>
import { ref } from 'vue'
const title = ref('我是子元素')
const setTitle = () => {
title.value = '新的子元素'
}
// 供父组件使用的属性和方法,需要导出
defineExpose({
title,
setTitle
})
</script>
<template>
<div>
{{ title }}
</div>
</template>
v-bind
使用子组件时绑定v-bind属性,在子组件内部可以获取到绑定在子组件中的所有属性和方法(除了在子组件defineProps中自定义的属性和在defineEmits中自定义的事件), 此属性在二次封装组件时非常好用。
Parent.vue
<script setup>
const setName = () => {
console.log('setName')
}
const setAge = () => {
console.log('setAge')
}
</script>
<template>
<Child v-bind="$attrs" userName="hello" userAge="30" @setName="setName" @setAge="setAge"></Child>
</template>
Child.vue
<script setup>
import { useAttrs } from 'vue'
// 在setup中使用需要注入,在template中可直接使用$attrs
const $attrs = useAttrs()
// 定义setAge方法后,通过$attrs对象就访问不到了,访问其他方法时都默认加了on前缀,并且是小驼峰式命名
const emits = defineEmits(['setAge])
const props = defineProps({
userName: {
type: String,
default: ''
}
})
</script>
<template>
<div>
我是子组件{{ $attrs.userName }}
<el-button type="primary" @click="$attrs.onSetName()"></el-button>
</div>
</template>
provide和inject
provide和inject也称为依赖注入,在路由界面中父子组件嵌套过深,并且需要一层一层传递数据的时候,用此方法就非常方便
mitt
类似于vue2中的eventBus, 可以在任意组件中通过事件进行交互, 需要安装模块 cnpm i mitt -S
mitt.js
import mitt from 'mitt'
export default mitt()
组件A.vue
<script setup>
import { onMounted } from 'vue'
import mitt from '@/mitt'
onMounted(() => {
// 监听
mitt.on('change', () => {
console.lgo('change')
})
})
</script>
<template>
</template>
组件B.vue
<script setup>
import mitt from '@/mitt'
const change = () => {
// 触发事件
mitt.emit('change')
}
</script>
<template>
<el-button type="primary" @click="change">调用mitt事件</el-button>
</template>
配置tailwindcss
安装模块
js
npm install -D tailwindcss postcss autoprefixer
初始化配置
执行命令在项目的根目录创建postcss.config.js
和tailwind.config.js
两个配置文件
js
npx tailwindcss init -p
配置postcss.config.js
js
export default {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
}
配置tailwind.config.js
js
export default {
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
theme: {
extend: {},
},
plugins: [],
}
引入模块样式
第一步:创建一个tailwind.css样式文件, 导入tailwind模块
css
@tailwind base;
@tailwind components;
@tailwind utilities;
第二步:在main.js中引入tailwind.css,注意需要在顶部引入(不然可能会对其他样式造成影响)
js
import './assets/tailwind.css'
...