Vue 3 企业级应用架构设计与最佳实践
Orion K Lv6

Vue 3 企业级应用架构设计与最佳实践

随着 Vue 3 的正式发布和生态系统的不断完善,越来越多的企业开始采用 Vue 3 构建大型前端应用。本文将深入探讨如何使用 Vue 3 设计和构建企业级应用架构,涵盖模块化开发、TypeScript 集成、微前端架构等关键技术。

企业级应用架构设计原则

1. 分层架构设计

企业级应用需要清晰的分层架构来保证代码的可维护性和可扩展性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// src/types/architecture.ts
/**
* 应用架构分层定义
* @description 定义企业级应用的分层架构
*/
export interface ArchitectureLayers {
presentation: PresentationLayer; // 表现层
business: BusinessLayer; // 业务层
data: DataLayer; // 数据层
infrastructure: InfrastructureLayer; // 基础设施层
}

/**
* 表现层接口定义
* @description 负责用户界面和用户交互
*/
export interface PresentationLayer {
components: ComponentModule[]; // 组件模块
pages: PageModule[]; // 页面模块
layouts: LayoutModule[]; // 布局模块
directives: DirectiveModule[]; // 指令模块
}

/**
* 业务层接口定义
* @description 负责业务逻辑处理
*/
export interface BusinessLayer {
services: ServiceModule[]; // 服务模块
stores: StoreModule[]; // 状态管理模块
composables: ComposableModule[]; // 组合式函数模块
validators: ValidatorModule[]; // 验证器模块
}

2. 模块化开发策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// src/modules/user/index.ts
/**
* 用户模块统一导出
* @description 用户相关功能的模块化封装
*/
export { default as UserService } from './services/UserService';
export { default as UserStore } from './stores/UserStore';
export { default as UserComponents } from './components';
export { default as UserRoutes } from './routes';
export * from './types';
export * from './composables';

// src/modules/user/services/UserService.ts
/**
* 用户服务类
* @description 处理用户相关的业务逻辑
*/
import { ApiClient } from '@/infrastructure/http';
import type { User, CreateUserDto, UpdateUserDto } from '../types';

export class UserService {
private apiClient: ApiClient;

constructor(apiClient: ApiClient) {
this.apiClient = apiClient;
}

/**
* 获取用户列表
* @param params 查询参数
* @returns Promise<User[]> 用户列表
* @throws {ApiError} 当请求失败时抛出异常
*/
async getUsers(params?: Record<string, any>): Promise<User[]> {
try {
const response = await this.apiClient.get('/users', { params });
return response.data;
} catch (error) {
throw new Error(`获取用户列表失败: ${error.message}`);
}
}

/**
* 创建用户
* @param userData 用户数据
* @returns Promise<User> 创建的用户信息
* @throws {ValidationError} 当数据验证失败时抛出异常
*/
async createUser(userData: CreateUserDto): Promise<User> {
try {
const response = await this.apiClient.post('/users', userData);
return response.data;
} catch (error) {
throw new Error(`创建用户失败: ${error.message}`);
}
}
}

export default UserService;

3. TypeScript 集成与类型安全

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
// src/types/api.ts
/**
* API 响应基础类型定义
* @description 统一的 API 响应格式
*/
export interface ApiResponse<T = any> {
code: number;
message: string;
data: T;
timestamp: number;
}

/**
* 分页响应类型定义
* @description 分页数据的响应格式
*/
export interface PaginatedResponse<T> extends ApiResponse<T[]> {
pagination: {
page: number;
pageSize: number;
total: number;
totalPages: number;
};
}

// src/infrastructure/http/ApiClient.ts
/**
* API 客户端类
* @description 封装 HTTP 请求,提供类型安全的 API 调用
*/
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import type { ApiResponse } from '@/types/api';

