☰
Vue3+Vite+ElementPlus中后台工程化实战:从环境搭建到权限路由与部署
2026/9/25 7:58:42 网站建设 项目流程

最近带团队做内部项目,正好用了一套相对完整的 Vue 工程化方案落地了一个中后台 Web 系统,项目代号就叫“3-15”。很多朋友在群里问这套东西怎么组织,从环境搭建到 ElementPlus 按需引入,再到权限路由、视频流播放、打包部署,零零散散的东西不少。干脆把整个过程复盘一遍,把能直接抄作业的部分都拆开揉碎写清楚。

这篇内容适合刚入门 Vue 想搞懂工程化实战的同学,也适合已经在写业务代码、但没系统梳理过项目结构和技术选型的前端。不绕弯子,直接讲项目里怎么做的,以及为什么这么做。中间会有很多我实际踩过的坑,这些才是常规文档里看不到的。

1. 项目全貌与工程化思路拆解

1.1 “3-15”到底是个什么样的项目

先解释下这个项目代号。3-15 是内部排期的一个节点标识,后来叫着叫着就成了项目代称。项目本身是一个典型的中后台 Web 管理系统,技术上选择了 Vue3 + Vite + ElementPlus 这套当前前端主流组合,涉及的功能包括用户登录鉴权、动态路由、后台布局、数据看板、列表表单、以及一个比较特殊的直播流预览模块。

这类项目在真实业务里非常常见,尤其是内部运营系统、数据管理平台、企业级中台系统。它不是那种纯展示官网页面,而是带有大量交互逻辑、权限控制和数据联动的 Web 应用。也正因为是这种复杂度,单纯用 Vue 写组件已经不够,必须上工程化体系来约束代码结构、构建流程、依赖管理和部署方式。这就是标题里“工程化”三个字的实际意义。

1.2 为什么是 Vue3 + Vite + ElementPlus 这套组合

技术选型往往是项目里最容易被低估但实际上最重要的一件事。3-15 项目立项的时候,团队里不是没有考虑过 React,也不是没人提出继续用 Vue2 的老套路,最后还是统一到了 Vue3 这条线。

Vue3 的组合式 API(Composition API)解决了过去 Options API 在复杂业务组件里逻辑分散的问题。同一个功能相关的代码可以集中在一个 setup 作用域里维护,不用再异步跳到 data、methods、computed 几个区块来回看。团队协作时,逻辑复用也变得非常直观,一个 useXxx 函数就是一块独立能力。

Vite 则把开发体验拉高了一大截。它基于原生 ES Module 启动开发服务器,冷启动速度比 Webpack 那是代差级的提升。以前用 Webpack 跑一个中大型项目,Dev Server 起来怎么也得等个十秒二十秒,改个代码热更新有时候也要停顿一下。Vite 这边基本上是秒开,保存代码后浏览器几乎是即时反馈。这种体感差异对开发效率的提升是实实在在的。

ElementPlus 是 ElementUI 的 Vue3 版本。当时选它没太多纠结,因为团队成员对 Element 生态足够熟悉,组件的覆盖范围、文档完整度、社区活跃度都是第一档。而且它有完善的暗黑模式支持和主题定制方案,给项目后期做 UI 品牌化留下了空间。

1.3 工程化到底解决了什么问题

不少刚接触前端的同学会把工程化理解成“用一些工具来打包代码”,这个理解太窄了。真正的工程化解决的是整个项目生命周期里的问题:

  • 环境一致性:团队里不同成员的系统环境不同,通过 lock 文件锁定依赖版本,避免“在我电脑上明明能跑”这类问题。
  • 规范化开发:ESLint 约束代码风格,Prettier 统一格式化,Commitlint 规范提交信息,这些都是在项目层面建立的自动化规则。
  • 模块化与复用:通过目录结构设计和组合式函数封装,让业务代码可以被拆分、被复用、被测试。
  • 构建与部署自动化:Vite 构建产物优化、环境变量管理、CI/CD 对接,让代码从提交到上线尽量自动化。

所以,3-15 项目能顺利推进,不是因为用了某个多先进的技术,而是工程化把团队协作的摩擦降到了最低。代码怎么写、状态怎么管、接口怎么调、页面怎么拆,从一开始就是有共识的。

2. 环境准备与项目骨架搭建

2.1 Node 版本和包管理器选择,别在这种小事上翻车

