Vite 工程化实战 | 从 0 配置一个企业级前端项目(按需引入 / 环境变量 / 打包优化)

29 阅读9分钟

零、为什么我们要“折腾”环境?

还记得你第一次用 create-vue 脚手架时的感受吗?一行命令,项目就跑起来了,那叫一个爽!

但是!当你真正进入公司项目,你会发现:

// 理想中的项目
npm run dev // 启动,完事!

// 现实中的项目
npm run dev // 报错!Node版本不对
npm run build // 报错!内存溢出
npm run lint // 报错!代码格式不对
npm run test // 报错!环境变量没配

这时候你才明白:脚手架给你的是“毛坯房”,企业级项目需要的是“精装修”

今天,我们就从一个空文件夹开始,一步步搭建一个企业级 Vue3 + Vite 项目。这不是简单的“搭环境”,而是“搭项目”!

一、项目初始化:从零开始的艺术

1.1 创建项目(这次不用脚手架)

# 创建项目目录
mkdir vite-enterprise-demo
cd vite-enterprise-demo

# 初始化 package.json
npm init -y

# 安装核心依赖
npm install vue@latest
npm install -D vite @vitejs/plugin-vue typescript vue-tsc

# 创建项目结构
mkdir -p src/{assets,components,views,router,store,utils,styles,types}
touch index.html vite.config.ts tsconfig.json src/main.ts src/App.vue

现在的项目结构应该是这样:

vite-enterprise-demo/
├── src/
│   ├── assets/        # 静态资源
│   ├── components/     # 组件
│   ├── views/         # 页面
│   ├── router/        # 路由
│   ├── store/         # 状态管理
│   ├── utils/         # 工具函数
│   ├── styles/        # 全局样式
│   ├── types/         # TypeScript类型
│   ├── main.ts        # 入口文件
│   └── App.vue        # 根组件
├── index.html         # 入口HTML
├── vite.config.ts     # Vite配置
├── tsconfig.json      # TypeScript配置
└── package.json       # 项目配置

1.2 配置入口文件

<!-- index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <link rel="icon" href="/favicon.ico">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Vite企业级项目实战</title>
</head>
<body>
  <div id="app"></div>
  <script type="module" src="/src/main.ts"></script>
</body>
</html>
// src/main.ts
import { createApp } from 'vue'
import App from './App.vue'

// 创建应用实例
const app = createApp(App)

// 挂载应用
app.mount('#app')
<!-- src/App.vue -->
<template>
  <div class="app">
    <h1>🚀 Vite企业级项目实战</h1>
    <p>从0开始,搭建一个生产可用的项目</p>
  </div>
</template>

<script setup lang="ts">
// 这里写逻辑
</script>

<style scoped>
.app {
  text-align: center;
  padding: 2rem;
  color: #2c3e50;
}
</style>

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'),
      '@components': resolve(__dirname, 'src/components'),
      '@views': resolve(__dirname, 'src/views'),
      '@utils': resolve(__dirname, 'src/utils'),
      '@styles': resolve(__dirname, 'src/styles')
    }
  },
  
  // 开发服务器配置
  server: {
    port: 3000,
    open: true, // 自动打开浏览器
    cors: true, // 允许跨域
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^/api/, '')
      }
    }
  },
  
  // 构建配置
  build: {
    target: 'es2015',
    outDir: 'dist',
    assetsDir: 'assets',
    assetsInlineLimit: 4096, // 小于4kb的图片转base64
    sourcemap: false, // 不生成sourcemap
    reportCompressedSize: false, // 关闭压缩大小报告
    chunkSizeWarningLimit: 500 // 块大小警告限制
  }
})
// package.json 添加脚本
{
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc && vite build",
    "preview": "vite preview",
    "type-check": "vue-tsc --noEmit"
  }
}

试试运行:

npm run dev

看到页面了吗?恭喜!你已经从0开始搭建了一个Vite项目!

二、TypeScript 配置:告别 any 恐惧症

2.1 配置 tsconfig.json

{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "node",
    "strict": true,
    "jsx": "preserve",
    "sourceMap": true,
    "resolveJsonModule": true,
    "esModuleInterop": true,
    "lib": ["ESNext", "DOM"],
    "types": ["vite/client"],
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"],
      "@views/*": ["src/views/*"],
      "@utils/*": ["src/utils/*"],
      "@styles/*": ["src/styles/*"]
    },
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
  "exclude": ["node_modules", "dist"]
}

