Pinia在Vue3中的使用
Pinia 是 Vue 3 的官方推荐状态管理库,它继承了 Vuex 的设计理念,但提供了更简洁的 API 和更好的 TypeScript 支持。
Pinia 的核心概念
Store
Pinia
的核心是Store
,它是一个独立的状态管理单元,用于存储状态(state
)、派生状态(getters
)和修改状态的方法(actions
)。每个 Store 都是独立的,便于管理和复用。
State
state
是 Store 中存储的数据,它是响应式的。在 Pinia 中,state
是通过一个函数返回的,类似于 Vue 3 的setup()
函数。
Getters
getters
是基于state
的派生状态,类似于 Vue 的计算属性。它可以通过computed
来实现。
Actions
actions
是用于修改state
的方法,可以是同步的,也可以是异步的。
Pinia 的安装与配置
安装 Pinia
在 Vue 3 项目中,可以通过 npm 安装 Pinia:
npm install pinia
配置 Pinia
在项目的入口文件(如main.js
或main.ts
)中引入并安装 Pinia:
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
const app = createApp(App);
const pinia = createPinia();
app.use(pinia);
app.mount('#app');
定义 Store
在src/stores
文件夹中创建一个 Store 文件(如counter.js
),并使用defineStore
定义一个 Store:
// src/stores/counter.js
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', {
// 定义状态
state: () => ({
count: 0
}),
// 定义派生状态
getters: {
doubleCount: (state) => state.count * 2
},
// 定义修改状态的方法
actions: {
increment() {
this.count++;
}
}
});
在组件中使用 Store
使用useCounterStore
在组件中引入并使用 Store:
<template>
<div>
<p>Count: {{ counterStore.count }}</p>
<p>Double Count: {{ counterStore.doubleCount }}</p>
<button @click="counterStore.increment">Increment</button>
</div>
</template>
<script setup>
import { useCounterStore } from '@/stores/counter';
const counterStore = useCounterStore();
</script>
使用storeToRefs
为了保持响应式,可以使用storeToRefs
解构 Store 的状态和 getters:
<script setup>
import { useCounterStore } from '@/stores/counter';
import { storeToRefs } from 'pinia';
const counterStore = useCounterStore();
const { count, doubleCount } = storeToRefs(counterStore);
</script>
异步 Actions
Pinia 支持异步操作,可以直接在actions
中使用async
:
// src/stores/user.js
import { defineStore } from 'pinia';
import axios from 'axios';
export const useUserStore = defineStore('user', {
state: () => ({
user: null
}),
actions: {
async fetchUser(userId) {
const response = await axios.get(`/api/users/${userId}`);
this.user = response.data;
}
}
});
在组件中调用异步action
:
<script setup>
import { useUserStore } from '@/stores/user';
const userStore = useUserStore();
async function loadUser() {
await userStore.fetchUser(1);
}
</script>
持久化存储
Pinia 提供了插件支持,可以将状态持久化到localStorage
或sessionStorage
。
安装持久化插件
npm install pinia-plugin-persistedstate
配置持久化
在 Store 中配置持久化:
// src/stores/counter.js
import { defineStore } from 'pinia';
import { createPinia } from 'pinia';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0
}),
persist: {
key: 'counter', // 持久化存储的 key
storage: localStorage // 使用 localStorage
}
});
调试
Pinia 支持 Vue Devtools,可以直接在 Devtools 中查看和调试 Store 的状态。
总结
Pinia 是 Vue 3 的新一代状态管理库,具有以下特点:
- 简洁的 API:比 Vuex 更简洁,更易于上手。
- 更好的 TypeScript 支持:提供了完整的类型推导。
- 模块化设计:每个 Store 都是独立的,便于管理和复用。
- 异步支持:支持异步操作,无需额外的库。
- 持久化支持:通过插件支持状态持久化。
通过合理使用 Pinia,可以更好地管理 Vue 3 项目中的状态,提高开发效率和代码可维护性。
Vue3中的组件通信
在 Vue 3 中,组件之间的通信是构建复杂应用时的核心需求之一。Vue 提供了多种方式来实现组件之间的通信,包括父子组件通信、兄弟组件通信以及跨层级通信。
父子组件通信
父组件向子组件通信:通过props
props
是子组件接收父组件数据的标准方式。父组件通过子组件的属性传递数据,子组件通过props
定义接收的数据。
子组件:
<template>
<div>
<p>{{ title }}</p>
</div>
</template>
<script>
export default {
props: {
title: String
}
};
</script>
父组件:
<template>
<ChildComponent :title="headerTitle" />
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: {
ChildComponent
},
data() {
return {
headerTitle: 'Hello Vue 3!'
};
}
};
</script>
子组件向父组件通信:通过$emit
子组件可以通过 $emit 触发事件,父组件监听这些事件来接收数据。
子组件:
<template>
<button @click="sendMessage">Send Message</button>
</template>
<script>
export default {
methods: {
sendMessage() {
this.$emit('message', 'Hello from child!');
}
}
};
</script>
父组件:
<template>
<ChildComponent @message="handleMessage" />
<p>{{ message }}</p>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: {
ChildComponent
},
data() {
return {
message: ''
};
},
methods: {
handleMessage(payload) {
this.message = payload;
}
}
};
</script>
兄弟组件通信
通过父组件中转
兄弟组件之间不能直接通信,但可以通过父组件中转数据。
父组件:
<template>
<ChildA :message="message" @update-message="updateMessage" />
<ChildB :message="message" />
</template>
<script>
import ChildA from './ChildA.vue';
import ChildB from './ChildB.vue';
export default {
components: {
ChildA,
ChildB
},
data() {
return {
message: 'Hello!'
};
},
methods: {
updateMessage(newMessage) {
this.message = newMessage;
}
}
};
</script>
ChildA:
<template>
<button @click="sendMessage">Send Message</button>
</template>
<script>
export default {
props: {
message: String
},
methods: {
sendMessage() {
this.$emit('update-message', 'New message from ChildA');
}
}
};
</script>
ChildB:
<template>
<p>{{ message }}</p>
</template>
<script>
export default {
props: {
message: String
}
};
</script>
使用全局状态管理(如 Pinia)
兄弟组件可以通过全局状态管理工具(如 Pinia)共享状态,从而实现通信。
Pinia Store:
import { defineStore } from 'pinia';
export const useSharedStore = defineStore('shared', {
state: () => ({
sharedMessage: 'Hello from Pinia!'
})
});
兄弟组件 A:
<template>
<button @click="updateMessage">Update Message</button>
</template>
<script setup>
import { useSharedStore } from '@/stores/shared';
const store = useSharedStore();
function updateMessage() {
store.sharedMessage = 'New message from ChildA';
}
</script>
兄弟组件 B:
<template>
<p>{{ sharedMessage }}</p>
</template>
<script setup>
import { useSharedStore } from '@/stores/shared';
const store = useSharedStore();
const { sharedMessage } = store;
</script>
跨层级通信
使用事件总线(不推荐)
事件总线是一种全局事件触发器,但它的缺点是难以维护,容易导致代码混乱。Vue 3 中不推荐使用事件总线,建议使用全局状态管理工具(如 Pinia)
使用全局状态管理(推荐)
通过 Pinia 或 Vuex 管理全局状态,任何组件都可以访问和修改状态,从而实现跨层级通信。
Pinia Store:
import { defineStore } from 'pinia';
export const useGlobalStore = defineStore('global', {
state: () => ({
globalMessage: 'Hello from Global Store!'
})
});
任意组件:
<template>
<p>{{ globalMessage }}</p>
<button @click="updateGlobalMessage">Update Global Message</button>
</template>
<script setup>
import { useGlobalStore } from '@/stores/global';
const store = useGlobalStore();
const { globalMessage } = store;
function updateGlobalMessage() {
store.globalMessage = 'New global message!';
}
</script>
提供/注入(Provide/Inject)
使用场景
适用于祖先组件向后代组件(包括多层嵌套的组件)传递数据,但不推荐用于常规的父子组件通信。
使用方法 祖先组件:
<template>
<ChildComponent />
</template>
<script>
import { provide, reactive } from 'vue';
import ChildComponent from './ChildComponent.vue';
export default {
components: {
ChildComponent
},
setup() {
const state = reactive({
message: 'Hello from Ancestor!'
});
provide('state', state);
return {};
}
};
</script>
后代组件:
<template>
<p>{{ message }}</p>
</template>
<script>
import { inject } from 'vue';
export default {
setup() {
const state = inject('state');
return {
message: state.message
};
}
};
</script>
总结
在 Vue 3 中,组件之间的通信可以通过以下几种方式实现:
父子组件通信:
- 父组件通过
props
向子组件传递数据。 - 子组件通过
$emit
向父组件传递数据。
兄弟组件通信:
- 通过父组件中转。
- 使用全局状态管理工具(如 Pinia)。
跨层级通信:
- 使用全局状态管理工具(推荐)。
- 使用提供/注入(适用于特定场景)。
全局状态管理:
- 使用 Pinia 或 Vuex 管理全局状态,适用于复杂应用。
选择哪种通信方式取决于你的具体需求和应用的复杂性。对于简单的父子组件通信,props
和$emit
是最直接的方式;对于复杂的应用,全局状态管理工具(如 Pinia)是更好的选择。
vue3 <Teleport> 标签
什么是<Teleport>?
<Teleport>
是 Vue 3 引入的一个内置组件,用于将组件的内容渲染到 DOM 树中的任意位置,而不受组件层级结构的限制。它的核心作用是将一个组件的子元素“传送”到另一个指定的 DOM 节点中,而不会改变组件在 Vue 组件树中的位置。
<Teleport>的基本用法
<Teleport>
通过to
属性指定目标容器,可以是一个 CSS 选择器字符串或一个 DOM 元素。
例如:
<template>
<teleport to="body">
<div class="modal">
<p>这是一个模态框</p>
</div>
</teleport>
</template>
在这个例子中,<div class="modal">
的内容会被渲染到<body>
元素的末尾,而不是其直接的父组件内部。
<Teleport>的适用场景
模态框和弹出窗口
模态框和弹出窗口通常需要显示在页面的最顶层,不受其他 DOM 元素的影响。使用<Teleport>
,可以轻松地将这些元素渲染到页面的最外层,确保它们总是显示在其他内容之上。
全局通知和提示
全局通知和提示通常需要显示在页面的顶部或特定位置,以便用户能够立即注意到。使用<Teleport>
可以将通知组件渲染到一个固定的位置,而不受页面其他部分布局的影响。
解决z-index
问题
在复杂的布局中,使用z-index
来控制元素的堆叠顺序可能会变得复杂和混乱。<Teleport>
提供了一种更优雅的方式来解决这类问题。
与第三方库集成
某些第三方库可能需要特定的 DOM 结构或位置来正确显示其内容。通过<Teleport>
,可以将与这些库相关的组件渲染到合适的位置,以确保它们能够正常工作。
上下文菜单
上下文菜单(如右键菜单)需要根据鼠标位置动态显示。使用<Teleport>
可以将菜单渲染到<body>
或其他容器中,从而避免被其他元素遮挡。
<Teleport>的高级用法
动态目标
<Teleport>
的目标容器可以通过动态绑定来改变。例如,可以根据屏幕大小将内容渲染到不同的位置:
<template>
<teleport :to="target">
<div class="content">
动态传送的内容
</div>
</teleport>
</template>
<script setup>
import { ref, onMounted } from 'vue';
const target = ref('body');
onMounted(() => {
if (window.innerWidth < 768) {
target.value = '#mobile-container';
}
});
</script>
多个<Teleport>
到同一目标
多个<Teleport>
组件可以将其内容依次挂载到同一个目标元素上,按照先后顺序追加:
<template>
<teleport to="#notifications">
<div class="notification">通知 1</div>
</teleport>
<teleport to="#notifications">
<div class="notification">通知 2</div>
</teleport>
</template>
条件性传送
可以通过disabled
属性控制<Teleport>
是否生效:
<template>
<teleport to="body" :disabled="isMobile">
<div class="modal">
<!-- 在移动端不会被传送,保持原位置 -->
</div>
</teleport>
</template>
<script setup>
import { ref, onMounted } from 'vue';
const isMobile = ref(false);
onMounted(() => {
isMobile.value = window.innerWidth < 768;
});
</script>
<Teleport>
的注意事项
- 目标容器必须存在:使用
<Teleport>
时,目标容器(如body
或其他 DOM 元素)必须在页面中存在,否则<Teleport>
无法正常工作。 - 事件冒泡和捕获:由于
<Teleport>
改变了元素在 DOM 树中的实际位置,需要注意事件冒泡和捕获的行为。 - 样式穿透:虽然
<Teleport>
改变了 DOM 结构,但组件的样式仍然可以通过:deep()
选择器进行穿透。
总结
<Teleport>
是 Vue 3 中一个非常强大的工具,特别适合处理那些需要脱离当前组件层级的 UI 元素。通过<Teleport>
,你可以更灵活地控制组件的渲染位置,而不必担心 DOM 结构的限制。它在以下场景中特别有用:
- 模态框和弹出窗口• 全局通知和提示 解决 z-index 问题
- 与第三方库集成
- 上下文菜单
总之,<Teleport>
提供了一种优雅的方式来解决复杂的布局问题,同时保持代码的简洁和可维护性。
Vue3中的<Suspense>组件
<Suspense>
是 Vue 3 引入的一个内置组件,用于处理异步组件和异步数据加载。它允许你在等待异步内容加载完成时显示一个备用内容(如加载指示器),从而提升用户体验。
<Suspense>
的基本概念
<Suspense>
是一个内置组件,用于处理异步组件或异步数据加载时的加载状态。它有两个主要作用:
- 显示加载状态:在异步内容加载完成之前,显示一个备用内容(如加载指示器)。
- 错误处理:捕获异步加载过程中可能发生的错误。
<Suspense>
提供了两个插槽:- #default :用于放置异步组件或需要等待异步操作的内容。
- #fallback :用于放置备用内容,在异步操作完成之前显示。
<Suspense>
的基本用法
异步组件加载
<Suspense>
常用于加载异步组件,例如通过defineAsyncComponent
定义的组件。
<template>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<div class="loading">加载中...</div>
</template>
</Suspense>
</template>
<script setup>
import { defineAsyncComponent } from 'vue';
const AsyncComponent = defineAsyncComponent(() =>
import('./components/HeavyComponent.vue')
);
</script>
在这个例子中,AsyncComponent
是一个异步加载的组件。在组件加载完成之前,<Suspense>
会显示#fallback
插槽中的内容(即“加载中...”)。
异步数据获取
<Suspense>
也可以用于处理组件内部的异步数据加载。
<template>
<Suspense>
<template #default>
<UserProfile :user="user" />
</template>
<template #fallback>
<div class="loading">加载用户数据...</div>
</template>
</Suspense>
</template>
<script setup>
import { ref } from 'vue';
import UserProfile from './UserProfile.vue';
async function fetchUserData() {
const response = await fetch('/api/user');
return await response.json();
}
const user = ref(await fetchUserData());
</script>
在这个例子中,fetchUserData
是一个异步函数,用于获取用户数据。在数据加载完成之前,<Suspense>
会显示#fallback
插槽中的内容(即“加载用户数据...”)。
<Suspense>
的高级用法
错误处理
<Suspense>
可以捕获异步加载过程中发生的错误,并显示错误信息。
<template>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<div class="loading">加载中...</div>
</template>
<template #error>
<div class="error">加载失败,请稍后再试。</div>
</template>
</Suspense>
</template>
如果异步加载过程中发生错误,<Suspense>
会显示#error
插槽中的内容。
嵌套<Suspense>
<Suspense>
可以嵌套使用,以支持更复杂的异步加载场景。
<template>
<Suspense>
<template #default>
<div class="nested">
<AsyncParent>
<Suspense>
<template #default>
<AsyncChild />
</template>
<template #fallback>
<div>加载子组件...</div>
</template>
</Suspense>
</AsyncParent>
</div>
</template>
<template #fallback>
<div>加载父组件...</div>
</template>
</Suspense>
</template>
动态组件切换
<Suspense>
可以与动态组件结合使用,支持异步组件的切换。
<template>
<div class="tabs">
<button
v-for="(_, tab) in tabs"
:key="tab"
@click="currentTab = tab"
>
{{ tab }}
</button>
<Suspense>
<template #default>
<component :is="tabs[currentTab]" />
</template>
<template #fallback>
<div class="loading">切换中...</div>
</template>
</Suspense>
</div>
</template>
<script setup>
import { ref, defineAsyncComponent } from 'vue';
const currentTab = ref('tab1');
const tabs = {
tab1: defineAsyncComponent(() => import('./tabs/Tab1.vue')),
tab2: defineAsyncComponent(() => import('./tabs/Tab2.vue')),
tab3: defineAsyncComponent(() => import('./tabs/Tab3.vue'))
};
</script>
<Suspense>
的注意事项
避免无限循环:
- 确保异步逻辑不会导致无限循环。
- 示例:
const data = await fetchData();
而不是data.value = await fetchData();
。
合理的超时处理:
如果加载时间过长,可以显示超时提示。
示例:
<template>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<div>
<loading-spinner />
<p v-if="timeout">加载时间过长,请检查网络连接。</p>
</div>
</template>
</Suspense>
</template>
<script setup>
import { ref, onMounted } from 'vue';
const timeout = ref(false);
onMounted(() => {
setTimeout(() => {
timeout.value = true;
}, 5000);
});
</script>
资源清理:
在组件销毁时清理未完成的异步请求。
示例:
<script setup>
import { onUnmounted } from 'vue';
const controller = new AbortController();
const data = await fetch('/api/data', {
signal: controller.signal
});
onUnmounted(() => {
controller.abort(); // 取消未完成的请求
});
</script>
<Suspense>
的最佳实践
将异步逻辑抽离到组合式函数:
示例:
export function useAsyncData(url) {
return new Promise(async (resolve) => {
const data = await fetch(url).then(r => r.json());
resolve(data);
});
}
提供更细粒度的加载状态:
示例:
<template>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<div class="loading-state">
<loading-spinner />
<loading-progress :progress="loadingProgress" />
<p>{{ loadingMessage }}</p>
</div>
</template>
</Suspense>
</template>
总结
<Suspense>
是 Vue 3 中一个非常强大的工具,用于处理异步组件和异步数据加载时的加载状态和错误处理。它通过两个插槽(#default
和#fallback
)提供了优雅的加载状态管理,并支持错误处理和嵌套使用。通过合理使用<Suspense>
,可以显著提升用户体验,尤其是在处理复杂的异步加载场景时。