项目开工第一步不是写代码,而是先把环境统一好。Node 版本这个问题看起来小,实际上坑特别多。Vite 4 要求 Node 版本 14.18+ 或 16+,Vite 5 则要求 18+。要是团队里有人还在用 Node 12,那装依赖的时候直接报错。

我当时在项目里直接锁定了 Node 18 LTS,并且写了.nvmrc文件放在项目根目录,内容是18.18.0。这样任何新成员加入团队,执行nvm use就能自动切到指定版本。这一步成本极低,但能省掉大量“为什么我跑不起来”的死磕时间。

包管理器我选了 pnpm,核心理由是它解决了依赖幽灵问题和磁盘空间浪费。pnpm 通过硬链接和符号链接把依赖存储在全局 store 里,多个项目共享同一份依赖副本,安装速度比 npm 快不少,更重要的是依赖结构非常干净。项目里同样留了packageManager字段在 package.json 中,例如:

{ "packageManager": "pnpm@8.15.0" }

这样配合 corepack 就能自动使用指定版本,团队里不会再出现“我用 npm 装的,你用 pnpm 装的,最后 lock 文件全乱了”的情况。

2.2 从零初始化一个 Vite + Vue3 项目

初始化项目我用的是官方脚手架,命令很简单:

pnpm create vite web-315 --template vue-ts

选 vue-ts 模板是因为项目明确要用 TypeScript。这里多说一句,如果你还在纠结业务项目要不要上 TypeScript,答案是:要上,而且越早上越好。3-15 项目里有大量接口数据交互和复杂对象传参,没有类型定义的时候,改一个字段要全局搜索,还容易漏。上了 TypeScript 之后,接口返回结构、组件 props、路由 meta 这些都有类型约束,重构的时候心里踏实很多。

初始化完成后的目录结构是 Vite 默认的,需要按照工程化思路重新调整。我习惯的目录组织方式如下:

src/ ├── api/ # 接口定义与请求封装 ├── assets/ # 静态资源(图片、全局样式) ├── components/ # 通用业务组件 ├── composables/ # 组合式函数(逻辑复用层) ├── directives/ # 自定义指令 ├── layouts/ # 布局组件(侧边栏、头部、内容区) ├── router/ # 路由配置 ├── stores/ # Pinia 状态管理 ├── styles/ # 全局样式与变量 ├── types/ # 全局类型定义 ├── utils/ # 工具函数 ├── views/ # 页面视图 ├── App.vue └── main.ts

这个结构最核心的思路是:通过目录名就能知道代码的职责。业务页面的代码待在 views 里,通用能力放在 components 和 composables 里,全局状态走 stores,接口请求统一在 api 层封装。任何人打开项目,不需要看文档就能快速找到要改的代码。这才是工程化目录设计的真正价值。

2.3 环境变量与模式管理

实际项目中,开发环境、测试环境、生产环境的接口地址和配置是不一样的。Vite 通过.env.development、.env.production等文件来管理,但还要考虑测试环境,所以我额外加了.env.staging。

项目里的环境变量文件大致长这样:

# .env.development VITE_APP_TITLE=315管理后台 VITE_API_BASE_URL=/api VITE_MODE=development
# .env.production VITE_APP_TITLE=315管理后台 VITE_API_BASE_URL=https://api.example.com VITE_MODE=production

注意,只有以VITE_前缀开头的变量才会被 Vite 暴露到客户端代码中。在业务代码里通过import.meta.env读取环境变量:

console.log(import.meta.env.VITE_API_BASE_URL)

另外在 package.json 里配置构建模式切换:

{ "scripts": { "dev": "vite", "build": "vue-tsc --noEmit && vite build", "build:staging": "vue-tsc --noEmit && vite build --mode staging" } }

vue-tsc --noEmit这一步非常重要,它会在打包前做类型检查,TypeScript 类型错误会被拦截在构建阶段,而不是到了运行时报错。这个习惯保持住,线上半夜告警的次数会大幅下降。

3. ElementPlus 接入与工程化配置

3.1 安装与插件化注册

ElementPlus 接入 Vite 项目很简单,先安装依赖:

pnpm add element-plus pnpm add -D unplugin-vue-components unplugin-auto-import

第二步安装的两个插件是工程化的关键。unplugin-vue-components和unplugin-auto-import可以让 ElementPlus 的组件和 API 按需自动导入,不在 main.ts 里全局注册,也不用手动在每一个组件里重复 import。

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()] }), Components({ resolvers: [ElementPlusResolver()] }) ] })

