零、为什么我们要“折腾”环境?
还记得你第一次用 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"
}
}
十二、总结:从“搭环境”到“搭项目”
回顾一下,我们做了什么:
- 项目初始化:从空文件夹开始,手动搭建了项目结构
- TypeScript配置:告别 any,拥抱类型安全
- 环境变量:一套代码,多套环境
- 路由配置:让页面动起来
- Pinia状态管理:比 Vuex 更香的选择
- Element Plus 按需引入:告别全量引入的臃肿
- Axios封装:统一的请求处理
- 代码规范:ESLint + Prettier + Husky
- 打包优化:代码分割、图片压缩、gzip
现在,你拥有的是一个真正的企业级项目模板,而不仅仅是一个“能跑的项目”。
为什么这很重要?
// 面试官问:你们的项目是怎么配置的?
// 初级回答:用的 create-vue 脚手架
// 中级回答:配置了路由、状态管理、按需引入
// 高级回答:从零搭建了完整的工程化体系,包括代码规范、打包优化、环境管理等
当你能够从零搭建一个企业级项目,你就掌握了前端工程化的核心能力。这不仅是一份工作,更是一种将想法转化为产品的能力。
下一步做什么?
- 添加单元测试:Vitest + Vue Test Utils
- 配置 CI/CD:GitHub Actions 自动部署
- 添加 Mock 服务:开发环境模拟数据
- 性能监控:集成 Sentry 等错误监控
- 文档生成:使用 VitePress 生成组件文档
记住:好的工程化不是一蹴而就的,而是在实践中不断完善的。现在,带着这个模板去创建你的下一个项目吧!🚀