2.2 添加类型声明

// src/types/shims-vue.d.ts
declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
}

// src/types/env.d.ts
/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_APP_TITLE: string
  readonly VITE_API_BASE_URL: string
  readonly VITE_ENABLE_MOCK: string
  // 更多环境变量...
}

interface ImportMeta {
  readonly env: ImportMetaEnv
}

三、环境变量配置:一套代码,多套环境

3.1 环境变量文件

# .env                # 所有环境共用
# .env.local          # 本地覆盖(不提交)
# .env.development    # 开发环境
# .env.production     # 生产环境
# .env.test           # 测试环境
# .env.development
VITE_APP_TITLE=开发环境
VITE_API_BASE_URL=http://localhost:8080/api
VITE_ENABLE_MOCK=true
VITE_LOG_LEVEL=debug

# .env.production
VITE_APP_TITLE=生产环境
VITE_API_BASE_URL=https://api.example.com
VITE_ENABLE_MOCK=false
VITE_LOG_LEVEL=error

3.2 使用环境变量

// src/utils/config.ts
export const config = {
  appTitle: import.meta.env.VITE_APP_TITLE,
  apiBaseUrl: import.meta.env.VITE_API_BASE_URL,
  enableMock: import.meta.env.VITE_ENABLE_MOCK === 'true',
  logLevel: import.meta.env.VITE_LOG_LEVEL,
  
  // 判断环境
  isDev: import.meta.env.DEV,
  isProd: import.meta.env.PROD,
  mode: import.meta.env.MODE
}

console.log('当前环境:', config.mode)
console.log('API地址:', config.apiBaseUrl)

四、路由配置:让页面"动"起来

4.1 安装路由

npm install vue-router@4

4.2 配置路由

// src/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'),
    meta: {
      title: '首页',
      requiresAuth: false
    }
  },
  {
    path: '/about',
    name: 'About',
    component: () => import('@views/About.vue'),
    meta: {
      title: '关于',
      requiresAuth: false
    }
  },
  {
    path: '/user',
    name: 'User',
    component: () => import('@views/User.vue'),
    meta: {
      title: '个人中心',
      requiresAuth: true
    }
  },
  {
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: () => import('@views/404.vue')
  }
]

// 创建路由实例
const router = createRouter({
  history: createWebHistory(),
  routes,
  scrollBehavior(to, from, savedPosition) {
    if (savedPosition) {
      return savedPosition
    } else {
      return { top: 0 }
    }
  }
})

// 全局前置守卫
router.beforeEach((to, from, next) => {
  // 设置页面标题
  document.title = to.meta.title ? `${to.meta.title} - Vite企业级项目` : 'Vite企业级项目'
  
  // 检查是否需要登录
  if (to.meta.requiresAuth) {
    const token = localStorage.getItem('token')
    if (token) {
      next()
    } else {
      next({ path: '/login', query: { redirect: to.fullPath } })
    }
  } else {
    next()
  }
})

export default router

4.3 创建页面组件

<!-- src/views/Home.vue -->
<template>
  <div class="home">
    <h2>🏠 首页</h2>
    <p>欢迎来到首页!</p>
    <button @click="goToAbout">去关于页面</button>
  </div>
</template>

<script setup lang="ts">
import { useRouter } from 'vue-router'

const router = useRouter()
const goToAbout = () => {
  router.push('/about')
}
</script>

4.4 在 main.ts 中注册路由

// src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

const app = createApp(App)

app.use(router) // 注册路由

app.mount('#app')

五、状态管理:Pinia 来了

5.1 为什么不用 Vuex?

// Vuex 的写法
mutations: {
  SET_USER(state, user) {
    state.user = user
  }
},
actions: {
  async fetchUser({ commit }) {
    const user = await api.getUser()
    commit('SET_USER', user)
  }
}

// Pinia 的写法
export const useUserStore = defineStore('user', {
  state: () => ({ user: null }),
  actions: {
    async fetchUser() {
      this.user = await api.getUser()
    }
  }
})

Pinia 更简洁、更 TypeScript 友好、更模块化!

5.2 安装 Pinia

npm install pinia
npm install pinia-plugin-persistedstate # 持久化插件

5.3 创建 store

// 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'

interface UserState {
  token: string | null
  userInfo: {
    id?: number
    name?: string
    avatar?: string
    role?: string
  } | null
}