export class ApiClient {
private instance: AxiosInstance;

constructor(baseURL: string, timeout: number = 10000) {
this.instance = axios.create({
baseURL,
timeout,
headers: {
'Content-Type': 'application/json',
},
});

this.setupInterceptors();
}

/**
* 设置请求和响应拦截器
* @description 统一处理请求头、错误处理等
*/
private setupInterceptors(): void {
// 请求拦截器
this.instance.interceptors.request.use(
(config) => {
const token = localStorage.getItem('access_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);

// 响应拦截器
this.instance.interceptors.response.use(
(response: AxiosResponse<ApiResponse>) => {
if (response.data.code !== 200) {
throw new Error(response.data.message);
}
return response;
},
(error) => {
if (error.response?.status === 401) {
// 处理未授权错误
localStorage.removeItem('access_token');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
}

/**
* GET 请求
* @param url 请求地址
* @param config 请求配置
* @returns Promise<AxiosResponse<ApiResponse<T>>> 响应数据
*/
async get<T = any>(
url: string,
config?: AxiosRequestConfig
): Promise<AxiosResponse<ApiResponse<T>>> {
return this.instance.get(url, config);
}

/**
* POST 请求
* @param url 请求地址
* @param data 请求数据
* @param config 请求配置
* @returns Promise<AxiosResponse<ApiResponse<T>>> 响应数据
*/
async post<T = any>(
url: string,
data?: any,
config?: AxiosRequestConfig
): Promise<AxiosResponse<ApiResponse<T>>> {
return this.instance.post(url, data, config);
}
}

状态管理架构设计

1. Pinia 企业级状态管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
// src/stores/modules/app.ts
/**
* 应用全局状态管理
* @description 管理应用级别的状态,如主题、语言、菜单等
*/
import { defineStore } from 'pinia';
import type { Theme, Language, MenuItem } from '@/types/app';

export const useAppStore = defineStore('app', () => {
// 状态定义
const theme = ref<Theme>('light');
const language = ref<Language>('zh-CN');
const menuCollapsed = ref(false);
const menuItems = ref<MenuItem[]>([]);
const loading = ref(false);

// 计算属性
const isDarkTheme = computed(() => theme.value === 'dark');
const currentLocale = computed(() => {
return language.value === 'zh-CN' ? 'zh' : 'en';
});

/**
* 切换主题
* @param newTheme 新主题
* @description 切换应用主题并持久化到本地存储
*/
const toggleTheme = (newTheme?: Theme) => {
theme.value = newTheme || (theme.value === 'light' ? 'dark' : 'light');
localStorage.setItem('app_theme', theme.value);
document.documentElement.setAttribute('data-theme', theme.value);
};

/**
* 切换语言
* @param newLanguage 新语言
* @description 切换应用语言并重新加载菜单
*/
const changeLanguage = async (newLanguage: Language) => {
language.value = newLanguage;
localStorage.setItem('app_language', newLanguage);
await loadMenuItems();
};

/**
* 加载菜单项
* @description 根据用户权限和语言加载菜单
*/
const loadMenuItems = async () => {
try {
loading.value = true;
// 这里应该调用 API 获取菜单数据
const response = await fetch(`/api/menus?lang=${language.value}`);
const data = await response.json();
menuItems.value = data.data;
} catch (error) {
console.error('加载菜单失败:', error);
} finally {
loading.value = false;
}
};

/**
* 初始化应用状态
* @description 从本地存储恢复状态并初始化应用
*/
const initializeApp = async () => {
// 恢复主题设置
const savedTheme = localStorage.getItem('app_theme') as Theme;
if (savedTheme) {
toggleTheme(savedTheme);
}

// 恢复语言设置
const savedLanguage = localStorage.getItem('app_language') as Language;
if (savedLanguage) {
language.value = savedLanguage;
}

// 加载菜单
await loadMenuItems();
};

return {
// 状态
theme,
language,
menuCollapsed,
menuItems,
loading,
// 计算属性
isDarkTheme,
currentLocale,
// 方法
toggleTheme,
changeLanguage,
loadMenuItems,
initializeApp,
};
});

2. 状态持久化与同步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// src/plugins/pinia-persistence.ts
/**
* Pinia 状态持久化插件
* @description 自动持久化指定的状态到本地存储
*/
import type { PiniaPluginContext } from 'pinia';

interface PersistenceOptions {
key?: string;
storage?: Storage;
paths?: string[];
serializer?: {
serialize: (value: any) => string;
deserialize: (value: string) => any;
};
}

/**
* 创建持久化插件
* @param options 持久化配置选项
* @returns Pinia 插件函数
*/
export function createPersistencePlugin(options: PersistenceOptions = {}) {
return ({ store, options: storeOptions }: PiniaPluginContext) => {
const {
key = store.$id,
storage = localStorage,
paths,
serializer = {
serialize: JSON.stringify,
deserialize: JSON.parse,
},
} = { ...options, ...storeOptions.persist };

// 从存储中恢复状态
const restoreState = () => {
try {
const stored = storage.getItem(key);
if (stored) {
const state = serializer.deserialize(stored);
if (paths) {
// 只恢复指定路径的状态
paths.forEach(path => {
if (path in state) {
store.$patch({ [path]: state[path] });
}
});
} else {
store.$patch(state);
}
}
} catch (error) {
console.error(`恢复状态失败 (${key}):`, error);
}
};

// 保存状态到存储
const saveState = () => {
try {
let stateToSave = store.$state;
if (paths) {
stateToSave = paths.reduce((acc, path) => {
if (path in store.$state) {
acc[path] = store.$state[path];
}
return acc;
}, {} as any);
}
storage.setItem(key, serializer.serialize(stateToSave));
} catch (error) {
console.error(`保存状态失败 (${key}):`, error);
}
};

// 恢复状态
restoreState();

// 监听状态变化并自动保存
store.$subscribe(() => {
saveState();
});
};
}

组件架构设计

1. 基础组件库架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
// src/components/base/Button/Button.vue
<template>
<button
:class="buttonClasses"
:disabled="disabled || loading"
:type="htmlType"
@click="handleClick"
>
<Icon v-if="loading" name="loading" class="animate-spin" />
<Icon v-else-if="icon" :name="icon" />
<span v-if="$slots.default" class="button-content">
<slot />
</span>
</button>
</template>

<script setup lang="ts">
/**
* 基础按钮组件
* @description 企业级应用的基础按钮组件,支持多种样式和状态
*/
import { computed } from 'vue';
import Icon from '../Icon/Icon.vue';

// 组件属性定义
interface ButtonProps {
type?: 'primary' | 'secondary' | 'success' | 'warning' | 'danger';
size?: 'small' | 'medium' | 'large';
variant?: 'solid' | 'outline' | 'ghost';
disabled?: boolean;
loading?: boolean;
icon?: string;
htmlType?: 'button' | 'submit' | 'reset';
block?: boolean;
}

// 事件定义
interface ButtonEmits {
click: [event: MouseEvent];
}

const props = withDefaults(defineProps<ButtonProps>(), {
type: 'primary',
size: 'medium',
variant: 'solid',
disabled: false,
loading: false,
htmlType: 'button',
block: false,
});

const emit = defineEmits<ButtonEmits>();

// 计算按钮样式类
const buttonClasses = computed(() => {
return [
'btn',
`btn--${props.type}`,
`btn--${props.size}`,
`btn--${props.variant}`,
{
'btn--disabled': props.disabled,
'btn--loading': props.loading,
'btn--block': props.block,
},
];
});

/**
* 处理按钮点击事件
* @param event 鼠标事件
* @description 在非禁用和非加载状态下触发点击事件
*/
const handleClick = (event: MouseEvent) => {
if (!props.disabled && !props.loading) {
emit('click', event);
}
};
</script>

<style scoped>
.btn {
@apply inline-flex items-center justify-center gap-2 px-4 py-2 text-sm font-medium rounded-md transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2;
}

.btn--small {
@apply px-3 py-1.5 text-xs;
}

.btn--large {
@apply px-6 py-3 text-base;
}

.btn--block {
@apply w-full;
}

.btn--primary.btn--solid {
@apply bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500;
}

.btn--primary.btn--outline {
@apply border-2 border-blue-600 text-blue-600 hover:bg-blue-50 focus:ring-blue-500;
}

.btn--disabled {
@apply opacity-50 cursor-not-allowed;
}

.animate-spin {
animation: spin 1s linear infinite;
}

@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
</style>

2. 高阶组件模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// src/components/hoc/withLoading.ts
/**
* 加载状态高阶组件
* @description 为组件添加加载状态功能
*/
import { defineComponent, h, ref, type Component } from 'vue';
import LoadingSpinner from '@/components/base/LoadingSpinner.vue';

interface WithLoadingOptions {
loadingText?: string;
showOverlay?: boolean;
}

/**
* 创建带加载状态的高阶组件
* @param WrappedComponent 被包装的组件
* @param options 配置选项
* @returns 包装后的组件
*/
export function withLoading<T extends Component>(
WrappedComponent: T,
options: WithLoadingOptions = {}
) {
return defineComponent({
name: `WithLoading(${WrappedComponent.name || 'Component'})`,
props: {
loading: {
type: Boolean,
default: false,
},
},
setup(props, { attrs, slots }) {
const { loadingText = '加载中...', showOverlay = true } = options;

return () => {
const wrappedComponent = h(WrappedComponent, attrs, slots);

if (!props.loading) {
return wrappedComponent;
}

if (showOverlay) {
return h('div', { class: 'relative' }, [
wrappedComponent,
h(
'div',
{
class: 'absolute inset-0 bg-white bg-opacity-75 flex items-center justify-center z-10',
},
[
h(LoadingSpinner, { text: loadingText }),
]
),
]);
}

return h('div', { class: 'space-y-4' }, [
h(LoadingSpinner, { text: loadingText }),
wrappedComponent,
]);
};
},
});
}

路由架构设计

1. 模块化路由配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// src/router/modules/user.ts
/**
* 用户模块路由配置
* @description 用户相关页面的路由定义
*/
import type { RouteRecordRaw } from 'vue-router';

const userRoutes: RouteRecordRaw[] = [
{
path: '/users',
name: 'Users',
component: () => import('@/layouts/DefaultLayout.vue'),
meta: {
title: '用户管理',
requiresAuth: true,
permissions: ['user:read'],
},
children: [
{
path: '',
name: 'UserList',
component: () => import('@/modules/user/pages/UserList.vue'),
meta: {
title: '用户列表',
keepAlive: true,
},
},
{
path: 'create',
name: 'UserCreate',
component: () => import('@/modules/user/pages/UserCreate.vue'),
meta: {
title: '创建用户',
},
},
{
path: ':id/edit',
name: 'UserEdit',
component: () => import('@/modules/user/pages/UserEdit.vue'),
meta: {
title: '编辑用户',
},
props: true,
},
],
},
];

export default userRoutes;

2. 路由守卫与权限控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
// src/router/guards/auth.ts
/**
* 认证路由守卫
* @description 处理用户认证和权限验证
*/
import type { NavigationGuardNext, RouteLocationNormalized } from 'vue-router';
import { useAuthStore } from '@/stores/modules/auth';
import { usePermissionStore } from '@/stores/modules/permission';

/**
* 认证守卫
* @param to 目标路由
* @param from 来源路由
* @param next 导航函数
* @description 检查用户是否已登录,未登录则跳转到登录页
*/
export async function authGuard(
to: RouteLocationNormalized,
from: RouteLocationNormalized,
next: NavigationGuardNext
) {
const authStore = useAuthStore();
const permissionStore = usePermissionStore();

// 检查是否需要认证
if (to.meta.requiresAuth) {
// 检查是否已登录
if (!authStore.isAuthenticated) {
// 尝试从 token 恢复用户信息
const token = localStorage.getItem('access_token');
if (token) {
try {
await authStore.getCurrentUser();
} catch (error) {
// token 无效,清除并跳转到登录页
authStore.logout();
next({
name: 'Login',
query: { redirect: to.fullPath },
});
return;
}
} else {
// 没有 token,跳转到登录页
next({
name: 'Login',
query: { redirect: to.fullPath },
});
return;
}
}

// 检查权限
if (to.meta.permissions) {
const hasPermission = permissionStore.hasPermissions(to.meta.permissions);
if (!hasPermission) {
next({ name: 'Forbidden' });
return;
}
}
}

next();
}

/**
* 权限守卫
* @param to 目标路由
* @param from 来源路由
* @param next 导航函数
* @description 检查用户是否有访问特定路由的权限
*/
export function permissionGuard(
to: RouteLocationNormalized,
from: RouteLocationNormalized,
next: NavigationGuardNext
) {
const permissionStore = usePermissionStore();

// 检查路由权限
if (to.meta.permissions) {
const hasPermission = permissionStore.hasPermissions(to.meta.permissions);
if (!hasPermission) {
next({ name: 'Forbidden' });
return;
}
}

// 检查角色权限
if (to.meta.roles) {
const hasRole = permissionStore.hasRoles(to.meta.roles);
if (!hasRole) {
next({ name: 'Forbidden' });
return;
}
}

next();
}

性能优化策略

1. 组件懒加载与代码分割

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// src/utils/lazy-loading.ts
/**
* 组件懒加载工具
* @description 提供组件懒加载和错误处理功能
*/
import { defineAsyncComponent, type AsyncComponentLoader } from 'vue';
import LoadingComponent from '@/components/base/LoadingComponent.vue';
import ErrorComponent from '@/components/base/ErrorComponent.vue';

interface LazyLoadOptions {
delay?: number;
timeout?: number;
suspensible?: boolean;
retryTimes?: number;
}

/**
* 创建懒加载组件
* @param loader 组件加载器函数
* @param options 懒加载配置选项
* @returns 异步组件
*/
export function createLazyComponent(
loader: AsyncComponentLoader,
options: LazyLoadOptions = {}
) {
const {
delay = 200,
timeout = 30000,
suspensible = false,
retryTimes = 3,
} = options;

let retryCount = 0;

const retryLoader = async () => {
try {
return await loader();
} catch (error) {
if (retryCount < retryTimes) {
retryCount++;
console.warn(`组件加载失败,正在重试 (${retryCount}/${retryTimes})...`);
// 延迟重试
await new Promise(resolve => setTimeout(resolve, 1000 * retryCount));
return retryLoader();
}
throw error;
}
};

return defineAsyncComponent({
loader: retryLoader,
loadingComponent: LoadingComponent,
errorComponent: ErrorComponent,
delay,
timeout,
suspensible,
});
}

/**
* 路由级别的懒加载
* @param importFn 动态导入函数
* @returns 路由组件
*/
export function lazyRoute(importFn: () => Promise<any>) {
return createLazyComponent(importFn, {
delay: 0,
timeout: 10000,
retryTimes: 2,
});
}

2. 虚拟滚动优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
<!-- src/components/base/VirtualList.vue -->
<template>
<div
ref="containerRef"
class="virtual-list"
:style="{ height: `${height}px` }"
@scroll="handleScroll"
>
<div
class="virtual-list-phantom"
:style="{ height: `${totalHeight}px` }"
></div>
<div
class="virtual-list-content"
:style="{
transform: `translateY(${offsetY}px)`,
}"
>
<div
v-for="item in visibleItems"
:key="getItemKey(item)"
class="virtual-list-item"
:style="{ height: `${itemHeight}px` }"
>
<slot :item="item" :index="item.index" />
</div>
</div>
</div>
</template>

<script setup lang="ts" generic="T">
/**
* 虚拟滚动列表组件
* @description 用于渲染大量数据的高性能列表组件
*/
import { ref, computed, onMounted, onUnmounted } from 'vue';

interface VirtualListProps<T> {
items: T[]; // 列表数据
itemHeight: number; // 每项高度
height: number; // 容器高度
buffer?: number; // 缓冲区大小
keyField?: keyof T; // 唯一键字段
}

const props = withDefaults(defineProps<VirtualListProps<T>>(), {
buffer: 5,
keyField: 'id' as keyof T,
});

// 容器引用
const containerRef = ref<HTMLElement>();

// 滚动位置
const scrollTop = ref(0);

// 计算总高度
const totalHeight = computed(() => props.items.length * props.itemHeight);

// 计算可见区域的起始和结束索引
const visibleRange = computed(() => {
const containerHeight = props.height;
const start = Math.floor(scrollTop.value / props.itemHeight);
const end = Math.min(
start + Math.ceil(containerHeight / props.itemHeight),
props.items.length
);

return {
start: Math.max(0, start - props.buffer),
end: Math.min(props.items.length, end + props.buffer),
};
});

// 计算可见项目
const visibleItems = computed(() => {
const { start, end } = visibleRange.value;
return props.items.slice(start, end).map((item, index) => ({
...item,
index: start + index,
}));
});

// 计算偏移量
const offsetY = computed(() => {
return visibleRange.value.start * props.itemHeight;
});

/**
* 获取项目唯一键
* @param item 列表项
* @returns 唯一键值
*/
const getItemKey = (item: T & { index: number }) => {
return item[props.keyField] || item.index;
};

/**
* 处理滚动事件
* @param event 滚动事件
* @description 更新滚动位置并触发重新计算
*/
const handleScroll = (event: Event) => {
const target = event.target as HTMLElement;
scrollTop.value = target.scrollTop;
};

// 防抖处理滚动事件
let scrollTimer: number | null = null;
const debouncedScroll = (event: Event) => {
if (scrollTimer) {
clearTimeout(scrollTimer);
}
scrollTimer = setTimeout(() => {
handleScroll(event);
}, 16); // 约 60fps
};

onMounted(() => {
if (containerRef.value) {
containerRef.value.addEventListener('scroll', debouncedScroll, {
passive: true,
});
}
});

onUnmounted(() => {
if (containerRef.value) {
containerRef.value.removeEventListener('scroll', debouncedScroll);
}
if (scrollTimer) {
clearTimeout(scrollTimer);
}
});
</script>

<style scoped>
.virtual-list {
position: relative;
overflow-y: auto;
}

.virtual-list-phantom {
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: -1;
}

.virtual-list-content {
position: absolute;
top: 0;
left: 0;
right: 0;
}

.virtual-list-item {
box-sizing: border-box;
}
</style>

测试策略

1. 单元测试配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// tests/unit/components/Button.test.ts
/**
* Button 组件单元测试
* @description 测试 Button 组件的各种功能和状态
*/
import { describe, it, expect, vi } from 'vitest';
import { mount } from '@vue/test-utils';
import Button from '@/components/base/Button/Button.vue';

describe('Button 组件', () => {
it('应该正确渲染默认按钮', () => {
const wrapper = mount(Button, {
slots: {
default: '点击我',
},
});

expect(wrapper.text()).toBe('点击我');
expect(wrapper.classes()).toContain('btn');
expect(wrapper.classes()).toContain('btn--primary');
expect(wrapper.classes()).toContain('btn--medium');
});

it('应该正确处理点击事件', async () => {
const handleClick = vi.fn();
const wrapper = mount(Button, {
props: {
onClick: handleClick,
},
slots: {
default: '点击我',
},
});

await wrapper.trigger('click');
expect(handleClick).toHaveBeenCalledTimes(1);
});

it('禁用状态下不应该触发点击事件', async () => {
const handleClick = vi.fn();
const wrapper = mount(Button, {
props: {
disabled: true,
onClick: handleClick,
},
slots: {
default: '点击我',
},
});

await wrapper.trigger('click');
expect(handleClick).not.toHaveBeenCalled();
expect(wrapper.classes()).toContain('btn--disabled');
});

it('加载状态下应该显示加载图标', () => {
const wrapper = mount(Button, {
props: {
loading: true,
},
slots: {
default: '点击我',
},
});

expect(wrapper.classes()).toContain('btn--loading');
expect(wrapper.findComponent({ name: 'Icon' }).exists()).toBe(true);
});
});

2. 集成测试示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// tests/integration/user-management.test.ts
/**
* 用户管理集成测试
* @description 测试用户管理模块的完整流程
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { mount } from '@vue/test-utils';
import { createPinia, setActivePinia } from 'pinia';
import { createRouter, createWebHistory } from 'vue-router';
import UserList from '@/modules/user/pages/UserList.vue';
import { useUserStore } from '@/modules/user/stores/UserStore';

// 模拟 API 响应
vi.mock('@/infrastructure/http', () => ({
ApiClient: vi.fn().mockImplementation(() => ({
get: vi.fn().mockResolvedValue({
data: {
code: 200,
data: [
{ id: 1, name: '张三', email: 'zhangsan@example.com' },
{ id: 2, name: '李四', email: 'lisi@example.com' },
],
},
}),
})),
}));

describe('用户管理集成测试', () => {
let router: any;
let pinia: any;

beforeEach(() => {
pinia = createPinia();
setActivePinia(pinia);

router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/users', component: UserList },
],
});
});

it('应该正确加载和显示用户列表', async () => {
const wrapper = mount(UserList, {
global: {
plugins: [pinia, router],
},
});

const userStore = useUserStore();
await userStore.loadUsers();

// 等待组件更新
await wrapper.vm.$nextTick();

expect(userStore.users).toHaveLength(2);
expect(wrapper.text()).toContain('张三');
expect(wrapper.text()).toContain('李四');
});

it('应该正确处理用户搜索', async () => {
const wrapper = mount(UserList, {
global: {
plugins: [pinia, router],
},
});

const searchInput = wrapper.find('[data-testid="search-input"]');
await searchInput.setValue('张三');
await searchInput.trigger('input');

// 等待搜索结果
await new Promise(resolve => setTimeout(resolve, 300));

expect(wrapper.text()).toContain('张三');
expect(wrapper.text()).not.toContain('李四');
});
});

部署与监控

1. Docker 容器化部署

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# Dockerfile
# 多阶段构建,优化镜像大小
FROM node:18-alpine AS builder

# 设置工作目录
WORKDIR /app

# 复制 package 文件
COPY package*.json ./
COPY pnpm-lock.yaml ./

# 安装 pnpm
RUN npm install -g pnpm

# 安装依赖
RUN pnpm install --frozen-lockfile

# 复制源代码
COPY . .

# 构建应用
RUN pnpm build

# 生产阶段
FROM nginx:alpine

# 复制构建产物
COPY --from=builder /app/dist /usr/share/nginx/html

# 复制 nginx 配置
COPY nginx.conf /etc/nginx/nginx.conf

# 暴露端口
EXPOSE 80

# 启动 nginx
CMD ["nginx", "-g", "daemon off;"]

2. 性能监控配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
// src/utils/performance-monitor.ts
/**
* 性能监控工具
* @description 监控应用性能指标并上报数据
*/
interface PerformanceMetrics {
fcp: number; // First Contentful Paint
lcp: number; // Largest Contentful Paint
fid: number; // First Input Delay
cls: number; // Cumulative Layout Shift
ttfb: number; // Time to First Byte
}

class PerformanceMonitor {
private metrics: Partial<PerformanceMetrics> = {};
private observer: PerformanceObserver | null = null;

/**
* 初始化性能监控
* @description 设置各种性能指标的监控
*/
init() {
this.observeWebVitals();
this.observeNavigation();
this.observeResources();
}

/**
* 监控 Web Vitals 指标
* @description 监控 FCP、LCP、FID、CLS 等核心指标
*/
private observeWebVitals() {
if ('PerformanceObserver' in window) {
// 监控 FCP 和 LCP
const paintObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.name === 'first-contentful-paint') {
this.metrics.fcp = entry.startTime;
}
}
});
paintObserver.observe({ entryTypes: ['paint'] });

// 监控 LCP
const lcpObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
this.metrics.lcp = lastEntry.startTime;
});
lcpObserver.observe({ entryTypes: ['largest-contentful-paint'] });

// 监控 FID
const fidObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
this.metrics.fid = entry.processingStart - entry.startTime;
}
});
fidObserver.observe({ entryTypes: ['first-input'] });

