likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 33

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

动态路由权限

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

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

  1. 动态添加路由权限时,匹配404的路由需要添加到末尾(等动态路由添加完成之后再添加),不然可能会出现404

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(),不然会出现空白页
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)
    }
))

路由配置管理

路由类型

vue3中路由一共有三种模式:hash,history,abstract

第一种:hash模式

hash是vue-router默认的模式,通过浏览器的hashchange事件监听来实现路由的切换,明显的标志是在url中带#,兼容性好,只是url看着不太美观。

第二种:history模式

history主要是使用浏览器的history api来实现,监听pushState以及replaceState事件来管理路由,与hash相比url中没有#,看着更舒服一点,两者的相同点是改变url地址网页并不会重新刷新。

需要注意的是如果使用history模式,打包部署到服务器后,需要在nginx中配置,不然刷新页面会404。

第三种:abstract模式

abstract主要用在非浏览器环境,例如在服务器端(SSR)渲染中使用,在这种模式下不使用真正的api,而是通过一个虚拟的location对象来处理路由的切换。

路由钩子

路由钩子一共分为三类:全局守卫,路由组件内守卫,路由独享守卫

1. 全局守卫

全局路由守卫钩子时最常用的,主要用来做认证以及路由权限处理。

router.beforeEach((to, from, next) => {
    next()
})
router.afterEach((to, from) => {
})
router.beforeResolve((to, from, next) => {
    next()
})

2. 组件内守卫

vue-router提供了onBeforeRouteEnter, onBeforeRouteLeave, onBeforeRouteUpdate三个钩子函数,但是在setup语法糖中目前无法使用,可以新增一个script标签实现

<script>
    import { defineComponent } from 'vue'
    export default defineComponent({
      beforeRouteEnter(to, from, next) {
        console.log('进入路由')
        next()
      },
      beforeRouteLeave(to, from) {
        console.log('离开路由')
      },
      beforeRouteUpdate(to, from,) {
        console.log('路由update')
      }
    })
</script>
<script setup>
    ...todo
</script>

3. 路由独享守卫

{
    id: '1',
    path: '/detail'
    name: 'Detail',
    component: () => import('xx.vue'),
    beforeEnter : (to, from, next) => {
        next()
    },
    meta: {
        title: '详情',
        keepAlive: true,
    },
}

路由传参

路由传递参数常用的有两类:通过url后面加上?拼接通过动态路由匹配参数拼接

url加上?传参

传递参数与get请求传递参数一样,在url后面拼接参数跳转, 通过定义的path或name属性进行跳转,然后再通过query属性获取,这种方式可以传递任意参数

<script setup>
    import { useRouter } from 'vue-router'
    const router = useRouter()
    const goDetail = () => {
      // 方法一: 直接通过path路径拼接
      router.push('/detail?id=10&content=hello')
      // 方法二: 通过path和query对象形式传递
      router.push({
        path: '/detail',
        query: {
          id: 10,
          content: 'hello'
        }
      })
      // 方法三: 通过name和query对象传递
      router.push({
        name: 'Detail',
        query: {
          id: 10,
          content: 'hello'
        }
      })
    }
</script>

<template>
  <el-button @click="goDetail">详情</el-button>
</template>
<script setup>
    import { useRoute } from 'vue-router'
    const route = useRoute()
    console.log(route.query) // 获取到url参数
</script>

<template>
    我是详情页
</template>

动态路由参数拼接

此方式会在路径中拼接动态参数进行传递,拼接的参数个数需要与路由配置中定义的一致,不然会报错或者获取不到(传递少报错,传递多则获取不到), 调用的方式有两种

{
    id: '2010',
    path: '/detail/:id/:content', // 此处需要配置好参数
    name: 'Detail',
    url: 'Detail',
    meta: {
        title: '详情',
        keepAlive: true,
    },
},
<script setup>
    import { useRouter } from 'vue-router'
    const router = useRouter()
    const goDetail = () => {
      // 方法一:通过path属性直接动态拼接,不再需要?连接
      router.push('/detail/10/hello')
      // 方法二:通过name属性进入跳转,params对象传递参数
      router.push({
        name: 'Detail',
        params: {
          id: 10,
          content: 'hello',
          title: 'hi', // 多传递的参数通过params获取不到
        }
      })
    }