配置完成后,在组件里直接写<el-button>按钮</el-button>就能用,对应的样式和组件代码会在编译时自动引入。像 ElMessage、ElMessageBox 这类函数式组件,AutoImport 会为它们自动生成类型声明文件,TypeScript 能正确识别类型,不需要手动维护 import 列表。实测下来,首屏包体积比全量引入小了大概 40% 左右,这个优化在移动端弱网环境下尤其明显。

3.2 按需自动导入的原理,理解了你就不怕配置出问题

按需自动导入看着神秘,实际上原理很简单。它就是一个编译期的代码转换工具。Vite 在启动时会在内部维护一个组件名到模块文件的映射,当扫描到源码里有<el-button>这样的标签时,自动在编译产物中插入对应的 import 语句。你写源码的时候不关心 import,最终编译出来的代码里 import 一行都不会少。

这个方案在开发体验和性能之间做到了很好的平衡。全量注册 ElementPlus 虽然省事,但打包完后的 vendor chunk 会非常大,首屏加载在那里转圈等资源,用户的耐心可没有加载进度条那么有韧性。手动按需导入又不现实,组件数量太多,心智负担重。自动插件方案恰好是第三个选项。

不过要留意一个陷阱:自动导入依赖编译器正确识别标签。如果你在动态组件里使用<component :is="'el-icon-user'">这种写法,编译器不会把它当作 ElementPlus 组件处理,也就不会自动导入。遇到这种情况,还是要手动引入一次对应的组件。我在项目中就碰到过动态渲染图标失效的问题,排查到最后就是这原因。

3.3 主题定制与暗色模式切换

ElementPlus 默认的主题色是蓝色,但实际项目里经常需要和品牌色统一。3-15 项目的品牌主色是偏青色调的颜色,修改方式如下。

在项目中新建src/styles/element/index.scss:

