Vue 3 + Vite 前端项目实践指南
适用版本(截至 2026 年 7 月):Vue 3.5+ | Vite 6/7 | TypeScript 5.6+ | Node.js ^20.19.0 || >=22.12.0
目标:从零搭建一个可投入生产的工程化项目,覆盖「初始化 → 架构 → 规范 → 联调 → 构建部署」全流程。
全景路线图
1环境准备 ──▶ 脚手架创建 ──▶ 目录规划 ──▶ Vite 配置 ──▶ 路由/状态/请求层 2 │ 3部署上线 ◀── 构建优化 ◀── 代码规范 ◀── 组件开发实践 ◀──────┘ 4
Step 01 · 环境准备
① 安装 Node.js(版本必须满足 ^20.19.0 || >=22.12.0,这是 Vue 官方文档的硬性要求)
推荐使用 nvm(Windows 用 nvm-windows)管理多版本:
1nvm install 22 2nvm use 22 3node -v # 验证 4npm -v 5
② 安装 pnpm(2026 年社区主流选择:磁盘占用小、安装快、幽灵依赖管控严格)
1npm install -g pnpm 2pnpm config set registry https://registry.npmmirror.com # 国内镜像加速 3
③ 编辑器:VS Code + 插件 Vue - Official(原 Volar,务必禁用旧的 Vetur)。
Step 02 · 创建项目
两条路线,按需选择:
| 方式 | 命令 | 特点 |
|---|---|---|
| create-vue(官方推荐) | pnpm create vue@latest | 交互式勾选 TS / Router / Pinia / ESLint / Prettier / Vitest,开箱即用 |
| create-vite(极简模板) | pnpm create vite my-app --template vue-ts | 只给最干净的骨架,一切自己装,适合想完全掌控配置的人 |
以官方脚手架为例:
1pnpm create vue@latest 2 3# 交互式选项(建议新手全部按下面选): 4# ✔ Project name: … my-vue-app 5# ✔ Add TypeScript? … Yes 6# ✔ Add JSX Support? … No(需要时再开) 7# ✔ Add Vue Router? … Yes 8# ✔ Add Pinia? … Yes 9# ✔ Add Vitest for Unit Testing? … Yes 10# ✔ Add ESLint? … Yes(生成 ESLint 9 扁平配置 eslint.config.js) 11# ✔ Add Prettier? … Yes 12 13cd my-vue-app 14pnpm install 15pnpm dev # 浏览器打开 http://localhost:5173 16
💡 实践建议:团队项目优先用 create-vue——它生成的 ESLint 9 flat config、TS 配置、env.d.ts 都是官方校准过的,能避开大量新手坑。
Step 03 · 规划目录结构
脚手架的默认结构只够写 Demo,正式项目建议重构为「按职责分层」:
1src/ 2├── api/ # 接口请求模块(按业务域拆分:user.ts、order.ts) 3├── assets/ # 静态资源(图片、字体、全局样式) 4│ └── styles/ 5│ ├── variables.scss # 设计变量 6│ └── reset.scss # 样式重置 7├── components/ # 全局通用组件(BaseButton、AppHeader…) 8├── composables/ # 组合式函数(useXxx,逻辑复用核心) 9├── layouts/ # 布局组件(DefaultLayout、BlankLayout) 10├── router/ # 路由配置与守卫 11│ ├── index.ts 12│ └── routes.ts 13├── stores/ # Pinia 状态模块 14│ └── modules/ 15├── types/ # 全局 TS 类型声明 16├── utils/ # 工具函数(request.ts、format.ts、storage.ts) 17├── views/ # 页面级组件(按路由组织) 18│ ├── home/ 19│ └── user/ 20├── App.vue 21└── main.ts 22
原则一句话:**views**** 只放页面,可复用的进 components,可复用的逻辑进 ****composables**。
Step 04 · 核心配置 vite.config.ts
1import { fileURLToPath, URL } from 'node:url' 2import { defineConfig, loadEnv } from 'vite' 3import vue from '@vitejs/plugin-vue' 4import AutoImport from 'unplugin-auto-import/vite' 5import Components from 'unplugin-vue-components/vite' 6 7export default defineConfig(({ mode }) => { 8 const env = loadEnv(mode, process.cwd()) 9 10 return { 11 plugins: [ 12 vue(), 13 // 自动导入 ref/computed/watch 等,免去满屏 import 14 AutoImport({ imports: ['vue', 'vue-router', 'pinia'] }), 15 // 组件按需自动注册,无需手动 import + components 声明 16 Components({ dirs: ['src/components'] }), 17 ], 18 resolve: { 19 alias: { 20 '@': fileURLToPath(new URL('./src', import.meta.url)), 21 }, 22 }, 23 css: { 24 preprocessorOptions: { 25 scss: { 26 // 全局注入设计变量,组件内无需重复 @use 27 additionalData: `@use "@/assets/styles/variables.scss" as *;`, 28 }, 29 }, 30 }, 31 server: { 32 port: 5173, 33 open: true, 34 // 开发代理:解决跨域,指向真实后端 35 proxy: { 36 '/api': { 37 target: env.VITE_API_BASE_URL, 38 changeOrigin: true, 39 rewrite: (path) => path.replace(/^\/api/, ''), 40 }, 41 }, 42 }, 43 build: { 44 target: 'es2020', 45 sourcemap: false, 46 chunkSizeWarningLimit: 1000, 47 rollupOptions: { 48 output: { 49 // 手动分包:第三方库独立 chunk,利用浏览器缓存 50 manualChunks: { 51 vue: ['vue', 'vue-router', 'pinia'], 52 }, 53 }, 54 }, 55 }, 56 } 57}) 58
同步配置 TS 路径别名(tsconfig.app.json):
1{ 2 "compilerOptions": { 3 "baseUrl": ".", 4 "paths": { "@/*": ["src/*"] } 5 } 6} 7
Step 05 · 多环境变量
根目录创建三个文件(注意:只有 VITE_ 前缀的变量才会暴露给客户端代码):
1# .env.development 2VITE_API_BASE_URL=http://localhost:8080 3VITE_APP_TITLE=我的应用(开发) 4 5# .env.production 6VITE_API_BASE_URL=https://api.example.com 7VITE_APP_TITLE=我的应用 8
补充类型提示(src/types/env.d.ts),让 import.meta.env 有智能补全:
1/// <reference types="vite/client" /> 2interface ImportMetaEnv { 3 readonly VITE_API_BASE_URL: string 4 readonly VITE_APP_TITLE: string 5} 6
Step 06 · 路由:Vue Router
src/router/routes.ts —— 全部使用路由懒加载,首屏只加载当前页面:
1import type { RouteRecordRaw } from 'vue-router' 2 3export const routes: RouteRecordRaw[] = [ 4 { 5 path: '/', 6 component: () => import('@/layouts/DefaultLayout.vue'), 7 children: [ 8 { path: '', name: 'Home', component: () => import('@/views/home/index.vue') }, 9 { 10 path: 'user/:id', 11 name: 'UserDetail', 12 component: () => import('@/views/user/detail.vue'), 13 meta: { title: '用户详情', requiresAuth: true }, 14 }, 15 ], 16 }, 17 { path: '/:pathMatch(.*)*', name: 'NotFound', component: () => import('@/views/404.vue') }, 18] 19
src/router/index.ts —— 全局守卫统一处理标题与鉴权:
1import { createRouter, createWebHistory } from 'vue-router' 2import { routes } from './routes' 3import { useUserStore } from '@/stores/modules/user' 4 5const router = createRouter({ 6 history: createWebHistory(import.meta.env.BASE_URL), 7 routes, 8 scrollBehavior: () => ({ top: 0 }), 9}) 10 11router.beforeEach((to) => { 12 document.title = (to.meta.title as string) ?? import.meta.env.VITE_APP_TITLE 13 const userStore = useUserStore() 14 if (to.meta.requiresAuth && !userStore.isLoggedIn) { 15 return { name: 'Login', query: { redirect: to.fullPath } } 16 } 17}) 18 19export default router 20
Step 07 · 状态管理:Pinia
推荐 Setup Store 写法(与 <script setup> 心智一致,TS 推导更顺):
src/stores/modules/user.ts
1import { ref, computed } from 'vue' 2import { defineStore } from 'pinia' 3import { fetchUserInfo, login as loginApi } from '@/api/user' 4import type { UserInfo, LoginParams } from '@/types/user' 5 6export const useUserStore = defineStore('user', () => { 7 // state 8 const token = ref(localStorage.getItem('token') ?? '') 9 const profile = ref<UserInfo | null>(null) 10 11 // getters 12 const isLoggedIn = computed(() => !!token.value) 13 14 // actions(支持 async) 15 async function login(params: LoginParams) { 16 const { data } = await loginApi(params) 17 token.value = data.token 18 localStorage.setItem('token', data.token) 19 } 20 21 async function loadProfile() { 22 const { data } = await fetchUserInfo() 23 profile.value = data 24 } 25 26 function logout() { 27 token.value = '' 28 profile.value = null 29 localStorage.removeItem('token') 30 } 31 32 return { token, profile, isLoggedIn, login, loadProfile, logout } 33}) 34
main.ts 挂载:
1import { createApp } from 'vue' 2import { createPinia } from 'pinia' 3import App from './App.vue' 4import router from './router' 5 6createApp(App).use(createPinia()).use(router).mount('#app') 7
💡 经验法则:只有「跨页面共享、需要持久化、服务端缓存」的数据才进 Pinia;组件内部状态留在 ref 里即可,不要滥用全局状态。
Step 08 · 请求层:Axios 二次封装
src/utils/request.ts —— 拦截器统一处理 Token、错误、Loading:
1import axios, { AxiosError } from 'axios' 2import type { AxiosInstance, InternalAxiosRequestConfig } from 'axios' 3import { useUserStore } from '@/stores/modules/user' 4import { ElMessage } from 'element-plus' 5 6const service: AxiosInstance = axios.create({ 7 baseURL: '/api', // 开发走 proxy,生产由部署层转发 8 timeout: 15_000, 9}) 10 11// 请求拦截:注入 Token 12service.interceptors.request.use((config: InternalAxiosRequestConfig) => { 13 const { token } = useUserStore() 14 if (token) config.headers.Authorization = `Bearer ${token}` 15 return config 16}) 17 18// 响应拦截:剥离 data 层 + 统一错误出口 19service.interceptors.response.use( 20 (res) => { 21 const { code, data, message } = res.data 22 if (code !== 0) { 23 ElMessage.error(message ?? '请求失败') 24 return Promise.reject(new Error(message)) 25 } 26 return data 27 }, 28 (error: AxiosError) => { 29 if (error.response?.status === 401) { 30 useUserStore().logout() 31 location.href = '/login' 32 } else { 33 ElMessage.error(error.message || '网络异常,请稍后重试') 34 } 35 return Promise.reject(error) 36 }, 37) 38 39export default service 40
src/api/user.ts —— 接口按业务域聚合,返回值必须标注类型:
1import request from '@/utils/request' 2import type { UserInfo, LoginParams, LoginResult } from '@/types/user' 3 4export const login = (data: LoginParams) => 5 request.post<unknown, LoginResult>('/auth/login', data) 6 7export const fetchUserInfo = () => 8 request.get<unknown, UserInfo>('/user/profile') 9
Step 09 · 组件开发实践
**① 统一使用 ****<script setup lang="ts">**,配合 defineProps 泛型声明:
1<script setup lang="ts"> 2interface Props { 3 title: string 4 status?: 'idle' | 'loading' | 'done' 5} 6const props = withDefaults(defineProps<Props>(), { status: 'idle' }) 7 8const emit = defineEmits<{ 9 confirm: [id: number] 10}>() 11</script> 12 13<template> 14 <section class="task-card"> 15 <h3>{{ title }}</h3> 16 <button :disabled="props.status === 'loading'" @click="emit('confirm', 1)"> 17 {{ props.status === 'loading' ? '处理中…' : '确认' }} 18 </button> 19 </section> 20</template> 21 22<style scoped lang="scss"> 23.task-card { /* 样式隔离,配合 BEM 或 CSS Modules */ } 24</style> 25
② 逻辑复用抽成 Composable(这是 Vue 3 区别于 Vue 2 mixins 的核心红利):
src/composables/usePagination.ts
1import { ref, reactive } from 'vue' 2 3export function usePagination(fetcher: (page: number, size: number) => Promise<any[]>) { 4 const list = ref<any[]>([]) 5 const loading = ref(false) 6 const pager = reactive({ page: 1, size: 20, total: 0 }) 7 8 async function load() { 9 loading.value = true 10 try { 11 list.value = await fetcher(pager.page, pager.size) 12 } finally { 13 loading.value = false 14 } 15 } 16 17 return { list, loading, pager, load } 18} 19
③ 样式策略:组件内用 scoped;主题级变量收敛到 variables.scss 的 CSS 自定义属性,支持暗色模式一行切换。
Step 10 · 代码规范与提交约束
create-vue 已生成 ESLint 9 扁平配置(eslint.config.js),再补两道保险:
① Git Hooks(提交前自动检查 + 格式化):
1pnpm add -D lint-staged 2npx husky init 3
1// package.json 2{ 3 "lint-staged": { 4 "*.{ts,vue}": ["eslint --fix", "prettier --write"] 5 } 6} 7
1# .husky/pre-commit 2pnpm lint-staged 3
② VS Code 保存自动修复(.vscode/settings.json,团队共享):
1{ 2 "editor.formatOnSave": true, 3 "editor.defaultFormatter": "esbenp.prettier-vscode", 4 "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }, 5 "eslint.useFlatConfig": true 6} 7
Step 11 · 构建、验收与部署
1pnpm build # 产物输出到 dist/ 2pnpm preview # 本地起静态服务预览生产包 —— 必做!很多问题只在生产包暴露 3
部署前验收清单:
pnpm build无 TS 报错、无 chunk 超大警告preview环境跑通核心链路(登录 → 主要页面 → 刷新不 404)- History 模式已配置服务器回退(Nginx 示例如下)
- 静态资源走 CDN,开启 gzip/brotli
1server { 2 listen 80; 3 root /usr/share/nginx/html; 4 index index.html; 5 6 location / { 7 try_files $uri $uri/ /index.html; # History 路由回退,关键! 8 } 9 location /api/ { 10 proxy_pass http://backend:8080/; # 生产代理转发 11 } 12} 13
Step 12 · 性能优化要点(按需启用)
| 手段 | 做法 |
|---|---|
| 路由/组件懒加载 | () => import(...),配合 defineAsyncComponent |
| 第三方库分包 | manualChunks 拆分 vue 生态 / UI 库 / 图表库 |
| 图片资源 | 小图转 base64(assetsInlineLimit),大图用 WebP + 懒加载 |
| 渲染优化 | v-once、v-memo、长列表虚拟滚动;避免在模板里写复杂表达式 |
| 依赖预构建 | Vite 自动处理;大型 CJS 库确认识别正常(看启动日志) |
| 体积分析 | pnpm add -D rollup-plugin-visualizer,构建后看 treemap 找大头 |
常见坑速查
| 现象 | 原因 / 解法 |
|---|---|
| 刷新页面 404 | History 模式未配 try_files 回退 |
| 环境变量是 undefined | 变量名没有 VITE_ 前缀,或改完 .env 没重启 dev server |
| 代理不生效 / 仍跨域 | proxy 的 key 必须和请求的 baseURL 前缀一致 |
| @ 别名 TS 报红 | vite.config.ts 和 tsconfig 要两边都配 |
| ESLint 规则不生效 | ESLint 9 时代认准 eslint.config.js,旧的 .eslintrc 已废弃 |
| 生产包能跑但白屏 | 多半是 base 路径问题(部署在子目录时需设 base: '/子目录/') |
一页纸总结
1pnpm create vue@latest ← 官方脚手架,TS + Router + Pinia + ESLint 全勾选 2 ↓ 3重构目录(api / composables / stores / views 分层) 4 ↓ 5vite.config.ts(别名 @ + 代理 /api + 分包)+ .env 多环境 6 ↓ 7封装 request.ts(拦截器统一 Token 与错误) 8 ↓ 9<script setup> + Composable 写业务,Pinia 只管跨页共享状态 10 ↓ 11husky + lint-staged 守住提交质量 12 ↓ 13build → preview 验收 → Nginx 部署(记得 try_files) 14
按这份指南走下来,你得到的不只是一个能跑的 Demo,而是一套类型安全、职责清晰、可持续迭代的工程底座。
《前端框架vue3,vite 开发前端项目实践步骤指南》 是转载文章,点击查看原文。
