前言
随着前端技术的快速发展,企业级前端应用对工程化、可维护性和开发效率的要求越来越高。Vue3的Composition API、TypeScript的类型安全以及Vite的现代化构建工具,为构建高质量的前端应用提供了强大的技术支撑。本文将详细介绍如何从零开始搭建一个基于Vue3、TypeScript和Vite的企业级前端项目架构,涵盖项目初始化、组件设计、状态管理、构建优化等完整流程。
项目初始化与基础配置
1.1 使用Vite创建项目
首先,我们使用Vite来创建Vue3项目。Vite作为新一代前端构建工具,具有快速的冷启动和热更新特性。
# 使用npm创建项目
npm create vite@latest my-vue3-app --template vue-ts
# 或使用yarn
yarn create vite my-vue3-app --template vue-ts
# 进入项目目录
cd my-vue3-app
创建完成后,项目会包含以下基本结构:
my-vue3-app/
├── public/
│ └── favicon.ico
├── src/
│ ├── assets/
│ ├── components/
│ ├── views/
│ ├── router/
│ ├── store/
│ ├── utils/
│ ├── styles/
│ ├── App.vue
│ └── main.ts
├── vite.config.ts
├── tsconfig.json
├── package.json
└── README.md
1.2 TypeScript配置优化
在tsconfig.json中,我们需要进行详细的配置以确保TypeScript的最佳实践:
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Node",
"strict": true,
"jsx": "preserve",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"types": ["vite/client"],
"lib": ["ES2020", "DOM", "DOM.Iterable"]
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}
1.3 Vite配置优化
在vite.config.ts中配置项目:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
},
server: {
port: 3000,
host: true,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
},
build: {
rollupOptions: {
output: {
manualChunks: {
vue: ['vue', 'vue-router', 'pinia'],
ui: ['element-plus'],
utils: ['axios', 'lodash']
}
}
}
}
})
组件设计与架构模式
2.1 组件结构设计
在企业级项目中,我们采用组件化架构设计,将组件分为以下几类:
// src/components/base/BaseButton.vue
<template>
<button
:class="['base-button', `base-button--${type}`, { 'is-disabled': disabled }]"
:disabled="disabled"
@click="handleClick"
>
<slot />
</button>
</template>
<script setup lang="ts">
import { defineProps, defineEmits } from 'vue'
interface Props {
type?: 'primary' | 'secondary' | 'danger'
disabled?: boolean
}
interface Emits {
(e: 'click', event: MouseEvent): void
}
const props = withDefaults(defineProps<Props>(), {
type: 'primary',
disabled: false
})
const emit = defineEmits<Emits>()
const handleClick = (event: MouseEvent) => {
if (!props.disabled) {
emit('click', event)
}
}
</script>
<style scoped lang="scss">
.base-button {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
&--primary {
background-color: #409eff;
color: white;
&:hover {
background-color: #66b2ff;
}
}
&--secondary {
background-color: #f5f5f5;
color: #333;
&:hover {
background-color: #e0e0e0;
}
}
&.is-disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
</style>
2.2 组件通信模式
采用Props、Emits和Provide/Inject的组合模式:
// src/components/advanced/DataTable.vue
<template>
<div class="data-table">
<table>
<thead>
<tr>
<th v-for="column in columns" :key="column.key">
{{ column.title }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="row in data" :key="row.id">
<td v-for="column in columns" :key="column.key">
<component
:is="column.component"
v-bind="column.props"
:value="row[column.key]"
@update="handleUpdate"
/>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script setup lang="ts">
import { defineProps, defineEmits, computed } from 'vue'
interface Column {
key: string
title: string
component?: string
props?: Record<string, any>
}
interface Row {
id: string | number
[key: string]: any
}
interface Props {
columns: Column[]
data: Row[]
}
interface Emits {
(e: 'update', key: string, value: any, row: Row): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const handleUpdate = (key: string, value: any, row: Row) => {
emit('update', key, value, row)
}
</script>
状态管理设计
3.1 Pinia状态管理
使用Pinia作为状态管理工具,相比Vuex更加轻量且TypeScript支持更好:
// src/store/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { User } from '@/types/user'
export const useUserStore = defineStore('user', () => {
const userInfo = ref<User | null>(null)
const isLoggedIn = computed(() => !!userInfo.value)
const login = (user: User) => {
userInfo.value = user
localStorage.setItem('userInfo', JSON.stringify(user))
}
const logout = () => {
userInfo.value = null
localStorage.removeItem('userInfo')
}
const initUser = () => {
const storedUser = localStorage.getItem('userInfo')
if (storedUser) {
userInfo.value = JSON.parse(storedUser)
}
}
return {
userInfo,
isLoggedIn,
login,
logout,
initUser
}
})
3.2 复杂状态管理示例
// src/store/complex.ts
import { defineStore } from 'pinia'
import { ref, computed, watch } from 'vue'
import { api } from '@/utils/request'
interface Product {
id: number
name: string
price: number
category: string
}
interface FilterState {
category: string
minPrice: number
maxPrice: number
}
export const useProductStore = defineStore('product', () => {
const products = ref<Product[]>([])
const loading = ref(false)
const filter = ref<FilterState>({
category: '',
minPrice: 0,
maxPrice: 10000
})
const filteredProducts = computed(() => {
return products.value.filter(product => {
const categoryMatch = !filter.value.category ||
product.category === filter.value.category
const priceMatch = product.price >= filter.value.minPrice &&
product.price <= filter.value.maxPrice
return categoryMatch && priceMatch
})
})
const fetchProducts = async () => {
loading.value = true
try {
const response = await api.get<Product[]>('/products')
products.value = response.data
} catch (error) {
console.error('Failed to fetch products:', error)
} finally {
loading.value = false
}
}
const updateFilter = (newFilter: Partial<FilterState>) => {
filter.value = { ...filter.value, ...newFilter }
}
// 监听过滤条件变化
watch(filter, () => {
console.log('Filter changed:', filter.value)
}, { deep: true })
return {
products,
filteredProducts,
loading,
filter,
fetchProducts,
updateFilter
}
})
路由设计与权限控制
4.1 路由配置
// src/router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import { useUserStore } from '@/store/user'
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue'),
meta: { requiresAuth: false }
},
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue'),
meta: { requiresAuth: false }
},
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
meta: { requiresAuth: true }
},
{
path: '/admin',
name: 'Admin',
component: () => import('@/views/Admin.vue'),
meta: { requiresAuth: true, roles: ['admin'] }
}
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes
})
// 路由守卫
router.beforeEach((to, from, next) => {
const userStore = useUserStore()
if (to.meta.requiresAuth && !userStore.isLoggedIn) {
next('/login')
} else if (to.meta.roles && !to.meta.roles.includes(userStore.userInfo?.role || '')) {
next('/403')
} else {
next()
}
})
export default router
4.2 权限控制组件
// src/components/auth/PermissionWrapper.vue
<template>
<div v-if="hasPermission" class="permission-wrapper">
<slot />
</div>
<div v-else class="permission-denied">
<slot name="denied" />
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useUserStore } from '@/store/user'
interface Props {
permission?: string
role?: string
}
const props = defineProps<Props>()
const userStore = useUserStore()
const hasPermission = computed(() => {
if (!props.permission && !props.role) {
return true
}
if (props.role && userStore.userInfo?.role) {
return userStore.userInfo.role === props.role
}
return true
})
</script>
<style scoped>
.permission-wrapper {
display: block;
}
.permission-denied {
padding: 16px;
background-color: #f5f5f5;
border-radius: 4px;
color: #999;
text-align: center;
}
</style>
API封装与请求处理
5.1 Axios封装
// src/utils/request.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import { useUserStore } from '@/store/user'
import { ElMessage } from 'element-plus'
// 创建axios实例
const service: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080',
timeout: 10000,
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
})
// 请求拦截器
service.interceptors.request.use(
(config: AxiosRequestConfig) => {
const userStore = useUserStore()
const token = userStore.userInfo?.token
if (token) {
config.headers = {
...config.headers,
Authorization: `Bearer ${token}`
}
}
return config
},
(error) => {
console.error('Request error:', error)
return Promise.reject(error)
}
)
// 响应拦截器
service.interceptors.response.use(
(response: AxiosResponse) => {
const res = response.data
if (res.code !== 200) {
ElMessage({
message: res.message || 'Error',
type: 'error',
duration: 5 * 1000
})
if (res.code === 401) {
const userStore = useUserStore()
userStore.logout()
location.reload()
}
return Promise.reject(new Error(res.message || 'Error'))
} else {
return res
}
},
(error) => {
console.error('Response error:', error)
ElMessage({
message: error.message || 'Network Error',
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)
export { service as api }
5.2 API服务层设计
// src/api/user.ts
import { api } from '@/utils/request'
import { User } from '@/types/user'
export interface LoginParams {
username: string
password: string
}
export interface LoginResponse {
token: string
user: User
}
export const userApi = {
login(params: LoginParams) {
return api.post<LoginResponse>('/auth/login', params)
},
getUserInfo() {
return api.get<User>('/user/info')
},
updateUserInfo(data: Partial<User>) {
return api.put<User>('/user/info', data)
},
getUsers(params?: { page?: number; limit?: number }) {
return api.get<User[]>('/users', { params })
}
}
工程化优化与构建配置
6.1 构建优化配置
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
vue(),
visualizer({
filename: 'dist/stats.html',
open: true,
gzipSize: true,
brotliSize: true
})
],
resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
},
build: {
rollupOptions: {
output: {
manualChunks: {
vue: ['vue', 'vue-router', 'pinia'],
ui: ['element-plus'],
utils: ['axios', 'lodash', 'dayjs']
}
}
},
assetsInlineLimit: 4096,
chunkSizeWarningLimit: 1000,
sourcemap: false
},
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "@/styles/variables.scss";`
}
}
}
})
6.2 环境变量管理
// .env
VITE_API_BASE_URL=http://localhost:8080
VITE_APP_TITLE=My Vue App
VITE_APP_VERSION=1.0.0
// .env.production
VITE_API_BASE_URL=https://api.myapp.com
VITE_APP_TITLE=My Production App
6.3 性能监控
// src/utils/performance.ts
export class PerformanceMonitor {
private startTime: number = 0
private endTime: number = 0
start() {
this.startTime = performance.now()
}
end() {
this.endTime = performance.now()
const duration = this.endTime - this.startTime
console.log(`Performance: ${duration.toFixed(2)}ms`)
return duration
}
static measure<T>(fn: () => T, name: string): T {
const monitor = new PerformanceMonitor()
monitor.start()
const result = fn()
monitor.end()
console.log(`${name} took ${monitor.endTime - monitor.startTime}ms`)
return result
}
}
// 使用示例
// PerformanceMonitor.measure(() => {
// // 你的代码逻辑
// }, 'Component Render')
TypeScript类型系统最佳实践
7.1 类型定义规范
// src/types/user.ts
export interface User {
id: number
username: string
email: string
role: 'admin' | 'user' | 'guest'
avatar?: string
createdAt: string
updatedAt: string
}
export interface Pagination {
page: number
limit: number
total: number
pages: number
}
export interface ApiResponse<T> {
code: number
message: string
data: T
pagination?: Pagination
}
// src/types/common.ts
export type Nullable<T> = T | null | undefined
export type Partial<T> = {
[P in keyof T]?: T[P]
}
export type Required<T> = {
[P in keyof T]-?: T[P]
}
export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
export type Pick<T, K extends keyof T> = {
[P in K]: T[P]
}
7.2 组件类型定义
// src/components/advanced/SmartForm.vue
import { defineComponent, ref, watch } from 'vue'
interface FormField {
name: string
label: string
type: 'text' | 'number' | 'select' | 'date'
required?: boolean
options?: Array<{ label: string; value: any }>
rules?: Array<(value: any) => boolean | string>
}
interface FormData {
[key: string]: any
}
export default defineComponent({
name: 'SmartForm',
props: {
fields: {
type: Array as () => FormField[],
required: true
},
modelValue: {
type: Object as () => FormData,
default: () => ({})
}
},
emits: ['update:modelValue', 'submit'],
setup(props, { emit }) {
const formData = ref<FormData>({ ...props.modelValue })
watch(
() => props.modelValue,
(newVal) => {
formData.value = { ...newVal }
},
{ deep: true }
)
const handleChange = (field: string, value: any) => {
formData.value[field] = value
emit('update:modelValue', formData.value)
}
const handleSubmit = () => {
emit('submit', formData.value)
}
return {
formData,
handleChange,
handleSubmit
}
}
})
测试策略与质量保证
8.1 单元测试配置
// src/__tests__/components/BaseButton.test.ts
import { mount } from '@vue/test-utils'
import BaseButton from '@/components/base/BaseButton.vue'
describe('BaseButton', () => {
it('renders correctly with default props', () => {
const wrapper = mount(BaseButton)
expect(wrapper.classes()).toContain('base-button')
expect(wrapper.classes()).toContain('base-button--primary')
})
it('renders correctly with custom props', () => {
const wrapper = mount(BaseButton, {
props: {
type: 'secondary',
disabled: true
}
})
expect(wrapper.classes()).toContain('base-button--secondary')
expect(wrapper.classes()).toContain('is-disabled')
})
it('emits click event', async () => {
const wrapper = mount(BaseButton)
await wrapper.trigger('click')
expect(wrapper.emitted('click')).toHaveLength(1)
})
})
8.2 端到端测试
// src/__tests__/e2e/login.cy.ts
describe('Login Page', () => {
beforeEach(() => {
cy.visit('/login')
})
it('should display login form', () => {
cy.get('[data-testid="login-form"]').should('exist')
cy.get('[data-testid="username-input"]').should('exist')
cy.get('[data-testid="password-input"]').should('exist')
cy.get('[data-testid="login-button"]').should('exist')
})
it('should login successfully', () => {
cy.get('[data-testid="username-input"]').type('testuser')
cy.get('[data-testid="password-input"]').type('password123')
cy.get('[data-testid="login-button"]').click()
cy.url().should('include', '/dashboard')
cy.get('[data-testid="welcome-message"]').should('contain', 'Welcome')
})
})
部署与持续集成
9.1 Docker部署配置
# Dockerfile
FROM node:16-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "run", "serve"]
9.2 CI/CD配置
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm run test
- name: Run lint
run: npm run lint
- name: Build
run: npm run build
- name: Upload coverage
uses: codecov/codecov-action@v2
总结
通过本文的详细介绍,我们构建了一个完整的基于Vue3、TypeScript和Vite的企业级前端项目架构。该架构具备以下特点:
- 现代化技术栈:充分利用Vue3的Composition API、TypeScript的类型安全和Vite的构建优势
- 可维护性:采用组件化、模块化的架构设计,便于代码维护和扩展
- 工程化实践:包含完整的构建优化、测试策略和部署流程
- 企业级特性:支持权限控制、状态管理、API封装等企业级应用需求
这个架构设计不仅满足了当前项目的需求,也为未来的功能扩展和团队协作提供了良好的基础。通过合理的架构设计和最佳实践,我们可以构建出高性能、高可用、易维护的企业级前端应用。
在实际项目中,还需要根据具体业务需求进行调整和优化,但这个基础架构为项目的成功奠定了坚实的基础。

评论 (0)