@forward 'element-plus/theme-chalk/src/common/var.scss' with ( $colors: ( 'primary': ( 'base': #18a058 ) ) );

然后修改 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({ css: { preprocessorOptions: { scss: { additionalData: `@use "@/styles/element/index.scss" as *;` } } }, plugins: [ vue(), AutoImport({ resolvers: [ElementPlusResolver()] }), Components({ resolvers: [ElementPlusResolver({ importStyle: 'scss' })] }) ] })

这里有两个细节容易踩坑。第一,SCSS 版本最好使用sass而不是node-sass,node-sass 在多次更换 Node 版本后大概率会让你重新编译半天,sass 是纯 JavaScript 实现的 Dart 版本,安装速度快,兼容性好。第二,Resolver 的参数importStyle: 'scss'和 CSS 层的additionalData要配套使用,否则主题变量不会生效。

暗色模式方面,ElementPlus 从 2.2.0 版本开始内置了暗色模式支持。在 html 标签上添加class="dark"即可启用。切换逻辑我用 Pinia 做了一个全局状态,把主题偏好持久化到 localStorage,下次进入系统时自动恢复上次的选择,这个体验细节用户看不到,但确实会提升使用好感。

4. 核心业务功能实战落地

4.1 后台布局的搭法与拆解

中后台项目的布局通常都是经典的三段式:顶部导航、侧边菜单、内容区域。3-15 项目用的布局方案如下:

src/layouts/ ├── index.vue # 布局主框架 ├── Sidebar/ │ └── index.vue # 侧边栏组件 ├── Navbar/ │ └── index.vue # 顶栏组件 └── AppMain.vue # 内容主区域

布局主框架的代码逻辑大致是:

<template> <el-container class="app-wrapper"> <el-aside :width="sidebarWidth"> <Sidebar /> </el-aside> <el-container> <el-header> <Navbar /> </el-header> <el-main> <router-view v-slot="{ Component }"> <transition name="fade-transform" mode="out-in"> <component :is="Component" /> </transition> </router-view> </el-main> </el-container> </el-container> </template> <script setup lang="ts"> import { computed } from 'vue' import { useAppStore } from '@/stores/modules/app' import Sidebar from './Sidebar/index.vue' import Navbar from './Navbar/index.vue' const appStore = useAppStore() const sidebarWidth = computed(() => (appStore.sidebarCollapsed ? '64px' : '220px')) </script>

侧边栏的菜单是根据路由表动态生成的,核心逻辑是遍历路由的meta信息,把标题和图标渲染成菜单。这里有一个实用性很强的技巧:通过路由的meta.hidden字段控制菜单显隐,有些路由页面存在但不想出现在菜单里,比如详情页、编辑页,设置hidden: true即可。

路由表的配置结构:

{ path: '/system', component: Layout, redirect: '/system/user', meta: { title: '系统管理', icon: 'Setting' }, children: [ { path: 'user', component: () => import('@/views/system/user/index.vue'), meta: { title: '用户管理', icon: 'User' } }, { path: 'role', component: () => import('@/views/system/role/index.vue'), meta: { title: '角色管理', icon: 'UserFilled' } } ] }

这里的icon字段对应 ElementPlus 的图标组件名。为了能通过字符串动态渲染图标,我封装了一个小组件做转换:

<template> <component :is="iconComponent" /> </template> <script setup lang="ts"> import { computed } from 'vue' import * as Icons from '@element-plus/icons-vue' const props = defineProps<{ icon: string }>() const iconComponent = computed(() => { return (Icons as Record<string, any>)[props.icon] || Icons.Menu }) </script>

因为这做法是直接注册了全部图标组件,所以它在实现上是最稳的。代价是打包时会带上完整的图标库,不过图标都是 SVG 组件,体积不算大,业务项目完全能接受。

4.2 路由权限控制,每个前端面试题都在问的东西

权限控制是后台系统绕不过去的功能。3-15 项目采用的是两种方案结合:登录后根据用户角色动态生成可访问路由 + 路由守卫做访问拦截。前者解决的是用户能跳转到哪些页面,后者解决的是未登录或无权访问时如何跳转。

核心代码在router/permission.ts中,采用一个全局前置守卫:

router.beforeEach((to, _from, next) => { const userStore = useUserStore() const token = userStore.token if (!token) { if (to.path === '/login') { next() } else { next(`/login?redirect=${encodeURIComponent(to.fullPath)}`) } return } if (to.path === '/login') { next('/') return } // 判断用户路由表是否已加载 if (userStore.routesLoaded) { next() } else { // 拉取用户信息并动态生成路由 userStore.generateRoutes().then(() => { next({ ...to, replace: true }) }).catch(() => { userStore.resetState() next(`/login?redirect=${encodeURIComponent(to.fullPath)}`) }) } })

动态路由生成的核心是在拿到当前用户角色后,把一份“完整路由表”中该角色有权限访问的路由过滤出来,再用router.addRoute注册。这里的细节是,过滤后的路由表本身也要存到 Pinia 里,因为侧边栏菜单要从这份数据渲染,而不是从router.options.routes读。

这个方案的实际效果是:用户登录后首先访问一个重定向页面,页面在加载用户信息和动态路由的过程中显示一个全屏 loading 状态,完成后再跳转到真正想去的页面。在 3-15 项目里,这个过程的耗时通常在 300ms 以内,体验上几乎无感。

需要注意的是,这套方案仅仅是前端层面的权限控制,权限的真正安全边界必须由后端接口守卫来保证。前端权限只是让界面更友好,避免无权限的人看到入口,但不该被当作安全措施来依赖。

4.3 视频直播流预览,m3u8 播放的工程化姿势

这是 3-15 项目里比较特殊的功能模块。业务方需要在管理后台内查看监控视频的实时直播流,视频源以 HLS 协议(m3u8 文件)提供。播放器选型上,团队里对比了 video.js 和 hls.js,最后选择了 hls.js 配合原生 video 标签。

hls.js 这个库的体积不大,而且对 HLS 协议的兼容度很好,在 PC 端的 Chrome、Firefox、Edge 上都有不错的播放表现。安装:

pnpm add hls.js

封装一个播放器组件:

<template> <video ref="videoRef" class="video-player" controls autoplay muted></video> </template> <script setup lang="ts"> import { onMounted, onBeforeUnmount, ref, watch } from 'vue' import Hls from 'hls.js' const props = defineProps<{ src: string }>() const videoRef = ref<HTMLVideoElement>() let hls: Hls | null = null const initPlayer = () => { if (!videoRef.value) return // 原生支持 HLS 的环境(如 Safari) if (videoRef.value.canPlayType('application/vnd.apple.mpegurl')) { videoRef.value.src = props.src return } if (Hls.isSupported()) { hls = new Hls({ enableWorker: true, lowLatencyMode: true, backBufferLength: 90 }) hls.loadSource(props.src) hls.attachMedia(videoRef.value) hls.on(Hls.Events.MANIFEST_PARSED, () => { videoRef.value?.play().catch(() => { // 自动播放被浏览器拦截时,让用户手动点击播放 }) }) } } onMounted(initPlayer) watch(() => props.src, initPlayer) onBeforeUnmount(() => { hls?.destroy() hls = null }) </script>

这里有几个细节必须注意。第一个是muted属性,直播流初始加载时如果带声音自动播放,几乎必然被浏览器自动播放策略拦截,先静音自动播放,让用户手动开启声音,这个交互模式已经是行业共识。第二个是backBufferLength参数,限制往后缓存的时间,避免长时间观看直播时内存持续增长,这对 24 小时开着的监控大屏特别重要。第三个是组件卸载时必须调用hls.destroy(),否则流会继续在后台拉取数据,既浪费带宽也可能导致报错。

实际项目中还遇到过 m3u8 跨域导致无法播放的问题。排查结果不是代码层面的问题,而是后端存储服务没有给流文件配置 CORS 响应头,在服务端加了Access-Control-Allow-Origin后正常。这类问题从浏览器控制台看报错不太明显,建议先确认网络请求的响应头是否正确。

4.4 axios 封装与接口层设计,前端工程化的必考题

3-15 项目的接口请求统一走的 axios 封装,目的很简单:统一处理 token 注入、错误提示、状态码等公共逻辑,开发人员在写业务代码时只管调用 api 层的函数,不需要关心请求细节。

封装的核心代码大致如下:

import axios, { type AxiosInstance, type InternalAxiosRequestConfig, type AxiosResponse } from 'axios' import { ElMessage, ElMessageBox } from 'element-plus' import { useUserStore } from '@/stores/modules/user' import router from '@/router' const service: AxiosInstance = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 15000 }) // 请求拦截器:注入 token service.interceptors.request.use( (config: InternalAxiosRequestConfig) => { const userStore = useUserStore() if (userStore.token) { config.headers.Authorization = `Bearer ${userStore.token}` } return config }, (error) => Promise.reject(error) ) // 响应拦截器:统一处理错误 service.interceptors.response.use( (response: AxiosResponse) => { const res = response.data // 业务状态码判断 if (res.code !== 0) { ElMessage.error(res.message || '请求失败') // 401:登录状态失效 if (res.code === 401) { const userStore = useUserStore() userStore.resetState() ElMessageBox.confirm('登录状态已失效,请重新登录', '提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => { router.push('/login') }) } return Promise.reject(new Error(res.message || '请求失败')) } return res }, (error) => { ElMessage.error(error.message || '网络异常') return Promise.reject(error) } ) export default service

在设计上,我统一了后端返回的数据结构为{ code, message, data },code 为 0 表示成功,非 0 表示业务异常。这样响应拦截器只需要统一判断一次,各业务模块就不需要重复处理错误分支了。

api 层再对 service 做一层业务封装:

// src/api/user.ts import request from '@/utils/request' export interface UserInfo { id: number username: string avatar: string roles: string[] } export const getUserInfo = () => { return request.get<{ data: UserInfo }>('/system/user/info') } export const updateUser = (data: Partial<UserInfo>) => { return request.put('/system/user/update', data) }

这样页面上调用时,传入的参数和返回的数据都有类型提示,接口变更时只需改 api 层一个文件,页面代码完全不受影响。这是工程化分层带来的最直观好处。

5. 工程化实战中的高发问题与排查技巧

5.1 打包后布局样式异常,排查思路要分层

3-15 项目在第一次打完生产包部署后,测试反馈某些页面的样式和开发环境完全不一样,按钮间距不对,表格边框缺失,看起来像 ElementPlus 的样式没有正确加载。

排查时,我先打开浏览器 Network 面板,查看 CSS 资源是否加载。确认加载了之后,查看 Elements 面板里元素的 class 和开发环境是否有差异。最后定位到问题原因是 ElementPlus 按需导入时样式部分没有正确引入,由于 Resolver 的importStyle没有配成scss,主题定制文件里定义的变量没有作用到组件样式上。把配置统一改成importStyle: 'scss'并加上additionalData之后,问题解决。

这类问题的排查思路并不复杂,但容易慌:先区分是资源加载问题、样式覆盖问题还是配置失效问题,层级分清楚,定位就很快。

5.2 图片防盗链 403,中后台项目常遇到的坑

系统里配置了一些外部图片 URL,结果页面上显示出来的全是裂图。打开控制台,403 Forbidden,请求头里明确写着服务器拒绝了这次访问。这类情况在互联网项目里很常见,尤其是引用了外部图床或第三方平台图片时,对方服务器会通过 Referer 做防盗链校验。

解决的办法通常有两个层面:

一是把图片下载到自有服务器或对象存储上,从中后台系统直接上传的图片需要走这个方案。二是在 HTML 的 head 区域加一行 meta 标签:

<meta name="referrer" content="no-referrer" />

加了这行之后,页面里的请求不再携带 Referer 字段,能绕过大部分简单的防盗链校验。但这个方案有副作用,可能会影响站内统计和一些需要 Referer 信息的场景,实际项目中建议在明确了解影响后再使用。

5.3 web 页面 PDF 打印,样式错位与内容截断

3-15 项目里有一个报表展示页面,业务方要求可以直接一键打印成 PDF 存档。最初的做法是用户直接用浏览器 Ctrl+P 打印,结果样式非常感人:表格被截断、背景色丢失、打印预览和屏幕显示完全是两个世界。

后来引入print-js库,配合专门的打印样式表解决:

pnpm add print-js

在打印时调用printJS({ printable: 'reportContent', type: 'html', targetStyles: ['*'] }),并在样式中通过@media print单独适配打印场景,比如设置-webkit-print-color-adjust: exact保证背景色正常,给表格设置page-break-inside: avoid防止行内截断。

还有一个细节是纸张方向,横向宽表格需要设置@page { size: A4 landscape; margin: 10mm; },否则内容会被强行压缩到纵向纸面上,可读性大打折扣。

5.4 依赖版本冲突,eslint 和 prettier 互相打架

工程化项目里最消磨耐心的不是业务逻辑,而是工具链冲突。3-15 项目就遇到过 ESLint 和 Prettier 的规则冲突,保存代码时 ESLint 报错说格式不正确,但 Prettier 格式化后反而报更多的错。

根因是两者在代码风格规则上有重叠但不完全一致,比如单引号和双引号、行尾分号等。解决办法是引入eslint-config-prettier,把 ESLint 中和 Prettier 冲突的规则全部关闭,让格式问题交给 Prettier 统一处理,ESLint 只负责代码质量和潜在错误检查。同时在.prettierrc里固定团队格式规范:

{ "semi": false, "singleQuote": true, "printWidth": 100, "trailingComma": "none" }

核心原则是:代码风格问题不要双头管理。格式化归属单一工具,团队里才不会出现今天你格式化后我改回来、明天我又改回去的荒谬情况。

5.5 vue-devtools 无法检测到 Vue3 项目

有些同事明明装了 Vue Devtools,打开 3-15 项目后却发现面板里没有 Vue 标签页。排查后发现是插件版本太老,只支持 Vue2。当前 Vue Devtools 的主流版本 6.x 同时支持 Vue3 和 Vue2,但需要确保浏览器安装的是 v6 以上的版本,且在 Vue3 项目的生产模式下插件默认不显示,需要在开发模式下使用。

还有一个不起眼但容易绊人的点:浏览器装了多个 Devtools 扩展时,排查问题前先禁用其他无关的扩展,避免干扰。这一条虽然不属于工程化范畴,但确实花掉过我不少时间。

6. 一点点项目完整体的实战心得

如果回头看,3-15 这个项目最值得留下来的不是具体某个功能代码,而是那套搭建过程和决策脉络。前端工程化这条路,很容易被各种工具、插件、配置牵着走,反而忘了自己最初的诉求是什么。

我在实际推进中的体会是:任何一项技术选型都得分清是解决你的问题,还是解决别人的广告问题。Vue3 + Vite + ElementPlus 这套组合,适合的是需要快速交付且要长期维护的中后台业务系统。如果你做的是重交互工具型产品,可能 React 生态更适合;如果你只做轻量官网,那连工程化都可以先不上,直接用单页 HTML 反而更快。项目本身没有“最牛的配置”,只有“最匹配的配置”。

另外有个小建议,工程化体系的维护要像写代码一样勤快。新成员加入时主动更新 README,依赖升级时记录变更原因,遇到特殊问题把解法沉淀到文档里。这些看不到的工作,才是让一个前端项目从“能跑”进化到“好维护”的分水岭。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询