七、状态管理(Pinia)
7.1 安装与配置
bash
npm install pinia
javascript
// 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')
7.2 定义 Store
javascript
// stores/user.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
// Option Store(类似 Vuex)
export const useUserStore = defineStore('user', {
// 状态
state: () => ({
name: '张三',
age: 25,
token: null
}),
// 计算属性
getters: {
userInfo: (state) => `${state.name} (${state.age}岁)`,
isLoggedIn: (state) => !!state.token
},
// 方法
actions: {
setName(name) {
this.name = name
},
setAge(age) {
this.age = age
},
login(token) {
this.token = token
},
logout() {
this.token = null
},
async fetchUser() {
const response = await fetch('/api/user')
const data = await response.json()
this.name = data.name
this.age = data.age
}
}
})
// Setup Store(推荐)
export const useCounterStore = defineStore('counter', () => {
// 状态
const count = ref(0)
const step = ref(1)
// 计算属性
const doubleCount = computed(() => count.value * 2)
// 方法
function increment() {
count.value += step.value
}
function decrement() {
count.value -= step.value
}
function setStep(newStep) {
step.value = newStep
}
function reset() {
count.value = 0
step.value = 1
}
return {
count,
step,
doubleCount,
increment,
decrement,
setStep,
reset
}
})
7.3 使用 Store
VUE
<template>
<div>
<!-- Option Store 使用 -->
<p>姓名: {
{ userStore.name }}</p>
<p>年龄: {
{ userStore.age }}</p>
<p>用户信息: {
{ userStore.userInfo }}</p>
<p>登录状态: {
{ userStore.isLoggedIn }}</p>
<button @click="userStore.setName('李四')">修改名字</button>
<button @click="userStore.login('token123')">登录</button>
<!-- Setup Store 使用 -->
<p>计数: {
{ counter.count }}</p>
<p>步长: {
{ counter.step }}</p>
<p>双倍: {
{ counter.doubleCount }}</p>
<button @click="counter.increment">增加</button>
<button @click="counter.decrement">减少</button>
<button @click="counter.setStep(2)">设置步长</button>
<button @click="counter.reset">重置</button>
</div>
</template>
<script setup>
import { useUserStore } from '@/stores/user'
import { useCounterStore } from '@/stores/counter'
// 直接使用 store
const userStore = useUserStore()
const counter = useCounterStore()
// 解构 store(使用 storeToRefs 保持响应性)
import { storeToRefs } from 'pinia'
const { name, age, userInfo, isLoggedIn } = storeToRefs(userStore)
const { count, step, doubleCount } = storeToRefs(counter)
const { increment, decrement } = counter
// 重置 store
const resetUser = () => {
userStore.$reset()
}
// 同时修改多个状态
const updateUser = () => {
userStore.$patch({
name: '王五',
age: 30
})
}
const updateUserWithFunction = () => {
userStore.$patch((state) => {
state.name = '赵六'
state.age = 28
})
}
// 订阅状态变化
userStore.$subscribe((mutation, state) => {
console.log('状态变化:', mutation, state)
})
// 订阅 action
counter.$onAction(({
name, // action 名称
store, // store 实例
args, // 参数数组
after, // 成功后回调
onError // 错误后回调
}) => {
console.log(`执行 action: ${name}`)
after((result) => {
console.log(`成功: ${result}`)
})
onError((error) => {
console.error(`错误: ${error}`)
})
})
</script>
7.4 Store 间通信
javascript
// stores/cart.js
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { useUserStore } from './user'
export const useCartStore = defineStore('cart', () => {
const items = ref([])
const userStore = useUserStore()
const totalItems = computed(() =>
items.value.reduce((sum, item) => sum + item.quantity, 0)
)
const totalPrice = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
)
function addItem(product, quantity = 1) {
const existing = items.value.find(item => item.id === product.id)
if (existing) {
existing.quantity += quantity
} else {
items.value.push({ ...product, quantity })
}
}
function removeItem(productId) {
const index = items.value.findIndex(item => item.id === productId)
if (index !== -1) {
items.value.splice(index, 1)
}
}
async function checkout() {
if (!userStore.isLoggedIn) {
throw new Error('请先登录')
}
// 提交订单
const response = await fetch('/api/orders', {
method: 'POST',
headers: {
'Authorization': `Bearer ${userStore.token}`
},
body: JSON.stringify({
userId: userStore.id,
items: items.value
})
})
if (response.ok) {
items.value = []
return true
}
return false
}
return {
items,
totalItems,
totalPrice,
addItem,
removeItem,
checkout
}
})
八、组合式函数(Composables)
8.1 创建组合式函数
javascript
// composables/useCounter.js
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const doubleCount = computed(() => count.value * 2)
const increment = () => {
count.value++
}
const decrement = () => {
count.value--
}
const reset = () => {
count.value = initialValue
}
return {
count,
doubleCount,
increment,
decrement,
reset
}
}
javascript
// composables/useFetch.js
import { ref, isRef, unref } from 'vue'
export function useFetch(url, options = {}) {
const data = ref(null)
const error = ref(null)
const loading = ref(false)
const execute = async () => {
loading.value = true
error.value = null
try {
const response = await fetch(unref(url), unref(options))
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
data.value = await response.json()
return data.value
} catch (err) {
error.value = err.message
throw err
} finally {
loading.value = false
}
}
// 立即执行
if (isRef(url) || options.immediate) {
execute()
}
return {
data,
error,
loading,
execute
}
}
javascript
// composables/useLocalStorage.js
import { ref, watch } from 'vue'
export function useLocalStorage(key, defaultValue) {
const storedValue = localStorage.getItem(key)
const data = ref(storedValue ? JSON.parse(storedValue) : defaultValue)
watch(data, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue))
}, { deep: true })
const remove = () => {
localStorage.removeItem(key)
data.value = defaultValue
}
return {
data,
remove
}
}
8.2 使用组合式函数
vue
<template>
<div>
<!-- useCounter -->
<h2>计数器</h2>
<p>计数: {
{ counter.count }}</p>
<p>双倍: {
{ counter.doubleCount }}</p>
<button @click="counter.increment">+</button>
<button @click="counter.decrement">-</button>
<button @click="counter.reset">重置</button>
<!-- useFetch -->
<h2>数据获取</h2>
<div v-if="fetchData.loading">加载中...</div>
<div v-else-if="fetchData.error">错误: {
{ fetchData.error }}</div>
<pre v-else>{
{ fetchData.data }}</pre>
<button @click="fetchData.execute">刷新</button>
<!-- useLocalStorage -->
<h2>本地存储</h2>
<input v-model="storage.data" placeholder="输入内容" />
<button @click="storage.remove">清除</button>
</div>
</template>
<script setup>
import { useCounter } from '@/composables/useCounter'
import { useFetch } from '@/composables/useFetch'
import { useLocalStorage } from '@/composables/useLocalStorage'
// 使用组合式函数
const counter = useCounter(10)
const fetchData = useFetch('https://api.example.com/data', { immediate: true })
const storage = useLocalStorage('my-key', '默认值')
// 组合多个组合式函数
function useUserData(userId) {
const user = ref(null)
const { data: userData, loading, error } = useFetch(`/api/users/${userId}`)
// 监听数据变化
watch(userData, (newData) => {
user.value = newData
})
const { data: preferences } = useLocalStorage(`user-${userId}-prefs`, {})
return {
user,
preferences,
loading,
error
}
}
</script>
九、指令与插件
9.1 自定义指令
javascript
// directives/focus.js
export default {
mounted(el) {
el.focus()
}
}
// directives/click-outside.js
export default {
mounted(el, binding) {
el.clickOutsideHandler = (event) => {
if (!(el === event.target || el.contains(event.target))) {
binding.value(event)
}
}
document.addEventListener('click', el.clickOutsideHandler)
},
unmounted(el) {
document.removeEventListener('click', el.clickOutsideHandler)
delete el.clickOutsideHandler
}
}
// directives/permission.js
export default {
mounted(el, binding) {
const permission = binding.value
const hasPermission = checkUserPermission(permission)
if (!hasPermission) {
el.parentNode?.removeChild(el)
}
}
}
// 全局注册
// main.js
import focus from '@/directives/focus'
import clickOutside from '@/directives/click-outside'
app.directive('focus', focus)
app.directive('click-outside', clickOutside)
vue
<template>
<!-- 使用自定义指令 -->
<input v-focus />
<div v-click-outside="handleClickOutside">
点击外部会触发
</div>
<button v-permission="'admin'">管理员按钮</button>
<!-- 带参数的指令 -->
<div v-color="'red'">红色文字</div>
<!-- 动态参数 -->
<div v-color:[colorName]="colorValue">动态颜色</div>
<!-- 多个值 -->
<div v-directive="{ arg1: value1, arg2: value2 }">多个参数</div>
</template>
<script setup>
// 局部注册指令
const vColor = {
mounted(el, binding) {
el.style.color = binding.value
},
updated(el, binding) {
el.style.color = binding.value
}
}
const handleClickOutside = () => {
console.log('点击外部')
}
</script>
9.2 插件开发
javascript
// plugins/toast.js
export default {
install(app, options = {}) {
const toast = {
show(message, type = 'info') {
const toastEl = document.createElement('div')
toastEl.className = `toast toast-${type}`
toastEl.textContent = message
document.body.appendChild(toastEl)
setTimeout(() => {
toastEl.remove()
}, options.duration || 3000)
},
success(message) {
this.show(message, 'success')
},
error(message) {
this.show(message, 'error')
},
warning(message) {
this.show(message, 'warning')
}
}
// 提供全局方法
app.config.globalProperties.$toast = toast
// 提供组合式 API
app.provide('toast', toast)
}
}
javascript
// plugins/i18n.js
import { ref, reactive } from 'vue'
export default {
install(app, options) {
const locale = ref(options.locale || 'zh-CN')
const messages = reactive(options.messages || {})
const t = (key) => {
const keys = key.split('.')
let value = messages[locale.value]
for (const k of keys) {
value = value?.[k]
if (!value) break
}
return value || key
}
const setLocale = (newLocale) => {
locale.value = newLocale
}
const addMessages = (lang, newMessages) => {
messages[lang] = {
...messages[lang],
...newMessages
}
}
app.config.globalProperties.$t = t
app.config.globalProperties.$setLocale = setLocale
app.provide('i18n', { t, setLocale, addMessages, locale })
}
}
javascript
// main.js
import Toast from '@/plugins/toast'
import I18n from '@/plugins/i18n'
const app = createApp(App)
app.use(Toast, { duration: 2000 })
app.use(I18n, {
locale: 'zh-CN',
messages: {
'zh-CN': {
hello: '你好',
welcome: '欢迎'
},
'en-US': {
hello: 'Hello',
welcome: 'Welcome'
}
}
})
app.mount('#app')
vue
<template>
<div>
<button @click="showToast">显示提示</button>
<button @click="showSuccess">成功提示</button>
<button @click="changeLanguage">切换语言</button>
<p>{
{ $t('hello') }}</p>
<p>{
{ $t('welcome') }}</p>
</div>
</template>
<script setup>
import { getCurrentInstance, inject } from 'vue'
// 选项式 API 中使用
const { proxy } = getCurrentInstance()
const showToast = () => {
proxy.$toast.show('这是一条消息')
}
const showSuccess = () => {
proxy.$toast.success('操作成功')
}
// 组合式 API 中使用
const toast = inject('toast')
const i18n = inject('i18n')
const changeLanguage = () => {
i18n.setLocale('en-US')
}
</script>
十、性能优化
10.1 组件懒加载
vue
<template>
<div>
<!-- 异步组件 -->
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<Loading />
</template>
</Suspense>
</div>
</template>
<script setup>
import { defineAsyncComponent } from 'vue'
// 基础异步组件
const AsyncComponent = defineAsyncComponent(() =>
import('./HeavyComponent.vue')
)
// 带配置的异步组件
const AsyncComponentWithOptions = defineAsyncComponent({
loader: () => import('./HeavyComponent.vue'),
loadingComponent: LoadingComponent,
errorComponent: ErrorComponent,
delay: 200, // 延迟显示加载组件
timeout: 3000, // 超时时间
suspensible: true,
onError(error, retry, fail, attempts) {
if (attempts < 3) {
retry()
} else {
fail()
}
}
})
</script>
10.2 虚拟滚动
bash
npm install vue-virtual-scroller
vue
<template>
<div>
<!-- 虚拟滚动列表 -->
<RecycleScroller
class="scroller"
:items="items"
:item-size="50"
key-field="id"
v-slot="{ item }"
>
<div class="item">
{
{ item.name }}
</div>
</RecycleScroller>
<!-- 动态高度 -->
<DynamicScroller
:items="items"
:min-item-size="50"
key-field="id"
>
<template v-slot="{ item, index, active }">
<DynamicScrollerItem
:item="item"
:active="active"
:size-dependencies="[item.text]"
:data-index="index"
>
<div class="item">
{
{ item.text }}
</div>
</DynamicScrollerItem>
</template>
</DynamicScroller>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { RecycleScroller, DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
const items = ref(Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `Item ${i}`,
text: `这是第 ${i} 个项目的详细内容`
})))
</script>
<style scoped>
.scroller {
height: 400px;
overflow-y: auto;
}
.item {
height: 50px;
padding: 10px;
border-bottom: 1px solid #eee;
}
</style>
10.3 其他优化技巧
vue
<script setup>
import { shallowRef, shallowReactive, markRaw } from 'vue'
// 1. 使用 shallowRef / shallowReactive(浅响应式)
// 适用于大型数据结构,不需要深度响应式
const largeData = shallowRef({
items: new Array(10000).fill(0)
})
// 修改时需要替换整个对象
largeData.value = { items: new Array(10000).fill(1) }
// 2. 使用 markRaw 标记非响应式对象
const externalObject = markRaw({
name: '外部对象',
method() {
console.log('这个方法不会触发响应式更新')
}
})
// 3. 冻结大型静态数据
const staticConfig = Object.freeze({
apiUrl: 'https://api.example.com',
timeout: 5000,
retryCount: 3
})
// 4. 使用 v-once 一次性渲染
// 适用于不需要更新的静态内容
</script>
<template>
<!-- v-once 一次性渲染 -->
<div v-once>
<h1>静态标题</h1>
<p>这段内容不会更新</p>
</div>
<!-- v-memo 缓存子树 -->
<div v-for="item in list" :key="item.id" v-memo="[item.id]">
<p>{
{ item.name }}</p>
</div>
<!-- 避免使用 v-if 和 v-for 在同一元素 -->
<!-- 不好 -->
<div v-for="item in list" v-if="item.active" :key="item.id">
{
{ item.name }}
</div>
<!-- 好:使用 computed 过滤 -->
<div v-for="item in activeList" :key="item.id">
{
{ item.name }}
</div>
</template>
十一、TypeScript 支持
11.1 组件类型定义
vue
<script setup lang="ts">
import { ref, reactive, computed, defineProps, defineEmits } from 'vue'
// 定义 Props 类型
interface Props {
title: string
count?: number
items: string[]
user: {
id: number
name: string
}
}
const props = defineProps<Props>()
// 带默认值的 Props
const propsWithDefaults = withDefaults(defineProps<Props>(), {
count: 0,
items: () => []
})
// 定义 Emits 类型
interface Emits {
(e: 'update', value: string): void
(e: 'change', id: number, name: string): void
}
const emit = defineEmits<Emits>()
// 定义 ref 类型
const count = ref<number>(0)
const user = ref<User | null>(null)
const items = ref<string[]>([])
// 定义 reactive 类型
interface State {
name: string
age: number
active: boolean
}
const state = reactive<State>({
name: '张三',
age: 25,
active: true
})
// 定义计算属性类型
const doubleCount = computed<number>(() => count.value * 2)
// 定义函数类型
const handleClick = (event: MouseEvent): void => {
console.log(event.clientX, event.clientY)
}
// 类型导入
import type { Ref, ComputedRef } from 'vue'
// 定义组件实例类型
export interface MyComponentInstance {
reset: () => void
getValue: () => string
}
// 使用模板 ref
const myComponentRef = ref<InstanceType<typeof MyComponent>>()
</script>
11.2 组合式函数类型
typescript
// composables/useCounter.ts
import { ref, computed, Ref, ComputedRef } from 'vue'
interface UseCounterReturn {
count: Ref<number>
doubleCount: ComputedRef<number>
increment: () => void
decrement: () => void
reset: () => void
}
export function useCounter(initialValue: number = 0): UseCounterReturn {
const count = ref<number>(initialValue)
const doubleCount = computed<number>(() => count.value * 2)
const increment = (): void => {
count.value++
}
const decrement = (): void => {
count.value--
}
const reset = (): void => {
count.value = initialValue
}
return {
count,
doubleCount,
increment,
decrement,
reset
}
}
typescript
// composables/useFetch.ts
import { ref, Ref, unref } from 'vue'
interface UseFetchOptions {
immediate?: boolean
headers?: HeadersInit
}
interface UseFetchReturn<T> {
data: Ref<T | null>
error: Ref<string | null>
loading: Ref<boolean>
execute: () => Promise<T>
}
export function useFetch<T = any>(
url: string | Ref<string>,
options: UseFetchOptions = {}
): UseFetchReturn<T> {
const data = ref<T | null>(null) as Ref<T | null>
const error = ref<string | null>(null)
const loading = ref<boolean>(false)
const execute = async (): Promise<T> => {
loading.value = true
error.value = null
try {
const response = await fetch(unref(url), {
headers: options.headers
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const result = await response.json()
data.value = result
return result
} catch (err) {
error.value = err instanceof Error ? err.message : String(err)
throw err
} finally {
loading.value = false
}
}
if (options.immediate) {
execute()
}
return {
data,
error,
loading,
execute
}
}
十二、最佳实践
12.1 项目结构
src/
├── api/ # API 接口
│ ├── modules/
│ │ ├── user.js
│ │ └── product.js
│ └── request.js
├── assets/ # 静态资源
│ ├── images/
│ ├── styles/
│ │ ├── variables.scss
│ │ └── global.scss
│ └── fonts/
├── components/ # 公共组件
│ ├── common/
│ │ ├── Button.vue
│ │ └── Modal.vue
│ └── layout/
│ ├── Header.vue
│ ├── Footer.vue
│ └── Sidebar.vue
├── composables/ # 组合式函数
│ ├── useCounter.js
│ ├── useFetch.js
│ └── useAuth.js
├── directives/ # 自定义指令
│ ├── focus.js
│ └── permission.js
├── layouts/ # 布局组件
│ ├── DefaultLayout.vue
│ └── AuthLayout.vue
├── plugins/ # 插件
│ ├── i18n.js
│ └── toast.js
├── router/ # 路由配置
│ └── index.js
├── stores/ # Pinia 状态管理
│ ├── user.js
│ └── cart.js
├── utils/ # 工具函数
│ ├── format.js
│ └── validate.js
├── views/ # 页面组件
│ ├── home/
│ │ └── index.vue
│ └── user/
│ ├── Profile.vue
│ └── Settings.vue
├── App.vue
└── main.js
12.2 代码规范
javascript
// 组件命名规范
// 使用 PascalCase 命名组件文件
// MyComponent.vue
// 在模板中使用 kebab-case
<my-component />
// 组合式函数命名
// use + 功能名
useCounter()
useLocalStorage()
useFetch()
// 自定义指令命名
// v + 驼峰式
vFocus
vClickOutside
// 事件命名
// 使用 kebab-case
@update-value
// Props 命名
// 使用 camelCase(JS)和 kebab-case(模板)
const props = defineProps({
userName: String // JS 中
})
// 模板中::user-name
// 常量命名
// 使用 UPPER_SNAKE_CASE
const MAX_COUNT = 100
const API_BASE_URL = 'https://api.example.com'
Vue 3 的世界充满创新和活力,愿本文成为你 Vue 学习之路上的重要指南。持续学习,深入实践,你一定能成为 Vue 3 专家!
来源:
http://unbgv.cn/