</script>

<template>
  <el-button @click="goDetail">详情</el-button>
</template>
<script setup>
    import { useRoute } from 'vue-router'
    const route = useRoute()
    console.log(route.params) // 获取参数
</script>

<template>
    我是详情页
</template>

路由缓存

路由名称

路由缓存使用keep-alive组件以及其name属性一起实现,定义路由的name名称有如下几种方式:

第一种:默认以组件的文件名作为name属性,HelloCom.vue路由name默认为HelloCom

第二种:在script结合setup语法糖之外,再新增一个script标签,类似vue2的写法

<script setup>
 // todo
</script>
<script>
  export default {
      name: "HelloCom"
  }
</script>

第三种:使用unplugin-vue-define-options插件实现

在vite.config.js中配置插件, 如果使用vue3.3+的版本,则不需要使用此插件,可以在组件中直接使用defineOptions

import DefineOptions from 'unplugin-vue-define-options/vite'
export default defineConfig({
    plugins: [
        DefineOptions()
    ]
})

在路由组件中定义name属性

<script setup>
    defineOptions({
        name: "HelloCom"
    })
</script>

第四种:使用vite-plugin-vue-setup-extend插件

在vite.config.js中配置插件

import vueSetupExtend from 'vite-plugin-vue-setup-extend'
export default defineConfig({
    plugins: [
        vueSetupExtend()
    ]
})

在路由组件中直接定义name属性

<script setup name="HelloCom">
</script>

路由缓存配置

在vue3中路由的缓存是结合keep-alive以及动态组件component实现,在keep-alive中通过inClude配置其需要缓存的路由name集合

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

动态缓存路由

通过keepAlive的inClude属性配置可缓存的路由之后,也可以根据项目的实际情况对所缓存的路由配置进行更改,只需要手动设置inClude对应的配置项,一般情况可以把配置项存储在全局状态管理中。

<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

import { createPinia } from 'pinia'
import persistedState from 'pinia-plugin-persistedstate' // 使用数据缓存插件

const pinia = createPinia()
pinia.use(persistedState)
export default pinia

在main.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

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

某些场景如果需要在pinia模块中进行路由跳转的,不能使用useRouter()方法,此时会返回undefined,useRouter只能在setup语法糖并且在顶层进行声明, 可以直接导入定义的router模块进行路由跳转

import router from '@/router'
router.push('/detail')

使用模块

在路由中直接引入数据模块

<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中state的数据

第一种:通过整个store对象直接修改属性和调用方法

<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响应式后,直接修改值, 此方法适合用来绑定数据

<script setup>
    import { storeToRefs } from 'pinia'
    import useUserStore from '@/pinia/userStore'
    const { userName } = storeToRefs(useUserStore())
    const changeName = () => {
        userName.value = 'hehe' // 修改属性
    }
</script>

第三种:通过$patch方法或$state属性修改

可用来批量修改state的数据

<script setup>
    import useUserStore from '@/pinia/userStore'
    const userStore = useUserStore()
    const changeName = () => {
        userStore.$patch({
            userName: 'hihi'
        })
        userStore.$state = {
         userName: 'hehe'
        }
    }
</script>

调用pinia中actions方法

通过模块初始化后的对象直接调用

<script setup>
    import useUserStore from '@/pinia/userStore'
    const userStore = useUserStore()
    const changeName = () => {
        userStore.updateState() // 调用actions里面的方法
    }
</script>

父子组件生命周期执行顺序?

vue3中组件常用的生命周期有如下几种,移除了beforeCreate和created,使用setup语法糖替代

  1. onBeforeMounte
  2. onMounted
  3. onBeforeUpdate
  4. onUpdated
  5. onBeforeUnmounte
  6. onUnmounted
  7. onActivated
  8. onDeactived

父子组件加载初始化生命周期执行的顺序为:

  1. 父组件 - onBeforeMounte
  2. 子组件 - onBeforeMounte
  3. 子组件 - onMounted
  4. 父组件 - onMounted

获取动态组件实例

单个的组件可以通过ref获取组件实例,组件遍历后也可以通过ref获取到每个组件对应的实例,只是使用方法有所区别

单个组件获取实例

<script setup>
    import { ref } from 'vue'
    import TestCom from './TestCom.vue'
    const testRef = ref(null)
