vue3项目开发中常见知识点记录

vue3项目开发中常见知识点记录,持续更新中...

动态路由权限

1. 动态添加路由权限,刷新界面后出现404或空白页

出现此场景可能与两个地方有关

  1. 动态添加路由权限时,匹配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)
  })
}
  1. 在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.jstailwind.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'
...
相关推荐
Jiaberrr5 小时前
前端实战:使用JS和Canvas实现运算图形验证码(uniapp、微信小程序同样可用)
前端·javascript·vue.js·微信小程序·uni-app
LvManBa6 小时前
Vue学习记录之六(组件实战及BEM框架了解)
vue.js·学习·rust
200不是二百6 小时前
Vuex详解
前端·javascript·vue.js
LvManBa6 小时前
Vue学习记录之三(ref全家桶)
javascript·vue.js·学习
深情废杨杨6 小时前
前端vue-父传子
前端·javascript·vue.js
工业互联网专业7 小时前
毕业设计选题:基于springboot+vue+uniapp的驾校报名小程序
vue.js·spring boot·小程序·uni-app·毕业设计·源码·课程设计
J不A秃V头A7 小时前
Vue3:编写一个插件(进阶)
前端·vue.js
司篂篂8 小时前
axios二次封装
前端·javascript·vue.js
姚*鸿的博客8 小时前
pinia在vue3中的使用
前端·javascript·vue.js
天下无贼!10 小时前
2024年最新版Vue3学习笔记
前端·vue.js·笔记·学习·vue