Vue 3 + TypeScript + Vite企业级项目架构设计与最佳实践

GreenWizard
GreenWizard 2026-02-10T05:01:04+08:00
0 0 0

前言

随着前端技术的快速发展,构建现代化的企业级应用已成为开发者的重要技能。Vue 3作为当前主流的前端框架,结合TypeScript的类型安全特性和Vite的极速构建能力,为现代Web开发提供了强大的技术栈组合。

本文将从零开始,详细阐述如何构建一个基于Vue 3、TypeScript和Vite的企业级项目架构,涵盖从项目初始化到代码组织、组件设计、状态管理等核心要素,分享实际开发中的最佳实践和经验总结。

项目初始化与环境配置

使用Vite创建Vue 3项目

首先,我们使用Vite的官方脚手架工具来创建项目:

npm create vite@latest my-vue-enterprise-app --template vue-ts
cd my-vue-enterprise-app
npm install

这个命令会创建一个包含Vue 3、TypeScript和Vite的完整项目结构。

项目目录结构设计

src/
├── assets/                 # 静态资源文件
│   ├── images/
│   ├── styles/
│   └── icons/
├── components/             # 公共组件
│   ├── common/
│   ├── layout/
│   └── ui/
├── views/                  # 页面组件
│   ├── home/
│   ├── dashboard/
│   └── user/
├── router/                 # 路由配置
│   └── index.ts
├── store/                  # 状态管理
│   ├── index.ts
│   ├── modules/
│   └── types/
├── services/               # API服务层
│   ├── api/
│   ├── http/
│   └── types/
├── utils/                  # 工具函数
│   ├── helpers/
│   ├── validators/
│   └── constants/
├── composables/            # 可复用逻辑
├── hooks/                  # 自定义Hook
├── types/                  # 类型定义
└── App.vue

TypeScript类型安全实践

定义应用基础类型

src/types/index.ts中定义基础类型:

// src/types/index.ts
export interface User {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user' | 'guest';
  createdAt: string;
}

export interface ApiResponse<T> {
  code: number;
  message: string;
  data: T;
  timestamp: number;
}

export interface Pagination {
  page: number;
  pageSize: number;
  total: number;
  totalPages: number;
}

export interface PageResponse<T> extends ApiResponse<T[]> {
  pagination: Pagination;
}

组件Props类型定义

// src/components/common/Avatar.vue
import { defineComponent, PropType } from 'vue';

interface User {
  id: number;
  name: string;
  avatar?: string;
}

export default defineComponent({
  name: 'Avatar',
  props: {
    user: {
      type: Object as PropType<User>,
      required: true
    },
    size: {
      type: String as PropType<'small' | 'medium' | 'large'>,
      default: 'medium'
    },
    showName: {
      type: Boolean,
      default: false
    }
  },
  setup(props) {
    const getSizeClass = () => {
      switch (props.size) {
        case 'small': return 'w-8 h-8';
        case 'large': return 'w-16 h-16';
        default: return 'w-12 h-12';
      }
    };

    return {
      getSizeClass
    };
  }
});

API响应类型定义

// src/services/types/user.ts
import { User, Pagination } from '@/types';

export interface UserListParams {
  page: number;
  pageSize: number;
  keyword?: string;
  role?: string;
}

export interface UserListResponse {
  users: User[];
  pagination: Pagination;
}

Vite构建优化配置

Vite配置文件优化

// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';
import AutoImport from 'unplugin-auto-import/vite';
import Components from 'unplugin-vue-components/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';