</script>
<template>
  <test-com ref="testRef"></test-com>
</template>

组件遍历获取

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

数据双向绑定

update:modelValue

vue2数据双向绑定时,使用的是v-model,但每次只能绑定一个v-model,如果需要绑定多个属性就会用到.sync修饰符。

vue3与vue2类似,还是使用v-model指令来实现数据双向绑定,但是针对组件的属性绑定,可以传递多个v-model,这样更加灵活方便,为了统一同时也移除了.sync。

<script setup>
    import { ref } from 'vue'
    const userName = ref('tony')
    const sex = ref('男')
</script>
<template>
    <child v-model="userName" v-model:sex="sex"></child>
</tempalte>
<script setup>
    import { ref } from 'vue'
    const emits = defineEmits(['update:modelValue', 'update:sex'])
    const porps = defineProps({
        modelValue: {
            type: String,
            default: ''
        },
        sex: {
            type: String,
            default: ''
        }
    })
    const changeUser = () => {
        emits('update:modelValue', 'sini')
        emits('update:sex', '女')
    }
</script>
<template>
    <div>姓名:{{ modelValue }}</div>
    <div>年龄:{{ sex }}</div>
    <el-button @click="changeUser">修改姓名年龄</el-button>
</tempalte>

defineModel

在vue3.3+版本中,新增了deifineModel宏,可以在代码中直接接受传递的v-model值, 不用在defineProps中定义,defineModel定义的数据返回一个ref响应式数据。

传统的v-model传递方式修改时必须通过调用事件处理, defineModel的响应式数据可以直接修改,方便很多.

<script>
    import { ref } from 'vue'
    const title =ref('myTitle')
    const content = ref('hello')
    const change = () => {
        title.value = 'myTitle'
        content.value = 'hello'
    }
</script>
<template>
   <Child v-model="title" v-model:content="content"></Child>
   <div @click="change">修改</div>
</template>
<script>
    const title = defineModel()
    const content = ref('content')
    const change = () => {
        title.value = 'new title'
        content.value = 'hi'
    }
</script>
<template>
    {{ title }} {{ content }}
    <div @click="change">修改</div>
</template>

数据监听

watch

用来监听响应式数据的变化,可以细颗粒度的对指定的数据进行监听, 同时也可以监听多个数据,使用数组包裹。

当监听整个reactive对象时,默认是深度监听,如果想单独监听对象的某个属性,则需要使用函数的方式。

<script setup>
    import { ref, reactive, watch } from 'vue'
    const userName = ref('tony')
    const sex = ref('男')
    const state = reactive({
        title: 'hello',
        content: 'this is a content'
    })
    // 监听userName
    const stop = watch([userName, sex], ([a,b],[c,d]) => {})
    // 停止监听
    stop()
    // 监听state整个对象
    watch(state, () => {}) // 默认深度监听
    // 监听userName的title属性
    watch(userName.title, () => {}) // 此方法无效
    watch(() => userName.title, () => {})
</script>

watchEffect

与watch不同的是,它不会指定监听某个响应式数据,而是直接传递一个回调函数,只要回调函数里面用到的响应式数据发生变化,就会触发回调函数的调用,它是一个副作用的监听,不能细颗粒的处理监听数据,在使用的时候需要注意场景。

watchEffect的回调函数会立即执行,watch的回调函数只有数据发生变化才会执行。

<script setup>
    import { ref, reactive, watch } from 'vue'
    const userName = ref('tony')
    watchEffect(() => {
        console.log(userName.value)
    })
</script>

watchPostEffect

相当于watch监听方法设置第二个参数{flush: 'post'}, 等组件渲染更新完毕之后在执行回调函数,否则在回调里面获取不到最新的dom元素。

计算属性

computed函数可以返回一个经过处理的ref响应式数据,默认情况下是只读的

import { ref, computed } from 'vue'
const age = ref(23)
const newAge = computed(() => {
    return age * 1.5
})
import { ref, computed } from 'vue'
const age = ref(23)
const newAge = computed({
    get() {
        return age.value
    },
    set(val) {
        age.value = val
    }
})

useSlots和useAttrs

useSlots和useAttrs函数对应template模板里面的$slots$attrs, 实际场景在template中使用的可能会多一点,一些特殊的逻辑判断场景可能需要在setup中使用。

