vue3笔记(2)自用
目录
一、作用域插槽
二、pinia的使用
一、Pinia 基本概念与用法
1. 安装与初始化
2. 创建 Store
3. 在组件中使用 Store
4. 高级用法
5、storeToRefs
二、Pinia 与 Vuex 的主要区别
三、为什么选择 Pinia?
三、定义全局指令
1.封装通用 DOM 操作,复用逻辑
2.增强模板的声明式编程能力
3.解耦组件逻辑,保持组件简洁
4.便于统一维护和扩展
四、指令钩子
1. 钩子函数列表及含义
2. 核心参数说明(以 mounted(el, binding, vnode, prevVnode) 为例)
3. 实际场景示例(用 v-focus 指令理解钩子)
4. 为什么需要指令钩子?
五、图片懒加载
六、路由传参
1、Vue 2 获取路由参数
1. 动态路由参数(路径中的参数,如 /user/:id)
2. 查询参数(URL 中的 ?key=value)
2、Vue 3 获取路由参数
1. 动态路由参数
2. 查询参数
对比
补充说明
总结
七、路由
1、默认请求参数
2、路由缓存问题
解决方法
一、作用域插槽
<template><div class="app"><el-table :data=list><el-table-column label="ID" prop="id"></el-table-column><el-table-column label="姓名" prop="name" width="150"></el-table-column><el-table-column label="籍贯" prop="place"></el-table-column><el-table-column label="操作" width="150"><template #default="{row}">//作用域插槽<el-button type="primary" @click="updateData(row.id)" link>编辑</el-button><el-button type="danger" @click="deleteData(row.id)">删除</el-button></template></el-table-column></el-table></div>
</template>
#default="{row}"
:这是 作用域插槽(scoped slot),row
代表当前行的数据对象。- 每个操作按钮都能通过
row
访问当前行的所有属性(如row.id
、row.name
)。
二、pinia的使用
一、Pinia 基本概念与用法
Pinia 是 Vue.js 的新一代状态管理库,由 Vue 核心团队开发,旨在替代 Vuex。它提供了更简洁的 API、更好的 TypeScript 支持和更小的体积。
1. 安装与初始化
npm install pinia
// main.js
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';const pinia = createPinia();
const app = createApp(App);app.use(pinia);
app.mount('#app');
或者
import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'const pinia = createPinia()
createApp(App).use(pinia).mount('#app')
2. 创建 Store
// stores/counter.js
import { defineStore } from 'pinia';// 使用组合式 API 风格
export const useCounterStore = defineStore('counter', () => {// 状态const count = ref(0);// 计算属性const doubleCount = computed(() => count.value * 2);// 方法const increment = () => {count.value++;};const reset = () => {count.value = 0;};return { count, doubleCount, increment, reset };
});// 或使用选项式 API 风格(类似 Vuex)
export const useUserStore = defineStore({id: 'user',state: () => ({name: '张三',age: 20}),getters: {fullName: (state) => `用户: ${state.name}`},actions: {updateName(newName) {this.name = newName;}}
});
3. 在组件中使用 Store
<template><div><p>Count: {{ counter.count }}</p><p>Double: {{ counter.doubleCount }}</p><button @click="counter.increment">+1</button><button @click="counter.reset">重置</button></div>
</template><script setup>
import { useCounterStore } from '../stores/counter';// 获取 store 实例
const counter = useCounterStore();// 直接访问 state
console.log(counter.count);// 调用 action
counter.increment();
</script>
4. 高级用法
// 订阅 state 变化
const unsubscribe = counter.$subscribe((mutation, state) => {console.log('State changed:', mutation.type, state);
});// 重置 state
counter.$reset();// 批量修改 state
counter.$patch({count: 100,// 其他属性
});// 插件示例
const loggerPlugin = (context) => {console.log('Store initialized:', context.store.$id);return {// 扩展 storehello: 'world'};
};pinia.use(loggerPlugin);
5、storeToRefs
storeToRefs
是 Pinia 提供的一个辅助函数,用于从 store 中提取状态并保持其响应式特性。它解决了在解构 store 时丢失响应式的问题,让你可以更优雅地在组件中使用 store 状态。
- 问题:直接解构 store 对象(如
const { count } = store
)会丢失响应式。 - 解决方案:
storeToRefs
将 store 中的状态转换为响应式引用(refs),保持响应式更新。
// ❌ 错误:解构后丢失响应式
const { count } = store;
// ✅ 转换为响应式引用
const { count } = storeToRefs(store);
storeToRefs用于响应式数据、coumputed。对于方法,直接解构即可。
二、Pinia 与 Vuex 的主要区别
特性 | Pinia | Vuex (4.x) |
---|---|---|
API 风格 | 组合式 API 为主,支持选项式 | 仅支持选项式 API |
模块化方式 | 自动模块,无嵌套结构 | 需要手动定义模块 |
TypeScript 支持 | 一流支持,无需额外配置 | 需要额外配置,类型定义复杂 |
体积 | 更小(~1kb) | 较大(~2kb) |
Mutation | 无 Mutation,直接修改 state | 必须通过 Mutation 修改 state |
代码结构 | 更简洁,更少样板代码 | 更多样板代码(state/getters/mutations/actions) |
插件系统 | 更灵活 | 相对固定 |
DevTools 支持 | 更现代,支持时间旅行调试 | 支持时间旅行调试 |
Vue 版本支持 | Vue 2 和 Vue 3 | Vue 2 和 Vue 3 |
三、为什么选择 Pinia?
-
更简洁的 API:
- 无需
mapState
、mapActions
等辅助函数。 - 直接通过
store.counter
访问状态,store.increment()
调用方法。
- 无需
-
更好的 TypeScript 支持:
- 自动类型推导,无需手动定义接口。
- 组合式 API 天然支持类型安全。
-
无 Mutation:
- 移除了 Vuex 中冗余的 Mutation 概念,直接在 Action 中修改 state。
三、定义全局指令
Vue 本身有像 v-if
、v-show
、v-model
这类内置指令,方便开发者操作 DOM、处理数据渲染等。而全局指令,是开发者借助 Vue 提供的机制,自己定义的、可在整个 Vue 应用里通用的指令。
它基于 Vue 的指令系统拓展而来,注册后,在所有组件的模板中,都能用 v-指令名
的形式(比如定义了全局指令 v-focus
,就可以在任意组件 <input v-focus />
这样用 )去调用,来实现特定功能。
1.封装通用 DOM 操作,复用逻辑
Vue 组件主要聚焦数据和界面的映射,但有些频繁的 DOM 底层操作(如元素自动聚焦、自定义滚动行为、表单输入格式化等 ),若在每个组件里重复写,既冗余又难维护。全局指令可以把这类操作封装起来,一处定义,处处使用。
比如定义一个 v-focus
全局指令,实现元素挂载后自动获取焦点:
// Vue2 中全局指令注册(main.js 里)
Vue.directive('focus', {inserted: function (el) { el.focus() }
})// Vue3 中全局指令注册(main.js 里)
const app = createApp(App)
app.directive('focus', {mounted(el) {el.focus()}
})
之后在任意组件的输入框上用 <input v-focus />
,就能自动聚焦,不用在每个组件里写获取焦点的逻辑。
2.增强模板的声明式编程能力
全局指令能让模板更简洁、语义化。
比如定义 v-permission
指令控制按钮权限:
// 假设根据用户权限决定元素是否显示
app.directive('permission', {mounted(el, binding) {const hasPerm = checkPermission(binding.value) if (!hasPerm) {el.parentNode && el.parentNode.removeChild(el) }}
})
模板中用 <button v-permission="'delete'">删除</button>
,语义清晰,一看就知道和权限控制有关,无需在组件里写一堆权限判断的 JS 逻辑。
3.解耦组件逻辑,保持组件简洁
把和 DOM 操作强相关的代码(比如复杂的动画初始化、第三方库初始化 )放到指令里,组件就不用关心这些细节,专注业务数据和基本渲染。
比如用指令初始化一个图表库:
app.directive('chart', {mounted(el, binding) {const chart = new Chart(el, binding.value.config) // 后续更新等逻辑也可在指令钩子处理}
})
组件里只需 <div v-chart="{ config: { /* 图表配置 */ } }"></div>
,组件代码更简洁,职责更单一。
4.便于统一维护和扩展
当需要修改指令功能(如调整权限判断逻辑、优化聚焦的时机 ),只需改全局指令的定义,所有用到该指令的组件都会生效,不用逐个组件修改,大大提升了代码的可维护性。
四、指令钩子
在 Vue 中,指令钩子(Directive Hooks) 是指令定义对象里的一组生命周期函数,用于在指令绑定的 DOM 元素的不同生命周期阶段执行自定义逻辑。它们像 “钩子”,能让你在元素从创建到销毁的各个关键节点介入,做一些 DOM 操作、数据处理或逻辑控制。
1. 钩子函数列表及含义
钩子函数 | 执行时机(Vue 3 场景) | 典型用途 |
---|---|---|
created | 指令绑定到元素后,DOM 渲染前调用(元素还没插入 DOM 树) | 可初始化一些与元素关联的 “纯数据逻辑”,比如根据指令参数准备数据,或添加事件监听(但注意此时 DOM 可能不可操作) |
beforeMount | 元素即将插入 DOM 树前调用(元素已存在于虚拟 DOM,但未真实渲染到页面) | 做一些 DOM 插入前的准备,比如根据指令动态修改元素属性(如 el.setAttribute ),但元素还没在页面可见 |
mounted | 元素插入 DOM 树后调用(真实 DOM 已渲染,可操作) | 最常用!比如操作 DOM(聚焦输入框 el.focus() )、初始化第三方库(基于 DOM 渲染图表、地图 ) |
beforeUpdate | 元素所在组件更新前调用(组件数据变化,虚拟 DOM 准备对比更新,真实 DOM 还未改变) | 可在更新前记录元素状态(比如旧的滚动位置、样式 ),用于更新后恢复或对比 |
updated | 元素所在组件更新后调用(虚拟 DOM 对比完成,真实 DOM 已更新) | 基于新的 DOM 状态做调整(比如重新计算元素尺寸、更新第三方库的配置 ) |
beforeUnmount | 元素所在组件卸载前调用(组件即将销毁,DOM 还未移除) | 清除定时器、移除自定义事件监听(避免内存泄漏),比如 el.removeEventListener |
unmounted | 元素所在组件卸载后调用(DOM 已从页面移除) | 收尾工作,比如释放第三方库占用的资源、彻底清理与该元素相关的全局状态 |
2. 核心参数说明(以 mounted(el, binding, vnode, prevVnode)
为例)
el
:指令绑定的真实 DOM 元素,可直接操作(如el.style.color = 'red'
)。binding
:指令的绑定信息,包含:binding.value
:指令的参数值(如v-my-directive="100"
里的100
)。binding.arg
:指令的参数(如v-my-directive:foo
里的foo
)。binding.modifiers
:指令的修饰符(如v-my-directive.bar
里的{ bar: true }
)。
vnode
:Vue 生成的虚拟 DOM 节点(描述当前元素的虚拟结构,一般用于高级场景,如对比新旧虚拟节点差异 )。prevVnode
:上一次的虚拟 DOM 节点(仅在更新 / 卸载阶段存在,用于对比变化 )。
3. 实际场景示例(用 v-focus
指令理解钩子)
需求:定义一个 v-focus
指令,让输入框插入 DOM 后自动聚焦。
<script setup>
import { createApp } from 'vue'const app = createApp(App)// 注册全局指令
app.directive('focus', {// 元素插入 DOM 后执行(此时可操作真实 DOM)mounted(el) {el.focus() // 让输入框自动聚焦}
})
</script><template><!-- 使用指令 --><input v-focus placeholder="自动聚焦的输入框" />
</template>
如果需要在组件更新后,根据条件重新聚焦,就可以用 updated
钩子:
app.directive('focus', {mounted(el) {el.focus()},updated(el, binding) {// 如果指令参数为 true,更新后重新聚焦if (binding.value) {el.focus()}}
})
在模板中动态控制:
<input v-focus="shouldFocus" placeholder="动态聚焦" />
4. 为什么需要指令钩子?
- 精准控制 DOM 生命周期:Vue 是数据驱动的框架,直接操作 DOM 容易和响应式逻辑冲突。指令钩子让你在 “安全” 的时机操作 DOM(比如
mounted
确保元素已渲染 )。 - 复用 DOM 操作逻辑:把聚焦、权限控制、第三方库初始化等逻辑封装到指令,避免在组件里重复写(比如 10 个输入框都要自动聚焦,用
v-focus
一行解决 )。 - 解耦组件与 DOM 操作:组件只关心业务数据,DOM 操作细节交给指令,代码更简洁、职责更清晰。
总结:指令钩子是 Vue 指令在 DOM 不同生命周期阶段的 “切入点”,让我们能优雅地操作 DOM、复用逻辑,同时避开直接操作 DOM 带来的风险,是 Vue 拓展 DOM 能力的核心机制之一。
五、图片懒加载
核心原理:图片进入视口区才发送资源请求
思路:定义一个全局自定义指令;利用vueUse的useIntersectionObserver判断元素是否进入视口;进入视口后再给img标签的src添加url地址
//main.jsimport { useIntersectionObserver } from '@vueuse/core'const app = createApp(App)app.use(createPinia())
app.use(router)app.mount('#app')// 定义全局指令
app.directive('pic-lazy',{mounted(el,binding){//el:指令绑定的元素//binding:指令对象console.log(el,binding)// 监听元素是否进入视口useIntersectionObserver(el,// 回调函数,isIntersecting 表示是否进入视口([{ isIntersecting }]) => {if (isIntersecting) {// console.log('元素进入视口啦!')// 这里可以写进入视口后的逻辑,比如加载图片、统计曝光等// stop() // 如果只想监听一次,进入视口后可以停止监听el.src = binding.valueconsole.log(isIntersecting)}},// 可选配置:设置进入视口的“占比”阈值(0 ~ 1),默认 0.1(即 10% 进入视口就算){ threshold: 0.5 })}
})
对于图片标签,把原来的<img :src="……" alt="" /> 改为 <img v-pic-lazy="……" alt="" />
六、路由传参
1、Vue 2 获取路由参数
在 Vue 2 中,路由参数主要通过 $route
对象获取,支持动态路由参数和查询参数。
1. 动态路由参数(路径中的参数,如 /user/:id
)
// 路由配置
{path: '/user/:id',component: User
}// 在组件中获取
export default {computed: {userId() {return this.$route.params.id; // 获取路径中的 :id 参数}},created() {console.log(this.$route.params.id); // 直接访问}
}
2. 查询参数(URL 中的 ?key=value
)
// URL: /user?name=john&age=20
export default {computed: {userName() {return this.$route.query.name; // 获取查询参数 name},userAge() {return this.$route.query.age; // 获取查询参数 age}}
}
2、Vue 3 获取路由参数
Vue 3 的组合式 API(Composition API)改变了获取路由参数的方式,需要通过 useRoute
函数引入路由实例。
1. 动态路由参数
import { useRoute } from 'vue-router';export default {setup() {const route = useRoute();// 方式一:直接在 setup 中使用console.log(route.params.id);// 方式二:定义为响应式数据(推荐)const userId = computed(() => route.params.id);return {userId};}
}
2. 查询参数
import { useRoute } from 'vue-router';export default {setup() {const route = useRoute();// 获取查询参数const userName = computed(() => route.query.name);const userAge = computed(() => route.query.age);return {userName,userAge};}
}
对比
场景 | Vue 2.x | Vue 3.x |
---|---|---|
动态路由参数 | this.$route.params.id | const route = useRoute(); route.params.id |
查询参数 | this.$route.query.name | const route = useRoute(); route.query.name |
响应式处理 | 通过 watch 监听 $route | 使用 computed 或 watch 监听 route |
补充说明
-
Vue 3 中仍可使用
$route
在 Vue 3 的选项式 API(Options API)中,仍可通过this.$route
访问路由参数,但组合式 API 推荐使用useRoute
。 -
响应式处理
- 在 Vue 3 中,路由参数变化时,需要使用
computed
或watch
确保数据响应式更新:import { useRoute, watch } from 'vue-router';setup() {const route = useRoute();// 监听路由变化watch(() => route.params.id, (newId, oldId) => {console.log('ID 变化:', newId);}); }
- 在 Vue 3 中,路由参数变化时,需要使用
-
路由配置示例
无论是 Vue 2 还是 Vue 3,路由配置中定义参数的方式相同:// 路由配置(Vue 2/Vue 3 通用) {path: '/user/:id', // 动态路由参数name: 'User',component: User }
总结
- Vue 2:依赖
this.$route
对象,在created
、computed
等选项中使用。 - Vue 3:通过
useRoute()
函数获取路由实例,在组合式 API 的setup
函数中使用,更灵活且类型安全。
七、路由
1、默认请求参数
下面这个接口默认传的值为1
function getNewthingAPI({params={}}){const {distributionSite='1'}=paramsreturn httpInstance({url:'/home/new',params:{distributionSite}})
}
调用时可以对这个值进行指定
getBannerAPI( {params: { distributionSite: '2' }})
如果不指定,就使用默认值1
getBannerAPI()
2、路由缓存问题
路由缓存问题通常指在单页应用( Vue、React)中,切换路由后组件状态未重置或数据未更新,主要原因包括:
- 组件复用机制:路由切换时,若新旧路由使用同一组件(如动态路由
/:id
),框架可能复用该组件实例,导致组件内的数据、生命周期钩子(如created
、mounted
)不重新执行,状态保留旧值。 - 缓存策略设置:主动使用缓存配置(如 Vue 的
<keep-alive>
)时,未正确配置缓存范围,导致不需要缓存的组件被缓存,数据未刷新。 - 数据依赖未更新:组件数据依赖路由参数,但未监听路由参数变化,参数改变后未触发数据重新获取(如 Vue 中未使用
watch
或onBeforeRouteUpdate
监听$route.params
)。 - 异步数据加载时机:数据请求放在
mounted
等只执行一次的钩子中,路由切换后组件复用,钩子不再触发,导致新数据未加载。
解决方法
-
禁用不必要的缓存
若无需缓存组件,移除<keep-alive>
或通过exclude
排除特定组件:<!-- Vue中排除不需要缓存的组件 --> <keep-alive exclude="DetailPage"><router-view /> </keep-alive>
-
监听路由参数变化
在复用组件中,监听路由参数变化并重新加载数据:// 方法1:使用watch监听$route watch: {$route(to, from) {if (to.params.id !== from.params.id) {this.loadData(to.params.id); // 重新加载数据}} }// 方法2:使用导航守卫 onBeforeRouteUpdate((to) => {this.loadData(to.params.id); });
-
强制组件重新渲染
通过key
使组件实例重新创建:<!-- Vue中给router-view添加key,确保路由变化时重新渲染 --> <router-view :key="$route.fullPath" />
$route.fullPath
包含完整路由信息,参数变化时key
改变,触发组件重新挂载。 -
调整数据加载时机
将数据请求放在每次路由进入时都执行的钩子中,onMounted
结合watch监听。 -
清除缓存数据
在组件离开时手动重置状态(如 Vue 的onBeforeUnmount
或 React 的useEffect
清理函数):// Vue中离开组件时重置数据 onBeforeUnmount() {this.data = null; }