Vue 最佳实践
目录
快速入门
安装与版本
当前使用版本为 Vue 3.5.32。
Vue 3 需要 Node.js 20+ 环境。
使用 create-vue 创建项目(推荐)
# npm
npm create vue@latest
cd <your-project-name>
npm install
npm run dev
# pnpm
pnpm create vue@latest
cd <your-project-name>
pnpm install
pnpm run dev
# yarn
yarn create vue@latest
cd <your-project-name>
yarn
yarn dev使用 Vite 创建项目
# npm
npm create vite@latest my-vue-app -- --template vue-ts
cd my-vue-app
npm install
npm run dev
# pnpm
pnpm create vite my-vue-app --template vue-ts
cd my-vue-app
pnpm install
pnpm run devCDN 方式(无构建步骤)
<!-- 使用 ESM 构建 -->
<script type="module">
import { createApp } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
createApp({ data() { return { message: 'Hello Vue!' } } }).mount('#app')
</script>Vue 3 vs Vue 2 主要变化
| 特性 | Vue 2 | Vue 3 |
|---|---|---|
| 响应式 | defineProperty |
Proxy |
| 组件定义 | Options API | Composition API |
| TypeScript | 通过装饰器 | 原生支持 |
| 悬悬组件 | slot |
v-slot |
| 全局状态 | Vuex | Pinia |
Composition API
Vue 3 的核心特性,使用 script setup 语法:
基本结构
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
// 响应式状态
const count = ref(0)
const message = ref('Hello')
// 计算属性
const doubled = computed(() => count.value * 2)
// 方法
function increment() {
count.value++
}
// 生命周期
onMounted(() => {
console.log('Component mounted')
})
</script>
<template>
<div>
<p>{{ message }} {{ count }}</p>
<p>Doubled: {{ doubled }}</p>
<button @click="increment">Increment</button>
</div>
</template>reactive
import { reactive } from 'vue'
// 对象响应式
const state = reactive({
count: 0,
user: {
name: 'Alice',
age: 30
}
})
// 修改
state.count++
state.user.name = 'Bob'解构与响应式
import { reactive, toRefs } from 'vue'
const state = reactive({
count: 0,
message: 'Hello'
})
// 解构后保持响应式
const { count, message } = toRefs(state)TypeScript 集成
定义 Props
import { defineProps, PropType } from 'vue'
interface User {
id: number
name: string
email: string
}
const props = defineProps<{
title: string
users: User[]
onSave: (user: User) => void
}>()定义 Emits
const emit = defineEmits<{
(e: 'update', value: string): void
(e: 'delete', id: number): void
}>()
// 使用
emit('update', 'new value')
emit('delete', 1)模板引用
import { ref, onMounted } from 'vue'
const inputRef = ref<HTMLInputElement | null>(null)
onMounted(() => {
inputRef.value?.focus()
})组件结构
单文件组件结构
<script setup lang="ts">
// 1. 导入
import { ref } from 'vue'
import BaseButton from '@/components/BaseButton.vue'
// 2. Props 和 Emits
const props = defineProps<{ title: string }>()
const emit = defineEmits<{ (e: 'submit'): void }>()
// 3. 响应式状态
const loading = ref(false)
// 4. 计算属性
const isValid = computed(() => props.title.length > 0)
// 5. 方法
async function handleSubmit() {
loading.value = true
try {
emit('submit')
} finally {
loading.value = false
}
}
</script>
<template>
<!-- 模板 -->
</template>
<style scoped>
/* 组件样式 */
</style>Pinia 状态管理
安装
npm install pinia创建 Store
// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
// 状态
const users = ref<User[]>([])
const currentUser = ref<User | null>(null)
// 计算属性
const userCount = computed(() => users.value.length)
const isLoggedIn = computed(() => currentUser.value !== null)
// 方法
async function fetchUsers() {
const response = await api.get('/users')
users.value = response.data
}
async function login(credentials: Credentials) {
const response = await api.post('/login', credentials)
currentUser.value = response.data
}
function logout() {
currentUser.value = null
}
return {
users,
currentUser,
userCount,
isLoggedIn,
fetchUsers,
login,
logout
}
})组件中使用
<script setup lang="ts">
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
// 使用
console.log(userStore.userCount)
userStore.login({ email: '[email protected]', password: '123' })
</script>Vue Router
配置
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue')
},
{
path: '/users/:id',
name: 'UserDetail',
component: () => import('@/views/UserDetail.vue'),
props: true
},
{
path: '/admin',
component: () => import('@/views/Admin.vue'),
meta: { requiresAuth: true }
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
// 导航守卫
router.beforeEach((to) => {
if (to.meta.requiresAuth && !isAuthenticated()) {
return '/login'
}
})
export default router组件中使用
<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
// 获取参数
const userId = route.params.id
// 导航
router.push('/dashboard')
</script>组合式函数 (Composables)
封装可复用的逻辑:
创建
// composables/useFetch.ts
import { ref, onMounted } from 'vue'
export function useFetch<T>(url: string) {
const data = ref<T | null>(null)
const loading = ref(false)
const error = ref<Error | null>(null)
async function fetchData() {
loading.value = true
error.value = null
try {
const response = await fetch(url)
data.value = await response.json()
} catch (e) {
error.value = e as Error
} finally {
loading.value = false
}
}
onMounted(fetchData)
return { data, loading, error, refetch: fetchData }
}使用
<script setup lang="ts">
import { useFetch } from '@/composables/useFetch'
interface User {
id: number
name: string
}
const { data: users, loading, error, refetch } = useFetch<User[]>('/api/users')
</script>性能优化
懒加载组件
<!-- 静态导入 -->
<script setup>
import HeavyComponent from './HeavyComponent.vue'
</script>
<!-- 动态导入(懒加载) -->
<script setup>
import { defineAsyncComponent } from 'vue'
const HeavyComponent = defineAsyncComponent(() =>
import('./HeavyComponent.vue')
)
</script>v-memo
<template>
<div v-for="item in list" :key="item.id" v-memo="[item.id === selectedId]">
<!-- 只有 selectedId 变化时更新 -->
</div>
</template>KeepAlive
<template>
<keep-alive include="UserList,ProductList">
<router-view />
</keep-alive>
</template>样式指南
Scoped CSS
<style scoped>
.button {
padding: 8px 16px;
border-radius: 4px;
}
/* 深度选择器 */
:deep(.el-input) {
border: none;
}
</style>CSS 变量
<style scoped>
:root {
--primary-color: #42b983;
--button-padding: 8px 16px;
}
.button {
background: var(--primary-color);
padding: var(--button-padding);
}
</style>测试
Vitest
npm install -D vitest @vue/test-utils jsdom组件测试
// components/Button.spec.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Button from './Button.vue'
describe('Button', () => {
it('renders with correct text', () => {
const wrapper = mount(Button, {
props: { label: 'Click me' }
})
expect(wrapper.text()).toContain('Click me')
})
it('emits click event', async () => {
const wrapper = mount(Button, {
props: { label: 'Click me' }
})
await wrapper.trigger('click')
expect(wrapper.emitted()).toHaveProperty('click')
})
})