<script setup>
</script>
<template>
    <child title="标题" content="内容">
        <template>
            默认插槽
        </template>
        <template #des>
            我是描述插槽
        </template>
    </child>
</template>
<script setup>
    import { useSlots, useAttrs } from 'vue'
    const $attrs = useAttrs() // 返回子组件绑定的属性对象
    const $slots = useSlots() // 返回在父组件中使用过的插槽对象,没有用到的插槽不返回
</script>
<template>
    <div>{{ $attrs.title }}</div>
    <div>{{ $attrs.content }}</div>
    <slot></slot>
    <slot name="des"></slot>
    <div v-if="$slots.des">如果父组件中使用了描述插槽则显示整个内容</div>
</template>

新增内置组件

teleport

此组件可以把元素绑定到界面其他任意的地方(传送门), 使用时需要注意的地方

第一点:确保目标元素在使用teleport的时候已经存在,否则会报找不到元素

第二点:目标元素不能是teleport标签元素所在组件的父组件或子组件

<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

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

父组件

<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

fragment不算是内置组件,是组件内部的一种实现方式

vue3中template模板可以绑定多个根元素,而vue2中只能绑定一个

vue2中为什么只能绑定一个根元素?

主要是由于vue2中编译器和虚拟dom的设计决策决定,这样简化了模板的处理和渲染逻辑,使得vue在编译和运行时更加高效。

vue中使用虚拟dom来提高性能,而虚拟dom需要一种一对一的关系,即一个组件对一个虚拟dom树节点,如果存在多个节点,这里面就会涉及到多个节点的合并和对并,反而会增加性能开销。

vue中编译器主要负责把模块转化为渲染函数,如果存在多个根元素,编译器可能要处理更复杂的逻辑,例如如何去合并多个根元素的渲染函数,以及v-for指令等等。

存在的缺点是可能会造成大量没用的标签元素存在。

vue3中为什么可以有多个根节点?

首先vue3中对底层的编译器和虚拟dom进行了重写(优化升级),其次引入了片段(fragment)的概念, 它容许你在模板中定义多个相邻的元素而无需用容器元素包裹, 这样提升了开发的灵活性和体验。

组合式函数-Hooks

组合式函数就是一个js模块函数,在模块内部可以使用vue的响应式把数据和逻辑进行封装返回,供其他组件模块使用,类似于vue2里面的混合(mixins)

定义方法的规范默认都是以use开头

经典的鼠标移动坐标为例

import { ref, onMounted, onUnmounted } from 'vue'
export const useMouse = () => {
    const x = ref(0)
    const y = ref(0)

    const update = (event) => {
        x.value = event.pageX
        y.value = event.pageY
    }

    onMounted(() => window.addEventListener('mousemove', update))
    onUnmounted(() => window.removeEventListener('mousemove', update))

    return { x, y }
}

在组件中直接使用

<script setup>
    import { useMouse } from '@/hooks/mouse.js'
    const { x, y } = useMouse()
</script>

<template>
    <div>{{ x }} {{ y }}</div>
</template>

插件-Plugins

vue中很多的模块都是以插件的形式提供,比如vue-routerpinia, 它主要的场景是帮助我们在全局注册或者定义一些属性,供所有组件路由使用

插件开发

插件本质上就是一个JS模块(或者可以理解为一个函数), 定义时可以返回一个对象或者一个函数

const myPlugin= {
    // options为接受的参数
    install(app, options) {
        ...todo
    }
}
export default myPlugin

插件使用

主要是在入口文件main.js中调用, 当use插件时,如果返回是对象,则调用对象的install方法,如果是函数直接调用。

import { createApp } from 'vue'
import App from './App.vue'
import myPlugin from '@/plugin/myPlugin'
const app = createApp(App)

app.use(myPlugin, {xx:xx}).mount('#app')

常用场景

1. 全局的注册组件或自定义指令

2. 全局注入provide

3. 添加全局的属性

异步加载组件

与路由懒加载类似,异步加载组件会分包,同步加载组件不会,根据实际场景使用能提高性能以及用户体验。

异步组件一般结合内置组件Suspense一起使用

<script>
    import { defineAsyncComponent } from 'vue'
    const AsyncCom = defineAsyncComponent(() => import('@/components/AsyncCom.vue'))