// 监控 CLS
const clsObserver = new PerformanceObserver((list) => {
let clsValue = 0;
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
clsValue += entry.value;
}
}
this.metrics.cls = clsValue;
});
clsObserver.observe({ entryTypes: ['layout-shift'] });
}
}

/**
* 监控导航性能
* @description 监控页面加载时间等导航相关指标
*/
private observeNavigation() {
window.addEventListener('load', () => {
const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
this.metrics.ttfb = navigation.responseStart - navigation.requestStart;
});
}

/**
* 监控资源加载性能
* @description 监控静态资源的加载时间
*/
private observeResources() {
const resourceObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
const resource = entry as PerformanceResourceTiming;
if (resource.duration > 1000) {
console.warn(`慢资源加载: ${resource.name}, 耗时: ${resource.duration}ms`);
}
}
});
resourceObserver.observe({ entryTypes: ['resource'] });
}

/**
* 上报性能数据
* @description 将收集的性能数据发送到监控服务
*/
async reportMetrics() {
try {
await fetch('/api/performance', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
metrics: this.metrics,
userAgent: navigator.userAgent,
url: window.location.href,
timestamp: Date.now(),
}),
});
} catch (error) {
console.error('性能数据上报失败:', error);
}
}

/**
* 获取当前性能指标
* @returns 性能指标对象
*/
getMetrics(): Partial<PerformanceMetrics> {
return { ...this.metrics };
}
}