export const useUserStore = defineStore('user', {
  state: (): UserState => ({
    token: localStorage.getItem('token'),
    userInfo: null
  }),
  
  getters: {
    isLoggedIn: (state) => !!state.token,
    userName: (state) => state.userInfo?.name || '游客',
    userRole: (state) => state.userInfo?.role || 'guest'
  },
  
  actions: {
    // 登录
    async login(credentials: { username: string; password: string }) {
      try {
        // 模拟登录请求
        const response = await fetch('/api/login', {
          method: 'POST',
          body: JSON.stringify(credentials)
        })
        const data = await response.json()
        
        this.token = data.token
        this.userInfo = data.userInfo
        
        localStorage.setItem('token', data.token)
        
        return true
      } catch (error) {
        console.error('登录失败:', error)
        return false
      }
    },
    
    // 登出
    logout() {
      this.token = null
      this.userInfo = null
      localStorage.removeItem('token')
    },
    
    // 获取用户信息
    async fetchUserInfo() {
      if (!this.token) return
        
      try {
        const response = await fetch('/api/user/info', {
          headers: {
            Authorization: `Bearer ${this.token}`
          }
        })
        this.userInfo = await response.json()
      } catch (error) {
        console.error('获取用户信息失败:', error)
      }
    }
  },
  
  persist: {
    key: 'user-store',
    storage: localStorage,
    paths: ['token'] // 只持久化 token
  }
})
// src/store/modules/app.ts
import { defineStore } from 'pinia'

interface AppState {
  sidebarCollapsed: boolean
  theme: 'light' | 'dark'
  language: 'zh' | 'en'
}

export const useAppStore = defineStore('app', {
  state: (): AppState => ({
    sidebarCollapsed: false,
    theme: 'light',
    language: 'zh'
  }),
  
  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: 'zh' | 'en') {
      this.language = language
    }
  },
  
  persist: true // 持久化整个 store
})

5.4 在组件中使用

<!-- src/views/User.vue -->
<template>
  <div class="user">
    <h2>👤 个人中心</h2>
    
    <div v-if="userStore.isLoggedIn">
      <p>欢迎回来,{{ userStore.userName }}!</p>
      <p>角色:{{ userStore.userRole }}</p>
      <button @click="handleLogout">退出登录</button>
    </div>
    
    <div v-else>
      <p>请先登录</p>
      <button @click="handleLogin">模拟登录</button>
    </div>
    
    <hr>
    
    <h3>应用设置</h3>
    <p>侧边栏状态: {{ appStore.isSidebarCollapsed ? '折叠' : '展开' }}</p>
    <button @click="appStore.toggleSidebar">切换侧边栏</button>
    
    <p>当前主题: {{ appStore.currentTheme }}</p>
    <button @click="appStore.setTheme('dark')">深色模式</button>
    <button @click="appStore.setTheme('light')">浅色模式</button>
  </div>
</template>

<script setup lang="ts">
import { useUserStore } from '@/store/modules/user'
import { useAppStore } from '@/store/modules/app'

const userStore = useUserStore()
const appStore = useAppStore()

const handleLogin = async () => {
  const success = await userStore.login({
    username: 'admin',
    password: '123456'
  })
  
  if (success) {
    alert('登录成功!')
  }
}

const handleLogout = () => {
  userStore.logout()
  alert('已退出登录')
}
</script>

六、UI 组件库集成:按需引入的艺术

6.1 安装 Element Plus

npm install element-plus
npm install -D unplugin-vue-components unplugin-auto-import

6.2 配置自动按需引入

// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
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()],
      imports: ['vue', 'vue-router', 'pinia'],
      dts: 'src/types/auto-imports.d.ts',
      eslintrc: {
        enabled: true, // 生成 .eslintrc-auto-import.json
        filepath: './.eslintrc-auto-import.json'
      }
    }),
    Components({
      resolvers: [ElementPlusResolver()],
      dts: 'src/types/components.d.ts',
      dirs: ['src/components'] // 自动注册自己的组件
    })
  ]
})

6.3 自定义主题

// src/styles/element.scss
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
  $colors: (
    'primary': (
      'base': #409eff,
    ),
    'success': (
      'base': #67c23a,
    ),
    'warning': (
      'base': #e6a23c,
    ),
    'danger': (
      'base': #f56c6c,
    ),
    'info': (
      'base': #909399,
    ),
  )
);

