Vite 已成为 Vue 项目的标配构建工具。本文从零搭建一个包含目录规范、请求封装、权限路由、Mock 数据、环境配置和 CI/CD 的企业级 Vue 3 项目。
一、项目初始化
创建项目
# 创建 Vue 3 项目
npm create vue@latest my-app
# 交互选择:
# ✓ TypeScript
# ✓ JSX Support
# ✓ Router
# ✓ Pinia
# ✓ Vitest
# ✓ ESLint
# ✓ Prettier
cd my-app
npm install安装核心依赖
# UI 组件库(按需选择)
npm install element-plus
npm install -D unplugin-vue-components unplugin-auto-import
# HTTP 请求
npm install axios
# 实用工具
npm install dayjs lodash-es @vueuse/core
# 图表(按需)
npm install echarts二、目录结构规范
my-app/
├── public/ # 静态资源(不参与构建)
├── src/
│ ├── api/ # API 接口定义
│ │ ├── modules/
│ │ │ ├── user.js
│ │ │ ├── order.js
│ │ │ └── product.js
│ │ └── index.js # 统一导出
│ ├── assets/ # 静态资源(参与构建)
│ │ ├── images/
│ │ └── styles/
│ ├── components/ # 全局公共组件
│ │ ├── AppTable/
│ │ │ └── index.vue
│ │ └── AppDialog/
│ ├── composables/ # 组合式函数
│ │ ├── useAuth.js
│ │ ├── usePagination.js
│ │ └── useRequest.js
│ ├── config/ # 项目配置
│ │ └── index.js
│ ├── constants/ # 常量定义
│ │ └── index.js
│ ├── directives/ # 自定义指令
│ │ ├── permission.js
│ │ └── lazy.js
│ ├── layouts/ # 布局组件
│ │ ├── DefaultLayout.vue
│ │ └── BlankLayout.vue
│ ├── plugins/ # 插件配置
│ │ └── elementPlus.js
│ ├── router/ # 路由
│ │ ├── index.js
│ │ ├── guards.js
│ │ └── routes/
│ │ ├── modules/ # 路由模块
│ │ └── index.js
│ ├── stores/ # Pinia Store
│ │ ├── user.js
│ │ ├── permission.js
│ │ └── app.js
│ ├── styles/ # 全局样式
│ │ ├── variables.scss
│ │ ├── mixins.scss
│ │ └── index.scss
│ ├── utils/ # 工具函数
│ │ ├── request.js # axios 封装
│ │ ├── storage.js
│ │ └── format.js
│ ├── views/ # 页面组件
│ │ ├── login/
│ │ ├── dashboard/
│ │ └── system/
│ ├── App.vue
│ └── main.js
├── .env # 所有环境通用
├── .env.development # 开发环境
├── .env.production # 生产环境
├── .env.staging # 预发布环境
├── vite.config.js
└── package.json三、Vite 配置详解
// vite.config.js
import { fileURLToPath, URL } from 'node:url'
import { defineConfig, loadEnv } 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(({ mode }) => {
const env = loadEnv(mode, process.cwd())
return {
plugins: [
vue(),
// 自动导入 Vue API
AutoImport({
imports: ['vue', 'vue-router', 'pinia'],
resolvers: [ElementPlusResolver()],
dts: 'src/auto-imports.d.js'
}),
// 组件自动注册
Components({
resolvers: [ElementPlusResolver()],
dts: 'src/components.d.js'
})
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
css: {
preprocessorOptions: {
scss: {
additionalData: `@use "@/styles/variables.scss" as *;`
}
}
},
server: {
port: 3000,
open: true,
proxy: {
'/api': {
target: env.VITE_API_BASE_URL,
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
},
build: {
// 输出目录
outDir: 'dist',
// 静态资源分包
rollupOptions: {
output: {
manualChunks: {
'vendor-vue': ['vue', 'vue-router', 'pinia'],
'vendor-ui': ['element-plus'],
'vendor-utils': ['lodash-es', 'dayjs']
},
// 分包体积警告阈值
chunkFileNames: 'assets/js/[name]-[hash].js',
entryFileNames: 'assets/js/[name]-[hash].js',
assetFileNames: 'assets/[ext]/[name]-[hash].[ext]'
}
},
// chunk 大小警告
chunkSizeWarningLimit: 1000,
// 生产环境移除 console
minify: 'terser',
terserOptions: {
compress: {
drop_console: env.VITE_DROP_CONSOLE === 'true',
drop_debugger: true
}
}
}
}
})四、环境变量配置
# .env — 通用配置
VITE_APP_TITLE=企业管理系统
# .env.development
VITE_API_BASE_URL=http://localhost:8080
VITE_DROP_CONSOLE=false
# .env.production
VITE_API_BASE_URL=https://api.example.com
VITE_DROP_CONSOLE=true// src/config/index.js
export const config = {
title: import.meta.env.VITE_APP_TITLE,
apiBaseUrl: import.meta.env.VITE_API_BASE_URL,
tokenKey: 'access_token',
tokenExpireKey: 'token_expire'
}五、Axios 封装
// src/utils/request.js
import axios from 'axios'
import { ElMessage } from 'element-plus'
import { useUserStore } from '@/stores/user'
import router from '@/router'
const service = axios.create({
baseURL: '/api',
timeout: 15000
})
// 请求拦截器
service.interceptors.request.use(
(config) => {
const userStore = useUserStore()
// 携带 token
if (userStore.token) {
config.headers.Authorization = `Bearer ${userStore.token}`
}
// GET 请求防缓存
if (config.method === 'get') {
config.params = { ...config.params, _t: Date.now() }
}
return config
},
(error) => Promise.reject(error)
)
// 响应拦截器
let isRefreshing = false
let pendingRequests = []
service.interceptors.response.use(
(response) => {
const { code, data, message } = response.data
// 业务状态码判断
switch (code) {
case 200:
return data
case 401:
handleUnauthorized()
return Promise.reject(new Error(message))
default:
ElMessage.error(message || '请求失败')
return Promise.reject(new Error(message))
}
},
async (error) => {
const { response } = error
if (response) {
switch (response.status) {
case 401:
handleUnauthorized()
break
case 403:
ElMessage.error('没有权限访问')
break
case 404:
ElMessage.error('请求的资源不存在')
break
case 500:
ElMessage.error('服务器错误')
break
default:
ElMessage.error(response.data?.message || '请求失败')
}
} else if (error.code === 'ECONNABORTED') {
ElMessage.error('请求超时')
} else {
ElMessage.error('网络异常')
}
return Promise.reject(error)
}
)
// Token 过期处理
async function handleUnauthorized() {
const userStore = useUserStore()
if (!isRefreshing) {
isRefreshing = true
try {
await userStore.refreshToken()
// 重放失败请求
pendingRequests.forEach(cb => cb())
pendingRequests = []
} catch {
userStore.logout()
router.push('/login')
} finally {
isRefreshing = false
}
}
return new Promise((resolve) => {
pendingRequests.push(() => resolve(service(error.config)))
})
}
export default service// src/api/modules/user.js
import request from '@/utils/request'
export const userApi = {
login: (data) => request.post('/auth/login', data),
logout: () => request.post('/auth/logout'),
getProfile: () => request.get('/user/profile'),
updateProfile: (data) => request.put('/user/profile', data),
changePassword: (data) => request.put('/user/password', data),
// 分页列表
getUsers: (params) => request.get('/users', { params }),
createUser: (data) => request.post('/users', data),
updateUser: (id, data) => request.put(`/users/${id}`, data),
deleteUser: (id) => request.delete(`/users/${id}`)
}六、路由与权限
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import { setupGuards } from './guards'
// 基础路由 — 无需权限
export const constantRoutes = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/login/index.vue'),
meta: { title: '登录' }
},
{
path: '/',
component: () => import('@/layouts/DefaultLayout.vue'),
redirect: '/dashboard',
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/dashboard/index.vue'),
meta: { title: '仪表盘', icon: 'Odometer' }
}
]
},
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: () => import('@/views/404.vue') }
]
const router = createRouter({
history: createWebHistory(),
routes: constantRoutes
})
setupGuards(router)
export default router// src/router/guards.js
import { useUserStore } from '@/stores/user'
import { usePermissionStore } from '@/stores/permission'
const whiteList = ['/login']
export function setupGuards(router) {
router.beforeEach(async (to, from, next) => {
document.title = `${to.meta.title || ''} - 系统名称`
const userStore = useUserStore()
if (userStore.token) {
if (to.path === '/login') {
next({ path: '/' })
} else {
// 未加载用户信息和动态路由
if (!userStore.user) {
try {
// 获取用户信息
await userStore.getProfile()
// 生成动态路由
const permissionStore = usePermissionStore()
const accessRoutes = await permissionStore.generateRoutes()
// 动态添加路由
accessRoutes.forEach(route => router.addRoute(route))
// 重新导航,确保路由已注册
next({ ...to, replace: true })
} catch (error) {
userStore.resetToken()
next(`/login?redirect=${to.path}`)
}
} else {
next()
}
}
} else {
// 白名单直接放行
if (whiteList.includes(to.path)) {
next()
} else {
next(`/login?redirect=${to.path}`)
}
}
})
}// src/stores/permission.js
import { defineStore } from 'pinia'
import api from '@/api'
// 视图模块映射
const viewModules = import.meta.glob('@/views/**/*.vue')
export const usePermissionStore = defineStore('permission', () => {
const routes = ref([])
async function generateRoutes() {
// 从后端获取菜单权限
const menus = await api.menu.getUserMenus()
// 转换后端菜单为路由
const asyncRoutes = transformRoutes(menus)
routes.value = asyncRoutes
return asyncRoutes
}
function transformRoutes(menus, parentPath = '') {
return menus.map(menu => {
const route = {
path: menu.path.startsWith('/') ? menu.path : `${parentPath}/${menu.path}`,
name: menu.name,
meta: { title: menu.title, icon: menu.icon },
children: []
}
// 组件解析
const componentPath = menu.component || 'layouts/DefaultLayout'
route.component = viewModules[`/src/views/${componentPath}.vue`]
if (menu.children?.length) {
route.children = transformRoutes(menu.children, route.path)
}
return route
})
}
return { routes, generateRoutes }
})七、Mock 数据
// vite.config.js 增加 mock 插件
import { viteMockServe } from 'vite-plugin-mock'
plugins: [
vue(),
viteMockServe({
mockPath: 'mock',
localEnabled: true
})
]// mock/user.js
export default [
{
url: '/api/user/profile',
method: 'get',
response: () => {
return {
code: 200,
data: {
id: 1,
name: '张三',
roles: ['admin'],
permissions: ['system:user:list', 'system:user:create']
}
}
}
},
{
url: '/api/users',
method: 'get',
response: ({ query }) => {
const { page = 1, pageSize = 10 } = query
const list = Array.from({ length: 50 }, (_, i) => ({
id: i + 1,
name: `用户${i + 1}`,
email: `user${i + 1}@example.com`,
status: i % 2
}))
return {
code: 200,
data: {
list: list.slice((page - 1) * pageSize, page * pageSize),
total: list.length
}
}
}
}
]八、代码规范
// eslint.config.js (Flat Config)
import js from '@eslint/js'
import vue from 'eslint-plugin-vue'
import prettier from 'eslint-config-prettier'
export default [
js.configs.recommended,
...vue.configs['flat/recommended'],
prettier,
{
rules: {
'vue/multi-word-component-names': 'off',
'no-console': 'warn',
'no-debugger': 'warn'
}
}
]// .prettierrc
{
"semi": false,
"singleQuote": true,
"printWidth": 100,
"trailingComma": "none",
"arrowParens": "always"
}九、Git 提交规范与 CI
// commitlint.config.js
export default {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [2, 'always', [
'feat', 'fix', 'docs', 'style',
'refactor', 'perf', 'test', 'chore'
]]
}
}# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install
run: npm ci
- name: Lint
run: npm run lint
- name: Test
run: npm run test:unit
- name: Build
run: npm run build
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: dist
path: dist十、小结
企业级 Vue 项目的工程化核心:规范的目录结构让团队协作有章可循,统一的请求封装处理认证与错误,动态路由实现权限控制,Mock 让前后端并行开发,ESLint + CI 保证代码质量。这套脚手架可以直接作为新项目的起点。
本文由 inspirecl.asia 原创,转载请注明出处。
评论 (0)