export const performanceMonitor = new PerformanceMonitor();

总结

本文深入探讨了 Vue 3 企业级应用架构设计的各个方面,从基础的分层架构设计到具体的技术实现,涵盖了以下核心内容:

核心特性

  1. 分层架构设计 - 清晰的表现层、业务层、数据层和基础设施层划分
  2. 模块化开发 - 基于功能模块的代码组织和管理策略
  3. TypeScript 集成 - 完整的类型安全和开发体验优化
  4. 状态管理架构 - 基于 Pinia 的企业级状态管理方案
  5. 组件架构设计 - 可复用的基础组件库和高阶组件模式
  6. 路由架构 - 模块化路由配置和权限控制系统
  7. 性能优化 - 懒加载、虚拟滚动等性能优化策略
  8. 测试策略 - 完整的单元测试和集成测试方案
  9. 部署监控 - 容器化部署和性能监控体系

最佳实践

  1. 代码组织 - 采用模块化和分层的代码组织方式
  2. 类型安全 - 充分利用 TypeScript 提供类型安全保障
  3. 性能优化 - 合理使用懒加载、代码分割等优化技术
  4. 测试覆盖 - 建立完善的测试体系保证代码质量
  5. 监控体系 - 建立性能监控和错误追踪机制

Vue 3 的 Composition API、更好的 TypeScript 支持和性能优化,使其完全具备了构建大型企业级应用的能力。通过合理的架构设计和最佳实践的应用,可以构建出高质量、可维护、可扩展的企业级前端应用。

本站由 提供部署服务