</script>
<template>
    我是父组件...
    <async-com></async-com>
</template>

属性透传

父组件中使用子组件定义一些属性或方法时,默认情况下会传递给子组件,可以在子组件中获取,实际开发中根据不同的情况会有所不同。

1. 子组件只有一个根节点

当子组件只有一个根节点时,给子组件传递的属性都会绑定在根节点上,例如样式属性class会累加在根节点的class后面,style属性会和根节点的style合并

<template>
    <div>
        我是父组件
        <child class="text-red" style="font-size: 20px" content="我是内容" des="我是描述说明"></child>
    </div>
</template>
<style scoped lang="scss">
    .text-red {
        color: red
    }
</style>
<template>
    <div>
        我是子组件
        <div>{{ $attrs.content }}</div>
        <div>{{ $attrs.des }}</div>
    </div>
</template>
<style scoped lang="scss">
    .text-color {
        color: green
    }
</style>

最终界面渲染的结果

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

2. 子组件有多个根节点

当子组件存在多个根节点时,由于不知道要绑定在那个节点,所以传递的属性不会绑定到子组件的任何元素上

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

手动绑定属性

如果需要绑定到某个节点上,可以手动的绑定v-bind="$attrs"

<template>
    <div v-bind="$attrs">我是子组件</div>
    <div>我是内容</div>
</template>

3. 子组件定义props和emits

透传的属性可以默认绑定在子组件的根节点,此外还可以通过$attrs获取到整个绑定的属性对象,在setup与tempalte中使用方式有所不同

注意点1:如果子组件是单个根节点, defineProps中定义了传递的属性,那么这些属性不会被绑定在根节点上

注意点2: 如果子组件defineProps中定义了传递的属性值以及defineEmits中定义的事件名,则$attrs对象中就不会包括这些属性

注意点3:如果是事件属性,在使用$attrs对象调用时,需要加上on前缀,这是vue内部默认添加的,使用小驼峰命令

<script setup>
    import { useAttrs } from 'vue'
    const $attrs = useAttrs()
    console.lgo($attrs) // 输出整个属性对象
</script
<template>
  <div>
      {{ $attrs.content }}
  </div>
</template>

$attrs对象

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

4. 禁用子组件根节点属性透传

如果不希望接受默认的属性透传,可以在子组件中进行配置, 禁用后不会再绑定到根节点,对$attrs的获取没有影响。

<script setup>
    defineOptions({
        inheritAttrs: false
    })
</script

样式属性scoped

样式隔离

在组件样式style标签中添加scoped后,它会根据当前组件生成一个唯一的data-v-hash值,在编译渲染模板时会把hash值添加到组件内每个标签元素上,生成样式时通过样式名以及属性选择器(hash)去匹配查找,这样每个组件的hash不同,就算样式名相同也不同冲突。

样式属性绑定

父子组件绑定时,子组件的根节点数会影响到父组件中属性和样式的传递

场景一:当子组件只有一个根节点时

<template>
    <div class="my-text">我是子组件</div>
</template>
<style scoped lang="scss">
    .my-text {
        color: red
    }
</style>
<template>
    <child class="text-color"></child>
</template>
<style scoped lang="scss">
    .font-22px {
        font-size: 22px
    }
</style>

子组件生成的dom根节点会包含父组件的hash值以及class类名

生成的dom节点

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

生成的样式

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

场景二:当子组件有多个根节点时

此时父组件传递的样式以及hash属性不会对子组件造成任何影响(不会绑定到子组件的任何节点上)

样式穿透

vue3中使用:deep()来实现样式穿透,从而修改子组件的样式,原理是在父组件中生成一个以父组件hash + 子组件中类名的样式规则,这样子组件就能访问到样式

<style lang="scss" scoped>
    :deep(.my-text) {
        font-size: 22px
    }
</style>

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

获取插槽

指令

vue中提供了大量的内置指令来满足业务场景的需要,但是某些特殊情况下还是需要自定义指令来满足场景需求,自定义指令一共提供了7个钩子函数, 可以在不同阶段通过el操作dom元素。

const config = {
    created(el, binding) {},
    beforeMount(el, binding){},
    mounted(el, binding){},
    beforeUpdate(el, binding){},
    updated(el, binding){},
    beforeUnmount(el, binding){},
    unmounted(){el, binding}{}
}

