vue3项目开发中常见知识点记录
vue3项目开发中常见知识点记录,持续更新中...
动态路由权限
1. 动态添加路由权限,刷新界面后出现404或空白页
出现此场景可能与两个地方有关
- 动态添加路由权限时,匹配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)
})
}
- 在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语法糖替代
- onBeforeMounte
- onMounted
- onBeforeUpdate
- onUpdated
- onBeforeUnmounte
- onUnmounted
- onActivated
- onDeactived
父子组件加载初始化生命周期执行的顺序为:
- 父组件 - onBeforeMounte
- 子组件 - onBeforeMounte
- 子组件 - onMounted
- 父组件 - 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-router
,pinia
, 它主要的场景是帮助我们在全局注册或者定义一些属性,供所有组件路由使用
插件开发
插件本质上就是一个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>
最终界面渲染的结果
2. 子组件有多个根节点
当子组件存在多个根节点时,由于不知道要绑定在那个节点,所以传递的属性不会绑定到子组件的任何元素上
手动绑定属性
如果需要绑定到某个节点上,可以手动的绑定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对象
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节点
生成的样式
场景二:当子组件有多个根节点时
此时父组件传递的样式以及hash属性不会对子组件造成任何影响(不会绑定到子组件的任何节点上)
样式穿透
vue3中使用:deep()来实现样式穿透,从而修改子组件的样式,原理是在父组件中生成一个以父组件hash + 子组件中类名的样式规则,这样子组件就能访问到样式
<style lang="scss" scoped>
:deep(.my-text) {
font-size: 22px
}
</style>
获取插槽
指令
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.js
和tailwind.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