// 如果需要,可以导入所有样式
// @use "element-plus/theme-chalk/src/index.scss" as *;
// vite.config.ts 添加 CSS 配置
export default defineConfig({
  // ... 其他配置
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@use "@/styles/element.scss" as *;`
      }
    }
  }
})

6.4 在组件中使用

<template>
  <div>
    <el-button type="primary" @click="handleClick">
      主要按钮
    </el-button>
    
    <el-table :data="tableData" style="width: 100%">
      <el-table-column prop="date" label="日期" width="180" />
      <el-table-column prop="name" label="姓名" width="180" />
      <el-table-column prop="address" label="地址" />
    </el-table>
    
    <el-pagination
      v-model:current-page="currentPage"
      :page-size="pageSize"
      :total="total"
      layout="prev, pager, next"
    />
  </div>
</template>

<script setup lang="ts">
// 不用手动导入,自动按需引入!
const handleClick = () => {
  ElMessage.success('点击成功!')
}

const tableData = [
  { date: '2024-01-01', name: '张三', address: '北京市' }
]

const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(100)
</script>

七、HTTP 请求封装:让 API 调用更优雅

7.1 封装 axios

npm install axios
// src/utils/request.ts
import axios from 'axios'
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import { useUserStore } from '@/store/modules/user'
import { ElMessage, ElMessageBox } from 'element-plus'

// 定义响应数据类型
export interface ApiResponse<T = any> {
  code: number
  data: T
  message: string
}

class Request {
  private instance: AxiosInstance
  
  constructor(config: AxiosRequestConfig) {
    this.instance = axios.create(config)
    
    // 请求拦截器
    this.instance.interceptors.request.use(
      (config) => {
        // 添加 token
        const userStore = useUserStore()
        if (userStore.token) {
          config.headers.Authorization = `Bearer ${userStore.token}`
        }
        
        // 开发环境打印请求信息
        if (import.meta.env.DEV) {
          console.log('🚀 请求:', config.method?.toUpperCase(), config.url)
          console.log('参数:', config.params || config.data)
        }
        
        return config
      },
      (error) => {
        return Promise.reject(error)
      }
    )
    
    // 响应拦截器
    this.instance.interceptors.response.use(
      (response: AxiosResponse<ApiResponse>) => {
        const { code, data, message } = response.data
        
        // 根据后端约定的 code 处理业务错误
        if (code !== 200) {
          ElMessage.error(message || '请求失败')
          return Promise.reject(new Error(message))
        }
        
        return data as any
      },
      (error) => {
        // 处理 HTTP 错误
        if (error.response) {
          switch (error.response.status) {
            case 401:
              handleUnauthorized()
              break
            case 403:
              ElMessage.error('没有权限访问')
              break
            case 404:
              ElMessage.error('请求的资源不存在')
              break
            case 500:
              ElMessage.error('服务器错误')
              break
            default:
              ElMessage.error(`请求失败: ${error.response.status}`)
          }
        } else if (error.request) {
          ElMessage.error('网络连接失败,请检查网络')
        } else {
          ElMessage.error('请求配置错误')
        }
        
        return Promise.reject(error)
      }
    )
  }
  
  // 统一请求方法
  public request<T = any>(config: AxiosRequestConfig): Promise<T> {
    return this.instance.request(config)
  }
  
  // GET 请求
  public get<T = any>(url: string, params?: any): Promise<T> {
    return this.instance.get(url, { params })
  }
  
  // POST 请求
  public post<T = any>(url: string, data?: any): Promise<T> {
    return this.instance.post(url, data)
  }
  
  // PUT 请求
  public put<T = any>(url: string, data?: any): Promise<T> {
    return this.instance.put(url, data)
  }
  
  // DELETE 请求
  public delete<T = any>(url: string, params?: any): Promise<T> {
    return this.instance.delete(url, { params })
  }
  
  // 上传文件
  public upload<T = any>(url: string, file: File, onProgress?: (progress: number) => void): Promise<T> {
    const formData = new FormData()
    formData.append('file', file)
    
    return this.instance.post(url, formData, {
      headers: {
        'Content-Type': 'multipart/form-data'
      },
      onUploadProgress: (progressEvent) => {
        if (onProgress && progressEvent.total) {
          const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total)
          onProgress(percentCompleted)
        }
      }
    })
  }
  
  // 下载文件
  public download(url: string, filename?: string): Promise<void> {
    return this.instance.get(url, {
      responseType: 'blob'
    }).then(response => {
      const blob = new Blob([response as any])
      const downloadUrl = window.URL.createObjectURL(blob)
      const link = document.createElement('a')
      link.href = downloadUrl
      link.download = filename || 'download'
      document.body.appendChild(link)
      link.click()
      document.body.removeChild(link)
      window.URL.revokeObjectURL(downloadUrl)
    })
  }
}

// 处理未授权
function handleUnauthorized() {
  ElMessageBox.confirm('登录已过期,请重新登录', '提示', {
    confirmButtonText: '去登录',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(() => {
    const userStore = useUserStore()
    userStore.logout()
    window.location.href = '/login'
  })
}

// 创建实例
const request = new Request({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 30000,
  headers: {
    'Content-Type': 'application/json;charset=UTF-8'
  }
})

export default request

7.2 封装 API 模块

// src/api/user.ts
import request from '@/utils/request'

export interface LoginParams {
  username: string
  password: string
}

export interface UserInfo {
  id: number
  name: string
  avatar: string
  role: string
  permissions: string[]
}

export const userApi = {
  // 登录
  login(data: LoginParams) {
    return request.post<{ token: string; userInfo: UserInfo }>('/user/login', data)
  },
  
  // 获取用户信息
  getUserInfo() {
    return request.get<UserInfo>('/user/info')
  },
  
  // 获取用户列表
  getUserList(params: { page: number; limit: number }) {
    return request.get<{ list: UserInfo[]; total: number }>('/user/list', params)
  },
  
  // 更新用户信息
  updateUserInfo(id: number, data: Partial<UserInfo>) {
    return request.put(`/user/${id}`, data)
  },
  
  // 删除用户
  deleteUser(id: number) {
    return request.delete(`/user/${id}`)
  }
}

八、代码规范:让团队代码像一个人写的

8.1 安装 ESLint + Prettier

npm install -D eslint prettier eslint-plugin-vue @vue/eslint-config-typescript
npm install -D @typescript-eslint/eslint-plugin @typescript-eslint/parser
npm install -D eslint-config-prettier eslint-plugin-prettier

8.2 配置 ESLint

// .eslintrc.js
module.exports = {
  root: true,
  env: {
    browser: true,
    es2021: true,
    node: true
  },
  extends: [
    'plugin:vue/vue3-recommended',
    '@vue/typescript/recommended',
    'plugin:prettier/recommended'
  ],
  parserOptions: {
    ecmaVersion: 2021,
    parser: '@typescript-eslint/parser',
    sourceType: 'module'
  },
  plugins: ['@typescript-eslint'],
  rules: {
    // 自定义规则
    'vue/multi-word-component-names': 'off',
    '@typescript-eslint/no-explicit-any': 'warn',
    '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
  },
  globals: {
    defineProps: 'readonly',
    defineEmits: 'readonly',
    defineExpose: 'readonly',
    withDefaults: 'readonly'
  }
}

8.3 配置 Prettier

// .prettierrc
{
  "semi": false,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "none",
  "printWidth": 100,
  "vueIndentScriptAndStyle": true,
  "endOfLine": "auto"
}

8.4 配置 Husky + lint-staged

npm install -D husky lint-staged

# 初始化 husky
npx husky install

# 添加 pre-commit 钩子
npx husky add .husky/pre-commit "npx lint-staged"
// package.json 添加 lint-staged 配置
{
  "lint-staged": {
    "*.{js,ts,vue}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{css,scss,html,json,md}": [
      "prettier --write"
    ]
  }
}

九、打包优化:让项目飞起来

9.1 配置打包分析

npm install -D rollup-plugin-visualizer
// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer'

export default defineConfig({
  plugins: [
    // ... 其他插件
    visualizer({
      filename: 'dist/stats.html',
      open: true,
      gzipSize: true,
      brotliSize: true
    })
  ]
})

9.2 代码分割优化

// vite.config.ts
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        // 手动分包
        manualChunks(id) {
          if (id.includes('node_modules')) {
            // 将 vue、vue-router、pinia 打包在一起
            if (id.includes('vue') || id.includes('pinia') || id.includes('vue-router')) {
              return 'vendor-vue'
            }
            
            // 将 element-plus 单独打包
            if (id.includes('element-plus')) {
              return 'vendor-element'
            }
            
            // 其他依赖打包在一起
            return 'vendor-other'
          }
        },
        
        // 自定义 chunk 文件名
        chunkFileNames: 'assets/js/[name]-[hash].js',
        entryFileNames: 'assets/js/[name]-[hash].js',
        assetFileNames: 'assets/[ext]/[name]-[hash].[ext]'
      }
    },
    
    // 启用/禁用 CSS 代码分割
    cssCodeSplit: true,
    
    // 设置资源大小限制
    assetsInlineLimit: 4096
  }
})

9.3 图片压缩

npm install -D vite-plugin-imagemin
// vite.config.ts
import viteImagemin from 'vite-plugin-imagemin'

export default defineConfig({
  plugins: [
    // ... 其他插件
    viteImagemin({
      gifsicle: {
        optimizationLevel: 7,
        interlaced: false
      },
      optipng: {
        optimizationLevel: 7
      },
      mozjpeg: {
        quality: 80
      },
      pngquant: {
        quality: [0.8, 0.9],
        speed: 4
      },
      svgo: {
        plugins: [
          {
            name: 'removeViewBox'
          },
          {
            name: 'removeEmptyAttrs',
            active: false
          }
        ]
      }
    })
  ]
})

9.4 打包进度条

npm install -D vite-plugin-progress
// vite.config.ts
import progress from 'vite-plugin-progress'

export default defineConfig({
  plugins: [
    // ... 其他插件
    progress()
  ]
})

9.5 压缩打包结果

npm install -D vite-plugin-compression
// vite.config.ts
import viteCompression from 'vite-plugin-compression'

export default defineConfig({
  plugins: [
    // ... 其他插件
    viteCompression({
      verbose: true,
      disable: false,
      threshold: 10240, // 大于10kb才压缩
      algorithm: 'gzip',
      ext: '.gz'
    })
  ]
})

十、完整的 vite.config.ts

// vite.config.ts
import { defineConfig, loadEnv } 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'
import { visualizer } from 'rollup-plugin-visualizer'
import viteImagemin from 'vite-plugin-imagemin'
import progress from 'vite-plugin-progress'
import viteCompression from 'vite-plugin-compression'

// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
  // 加载环境变量
  const env = loadEnv(mode, process.cwd())
  
  return {
    plugins: [
      vue(),
      
      // 自动导入
      AutoImport({
        resolvers: [ElementPlusResolver()],
        imports: ['vue', 'vue-router', 'pinia'],
        dts: 'src/types/auto-imports.d.ts',
        eslintrc: {
          enabled: true,
          filepath: './.eslintrc-auto-import.json'
        }
      }),
      
      // 自动注册组件
      Components({
        resolvers: [ElementPlusResolver()],
        dts: 'src/types/components.d.ts',
        dirs: ['src/components']
      }),
      
      // 图片压缩
      viteImagemin({
        optipng: { optimizationLevel: 7 },
        mozjpeg: { quality: 80 },
        pngquant: { quality: [0.8, 0.9], speed: 4 },
        svgo: { plugins: [{ name: 'removeViewBox' }] }
      }),
      
      // 打包进度条
      progress(),
      
      // gzip压缩
      viteCompression({
        threshold: 10240,
        algorithm: 'gzip',
        ext: '.gz'
      }),
      
      // 打包分析(只在分析时开启)
      ...(mode === 'analyze' ? [visualizer({
        filename: 'dist/stats.html',
        open: true,
        gzipSize: true,
        brotliSize: true
      })] : [])
    ],
    
    // 路径别名
    resolve: {
      alias: {
        '@': resolve(__dirname, 'src'),
        '@components': resolve(__dirname, 'src/components'),
        '@views': resolve(__dirname, 'src/views'),
        '@utils': resolve(__dirname, 'src/utils'),
        '@styles': resolve(__dirname, 'src/styles')
      }
    },
    
    // CSS 配置
    css: {
      preprocessorOptions: {
        scss: {
          additionalData: `@use "@/styles/element.scss" as *;`
        }
      }
    },
    
    // 开发服务器配置
    server: {
      port: 3000,
      open: true,
      cors: true,
      proxy: {
        '/api': {
          target: env.VITE_API_BASE_URL,
          changeOrigin: true,
          rewrite: (path) => path.replace(/^/api/, '')
        }
      }
    },
    
    // 构建配置
    build: {
      target: 'es2015',
      outDir: 'dist',
      assetsDir: 'assets',
      assetsInlineLimit: 4096,
      sourcemap: env.VITE_BUILD_SOURCEMAP === 'true',
      reportCompressedSize: false,
      chunkSizeWarningLimit: 500,
      
      rollupOptions: {
        output: {
          manualChunks(id) {
            if (id.includes('node_modules')) {
              if (id.includes('vue') || id.includes('pinia') || id.includes('vue-router')) {
                return 'vendor-vue'
              }
              if (id.includes('element-plus')) {
                return 'vendor-element'
              }
              return 'vendor-other'
            }
          },
          chunkFileNames: 'assets/js/[name]-[hash].js',
          entryFileNames: 'assets/js/[name]-[hash].js',
          assetFileNames: 'assets/[ext]/[name]-[hash].[ext]'
        }
      }
    }
  }
})

十一、package.json 完整配置

{
  "name": "vite-enterprise-demo",
  "version": "1.0.0",
  "description": "Vite企业级项目实战",
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc && vite build",
    "build:analyze": "vue-tsc && vite build --mode analyze",
    "preview": "vite preview",
    "type-check": "vue-tsc --noEmit",
    "lint": "eslint . --ext .vue,.js,.ts --fix",
    "format": "prettier --write 'src/**/*.{vue,js,ts,css,scss}'",
    "prepare": "husky install"
  },
  "dependencies": {
    "axios": "^1.6.0",
    "element-plus": "^2.4.0",
    "pinia": "^2.1.0",
    "pinia-plugin-persistedstate": "^3.2.0",
    "vue": "^3.3.0",
    "vue-router": "^4.2.0"
  },
  "devDependencies": {
    "@types/node": "^20.0.0",
    "@typescript-eslint/eslint-plugin": "^6.0.0",
    "@typescript-eslint/parser": "^6.0.0",
    "@vitejs/plugin-vue": "^4.0.0",
    "@vue/eslint-config-typescript": "^12.0.0",
    "eslint": "^8.0.0",
    "eslint-config-prettier": "^9.0.0",
    "eslint-plugin-prettier": "^5.0.0",
    "eslint-plugin-vue": "^9.0.0",
    "husky": "^8.0.0",
    "lint-staged": "^15.0.0",
    "prettier": "^3.0.0",
    "rollup-plugin-visualizer": "^5.9.0",
    "sass": "^1.69.0",
    "typescript": "^5.0.0",
    "unplugin-auto-import": "^0.16.0",
    "unplugin-vue-components": "^0.25.0",
    "vite": "^4.0.0",
    "vite-plugin-compression": "^0.5.0",
    "vite-plugin-imagemin": "^0.6.0",
    "vite-plugin-progress": "^0.0.7",
    "vue-tsc": "^1.8.0"
  }
}

十二、总结:从“搭环境”到“搭项目”

回顾一下,我们做了什么:

  1. 项目初始化:从空文件夹开始,手动搭建了项目结构
  2. TypeScript配置:告别 any,拥抱类型安全
  3. 环境变量:一套代码,多套环境
  4. 路由配置:让页面动起来
  5. Pinia状态管理:比 Vuex 更香的选择
  6. Element Plus 按需引入:告别全量引入的臃肿
  7. Axios封装:统一的请求处理
  8. 代码规范:ESLint + Prettier + Husky
  9. 打包优化:代码分割、图片压缩、gzip

现在,你拥有的是一个真正的企业级项目模板,而不仅仅是一个“能跑的项目”。

为什么这很重要?

// 面试官问:你们的项目是怎么配置的?
// 初级回答:用的 create-vue 脚手架
// 中级回答:配置了路由、状态管理、按需引入
// 高级回答:从零搭建了完整的工程化体系,包括代码规范、打包优化、环境管理等

当你能够从零搭建一个企业级项目,你就掌握了前端工程化的核心能力。这不仅是一份工作,更是一种将想法转化为产品的能力。

下一步做什么?

  1. 添加单元测试:Vitest + Vue Test Utils
  2. 配置 CI/CD:GitHub Actions 自动部署
  3. 添加 Mock 服务:开发环境模拟数据
  4. 性能监控:集成 Sentry 等错误监控
  5. 文档生成:使用 VitePress 生成组件文档

记住:好的工程化不是一蹴而就的,而是在实践中不断完善的。现在,带着这个模板去创建你的下一个项目吧!🚀