全局子定义指令

const globalDirectives = {
    install(app) {
        app.directive('foucs', config)
    }
}

在入口文件中注入

import globalDirectives from './directive/index.js
app.use(globalDirectives)

局部自定义指令

<script setup>
    import { ref } from 'vue'
    const vFoucs = config
    const userName = ref('')
</script>
<template>
    <el-input v-model="userName" v-focus />
</template>

指令使用场景

场景一:按钮权限

传入按钮的唯一标识,跟接口返回的按钮权限列表做对比,如果存在则显示,不存在则隐藏

场景二:图片懒加载

显示图片列表时,默认给一个本地的图片,当滚动页面图片元素在可视区域时在把远程图片地址设置到图片的src属性

场景三:输入框限制

当文本输入不同数字时,文本框的背景颜色发生变化

插槽

插槽在组件封装中基本上是必用的一个功能,它在组件中提供一个或多个区域,在使用组件时可以往这些区域中插入自定义内容

默认插槽

<template>
    <Child>
        <template>
           我是默认插槽
        </template>
    </Child>
</template>
<template>
  <div>
      我是子组件
      <slot></slot>
  </div>
</template>

具名插槽

<template>
    <Child>
        <template #content>
           我是默认插槽
        </template>
    </Child>
</template>
<template>
  <div>
      我是子组件
      <slot name="content"></slot>
  </div>
</template>

作用域插槽

作用域插槽可以把子组件中的数据通过属性传递到父组件

<template>
    <Child>
        <template #content={userName}>
           我是作用域插槽{{ userName }}
        </template>
    </Child>
</template>
<template>
  <div>
      我是子组件
      <slot name="content" user-name="hello"></slot>
  </div>
</template>

组件交互

props和emits

props和emits是最常见的父子组件交互的方式, 子组件通过props接受父组件传递的参数,通过emits触发监听事件

<script setup>
    const setName = () => {
        console.log('setName')
    }
</script>
<template>
    <Child userName="hello" userAge="30" @setName="setName"></Child>
</template>
<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导出

<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>
<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中自定义的事件), 此属性在二次封装组件时非常好用。

<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>
<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也称为依赖注入,在路由界面中父子组件嵌套过深,并且需要一层一层传递数据的时候,用此方法就非常方便

<script setup>
    import { provide, reactive } from 'vue'
    // 使数据变为响应式,如果不设为响应式,修改对象属性后,子级的数据不会同步修改
    const myInfo = reactive({
        userName: 'hello',
        sex: '男'
    })
    provide('myInfo', myInfo)
</script>
<script setup>
    import { inject } from 'vue'
    const myInfo = inject('myInfo')
</script>
<template>
    {{ myInfo.userName }]
</tempalte>

mitt

类似于vue2中的eventBus, 可以在任意组件中通过事件进行交互, 需要安装模块

cnpm i mitt -S
import mitt from 'mitt'
export default mitt()
<script setup>
    import { onMounted } from 'vue'
    import mitt from '@/mitt'
    onMounted(() => {
        // 监听
        mitt.on('change', () => {
            console.lgo('change')
        })
    })
</script>
<script setup>
    import mitt from '@/mitt'
    const change = () => {
        // 触发事件
        mitt.emit('change')
    }
</script>
<template>
    <el-button type="primary" @click="change">调用mitt事件</el-button>
</template>

配置tailwindcss

安装模块

npm install -D tailwindcss postcss autoprefixer

初始化配置

执行命令在项目的根目录创建postcss.config.jstailwind.config.js两个配置文件

npx tailwindcss init -p

配置postcss.config.js

export default {
  plugins: {
    tailwindcss: {},
    autoprefixer: {}
  }
}

配置tailwind.config.js

export default {
  content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
  theme: {
    extend: {},
  },
  plugins: [],
}

引入模块样式

第一步:创建一个tailwind.css样式文件, 导入tailwind模块

@tailwind base;
@tailwind components;
@tailwind utilities;

第二步:在main.js中引入tailwind.css,注意需要在顶部引入(不然可能会对其他样式造成影响)

import './assets/tailwind.css'
...to do
转载自:https://juejin.cn/post/7324384460136185866
评论
请登录