简介:这是基于 Vite 构建的 Vue3 项目组织模板,压缩包共 233 个文件、约 9.9 MB,其中 159 个 Markdown 文档覆盖使用说明与知识点记录,17 个 JavaScript、11 个 TypeScript、5 个 Vue 与 5 个 CSS 文件构成完整前端工程骨架,另有少量 SQL、JSON、SVG、HTML 等辅助资源。模板按根组件、路由、布局与业务组件分层组织,采用 Composition API 管理逻辑,集成 Vue Router 路由、Vuex 状态管理、ESLint/Prettier 代码规范与基础测试配置;同时附带多套主题样式及常用布局、正则、rem 单位等示例页面,可直接复用或按需调整。模板目录中还包含构建配置、公共工具函数与静态资源模块,方便直接运行和二次开发。面向中高级前端开发者,可显著减少项目初始化与架构搭建时间,帮助形成清晰、可维护的代码组织方式,对理解 Vite 工作流、组件拆分和状态管理也有实际参考价值。目前已有 786 人学习下载,适合作为 Vue3 工程化实践或新项目起步的实用模板。
1. Vue3 项目模板:它组织代码的方式比脚手架本身更值钱
拿到这份 vue3 项目模板的压缩包时,我先扫了一遍目录树。和常见脚手架直接抛给你一个能跑的 demo 不一样,它把 api、stores、views、components、modules 分域铺开,路由权限、请求封装、环境变量都给出了统一写法。
这套组织方式适合两类人:准备从 vue2 或原生 Vite 工程迁移过来的开发者,以及想统一团队规范、不想每次起新项目都争论目录怎么摆的人。模板自带 Pinia 状态管理、axios 请求层、自动导入和 mock 数据,解压装上依赖就能跑。
一句话:它解决的不是「怎么把 demo 启动」,而是「项目变大之后代码还找得到、改得动」。下面按落地顺序拆开讲。
2. 工程结构拆解:src 分域目录与构建配置三件套
模板的优雅体现在一个细节:整个 src 都按职责分域,而不是按页面堆文件。我第一次打开时数了一下,顶层就十来个文件夹,每个名字都指向一类明确的改动。这种组织方式在只有两三个页面的小项目里看不出优势,一旦超过十个页面、前后端并行开发时,好处会明显放大。
2.1 目录树:每个文件夹只干一件事
# 解压后的顶层结构(省略 node_modules 和 dist) ├── .env # 所有环境共享的变量 ├── .env.development # 本地开发环境 ├── .env.production # 生产构建环境 ├── vite.config.ts # Vite 构建配置 ├── src/ │ ├── api/ # 接口层,按业务域拆文件 │ │ ├── request.ts # axios 实例与拦截器 │ │ └── user.ts # 用户相关接口 │ ├── assets/ # 静态资源 │ ├── components/ # 通用组件(自动导入) │ ├── layout/ # 后台布局:侧边栏、顶栏 │ ├── modules/ # 业务模块,可插拔的小应用 │ ├── mock/ # 开发环境 mock 数据 │ ├── router/ # 路由表 + 守卫 │ ├── stores/ # Pinia 状态 │ ├── styles/ # 全局样式变量 │ ├── types/ # 全局 TS 类型 │ ├── utils/ # 纯函数工具 │ ├── views/ # 页面组件,按业务分文件夹 │ ├── App.vue │ └── main.ts └── package.jsonviews 是唯一放页面组件的地方,我会让 views 下的文件夹和后端模块一一对应,比如 user、order、goods,一个文件夹里放页面、子组件和自己私有的样式。api 层跟 views 同构,但只做接口定义和类型导出,不掺业务逻辑。layout 负责整个后台的骨架,页面路由只需要作为 children 挂进去。
modules 目录是这套模板里比较灵活的设计,适合放相对独立的业务能力,比如扫码登录、批量导出、消息中心。它内部自带 api、views、store,像是可以插拔的小应用。如果多人协作,每个人认领一个 module 就能明显减少代码冲突。utils 只放纯函数,比如日期格式化、防抖节流,原则是不允许 import 组件或者 store,否则很容易形成循环依赖。types 放全局类型,比如后端返回结构 Result<T>、分页参数 PageParams,api 和 views 都引用这些类型,集中定义能避免每个文件重复手写。
2.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' export default defineConfig({ plugins: [ vue(), AutoImport({ imports: ['vue', 'vue-router', 'pinia'] }), Components({ dirs: ['src/components'] }) ], resolve: { alias: { '@': '/src' } }, server: { port: 5173 } })AutoImport 里写了 vue 之后,ref、reactive、computed 不用每个文件手动 import;组件注册同理,放在 src/components 里的组件在模板中直接用,不用写 import。resolve.alias 里的 '@' 指向 /src,主要价值是摆脱 ../../../ 这种相对路径。注意自动导入省的是 import 语句,不会帮你做 Tree-shaking 决策,该用按需引入的第三方库还是要显式引。
环境变量这块,模板用 Vite 的标准机制把环境差异收敛到根目录的几个文件里:
| 变量名 | 开发环境值 | 生产环境值 | 作用 |
|---|---|---|---|
| VITE_API_BASE | /api | /prod-api | 所有请求的统一前缀 |
| VITE_MOCK | true | false | 是否注入 mock 数据 |
| VITE_APP_TITLE | 本地开发 | 生产环境 | 浏览器标题、水印等运行时变量 |
# .env.development VITE_API_BASE=/api VITE_MOCK=true VITE_APP_TITLE=本地开发Vite 约定以 VITE_ 开头的变量才会暴露到 import.meta.env,其余变量业务代码里根本拿不到。生产环境值放在同目录的 .env.production 里,同一个 key 会被覆盖。业务代码里判断环境只有一个标准——import.meta.env.MODE,不要在 Header 里猜域名,那都是玄学。改完 env 文件必须重启 dev server,Vite 不会热更新 env 配置。
2.3 规范层:eslint、prettier、commitlint 怎么协同
{ "scripts": { "dev": "vite", "build": "vue-tsc --noEmit && vite build", "preview": "vite preview", "lint": "eslint src --ext .ts,.vue", "format": "prettier --write src" } }build 前先跑 vue-tsc 类型检查,类型错误直接中断构建,能把运行时 bug 提前挡掉一半。lint 只扫 src 不扫 node_modules,配合 --max-warnings 0 之后警告也按错误处理,团队里不会出现「我代码有 warning 但能跑」的争论。Prettier 只管格式化,ESLint 管代码质量,两者分工明确,模板里再用 editorconfig 统一缩进和换行符。husky 的 pre-commit 钩子只检查暂存区文件,避免全量检查浪费时间。commitlint 强制提交信息带 feat/fix 这类类型前缀,后面用 git log 回溯改动时会轻松很多。
3. 核心模块拆解:一个请求从点击到渲染的完整链路
这一章是模板里最值得抄的三块代码,我放在一条链路上讲:用户在页面点一个按钮,调 api 发请求,路由守卫判断要不要放行,store 保存状态回填页面。模块各自独立,串起来就是一次完整的数据流。
3.1 请求层:axios 实例与业务码约定
// src/api/request.ts import axios from 'axios' import { useUserStore } from '@/stores/user' import router from '@/router' const service = axios.create({ baseURL: import.meta.env.VITE_API_BASE, timeout: 15000 }) // 请求拦截:把 token 带进 header service.interceptors.request.use((config) => { const userStore = useUserStore() if (userStore.token) { config.headers.Authorization = `Bearer ${userStore.token}` } return config }) // 响应拦截:统一处理后端返回的 code service.interceptors.response.use( (response) => { // 约定返回格式为 { code, message, data } const { code, message, data } = response.data if (code === 0) return data return Promise.reject(new Error(message)) }, (error) => { if (error.response?.status === 401) { // token 失效,清掉登录态并跳回登录页 const userStore = useUserStore() userStore.logout() router.push({ name: 'login' }) } return Promise.reject(error) } ) export function get<T>(url: string, params?: object) { return service.get<T, T>(url, { params }) } export function post<T>(url: string, data?: object) { return service.post<T, T>(url, data) }baseURL 直接取 env 里的 VITE_API_BASE,本地开发值是 /api,生产环境由部署时的变量覆盖。timeout 设了 15 秒,超过就触发 error 分支;弱网项目可以调到 30 秒,但要注意超时不代表请求被取消,后端可能还在处理,写 loading 状态时别依赖超时来复位。
请求拦截只干一件事——把 token 从 store 取出来塞进 header,不要在拦截器里写死 token 值,token 统一由 store 管理。响应拦截把 code=0 的 data 直接剥出来,业务代码拿到的就是干净的数据,不用在每个页面里都写 if (res.code === 0)。401 分支放在拦截器里统一跳登录,避免每个接口都重复一遍。一个常见的误用是:上传或下载接口的 responseType 是 blob,拦截器盲目判断 code 会把文件流搞坏,模板的做法是下载单独封装一个方法,不走这个拦截逻辑。
3.2 路由层:白名单、登录跳转与动态路由入口
// src/router/index.ts import { createRouter, createWebHistory } from 'vue-router' const routes = [ { path: '/login', name: 'login', component: () => import('@/views/login/index.vue'), meta: { public: true } }, { path: '/', component: () => import('@/layout/index.vue'), children: [ { path: 'dashboard', name: 'dashboard', component: () => import('@/views/dashboard/index.vue') } ] } ] const router = createRouter({ history: createWebHistory(), routes }) router.beforeEach((to) => { const userStore = useUserStore() if (!to.meta.public && !userStore.token) { return { name: 'login', query: { redirect: to.fullPath } } } if (to.name === 'login' && userStore.token) { return { name: 'dashboard' } } }) export default routercreateWebHistory 是 HTML5 history 模式,URL 干干净净;如果服务端实在没法配回退,可以换成 createWebHashHistory 用 #/ 兜底。meta.public 用来标记不需要登录的页面,login 进白名单。用 to.meta 判断而不是全局判断,扩展性更好——以后加找回密码、公开活动页,只要标 public 即可,不用改守卫逻辑。redirect 参数把用户本来想去的地址带上,登录完直接回跳,省掉在 localStorage 里存上一跳地址的麻烦。
模板给 modules 预留了动态路由入口:每个 module 导出自己的 routes 数组,登录后按角色权限 addRoute 挂载。这里有个血泪经验:addRoute 重复添加同一 name 会告警,挂载前先用 hasRoute 判断;退出登录时要 removeRoute 清理,否则换账号能看到上一个账号的菜单。
3.3 状态层:Pinia setup store 的写法约定
// src/stores/user.ts import { defineStore } from 'pinia' import { loginApi } from '@/api/user' export const useUserStore = defineStore('user', () => { const token = ref(localStorage.getItem('token') || '') const userInfo = ref(null) const isLoggedIn = computed(() => !!token.value) async function login(payload: { username: string; password: string }) { const res = await loginApi(payload) token.value = res.token userInfo.value = res.userInfo localStorage.setItem('token', token.value) } function logout() { token.value = '' userInfo.value = null localStorage.removeItem('token') } return { token, userInfo, isLoggedIn, login, logout } })defineStore 第二个参数传函数就是 setup 写法,ref 就是 state,computed 是 getter,function 是 action,最后统一 return 出去。组件里 useUserStore() 之后要注意:直接解构 state 会丢失响应性,要用 storeToRefs 解构属性,方法直接取就行。login 里调用的是 api 层导出的 loginApi,store 不直接 import axios,数据流保持单向:页面 -> store -> api -> 后端。
提示:不要在组件顶层解构整个 store 对象,这是 Pinia 最经典的翻车点。
为什么选 Pinia 不选 Vuex:Pinia 去掉了 mutations,写法更接近组合式,类型推导对 TS 友好。vue2 迁移过来的同事可能会不习惯直接改 state,但恰恰是这一步减少了大量样板代码。持久化用 localStorage 做最简版就够,复杂场景可以上持久化插件,但插件的序列化方案要自己验证,不推荐把大对象塞进 localStorage。
4. 从解压到上线:跑通模板与部署回退的完整步骤
拿到压缩包后的动作顺序是:环境 -> 依赖 -> 本地调试 -> 构建 -> 部署。每一步都有对应的失败点,我按实际跑通的流程讲。
4.1 前置检查:Node 版本与依赖安装
先看本机 Node 版本,常见做法是要求 Node 18 以上,16 也能跑但第三方依赖容易报一些奇奇怪怪的告警。打开终端执行 node -v 确认版本,没问题就装依赖:
cd vue3-project-template npm install依赖安装阶段的报错九成来自网络和版本缓存。npm install 中途失败时,先删掉 node_modules 和 package-lock.json 再重装,比反复重试省时间。如果你发现装完的依赖和模板要求的版本对不上,以 lock 文件为准,锁定版本才能保证团队环境一致。装完后先跑 npm run dev 而不是急着改代码,确认模板能起再动手。
提示:第一次启动如果端口被占用,Vite 会自动换端口,浏览器地址以终端输出为准。
4.2 本地调试:mock 开关与接口约定
模板的 mock 由 VITE_MOCK 控制,只在开发环境打开。mock 数据按业务域写在 src/mock 下,比如登录接口:
// src/mock/user.ts const userMock = [ { url: '/api/login', method: 'post', response: () => ({ code: 0, data: { token: 'mock-token', userInfo: { name: 'admin' } } }) } ] export default userMockmock 的 url 要和 VITE_API_BASE 对得上。request.ts 里 baseURL 是 /api,mock 文件的 url 也是 /api/login,拼起来一致才能命中。开发时接口 404,第一反应就去核对这两个前缀,比查半天插件配置快得多。mock 只在开发阶段用,生产构建不要带进去,通过 .env.production 里 VITE_MOCK=false 关掉。登录态的处理是:mock 返回 token,store 把它写进 localStorage,刷新页面不会丢登录。
4.3 构建与部署:路径回退与子路径配置
npm run build npm run previewbuild 会先跑 vue-tsc 类型检查再打包,dist 产物可以直接扔给静态服务器。preview 是本地模拟线上环境,验证 history 路由刷新是否 404 最方便,部署前先跑一遍能省一次发布事故。
server { listen 80; root /var/www/html; location / { try_files $uri $uri/ /index.html; } }这行回退的含义是:访问 /dashboard 时服务器先找物理文件,找不到就返回 index.html,由前端路由接管。如果部署在子路径下,比如 https://站点/admin/,vite.config 里要设 base: '/admin/',服务端也要在 location /admin/ 里做同样的回退,两边对不上就会出现「首页能开、一刷新就 404」。assets 里的文件带了内容 hash,部署后可以放心开强缓存。
5. 避坑记录:模板跑起来之后的五个高发问题
这些坑我在不同项目里真实撞过,按 现象 -> 原因 -> 解决 的顺序写。前四条跟模板直接相关,第五条是接手任何 Vite 工程都会遇到的高发问题。
5.1 开发环境接口全部 404,mock 却没生效
现象:页面能正常打开,Network 里 /api/login 全是 404,mock 一条都没命中。
原因:模板把 mock 挂在 vite.config 的 plugins 里,只有 VITE_MOCK 为 true 才注入。常见两种情况:一是 .env.development 里变量名拼错,比如写成 MOCK=true 少了 VITE_ 前缀,插件拿到的值是 undefined;二是 mock 文件的 url 写的是 /login,而请求层 baseURL 是 /api,拼接后 /api/login 对不上 /login。
解决:先看 env 文件变量名是不是 VITE_ 开头,再看 mock 的 url 是否包含 /api 前缀,两者一致后重启 dev server。env 改动不会热更新,重启是必须的。多人协作时 mock 文件合并冲突很常见,建议一个业务域一个 mock 文件,别都堆在单文件里。
5.2 history 路由刷新白屏或 404
现象:本地 dev 一切正常,部署到服务器后刷新 /dashboard 变成 404,首页能开。
原因:浏览器地址栏发的是真实 HTTP 请求,服务器在 /dashboard 这个路径下找不到 index.html。开发时 Vite 内置了回退逻辑,生产环境没人管这一层。
解决:用 4.3 的服务端回退配置。如果服务端实在没法改,模板层面可以退一步用 createWebHashHistory,代价是 URL 里多一个 #/,刷新时 hash 部分不会发给服务器,也能解决。
5.3 自动导入的组件在 TS 里报找不到
现象:页面渲染正常,编辑器里组件名一直标红,命令行类型检查也报 Cannot find module。
原因:unplugin-vue-components 在 dev 启动时生成 components.d.ts,记录哪些组件可以自动导入。新增组件后如果没重启 dev server 或重新触发类型检查,d.ts 还停留在旧状态;tsconfig.json 的 include 里漏掉这个文件,TS 也会看不到类型。
解决:检查 tsconfig include 是否覆盖 auto-imports.d.ts 和 components.d.ts,然后重启 TS 服务,再跑一次 dev 触发类型文件刷新。这类生成文件不要手写,也不要提交后反复手改,它本身就是要靠工具链维护的。
5.4 路由守卫里用 Pinia 报 getActivePinia
现象:打开页面直接白屏,控制台报 "getActivePinia was called with no active Pinia"。
原因:路由文件的创建发生在 main.ts 执行完 app.use(pinia) 之前,而某个文件在模块顶层就调用了 useUserStore()。Pinia 实例还没安装,store 自然拿不到上下文。
解决:把 store 的获取挪到 beforeEach 回调里执行,或者保证 createPinia 在 import router 之前完成。模板的约定是:路由文件顶层绝不写 useXxxStore(),只在函数体内取。顶层调用的问题在组件里不明显,因为组件挂在 app 之后,最容易翻车的就是路由和工具函数模块。
5.5 环境变量读出来全是 undefined
现象:import.meta.env.VITE_API_BASE 在业务代码里打印是 undefined,请求发出去了但前缀是空的。
原因:三类原因,按概率排:env 文件放错了位置,Vite 只读项目根目录的 .env,放 src 下不生效;变量名没带 VITE_ 前缀,Vite 默认不暴露给客户端;改完 env 没重启 dev,加载的还是旧文件。
解决:把 .env、.env.development 放到和 vite.config.ts 同级;变量统一 VITE_ 开头;每次改完 env 强制重启 dev server。生产环境出问题先确认 .env.production 是否存在,很多部署事故都来自漏了这个文件。
6. 二次开发:从模板长成业务系统的三个切入点
拿到模板先别急着删文件,我通常从三个切入点动手。
第一个切入点:把 api 层变成与后端模块一一对应的文件组。模板里的 user.ts 只是示例,业务里我会按 order.ts、goods.ts、stat.ts 拆分,每个文件导出针对单一资源的函数,views 里按业务模块建文件夹,组件只依赖对应 api 文件,跨模块调用统一走 store。后端接口变动时,改动范围能控制在两个文件内。
第二个切入点:权限从静态路由变成动态路由。登录后根据后端返回的角色和路由名,用 router.addRoute 把对应模块的路由挂上去,beforeEach 里做白名单校验。改的时候注意 addRoute 重复挂载会告警,先用 hasRoute 判断;退出登录要 removeRoute 清掉,否则换账号能看见上一个账号的菜单。
第三个切入点:把自动导入的粒度调小。模板默认整目录自动导入,业务大了容易误伤同名组件,我在 vite.config 里给 Components 加 include 匹配,只让通用组件走自动导入,页面级组件手动 import。这样既保留开发效率,又让组件引用关系变得可读。
这套模板我留存着,每次起新项目都会走一遍同样的约定。从那以后我接手任何工程,第一件事不再是问框架版本,而是先看目录结构怎么摆、数据流从哪进、出错往哪报;哪怕要推倒重写,也先把分域规则立住。这份模板我已经整理成可复现的压缩包,解压后从第 4 章开始跑,十分钟内能看到登录页。希望帮到你。
本文还有配套的精品资源,点击获取