引言
在现代前端开发中,构建高性能、可维护的应用程序已成为开发者的首要任务。Vue 3、TypeScript 和 Vite 的组合为开发者提供了强大的工具链,能够构建出既高效又可靠的前端应用。本文将从零开始,详细介绍如何使用这些技术构建现代化前端应用,并深入探讨性能优化策略。
项目初始化与环境搭建
1.1 创建项目
首先,我们需要使用 Vite 创建一个新的 Vue 3 项目。Vite 是一个现代化的前端构建工具,它提供了极快的开发服务器启动速度和热更新体验。
# 使用 npm
npm create vite@latest my-vue-app -- --template vue-ts
# 使用 yarn
yarn create vite my-vue-app --template vue-ts
# 使用 pnpm
pnpm create vite my-vue-app --template vue-ts
选择 vue-ts 模板会自动配置 TypeScript 支持。项目创建完成后,进入项目目录并安装依赖:
cd my-vue-app
npm install
1.2 项目结构分析
创建的项目结构如下:
my-vue-app/
├── public/
│ └── vite.svg
├── src/
│ ├── assets/
│ │ └── vue.svg
│ ├── components/
│ │ └── HelloWorld.vue
│ ├── views/
│ ├── router/
│ ├── store/
│ ├── styles/
│ ├── utils/
│ ├── App.vue
│ └── main.ts
├── env.d.ts
├── index.html
├── package.json
├── tsconfig.json
├── vite.config.ts
└── README.md
1.3 TypeScript 配置
在 tsconfig.json 中,我们通常会看到这样的配置:
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Node",
"strict": true,
"jsx": "preserve",
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"noEmit": true
},
"include": ["src/**/*", "src/**/*.vue"]
}
这个配置确保了 TypeScript 能够正确处理 Vue 组件和现代 JavaScript 特性。
组件设计与开发
2.1 Vue 3 组件基础
Vue 3 引入了 Composition API,提供了更灵活的组件逻辑组织方式。让我们创建一个简单的用户卡片组件:
<!-- src/components/UserCard.vue -->
<template>
<div class="user-card" :class="{ 'is-loading': loading }">
<div v-if="loading" class="loading-placeholder">
<div class="skeleton-line"></div>
<div class="skeleton-line"></div>
<div class="skeleton-line"></div>
</div>
<div v-else class="user-content">
<img :src="user.avatar" :alt="user.name" class="avatar" />
<div class="user-info">
<h3 class="user-name">{{ user.name }}</h3>
<p class="user-email">{{ user.email }}</p>
<div class="user-actions">
<button @click="handleEdit" class="btn btn-edit">编辑</button>
<button @click="handleDelete" class="btn btn-delete">删除</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, defineProps, defineEmits } from 'vue'
// 定义 props
interface User {
id: number
name: string
email: string
avatar: string
}
const props = defineProps<{
user: User
loading?: boolean
}>()
// 定义 emits
const emit = defineEmits<{
(e: 'edit', id: number): void
(e: 'delete', id: number): void
}>()
// 处理编辑事件
const handleEdit = () => {
emit('edit', props.user.id)
}
// 处理删除事件
const handleDelete = () => {
emit('delete', props.user.id)
}
</script>
<style scoped lang="scss">
.user-card {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 16px;
margin: 16px 0;
background: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
&.is-loading {
.user-content {
display: none;
}
.loading-placeholder {
.skeleton-line {
height: 12px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: loading 1.5s infinite;
margin-bottom: 8px;
border-radius: 4px;
}
.skeleton-line:last-child {
margin-bottom: 0;
}
}
}
}
.avatar {
width: 60px;
height: 60px;
border-radius: 50%;
object-fit: cover;
margin-right: 16px;
}
.user-info {
display: flex;
flex-direction: column;
}
.user-name {
margin: 0 0 8px 0;
font-size: 18px;
font-weight: 600;
}
.user-email {
margin: 0 0 16px 0;
color: #666;
font-size: 14px;
}
.user-actions {
display: flex;
gap: 8px;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
&.btn-edit {
background: #007bff;
color: white;
}
&.btn-delete {
background: #dc3545;
color: white;
}
}
@keyframes loading {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
</style>
2.2 组件通信机制
在 Vue 3 中,组件间通信主要通过 props、emits 和 provide/inject 实现:
<!-- src/components/ParentComponent.vue -->
<template>
<div>
<h2>父组件</h2>
<UserCard
:user="currentUser"
:loading="loading"
@edit="handleEdit"
@delete="handleDelete"
/>
<button @click="fetchUser">获取用户</button>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import UserCard from './UserCard.vue'
interface User {
id: number
name: string
email: string
avatar: string
}
const currentUser = ref<User | null>(null)
const loading = ref(false)
const fetchUser = async () => {
loading.value = true
try {
// 模拟 API 调用
const response = await fetch('/api/user')
currentUser.value = await response.json()
} catch (error) {
console.error('获取用户失败:', error)
} finally {
loading.value = false
}
}
const handleEdit = (id: number) => {
console.log('编辑用户:', id)
}
const handleDelete = (id: number) => {
console.log('删除用户:', id)
currentUser.value = null
}
</script>
状态管理
3.1 Pinia 状态管理
Pinia 是 Vue 3 推荐的状态管理库,它提供了更简洁的 API 和更好的 TypeScript 支持:
npm install pinia
// src/stores/userStore.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export interface User {
id: number
name: string
email: string
avatar: string
}
export const useUserStore = defineStore('user', () => {
const users = ref<User[]>([])
const currentUser = ref<User | null>(null)
const loading = ref(false)
const error = ref<string | null>(null)
// 获取用户列表
const fetchUsers = async () => {
loading.value = true
error.value = null
try {
const response = await fetch('/api/users')
users.value = await response.json()
} catch (err) {
error.value = '获取用户列表失败'
console.error(err)
} finally {
loading.value = false
}
}
// 获取单个用户
const fetchUser = async (id: number) => {
loading.value = true
error.value = null
try {
const response = await fetch(`/api/users/${id}`)
currentUser.value = await response.json()
} catch (err) {
error.value = '获取用户失败'
console.error(err)
} finally {
loading.value = false
}
}
// 创建用户
const createUser = async (userData: Omit<User, 'id'>) => {
loading.value = true
error.value = null
try {
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userData),
})
const newUser = await response.json()
users.value.push(newUser)
return newUser
} catch (err) {
error.value = '创建用户失败'
console.error(err)
} finally {
loading.value = false
}
}
// 更新用户
const updateUser = async (id: number, userData: Partial<User>) => {
loading.value = true
error.value = null
try {
const response = await fetch(`/api/users/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userData),
})
const updatedUser = await response.json()
const index = users.value.findIndex(user => user.id === id)
if (index !== -1) {
users.value[index] = updatedUser
}
if (currentUser.value?.id === id) {
currentUser.value = updatedUser
}
return updatedUser
} catch (err) {
error.value = '更新用户失败'
console.error(err)
} finally {
loading.value = false
}
}
// 删除用户
const deleteUser = async (id: number) => {
loading.value = true
error.value = null
try {
await fetch(`/api/users/${id}`, {
method: 'DELETE',
})
users.value = users.value.filter(user => user.id !== id)
if (currentUser.value?.id === id) {
currentUser.value = null
}
} catch (err) {
error.value = '删除用户失败'
console.error(err)
} finally {
loading.value = false
}
}
// 计算属性
const userCount = computed(() => users.value.length)
const hasError = computed(() => error.value !== null)
return {
users,
currentUser,
loading,
error,
userCount,
hasError,
fetchUsers,
fetchUser,
createUser,
updateUser,
deleteUser,
}
})
3.2 在组件中使用状态管理
<!-- src/views/UserList.vue -->
<template>
<div class="user-list">
<h1>用户列表</h1>
<div v-if="loading" class="loading">
<div class="spinner"></div>
<p>加载中...</p>
</div>
<div v-else-if="hasError" class="error">
<p>{{ error }}</p>
<button @click="fetchUsers">重试</button>
</div>
<div v-else>
<p>总用户数: {{ userCount }}</p>
<div class="users-grid">
<UserCard
v-for="user in users"
:key="user.id"
:user="user"
@edit="handleEdit"
@delete="handleDelete"
/>
</div>
</div>
<button @click="showCreateModal = true" class="btn btn-primary">
添加用户
</button>
<CreateUserModal
v-if="showCreateModal"
@close="showCreateModal = false"
@created="handleUserCreated"
/>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useUserStore } from '../stores/userStore'
import UserCard from '../components/UserCard.vue'
import CreateUserModal from '../components/CreateUserModal.vue'
const userStore = useUserStore()
const { users, loading, error, hasError, userCount, fetchUsers } = userStore
const showCreateModal = ref(false)
onMounted(() => {
fetchUsers()
})
const handleEdit = (id: number) => {
console.log('编辑用户:', id)
}
const handleDelete = async (id: number) => {
if (confirm('确定要删除这个用户吗?')) {
await userStore.deleteUser(id)
}
}
const handleUserCreated = () => {
showCreateModal.value = false
fetchUsers()
}
</script>
<style scoped lang="scss">
.user-list {
padding: 20px;
}
.loading {
text-align: center;
padding: 40px;
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 16px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
}
.error {
text-align: center;
padding: 40px;
color: #dc3545;
}
.users-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
margin: 20px 0;
}
.btn-primary {
background: #007bff;
color: white;
padding: 12px 24px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
margin-top: 20px;
}
</style>
路由配置
4.1 路由设置
npm install vue-router@4
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from '../stores/userStore'
const routes = [
{
path: '/',
redirect: '/users'
},
{
path: '/users',
name: 'UserList',
component: () => import('../views/UserList.vue'),
meta: { requiresAuth: true }
},
{
path: '/users/:id',
name: 'UserDetail',
component: () => import('../views/UserDetail.vue'),
meta: { requiresAuth: true }
},
{
path: '/login',
name: 'Login',
component: () => import('../views/Login.vue'),
meta: { requiresGuest: true }
}
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes
})
// 路由守卫
router.beforeEach(async (to, from, next) => {
const userStore = useUserStore()
// 检查是否需要登录
if (to.meta.requiresAuth && !userStore.currentUser) {
next('/login')
return
}
// 检查是否需要未登录用户
if (to.meta.requiresGuest && userStore.currentUser) {
next('/users')
return
}
next()
})
export default router
4.2 主应用配置
// src/main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')
性能优化策略
5.1 代码分割与懒加载
Vue 3 支持动态导入和懒加载,可以有效减少初始包大小:
// 路由中的懒加载
const routes = [
{
path: '/users',
component: () => import('../views/UserList.vue')
},
{
path: '/dashboard',
component: () => import('../views/Dashboard.vue')
}
]
5.2 组件缓存优化
使用 keep-alive 缓存组件状态:
<!-- src/App.vue -->
<template>
<div id="app">
<router-view v-slot="{ Component }">
<keep-alive :include="cachedComponents">
<component :is="Component" />
</keep-alive>
</router-view>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const cachedComponents = ref(['UserList', 'Dashboard'])
</script>
5.3 图片优化
实现图片懒加载和响应式图片:
<!-- src/components/LazyImage.vue -->
<template>
<img
:src="src"
:alt="alt"
:class="{ 'lazy-loaded': loaded, 'lazy-placeholder': !loaded }"
@load="onLoad"
@error="onError"
/>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
const props = defineProps<{
src: string
alt?: string
}>()
const loaded = ref(false)
const observer = ref<IntersectionObserver | null>(null)
const onLoad = () => {
loaded.value = true
}
const onError = () => {
console.error('图片加载失败:', props.src)
}
const handleIntersection = (entries: IntersectionObserverEntry[]) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target as HTMLImageElement
img.src = props.src
observer.value?.unobserve(img)
}
})
}
onMounted(() => {
if ('IntersectionObserver' in window) {
observer.value = new IntersectionObserver(handleIntersection, {
rootMargin: '50px'
})
const img = document.querySelector('img')
if (img) {
observer.value.observe(img)
}
}
})
onUnmounted(() => {
if (observer.value) {
observer.value.disconnect()
}
})
</script>
<style scoped>
.lazy-loaded {
opacity: 1;
transition: opacity 0.3s ease;
}
.lazy-placeholder {
opacity: 0.5;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: loading 1.5s infinite;
}
@keyframes loading {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
</style>
5.4 状态管理优化
使用 Pinia 的持久化存储:
npm install pinia-plugin-persistedstate
// src/stores/index.ts
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
export default pinia
5.5 构建优化
配置 Vite 优化选项:
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [
vue(),
],
resolve: {
alias: {
'@': resolve(__dirname, './src'),
},
},
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['vue', 'vue-router', 'pinia'],
ui: ['element-plus'],
},
},
},
},
optimizeDeps: {
include: ['vue', 'vue-router', 'pinia'],
},
})
TypeScript 最佳实践
6.1 类型定义
为 API 响应和组件 props 创建严格的类型定义:
// src/types/user.ts
export interface User {
id: number
name: string
email: string
avatar: string
createdAt: string
updatedAt: string
}
export interface UserFormData {
name: string
email: string
avatar?: string
}
export interface ApiResponse<T> {
data: T
message?: string
status: number
}
6.2 泛型和工具类型
使用 TypeScript 泛型和工具类型提高代码复用性:
// src/utils/api.ts
import { User } from '../types/user'
export type ApiResponse<T> = {
data: T
message?: string
status: number
}
export type PaginatedResponse<T> = ApiResponse<{
items: T[]
total: number
page: number
pageSize: number
}>
export async function fetchWithAuth<T>(
url: string,
options?: RequestInit
): Promise<T> {
const token = localStorage.getItem('authToken')
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
...options?.headers,
},
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
return response.json()
}
6.3 组件类型安全
确保组件的类型安全:
<!-- src/components/UserForm.vue -->
<template>
<form @submit.prevent="handleSubmit" class="user-form">
<div class="form-group">
<label for="name">姓名</label>
<input
id="name"
v-model="form.name"
type="text"
required
class="form-input"
/>
</div>
<div class="form-group">
<label for="email">邮箱</label>
<input
id="email"
v-model="form.email"
type="email"
required
class="form-input"
/>
</div>
<div class="form-group">
<label for="avatar">头像</label>
<input
id="avatar"
v-model="form.avatar"
type="url"
class="form-input"
/>
</div>
<button type="submit" :disabled="loading" class="btn btn-primary">
{{ loading ? '提交中...' : '提交' }}
</button>
</form>
</template>
<script setup lang="ts">
import { ref, defineEmits } from 'vue'
import { UserFormData } from '../types/user'
const props = defineProps<{
loading?: boolean
}>()
const emit = defineEmits<{
(e: 'submit', data: UserFormData): void
}>()
const form = ref<UserFormData>({
name: '',
email: '',
avatar: '',
})
const handleSubmit = () => {
emit('submit', form.value)
}
</script>
<style scoped lang="scss">
.user-form {
max-width: 500px;
margin: 0 auto;
}
.form-group {
margin-bottom: 16px;
}
.form-input {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
&.btn-primary {
background: #007bff;
color: white;
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
</style>
测试策略
7.1 单元测试
使用 Vitest 和 Vue Test Utils 进行测试:
// src/components/UserCard.spec.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import UserCard from './UserCard.vue'
describe('UserCard', () => {
const mockUser = {
id: 1,
name: '张三',
email: 'zhangsan@example.com',
avatar: '/avatar.jpg'
}
it('renders user information correctly', () => {
const wrapper = mount(UserCard, {
props: {
user: mockUser
}
})
expect(wrapper.text()).toContain('张三')
expect(wrapper.text()).toContain('zhangsan@example.com')
expect(wrapper.find('img').attributes('src')).toBe('/avatar.jpg')
})
it('emits edit event when edit button is clicked', async () => {
const wrapper = mount(UserCard, {
props: {
user: mockUser
}
})
await wrapper.find('.btn-edit').trigger('click')
expect(wrapper.emitted('edit')).toBeTruthy()
expect(wrapper.emitted('edit')?.[0]).toEqual([1])
})
it('emits delete event when delete button is clicked', async () => {
const wrapper = mount(UserCard, {
props: {
user: mockUser
}
})
await wrapper.find('.btn-delete').trigger('click')
expect(wrapper.emitted('delete')).toBeTruthy()
expect(wrapper.emitted('delete')?.[0]).toEqual([1])
})
})
``
评论 (0)