ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Vue3 + Vite + TypeScript 从零搭建后台管理系统完整指南

Vue3 + Vite + TypeScript 从零搭建后台管理系统完整指南 最近后台管理系统、可视化大屏、中后台项目的咨询明显多了起来新人问得最多的问题基本绕不开同一个组合——Vue3 Vite TypeScript。大家搜索vue3官网vite创建vue3项目vue3安装及环境配置这类关键词的频率非常高说明这个技术栈已经成了当下新建前端项目的默认选项。这个组合不是某个公司内部的偏门选型而是整个社区经过两三年磨合之后沉淀下来的主流方案Vite 负责开发体验和构建速度TypeScript 负责类型安全和代码可维护性Vue3 负责组件化和响应式能力。这篇文章我会从零开始把整个搭建流程走一遍包括初始化、工程化配置、路由状态管理、环境变量、常见报错排查所有步骤都是实测过的适合刚准备上手 Vue3 TS 的开发者直接照着操作也适合已经在用但想理顺工程化配置细节的同学查漏补缺。1. 组合选型为什么默认选 Vue3 Vite TS1.1 Vite 相比 Webpack 的根本优势很多人第一次接触 Vite 是从启动快这个印象开始的但它的优势不是单纯的速度而是思路上的变化。Webpack 的工作方式是先把整个项目所有模块打包成一个 bundle然后启动开发服务器项目越大启动越慢改一行代码还要等模块重新编译体感非常痛苦。Vite 的思路完全不同它利用浏览器原生 ES Module 的能力开发环境下根本不做整体打包而是把源码直接发给浏览器由浏览器按需请求模块。所以冷启动速度基本秒开文件改动后的热更新也是毫秒级因为只更新修改的那个模块而不是重新构建整个应用。我在实际项目里做过对比同样一个中等规模的后台管理系统Webpack 配置下冷启动大概需要 8 到 15 秒Vite 基本在 1 秒左右热更新体感几乎是即时刷新。这不只是省时间的问题而是开发体验的质变。你改一个组件样式浏览器几乎是同步反馈调试起来会顺很多。生产构建方面 Vite 默认用 Rollup 做打包产物也做了很好的代码分割配合 preload 指令首屏加载性能并不比 Webpack 差甚至更好。1.2 TypeScript 在项目中到底带来什么TypeScript 的现实价值用一句话说就是把很多运行时才会暴露的问题提前到编码阶段暴露。比如接口返回的数据结构你用纯 JS 写拿到 res.data 之后随便点属性运行到那一刻可能才发现 undefined但用 TS 定义一个 interfaceVSCode 里敲代码的时候就会给出提示属性拼错了直接标红。做后台管理系统这种大量数据交互的项目类型定义能省掉非常多联调时间。不过 TS 也有学习成本初期写起来会觉得代码量变多什么都要定义类型。我的建议是不要一开始就追求全项目零 any先把常用的 interface 定义起来从 API 层和组件 props 开始用慢慢扩展。实际体验下来TS 是那种前期有点烦、后期越用越香的东西。特别是项目规模变大以后改一个接口字段所有用到这个类型的地方都会给你标出来这种安全感是纯 JS 给不了的。1.3 当前生态的成熟度现在这个时间点选择 Vue3 Vite TS生态已经非常成熟了。Element Plus、Naive UI、Arco Design 这些主流 UI 库都同时支持 Vue3 和 TSPinia 作为 Vue3 官方推荐的状态管理库设计上就完美兼容 TSvue-router 4 也重新用 TS 写过。可以说你日常开发的每个环节都有类型推导和社区支持遇到问题搜一下基本都有解决方案。和 Vue2 时代对比最大的变化是组合式 API 让逻辑复用变得非常自然自定义 Hook 就像写函数一样简单。比如一个表格页的逻辑以前用 mixin 会带来命名冲突和数据来源不清的问题现在抽成 useTableData 这种组合式函数每个页面都可以按需调用代码清晰度完全不在一个层次上。Vite 相比 Webpack 还有一个好处就是 Vite 生态里很多插件天然支持 TS像 vite-plugin-svg-icons、unplugin-auto-import、unplugin-vue-components 这些配置起来都很顺手。2. 从零初始化环境准备与脚手架创建2.1 Node 版本和包管理器第一步最容易踩坑创建项目之前先把 Node 环境搞定。很多同学在vite创建vue3项目这一步就报错大概率是 Node 版本太老。Vite 5 和 Vite 6 都要求 Node 版本 18而且要求是 18.0 以上最好直接用 Node 20 或者 Node 22 的 LTS 版本因为 Vite 内部用到了较新的 JavaScript API老版本 Node 跑不起来。装 Node 不推荐去官网一个个点安装包建议直接用 nvm-windowsWindows或 nvmMac/Linux来管理这样后面可以在多个 Node 版本之间自由切换不同项目用不同版本就不会打架。包管理器方面npm、pnpm、yarn 都能用但我现在基本推荐 pnpm。原因很实在pnpm 的依赖安装速度快磁盘占用小而且它对依赖的隔离做得很严格避免了很多 npm 时代我本地能跑别人拉下来跑不了的版本混乱问题。使用 pnpm 之前先通过 corepack 启用一下Node 20 自带 corepack执行 corepack enable 就可以开启 pnpm。如果团队其他人都用 npm那保持一致也行工具没有绝对的好坏团队统一最重要。2.2 使用 create-vue 创建项目并打开工程化选项创建项目直接用官方脚手架 create-vue它是 Vue 官方维护的命令行工具会给你一套带 TypeScript、ESLint、Prettier、Vue Router、Pinia 的规范化初始结构省掉自己拼装的麻烦。执行命令pnpm create vuelatest运行后会出现交互式选项大概长这样✔ Project name: my-admin ✔ Add TypeScript? Yes ✔ Add JSX Support? Yes ✔ Add Vue Router for Single Page Application? Yes ✔ Add Pinia for state management? Yes ✔ Add Vitest for Unit Testing? No ✔ Add Cypress for End-to-End Testing? No ✔ Add ESLint for code quality? Yes ✔ Add Prettier for code formatting? Yes我的建议是新项目第一次搭的时候TypeScript、Vue Router、Pinia、ESLint、Prettier 全部选上JSX 看你个人习惯如果后续想写 tsx 组件或函数式组件就选是不选也没关系后面可以随时加。Vitest 如果项目需要写单元测试可以选不需要就先不装Cypress 这种端到端测试工具也按需选择选了反而增加初次上手的学习负担。脚手架装完以后进到项目目录安装依赖然后启动pnpm install pnpm dev这时候浏览器打开 http://localhost:5173就能看到默认的启动页面。Vite 默认端口是 5173如果被占用会自动切换成 5174不要看到端口变了就慌属正常现象。2.3 初始目录结构与规范意识的建立create-vue 生成的项目结构比较规整我建议拿到项目后第一件事是把 src 下的目录按自己的业务习惯重新规划一下而不是在默认结构上堆代码。我常用的目录结构是这样src/ ├── api/ # 接口请求定义 ├── assets/ # 静态资源 ├── components/ # 公共组件 ├── composables/ # 组合式函数Hooks ├── directives/ # 自定义指令 ├── layouts/ # 布局组件 ├── router/ # 路由配置 ├── stores/ # Pinia 状态管理 ├── styles/ # 全局样式 ├── types/ # TS 类型定义 ├── utils/ # 工具函数 ├── views/ # 页面组件 ├── App.vue └── main.ts这样分层的逻辑是把接口请求页面公共逻辑类型定义完全分开页面组件只负责渲染和交互数据从 store 或者 api 层取类型定义集中在 types 里统一维护。项目小的时候会觉得这种分层多此一举但项目一旦超过二十个页面、三五个模块这个结构的优势就非常明显——找一个接口定义、找一个公共组件、改一个全局类型位置都是固定的不需要猜。3. 核心配置补充别名、路由与状态管理3.1 配置路径别名并同步 tsconfig项目一大了相对导入路径就会变成噩梦比如import { getList } from ../../../../api/list层级一多稍微改一下目录位置这个路径就得重新算。解决办法是配置路径别名让 代表 src 目录。先改vite.config.tsimport { fileURLToPath, URL } from node:url import { defineConfig } from vite export default defineConfig({ resolve: { alias: { : fileURLToPath(new URL(./src, import.meta.url)) } } })这里用 node:url 的 fileURLToPath 方法来处理路径比直接用 path.resolve(__dirname, src) 更符合 ESM 规范不会在 Windows 和 Mac 之间出现斜杠兼容问题。然后同步修改 tsconfig 里的路径映射否则编辑器里会报找不到模块 /xxx的错误。create-vue 生成的项目通常有tsconfig.app.json和tsconfig.node.json两个文件编辑代码相关的配置在tsconfig.app.json里加上compilerOptions这一段{ compilerOptions: { baseUrl: ., paths: { /*: [./src/*] } } }这里要注意baseUrl是必须的paths 里的相对路径指向的是以 baseUrl 为基准的目录。改完之后保存VSCode 里如果还是标红重启一下 TypeScript 服务CtrlShiftP 输入 TypeScript: Restart TS Server就好。还有一个常见的警告就是baseUrl 已弃用新版 TypeScript 建议不写 baseUrl直接写相对路径{ compilerOptions: { paths: { /*: [./src/*] } } }这种方式在较新的 TypeScript 版本中更推荐如果你们项目的 TS 版本已经比较新直接去掉 baseUrl 即可避免那个弃用警告。3.2 路由配置createRouter 与路由拆分路由方面主要是把 vue-router 4 的 createRouter 配置好然后按模块拆分。我的习惯是把路由分为静态路由和动态路由两批静态路由放登录页、404 页、布局框架这些固定页面动态路由在登录后根据用户权限动态添加。先看最简单的入门写法router/index.tsimport { createRouter, createWebHistory } from vue-router import type { RouteRecordRaw } from vue-router import HomeView from /views/HomeView.vue const routes: RouteRecordRaw[] [ { path: /, name: home, component: HomeView, meta: { title: 首页 } }, { path: /login, name: login, component: () import(/views/LoginView.vue), meta: { title: 登录 } }, { path: /:pathMatch(.*)*, name: not-found, component: () import(/views/NotFoundView.vue), meta: { title: 页面不存在 } } ] const router createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes }) export default router入门以后用得最多的是路由懒加载通过() import(/views/xxx.vue)的方式按需加载首屏只加载当前页面的代码其他页面在路由跳转时才加载这对首屏性能的提升立竿见影。另外注意meta字段title 可以用来做浏览器标签标题后续封装权限指令或页面缓存时也会用到。实际项目里我更推荐把路由配置抽成模块化结构每个模块一个文件比如router/modules/system.ts、router/modules/order.ts这样每个模块路由独立维护避免大家在同一个 routes 数组里改来改去产生冲突。3.3 Pinia 状态管理setup 式 storePinia 是 Vue3 官方推荐的状态管理方案相比 Vuex 最直观的改进是去掉了 mutation状态可以直接修改写法上也有选项式和setup 式两种风格。新手我的建议直接用 setup 式因为它和组合式 API 风格一致写起来就像写一个普通的组合式函数。以下是一个典型的用户信息 storeimport { ref, computed } from vue import { defineStore } from pinia import { getUserInfo } from /api/user import type { UserInfo } from /types/user export const useUserStore defineStore(user, () { // state const token ref() const userInfo refUserInfo | null(null) // getter const isLogin computed(() Boolean(token.value)) // action async function fetchUserInfo() { userInfo.value await getUserInfo() } function logout() { token.value userInfo.value null } return { token, userInfo, isLogin, fetchUserInfo, logout } })然后在 main.ts 里注册 Piniaimport { createApp } from vue import { createPinia } from pinia import App from ./App.vue const app createApp(App) app.use(createPinia()) app.mount(#app)在组件里使用时直接调用 useUserStoreimport { storeToRefs } from pinia import { useUserStore } from /stores/user const userStore useUserStore() const { token, userInfo } storeToRefs(userStore)这里有个小坑直接解构 store 会丢失响应性所以要用storeToRefs来解构 ref 和 computed。这个用法容易忘建议一开始就养成习惯。4. 工程化规范代码校验、格式化与提交校验4.1 ESLint Prettier 版本匹配要点create-vue 脚手架生成的项目已经自带 ESLint 和 Prettier 的基础配置但很多人会遇到编辑器里明明装了插件却不起作用的情况。最常见的原因是 VSCode 没有配置保存时自动格式化需要在.vscode/settings.json里加配置{ editor.formatOnSave: true, editor.defaultFormatter: esbenp.prettier-vscode, editor.codeActionsOnSave: { source.fixAll.eslint: explicit } }如果是针对 Vue 单文件组件还需要设置 eslint.validate 或者直接用 VolarVue 官方插件。现在的 Volar 已经内置了对 template 区域的类型检查支持配合 ESLint 使用体验还算顺滑。有一个容易被坑的地方是 ESLint 和 Prettier 的新版本配置方式变化比较大。比如 ESLint 9 默认使用 flat configeslint.config.js而不是老的.eslintrc.cjsPrettier 3 的配置项也有变更。如果你用 create-vue 最新版脚手架生成的配置文件是eslint.config.ts或eslint.config.js不要拿老版本教程里的.eslintrc.js去对照很多规则在 flat config 里写法不一样。4.2 自动导入和组件自动注册写 Vue 组件时最烦的应该是每个文件顶部都要写import { ref, computed } from vue而 unplugin-auto-import 和 unplugin-vue-components 两个插件就是为了解决这个问题。在vite.config.ts里添加import AutoImport from unplugin-auto-import/vite import Components from unplugin-vue-components/vite export default defineConfig({ plugins: [ AutoImport({ imports: [vue, vue-router, pinia], dts: src/auto-imports.d.ts }), Components({ dts: src/components.d.ts }) ] })配置完之后组件里就不用写import { ref } from vue了ref、computed、watch 这些 API 直接就可用。同时el-button、BaseModal这类组件也会被自动注册不再需要每次 import 组件并在 components 里声明。这个体验提升非常大特别是页面写多了以后代码看起来干净很多。不过要注意自动导入生成的两个 d.ts 文件auto-imports.d.ts 和 components.d.ts一定要提交到 git 仓库否则同事拉代码后编辑器会一直报错找不到 API体验很糟心。4.3 提交前校验Husky lint-staged代码规范不能只靠开发者自觉更可靠的做法是在 git 提交前强制执行。用 husky 加 lint-staged可以让每次提交的时候只对暂存区的文件进行 ESLint 检查和 Prettier 格式化不合规就不允许提交。安装和初始化pnpm add -D husky lint-staged npx husky init然后修改package.json里的 lint-staged 配置{ lint-staged: { *.{ts,vue}: [eslint --fix, prettier --write] } }在.husky/pre-commit文件里添加pnpm exec lint-staged这样每次 git commit 的时候eslint 会自动修复暂存区文件的格式问题修不掉就直接拦截提示你先处理报错。这一套配置之后团队里不管谁提交代码基础规范和格式都是统一的。踩过的一个坑是husky 初始化的时候它会重新生成 .husky 目录注意保留 pre-commit 文件里的内容不要被覆盖掉。旧版本 husky 需要在 package.json 里配置 prepare 脚本新版本通过husky init初始化的结构略有差异以官方文档为准。5. 环境变量与构建部署区分不同发布环境5.1 .env 文件体系与 import.meta.envVue3 Vite 项目的环境变量处理比 Vue2 Webpack 时代简单清晰得多。Vite 内置了对 .env 文件的支持约定好了不同文件名对应不同环境.env # 所有环境共用的公共配置 .env.development # 开发环境 .env.production # 生产环境 .env.test # 测试环境需要指定 mode 才会加载文件的命名要严格对齐 Vite 的模式机制。pnpm dev对应 development 模式加载 .env 和 .env.developmentpnpm build对应 production 模式加载 .env 和 .env.production。而pnpm build --mode test会以 test 模式构建加载 .env 和 .env.test。文件内容格式是KEYVALUE在代码里通过import.meta.env.KEY使用。注意只有以VITE_开头的变量才会被暴露到客户端代码中像 NODE_ENV 这种内部变量是无法在业务代码里直接读取的。举例VITE_APP_TITLE后台管理系统 VITE_API_BASE_URL/api VITE_ENVtest在代码里const baseURL import.meta.env.VITE_API_BASE_URL console.log(import.meta.env.VITE_APP_TITLE)这里有个很多新手会踩的坑环境变量是构建时静态替换的不是运行时从环境读取的所以当你修改了 .env 文件里的变量之后必须重启 dev server 或者重新 build 才能生效。5.2 vite build --mode test 到底在做什么vite build --mode test这个命令的出现是因为实际项目里往往需要构建一套测试环境的产物给测试人员使用。默认情况下vite build只加载 .env.production 文件但很多公司有三套环境开发环境、测试环境、生产环境。开发环境用 dev server 连本地或联调环境测试环境需要一套构建出来的静态资源给 QA 去测生产环境直接用正式域名和正式接口。通过--mode testVite 会以 test 模式执行构建这时候加载的配置是.env.test代码里import.meta.env.MODE也会变成test。我一般在 package.json 里配三个脚本{ scripts: { dev: vite, build:prod: vue-tsc -b vite build, build:test: vue-tsc -b vite build --mode test } }这样测试环境构建一次之后把 dist 目录部署到测试服务器接口地址通过 .env.test 里的 VITE_API_BASE_URL 指向测试后端不需要改任何业务代码。这个模式在 CI/CD 流水线里尤其好用一条命令就能打出指定环境的包。5.3 构建产物的部署注意base 路径和路由 history构建部署时最常遇到的白屏问题一半以上是 base 路径配错了。如果你的项目部署在域名根路径比如 https://example.com/那不用管 base但如果部署在子路径比如 https://example.com/admin/就必须在 vite.config.ts 里设置 baseexport default defineConfig({ base: /admin/ })然后 Vue Router 的 history 模式也要配置对应的 basecreateWebHistory(/admin/)还有一个相关的问题是刷新页面 404。当路由使用 history 模式即 createWebHistory时刷新某个子路由页面Nginx 找不到对应的文件就会返回 404。解决办法是在 Nginx 配置里加一行 try_fileslocation / { try_files $uri $uri/ /index.html; }这个配置的意思是请求的文件不存在时回退到 index.html由前端路由接管页面渲染。6. 常见问题清单与排查思路6.1 TypeScript 类型相关vue 文件识别与 baseurl 弃用提示问得最多的是main.ts 里 import App from ./App.vue 报错找不到模块。这个问题的原因是 TS 不认识 .vue 文件需要在src/env.d.ts或src/vite-env.d.ts里加入/// reference typesvite/client / declare module *.vue { import type { DefineComponent } from vue const component: DefineComponent{}, {}, any export default component }create-vue 新版已经带了这个文件但有些同学从旧项目迁移或者自己手动搭项目时容易漏掉。这个文件的作用就是告诉 TypeScript.vue 文件是一种可以被导入的模块类型。还有热搜词里提到的选项 baseUrl 已弃用并将停止在 TypeScript 7.0 中运行这个提示在新版本 TS 里会出现。如果你用的 TypeScript 5.x 以后可以按前面说的把 tsconfig 里的 baseUrl 去掉paths 直接写{ compilerOptions: { paths: { /*: [./src/*] } } }这样从项目根目录计算相对路径Vite 的别名解析和 TS 的路径解析就能对齐两边的 指向一致不再需要 baseUrl。还有一类是若依 vue3 ts 报错这类开源项目的衍生问题。很多开源后台管理系统把整套工程从 JS 迁移到 TS 后由于依赖版本更新频繁clone 下来后经常遇到Property xxx does not exist on type这类类型报错。排查思路一般是先看 tsconfig 里 types 字段有没有把需要的类型库包含进来再看 package.json 里 types 包和主包版本是否匹配。6.2 构建与工具链报错内存告警和 Node 版本有一个热搜词是node_options--max-old-space-size4096 vite node_options 不是内部或外部命令这是 Windows 环境下设置 Node 内存上限的方式不对。在 Windows CMD 里要用set NODE_OPTIONS--max-old-space-size4096PowerShell 里要用$env:NODE_OPTIONS--max-old-space-size4096直接在命令开头写成node_options...显然不行。当项目规模变大vue-tsc 或者 Rollup 构建时报JavaScript heap out of memory时我建议直接在 package.json 的构建脚本里用 cross-env 设置{ scripts: { build: cross-env NODE_OPTIONS--max-old-space-size4096 vue-tsc -b vite build } }cross-env 的作用是跨平台统一设置环境变量安装它只需要pnpm add -D cross-env。这样 Windows、Mac、Linux 上执行构建脚本都不会出问题。另一个经常踩到的是 Node 版本不匹配。Vite 核心代码对 Node 版本有硬性要求Node 16 之前跑 Vite 5 会直接报错This version of Vite requires Node.js 18。所以装好 Node 之后先执行node -v确认版本低于 18 就直接升不要在这个问题上浪费时间。6.3 样式与第三方库pxtorem 对 echarts 无效和 tabs 样式覆盖做可视化大屏或者移动端项目时很多人会用 postcss-pxtorem 实现 px 自动转 rem 的方案。但是有经验的人会发现pxtorem 对 ECharts 完全不生效图表的字体和宽高还是固定 px。原因是 ECharts 的图表是通过 canvas 绘制的canvas 里的尺寸是在初始化时根据传入的参数计算的不是 CSS 像素postcss 的转译过程根本拦截不到 canvas 内部。解决方案一般是两种一是监听窗口 resize 事件重新设置图表的宽高和字体大小二是基于 rem 计算出一个缩放比例在初始化 ECharts 时手动乘以这个比例。另一个常见需求是修改 Element Plus 的 tabs 标签页样式特别是要改 active 标签底部那条蓝色边框、字体颜色。Element Plus 的样式使用了 CSS 变量和 BEM 命名直接覆盖不生效的原因通常是组件内部使用的是 scoped 样式而我们覆盖样式写的选择器优先级不够。解决方式有两种在全局样式文件非 scoped里覆盖或者使用:deep()深选择器style scoped :deep(.el-tabs__item.is-active) { color: #409eff; font-weight: bold; } /style记住 :deep() 包裹的部分不会加上当前组件的>
返回列表