export default defineConfig({
  plugins: [
    vue(),
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [ElementPlusResolver()],
    }),
  ],
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src'),
    },
  },
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@import "src/assets/styles/variables.scss";`,
      },
    },
  },
  server: {
    port: 3000,
    host: true,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
      },
    },
  },
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['vue', 'vue-router', 'pinia'],
          ui: ['element-plus'],
        },
      },
    },
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true,
      },
    },
  },
});

构建性能优化

// vite.config.ts - 预加载优化
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        // 分块策略优化
        manualChunks: {
          vendor: ['vue', 'vue-router', 'pinia'],
          ui: ['element-plus'],
          utils: ['lodash-es', 'axios'],
        },
        // 预加载配置
        assetFileNames: (assetInfo) => {
          if (assetInfo.name.endsWith('.css')) {
            return 'assets/css/[name].[hash].css';
          }
          return 'assets/[name].[hash].[ext]';
        },
      },
    },
  },
});

组件化设计与最佳实践

基础组件库封装

// src/components/common/Button.vue
<template>
  <button 
    :class="[
      'btn',
      `btn--${type}`,
      { 'btn--disabled': disabled, 'btn--loading': loading }
    ]"
    :disabled="disabled || loading"
    @click="handleClick"
  >
    <span v-if="loading" class="btn__spinner"></span>
    <slot />
  </button>
</template>

<script setup lang="ts">
import { defineProps, defineEmits } from 'vue';

interface Props {
  type?: 'primary' | 'secondary' | 'danger' | 'success';
  disabled?: boolean;
  loading?: boolean;
}

interface Emits {
  (e: 'click', event: Event): void;
}

const props = withDefaults(defineProps<Props>(), {
  type: 'primary',
  disabled: false,
  loading: false
});

const emit = defineEmits<Emits>();

const handleClick = (event: Event) => {
  if (!props.disabled && !props.loading) {
    emit('click', event);
  }
};
</script>

<style scoped lang="scss">
.btn {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.3s ease;
  
  &--primary {
    background-color: #007bff;
    color: white;
    
    &:hover:not(.btn--disabled) {
      background-color: #0056b3;
    }
  }
  
  &--secondary {
    background-color: #6c757d;
    color: white;
    
    &:hover:not(.btn--disabled) {
      background-color: #545b62;
    }
  }
  
  &--disabled {
    opacity: 0.6;
    cursor: not-allowed;
  }
  
  &__spinner {
    display: inline-block;
    width: 16px;
    height: 16px;
    border: 2px solid #f3f3f3;
    border-top: 2px solid #007bff;
    border-radius: 50%;
    animation: spin 1s linear infinite;
  }
}

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

响应式组件设计

// src/components/common/DataTable.vue
<template>
  <div class="data-table">
    <div class="data-table__header">
      <div class="data-table__search">
        <el-input 
          v-model="searchKeyword" 
          placeholder="搜索..." 
          @input="handleSearch"
        />
      </div>
      <div class="data-table__actions">
        <slot name="actions"></slot>
      </div>
    </div>
    
    <el-table 
      :data="tableData" 
      :loading="loading"
      @selection-change="handleSelectionChange"
    >
      <el-table-column type="selection" width="50" />
      
      <el-table-column
        v-for="column in columns"
        :key="column.prop"
        :prop="column.prop"
        :label="column.label"
        :width="column.width"
        :formatter="column.formatter"
      />
      
      <el-table-column label="操作" width="120">
        <template #default="{ row }">
          <slot name="actions" :row="row"></slot>
        </template>
      </el-table-column>
    </el-table>
    
    <div class="data-table__pagination">
      <el-pagination
        v-model:current-page="currentPage"
        v-model:page-size="pageSize"
        :total="total"
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
      />
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, watch } from 'vue';
import type { TableProps } from 'element-plus';

interface Column {
  prop: string;
  label: string;
  width?: number;
  formatter?: (row: any, column: any, cellValue: any) => string;
}

interface Props {
  columns: Column[];
  data: any[];
  loading?: boolean;
  total?: number;
  currentPage?: number;
  pageSize?: number;
  searchFields?: string[];
}

const props = withDefaults(defineProps<Props>(), {
  data: () => [],
  loading: false,
  total: 0,
  currentPage: 1,
  pageSize: 10,
  searchFields: () => []
});

const emit = defineEmits<{
  (e: 'update:currentPage', page: number): void;
  (e: 'update:pageSize', size: number): void;
  (e: 'search', keyword: string): void;
  (e: 'selection-change', selection: any[]): void;
}>();

const searchKeyword = ref('');
const tableData = ref(props.data);
const currentPage = ref(props.currentPage);
const pageSize = ref(props.pageSize);

// 处理搜索
const handleSearch = () => {
  emit('search', searchKeyword.value);
};

// 处理分页变化
const handleSizeChange = (size: number) => {
  pageSize.value = size;
  emit('update:pageSize', size);
};

const handleCurrentChange = (page: number) => {
  currentPage.value = page;
  emit('update:currentPage', page);
};

// 处理选择变化
const handleSelectionChange = (selection: any[]) => {
  emit('selection-change', selection);
};

// 监听props变化
watch(() => props.data, (newData) => {
  tableData.value = newData;
});

watch(() => props.currentPage, (newPage) => {
  currentPage.value = newPage;
});
</script>

<style scoped lang="scss">
.data-table {
  &__header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 16px;
    gap: 16px;
  }
  
  &__search {
    flex: 1;
  }
  
  &__actions {
    display: flex;
    gap: 8px;
  }
  
  &__pagination {
    margin-top: 16px;
    text-align: right;
  }
}
</style>

状态管理设计

Pinia状态管理实现

// src/store/index.ts
import { createPinia } from 'pinia';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';

const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);

export default pinia;

用户状态模块

// src/store/modules/user.ts
import { defineStore } from 'pinia';
import type { User } from '@/types';
import { login, logout, getUserInfo } from '@/services/api/auth';

interface UserState {
  userInfo: User | null;
  token: string | null;
  permissions: string[];
  loading: boolean;
}

export const useUserStore = defineStore('user', {
  state: (): UserState => ({
    userInfo: null,
    token: localStorage.getItem('token') || null,
    permissions: [],
    loading: false
  }),

  getters: {
    isAuthenticated: (state) => !!state.token && !!state.userInfo,
    hasPermission: (state) => (permission: string) => {
      return state.permissions.includes(permission);
    }
  },

  actions: {
    async login(credentials: { username: string; password: string }) {
      this.loading = true;
      try {
        const response = await login(credentials);
        const { token, user } = response.data;
        
        this.token = token;
        this.userInfo = user;
        this.permissions = user.role === 'admin' ? ['read', 'write', 'delete'] : ['read'];
        
        // 存储token到localStorage
        localStorage.setItem('token', token);
        
        return response;
      } catch (error) {
        throw error;
      } finally {
        this.loading = false;
      }
    },

    async logout() {
      try {
        await logout();
        this.token = null;
        this.userInfo = null;
        this.permissions = [];
        localStorage.removeItem('token');
      } catch (error) {
        console.error('Logout failed:', error);
      }
    },

    async fetchUserInfo() {
      if (!this.token) return;
      
      try {
        const response = await getUserInfo();
        this.userInfo = response.data;
        this.permissions = response.data.role === 'admin' ? 
          ['read', 'write', 'delete'] : ['read'];
      } catch (error) {
        console.error('Failed to fetch user info:', error);
      }
    },

    setUserInfo(userInfo: User) {
      this.userInfo = userInfo;
    }
  },

  persist: {
    key: 'user-store',
    paths: ['token', 'userInfo', 'permissions']
  }
});

应用状态管理

// src/store/modules/app.ts
import { defineStore } from 'pinia';

interface AppState {
  sidebarCollapsed: boolean;
  theme: 'light' | 'dark';
  language: string;
  loading: boolean;
}

export const useAppStore = defineStore('app', {
  state: (): AppState => ({
    sidebarCollapsed: false,
    theme: 'light',
    language: 'zh-CN',
    loading: false
  }),

  getters: {
    isSidebarCollapsed: (state) => state.sidebarCollapsed,
    currentTheme: (state) => state.theme,
    currentLanguage: (state) => state.language
  },

  actions: {
    toggleSidebar() {
      this.sidebarCollapsed = !this.sidebarCollapsed;
    },

    setTheme(theme: 'light' | 'dark') {
      this.theme = theme;
      document.documentElement.setAttribute('data-theme', theme);
    },

    setLanguage(language: string) {
      this.language = language;
    },

    setLoading(loading: boolean) {
      this.loading = loading;
    }
  },

  persist: {
    key: 'app-store',
    paths: ['sidebarCollapsed', 'theme', 'language']
  }
});

路由配置与权限控制

路由基础配置

// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router';
import { useUserStore } from '@/store/modules/user';
import type { RouteRecordRaw } from 'vue-router';

const routes: RouteRecordRaw[] = [
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/auth/Login.vue'),
    meta: { requiresAuth: false }
  },
  {
    path: '/',
    redirect: '/dashboard'
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import('@/views/dashboard/Dashboard.vue'),
    meta: { requiresAuth: true, title: '仪表板' }
  },
  {
    path: '/users',
    name: 'Users',
    component: () => import('@/views/user/UserList.vue'),
    meta: { requiresAuth: true, title: '用户管理', permission: 'read' }
  },
  {
    path: '/settings',
    name: 'Settings',
    component: () => import('@/views/settings/Settings.vue'),
    meta: { requiresAuth: true, title: '系统设置' }
  }
];

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes
});

// 路由守卫
router.beforeEach((to, from, next) => {
  const userStore = useUserStore();
  
  if (to.meta.requiresAuth && !userStore.isAuthenticated) {
    next({
      path: '/login',
      query: { redirect: to.fullPath }
    });
  } else if (to.meta.permission && !userStore.hasPermission(to.meta.permission as string)) {
    next('/403');
  } else {
    next();
  }
});

export default router;

动态路由实现

// src/router/dynamic.ts
import type { RouteRecordRaw } from 'vue-router';
import { useUserStore } from '@/store/modules/user';

export const generateRoutes = (): RouteRecordRaw[] => {
  const userStore = useUserStore();
  
  if (!userStore.isAuthenticated) {
    return [];
  }
  
  // 根据用户权限动态生成路由
  const routes: RouteRecordRaw[] = [];
  
  if (userStore.hasPermission('read')) {
    routes.push({
      path: '/users',
      name: 'Users',
      component: () => import('@/views/user/UserList.vue'),
      meta: { title: '用户管理' }
    });
  }
  
  if (userStore.hasPermission('write')) {
    routes.push({
      path: '/users/create',
      name: 'UserCreate',
      component: () => import('@/views/user/UserCreate.vue'),
      meta: { title: '创建用户' }
    });
  }
  
  if (userStore.hasPermission('delete')) {
    routes.push({
      path: '/users/delete',
      name: 'UserDelete',
      component: () => import('@/views/user/UserDelete.vue'),
      meta: { title: '删除用户' }
    });
  }
  
  return routes;
};

API服务层设计

HTTP客户端封装

// src/services/http/index.ts
import axios, { type AxiosInstance, type AxiosRequestConfig } from 'axios';
import { useUserStore } from '@/store/modules/user';

class HttpClient {
  private instance: AxiosInstance;
  
  constructor() {
    this.instance = axios.create({
      baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
      timeout: 10000,
      headers: {
        'Content-Type': 'application/json'
      }
    });
    
    this.setupInterceptors();
  }
  
  private setupInterceptors() {
    // 请求拦截器
    this.instance.interceptors.request.use(
      (config) => {
        const userStore = useUserStore();
        const token = userStore.token;
        
        if (token) {
          config.headers.Authorization = `Bearer ${token}`;
        }
        
        return config;
      },
      (error) => {
        return Promise.reject(error);
      }
    );
    
    // 响应拦截器
    this.instance.interceptors.response.use(
      (response) => {
        return response.data;
      },
      (error) => {
        if (error.response?.status === 401) {
          const userStore = useUserStore();
          userStore.logout();
          window.location.href = '/login';
        }
        
        return Promise.reject(error);
      }
    );
  }
  
  public get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
    return this.instance.get(url, config);
  }
  
  public post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    return this.instance.post(url, data, config);
  }
  
  public put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    return this.instance.put(url, data, config);
  }
  
  public delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
    return this.instance.delete(url, config);
  }
}

export const http = new HttpClient();

API服务封装

// src/services/api/auth.ts
import { http } from '../http';
import type { User } from '@/types';

export interface LoginParams {
  username: string;
  password: string;
}

export interface LoginResponse {
  token: string;
  user: User;
}

export const login = (params: LoginParams) => {
  return http.post<LoginResponse>('/auth/login', params);
};

export const logout = () => {
  return http.post('/auth/logout');
};

export const getUserInfo = () => {
  return http.get<User>('/user/info');
};
// src/services/api/user.ts
import { http } from '../http';
import type { User, UserListParams, UserListResponse } from '@/types';

export const getUserList = (params: UserListParams) => {
  return http.get<UserListResponse>('/users', { params });
};

export const createUser = (data: Partial<User>) => {
  return http.post<User>('/users', data);
};

export const updateUser = (id: number, data: Partial<User>) => {
  return http.put<User>(`/users/${id}`, data);
};

export const deleteUser = (id: number) => {
  return http.delete(`/users/${id}`);
};

工具函数与常量定义

常用工具函数

// src/utils/helpers/index.ts
import { ref, type Ref } from 'vue';

/**
 * 防抖函数
 */
export function debounce<T extends (...args: any[]) => any>(
  func: T,
  wait: number
): T {
  let timeoutId: ReturnType<typeof setTimeout> | null = null;
  
  return function (...args: Parameters<T>) {
    if (timeoutId) {
      clearTimeout(timeoutId);
    }
    
    timeoutId = setTimeout(() => func.apply(this, args), wait);
  } as T;
}

/**
 * 节流函数
 */
export function throttle<T extends (...args: any[]) => any>(
  func: T,
  limit: number
): T {
  let inThrottle: boolean;
  let lastFunc: ReturnType<typeof setTimeout> | null = null;
  
  return function (...args: Parameters<T>) {
    if (!inThrottle) {
      func.apply(this, args);
      inThrottle = true;
      
      setTimeout(() => {
        inThrottle = false;
      }, limit);
    }
  } as T;
}

/**
 * 格式化日期
 */
export function formatDate(date: Date | string, format: string = 'YYYY-MM-DD'): string {
  const d = new Date(date);
  
  const year = d.getFullYear();
  const month = String(d.getMonth() + 1).padStart(2, '0');
  const day = String(d.getDate()).padStart(2, '0');
  
  return format
    .replace('YYYY', year.toString())
    .replace('MM', month)
    .replace('DD', day);
}

/**
 * 深度克隆
 */
export function deepClone<T>(obj: T): T {
  if (obj === null || typeof obj !== 'object') return obj;
  
  if (obj instanceof Date) return new Date(obj.getTime()) as any;
  if (obj instanceof Array) return obj.map(item => deepClone(item)) as any;
  if (typeof obj === 'object') {
    const clonedObj = {} as any;
    for (const key in obj) {
      if (obj.hasOwnProperty(key)) {
        clonedObj[key] = deepClone(obj[key]);
      }
    }
    return clonedObj;
  }
  
  return obj;
}

/**
 * 空值检查
 */
export function isNil(value: any): boolean {
  return value === null || value === undefined;
}

常量定义

// src/utils/constants/index.ts
export const API_STATUS = {
  SUCCESS: 200,
  UNAUTHORIZED: 401,
  FORBIDDEN: 403,
  NOT_FOUND: 404,
  SERVER_ERROR: 500
};

export const PAGE_SIZE_OPTIONS = [10, 20, 50, 100];

export const USER_ROLES = {
  ADMIN: 'admin',
  USER: 'user',
  GUEST: 'guest'
};

export const STATUS_OPTIONS = [
  { label: '启用', value: 'active' },
  { label: '禁用', value: 'inactive' }
];

export const FORM_RULES = {
  required: [{ required: true, message: '此项为必填项', trigger: 'blur' }],
  email: [
    { required: true, message: '请输入邮箱地址', trigger: 'blur' },
    { type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
  ],
  phone: [
    { required: true, message: '请输入手机号码', trigger: 'blur' },
    { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
  ]
};

组件可复用逻辑封装

自定义Hook实现

// src/hooks/usePagination.ts
import { ref, watch } from 'vue';

interface PaginationState {
  page: number;
  pageSize: number;
  total: number;
}

export function usePagination(initialState: Partial<PaginationState> = {}) {
  const page = ref(initialState.page || 1);
  const pageSize = ref(initialState.pageSize || 10);
  const total = ref(initialState.total || 0);
  
  const handleSizeChange = (size: number) => {
    pageSize.value = size;
    page.value = 1;
  };
  
  const handleCurrentChange = (current: number) => {
    page.value = current;
  };
  
  const reset = () => {
    page.value = 1;
    pageSize.value
相关推荐
广告位招租

相似文章

    评论 (0)

    0/2000