Vue项目实战全流程:从环境搭建到打包部署的踩坑指南 很多朋友学 Vue 都会经历一个阶段教程刷了不少hello world 也写了好几个但真到了要自己从零搭一个项目的时候突然不知道从哪儿下手。路由怎么配、状态放哪里、接口怎么联、打包之后为什么样式全乱了——这些 “没人明说但一定会遇到” 的问题才是实际开发里的重头戏。这篇文章我就用一个内部数据看板项目的实战例子把 Vue 开发里最常用到的路由、状态管理、视频流播放、Excel 导出、WebSocket 实时通信、前后端联调、打包部署全部走一遍。文里的代码都是可以直接拿来改的每一步我都会说明为什么这么做以及我自己踩过哪些坑。适合刚学完 Vue 基础、准备做第一个完整项目的人也适合已经写过项目但想系统补一遍常见场景的开发者。1. 环境搭建与项目脚手架这一步最容易劝退新手很多人在项目还没开始写第一行业务代码之前就被环境折腾到想放弃。Node.js 版本不对、依赖装不上、脚手架启动报错每一个问题都够卡半小时。其实环境这块只要搞明白三件事后面就顺了。1.1 Node.js 版本选择与安装细节Vue 3 配上 Vite 之后对 Node.js 的版本要求比之前高了不少。Vite 4 需要 Node 14.18 或 16Vite 5 开始要求 Node 18。这里我的建议非常直接不要用太老的版本直接装 Node 18 或 20 的 LTS 版本省得后面各种依赖因为 Node 版本太低而报错。安装的时候有个细节容易忽略Windows 用户装完 Node 之后一定要确认 npm 的全局路径是否正常。可以在命令行里跑一句node -v npm -v两个都能输出版本号说明基础环境没问题。如果你用的是 nvmNode 版本管理器注意切换版本之后要重新确认 npm 的 registry 源国内网络环境建议提前设置 npm 镜像否则后面 npm install 会等到怀疑人生。1.2 创建项目的两种方式和我的选择创建 Vue 项目的官方命令是npm create vuelatest底层用的是 create-vue 这个脚手架。它会问你需不需要 TypeScript、Vue Router、Pinia、ESLint、Prettier 这些按需勾选就行。我更推荐这个方式因为它在生成项目的同时就把规范和基础依赖都配好了比手动去 Vite 里一个个加要省事得多。比如我创建数据看板项目用的命令是npm create vuelatest vue-dashboard过程中我会选择TypeScript选上。虽然写起来比 JS 多几个类型标注但在项目变大之后收益非常明显。Vue Router选上。做单页应用基本逃不掉。Pinia选上。后面状态管理会用到。ESLint Prettier选上。团队协作或者自己维护代码的时候统一格式能少吵很多架。项目生成之后先npm install把依赖装好这一步在国内建议用配置好的镜像源然后npm run dev能正常打开页面脚手架这一步就算过了。1.3 项目目录结构怎么看create-vue 生成的结构很清晰但你如果第一次看可能会懵。核心就几个地方src/ views/ # 页面组件每个路由对应一个 components/ # 公共组件 router/ # 路由配置 stores/ # Pinia 状态 assets/ # 静态资源 App.vue # 根组件 main.ts # 入口文件理解这个结构有个简单的方法页面往 views 里放公共部分往 components 里放凡是多个页面要共享的数据往 stores 里放就对了。别把逻辑写成一坨塞在单个组件里后面维护会非常痛苦。1.4 离线安装依赖的问题热词里有个“离线安装 vue”这个场景一般出现在内网开发环境。做法是找一台能联网的机器把项目依赖完整npm install一次然后用npm pack或直接把 node_modules 打包拷贝到内网机器上。更规范的做法是部署一个私有 npm 仓库比如 Verdaccio把需要的包缓存在内网。小项目图省事直接拷 node_modules 也能跑但换机器容易出现二进制模块不匹配的问题能走私有仓库尽量走私有仓库。2. 路由从入门到够用参数传递、懒加载和登录拦截路由是 Vue 单页应用的门面也是新手最容易用得稀里糊涂的部分。我见过不少项目路由配置里清一色全是component: () import(...)但对参数传递、拦截器、懒加载这些到底怎么设计完全没有概念。这一节我把数据看板项目里实际用到的路由知识串一遍。2.1 路由参数传递的三种方式路由参数说白了就三种场景通过路径传参、通过 query 传参、通过配置的 props 传参。路径传参适合那种“某个资源详情页”的场景比如点击一条告警记录进入详情页{ path: /alarm/detail/:id, name: AlarmDetail, component: () import(/views/alarm/AlarmDetail.vue), props: true }路由配置里写上props: true之后组件里就能直接用defineProps([id])拿到这个参数比用route.params.id要清爽很多代码也更好测试。query 传参适合列表页带到详情页的附加信息比如排序字段、来源页面这类不参与资源定位的内容router.push({ name: AlarmDetail, query: { from: dashboard, page: 2 } })组件里用route.query.from拿。注意 query 传的值都会变成字符串数字类型要自己转换。还有一种场景是“直接把整份数据带过去”但我不推荐你这么做。有人喜欢用query: { data: JSON.stringify(obj) }或者更直接的用 sessionStorage 临时存一份说这样能少发一次请求。实际项目里这种做法的坏处很明显页面一刷新数据就没了而且 URL 会变得特别长还会把不必要的数据暴露出去。详情页需要什么数据就按 id 调接口拿这个习惯越早养成越好。2.2 路由拦截器实现登录校验数据看板这种内部系统基本都要求登录后才能访问。路由拦截器的作用就是在跳转之前做一次身份校验没登录就直接踢到登录页。用 create-vue 的项目路由文件是src/router/index.tsimport { createRouter, createWebHistory } from vue-router const router createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: /login, name: login, component: () import(/views/Login.vue), meta: { public: true } }, { path: /dashboard, name: dashboard, component: () import(/views/Dashboard.vue), meta: { title: 数据总览, requiresAuth: true } } ] }) router.beforeEach((to, from, next) { document.title to.meta.title ? ${to.meta.title} - 数据看板 : 数据看板 const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next({ name: login, query: { redirect: to.fullPath } }) } else if (to.name login token) { next({ name: dashboard }) } else { next() } })这里有两个容易踩的坑。第一个是query: { redirect: to.fullPath }。这个设计是为了登录成功之后能跳回用户原本想访问的页面体验会好很多。登录页拿到 redirect 参数之后登录成功就router.replace(redirect)而不是死板地跳首页。第二个是meta.public和meta.requiresAuth这类标记一定要显式设计。我之前见过一个项目判断逻辑是“凡是路由名不是 login 的都要登录”结果后面加了一个页面忘记写requiresAuth: false整个页面变成了登录白名单之外的重灾区。后面用 meta 标记反而更稳默认全部要登录个别页面用meta.public: true放行。2.3 路由懒加载与打包体积优化老项目里常见的问题是把所有页面都import进来导致首屏加载一个巨大的 JS 文件。Vue Router 的懒加载写法非常简单// 不要这样 import Dashboard from /views/Dashboard.vue // 要这样 const Dashboard () import(/views/Dashboard.vue)因为一个路由对应一个页面所以“路由懒加载”和“组件懒加载”在大多数场景下是一回事。打包的时候 Vite 会按照动态 import 自动把代码拆成小块每个页面一个 chunk用户访问哪个页面就加载哪个页面的 JS首屏速度会明显提升。数据看板项目里的大图表库比如 ECharts开销比较大可以把图表组件单独做成一个异步组件用到再加载也能让首屏压力小很多。3. 状态管理选型实录为什么我放弃 Vuex 转向 Pinia说到 Vue 的状态管理很多人的第一反应还是 Vuex。但现在新项目我基本直接用 Pinia。原因不复杂Pinia 的 API 设计更贴近 Vue 3 的组合式风格没有 mutations 这个概念不用写一堆模板代码TypeScript 支持也更好。这一节我顺便把两者对比和实际使用例子都给你。3.1 Pinia 与 Vuex 的核心差异对比最直观的方式就是看代码量。Vuex 里要修改 state 必须走 mutations即使只是把用户名改一下也要写 action 调 mutation多了一层概念。Pinia 把 action 和 state 放在一起一个函数直接改数据心智负担小很多。热词里既然提到“vue pinia vs vuex”我就多说几句选型建议对比维度PiniaVuexAPI 风格组合式简洁选项式概念多mutations已移除必须写TypeScript类型推导好需要手动标注较多学习成本低中等适合场景新项目、中小型项目老项目迁移、已深度依赖 Vuex 插件的场景如果项目已经用了 Vuex 4并且跑得没问题没必要强行迁移。但如果你是在为新项目做选型Pinia 是目前更合适的方向。3.2 用 Pinia 写一个用户状态存储数据看板项目里登录用户信息、token、权限列表这些全局数据会放在一个 user store 里。用组合式写法定义一个 store 是这样的import { ref, computed } from vue import { defineStore } from pinia import { loginApi, logoutApi, getUserInfoApi } from /api/user export const useUserStore defineStore(user, () { const token ref(localStorage.getItem(token) || ) const userInfo ref(null) const isLoggedIn computed(() !!token.value) const isAdmin computed(() userInfo.value?.role admin) async function login(loginForm) { const res await loginApi(loginForm) token.value res.token userInfo.value res.user localStorage.setItem(token, res.token) } async function fetchUserInfo() { const res await getUserInfoApi() userInfo.value res } function logout() { token.value userInfo.value null localStorage.removeItem(token) } return { token, userInfo, isLoggedIn, isAdmin, login, fetchUserInfo, logout } })注意这里我把 token 同步写进了localStorage。这样刷新页面之后ref(localStorage.getItem(token) || )会直接从本地把 token 读回来登录态不会因为刷新就丢失。组件里用的时候是这样的import { useUserStore } from /stores/user const userStore useUserStore() async function handleLogin() { await userStore.login({ username, password }) router.push(route.query.redirect || /dashboard) }store 里的状态会响应式地同步到所有组件一个地方改全部页面跟着变这就是全局状态存在的意义。3.3 composable 复用逻辑与 React Hooks 的对比热词里有“composable vue”这里也顺带讲一下。composable 就是把可复用的逻辑抽成一个函数函数内部可以用 ref、computed、watch 这些组合式 API然后返回需要暴露的内容。举个例子数据看板里做 WebSocket 断线重连逻辑可以抽成一个useWebSocketexport function useWebSocket(url, { onMessage, onOpen, onClose } {}) { const ws ref(null) const status ref(closed) let retryCount 0 let retryTimer null function connect() { ws.value new WebSocket(url) ws.value.onopen () { status.value open retryCount 0 onOpen?.() } ws.value.onmessage (event) { onMessage?.(JSON.parse(event.data)) } ws.value.onclose () { status.value closed handleReconnect() } } function handleReconnect() { if (retryCount 5) return retryTimer setTimeout(() { retryCount connect() }, 1000 * Math.pow(2, retryCount)) } function close() { clearTimeout(retryTimer) ws.value?.close() } onBeforeUnmount(close) return { status, connect, close } }如果你用过 React 的话会发现Vue 的 composable 和 React Hooks 在“把逻辑抽出来复用”这件事上思路是一致的。区别在于 Vue 的 composable 不需要关心调用顺序也不会因为条件分支导致 Hook 规则报错心智负担小很多。React Hooks 强调函数式纯度Vue composable 则更像是把响应式数据封装在一个普通函数里两者各有所长没有谁碾压谁的结论。4. 数据看板里最常用的四个功能例子视频流、Excel 导出、WebSocket 与扫码一个数据看板项目功能点往往五花八门。我挑了四个出现频率很高、而且每个都值得单独写一篇教程的功能m3u8 视频播放、多表格导出 Excel、WebSocket 实时推送、二维码扫码。这几个能力我都在项目里实际用过下面讲的内容全部来自实操。4.1 用 hls.js 播放 m3u8 视频流热词里“vue 播放 m3u8”搜的人很多说明这是个真实痛点。m3u8 是 HLS 协议的视频索引文件浏览器原生并不支持这种格式Safari 除外所以在 Chrome、Edge 里播放 m3u8 需要用 hls.js 这个库。安装依赖npm install hls.js在 Vue 组件里封装一个视频播放的通用逻辑template video refvideoRef controls autoplay muted/video /template script setup import { ref, onMounted, onBeforeUnmount } from vue import Hls from hls.js const props defineProps({ src: { type: String, required: true } }) const videoRef ref(null) let hls null function play() { const video videoRef.value if (!video) return if (Hls.isSupported()) { hls new Hls({ maxBufferLength: 30, liveSyncDurationCount: 3 }) hls.loadSource(props.src) hls.attachMedia(video) hls.on(Hls.Events.ERROR, (event, data) { if (!data.fatal) return if (data.type Hls.ErrorTypes.NETWORK_ERROR) { hls.startLoad() } else if (data.type Hls.ErrorTypes.MEDIA_ERROR) { hls.recoverMediaError() } }) } else if (video.canPlayType(application/vnd.apple.mpegurl)) { // Safari 原生支持 HLS video.src props.src } } onMounted(play) onBeforeUnmount(() { hls?.destroy() }) /script关于播放有几个注意点Autoplay 策略。浏览器对带声音的视频自动播放限制很严格所以代码里给 video 加了muted属性静音状态下的自动播放成功率会高很多。如果业务要求必须有声音就别用 autoplay改成用户点击播放。直播流的liveSyncDurationCount参数。这个参数控制追播速度值越小延迟越低但太低了会导致频繁缓冲。我一般给直播流设置 3 到 5点播流可以不用管。组件卸载的时候一定要调用hls.destroy()否则会残留定时器和事件监听切换页面之后视频还在后台跑内存就慢慢上去了。4.2 多个表格数据导出成一个 Excel热词里“vue 多个表格导出一个 excel”也是高频需求。数据看板通常会有多个统计表格比如“各区域设备状态表”“最近一周告警列表”“人员值班表”用户希望点一下按钮就把这些表格合并导出到一个 Excel 文件里每个表格一个 sheet。我用的是 SheetJS即 xlsx 库npm install xlsx导出多 sheet 的核心函数import * as XLSX from xlsx function exportMultipleSheets(sheets, fileName 统计报表.xlsx) { const wb XLSX.utils.book_new() sheets.forEach(({ name, data }) { const ws XLSX.utils.json_to_sheet(data) XLSX.utils.book_append_sheet(wb, ws, name) }) XLSX.writeFile(wb, fileName) }调用的时候传一个数组进去每个元素对应一个 sheetexportMultipleSheets([ { name: 设备状态, data: deviceStatusList.value }, { name: 告警记录, data: alarmList.value }, { name: 值班人员, data: dutyList.value } ])这里的坑主要在中文 sheet 名上。Excel 对 sheet 名的限制是 31 个字符以内且不能包含\/?*[]:这些特殊字符导出前要做一层校验。另外json_to_sheet生成的列顺序默认跟对象 key 顺序一致如果表格列很多、字段名不直观可以先做一层字段映射再转。如果是超大表格几万行以上SheetJS 的社区版性能比较吃力可以考虑用 exceljs 做流式写入。但对绝大多数后台管理场景来说xlsx 库已经完全够用。4.3 WebSocket 实时推送与组件生命周期管理数据看板页面要展示设备实时状态用轮询接口虽然简单但浪费资源而且不及时。更合理的方案是 WebSocket 长连接让后端把变化主动推过来。组件里的基本用法import { onMounted, onBeforeUnmount } from vue let socket null function connectWebSocket() { const protocol location.protocol https: ? wss:// : ws:// socket new WebSocket(${protocol}${window.location.host}/ws/dashboard) socket.onopen () { console.log(WebSocket 已连接) } socket.onmessage (event) { const data JSON.parse(event.data) // 根据消息类型更新对应图表 if (data.type device-status) { deviceStatusList.value data.payload } } socket.onclose () { console.log(WebSocket 已断开) } } onMounted(connectWebSocket) onBeforeUnmount(() { socket?.close() })这里最容易被忽略的问题是组件被销毁之后WebSocket 连接可能还挂着。尤其是在路由切换的场景下如果不主动 close连接会一直占用资源。所以onBeforeUnmount里关闭连接是必须的。如果想做得更完整可以参考前面那个useWebSocketcomposable把断线重连、心跳检测、消息分发都封装进去。心跳检测这块很多团队会忽略实际上很多网关设备会在一定时间后自动断开空闲连接客户端需要定期发送 ping 保持存活否则会突然掉线。4.4 扫码与地图两个高频集成场景热词里还有 “vue html5-qrcode” 和 “vue 地图”。扫码场景常见于设备巡检、物料入库这类移动端 H5 页面。html5-qrcode 这个库可以直接调用摄像头识别二维码npm install html5-qrcode核心用法是创建一个 Html5Qrcode 实例传入页面上的 div id然后 start 摄像头扫描。注意这个功能必须走 https 或者 localhost因为浏览器只有在安全上下文里才允许调用摄像头权限。地图的话国内项目用腾讯地图或高德地图都行都有对应的 Vue 组件封装。腾讯地图一个比较典型的操作是初始化地图之后添加标记点const map new TMap.Map(document.getElementById(map), { center: new TMap.LatLng(39.908, 116.397), zoom: 11 })这类引入全局 SDK 的操作有个共性SDK 是直接在 HTML 里通过 script 标签加载的Vue 组件里要用的时候会找不到全局变量。我的处理方式是在index.html里加载 SDK然后在组件里把全局变量取出来放到响应式数据里或者直接使用window.TMap。如果你用 TypeScript可以给 window 上声明自定义属性避免类型报错。5. 前后端联调与框架选型Spring Boot 搭档 Vue 的实战姿势热词里有“基于 springboot vue 的项目”“springboot vue 前后端分离”“若依 vue 开源项目在 idea 中部署”说明很多人其实就是 Vue Spring Boot 这个组合。这个组合非常成熟但联调阶段有很多细节。另外也有不少人会纠结 Vue、React、Angular 到底选什么我在这一节里把我的看法和实际经验都说一下。5.1 Vite 代理配置开发环境跨域解决方案前后端分离之后前端跑在 5173 端口后端跑在 8080 端口直接请求接口必然跨域。解决方式有两种后端配 CORS 或者前端配代理。我推荐开发环境用前端代理因为这样后端不用改任何配置也更贴近生产环境的 nginx 转发方式。Vite 项目在vite.config.ts里配export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, // 如果后端接口没有 /api 前缀就需要重写 rewrite: (path) path.replace(/^\/api/, ) } } } })配置完成后前端请求/api/user/login就会被转发到http://localhost:8080/user/login。这样做的好处是前端代码里统一写/api开头后端换地址时只需要改代理配置不用动业务代码。changeOrigin: true这个参数要记得设置否则后端如果校验请求来源的 Host 头会出现奇怪的 403 或者 Nginx 转发失败的情况。5.2 部署环境 nginx 的 try_files 配置打包之后的前端是纯静态文件交给 nginx 托管时有个非常经典的 404 问题你直接访问http://your-site/dashboard会 404但如果访问http://your-site/再点按钮跳过去就正常。原因是 vue-router 用的是 history 模式路由路径在服务端并不存在对应的物理文件nginx 默认会去找/dashboard这个文件或目录找不到就 404。解决办法是配置 try_files让 nginx 把所有请求都指向 index.htmllocation / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }try_files $uri $uri/ /index.html的意思很简单先看有没有对应的静态文件有就直接返回没有就返回 index.html由前端路由接管。配套的还有静态资源缓存策略。打包后的 JS/CSS 文件名都带了 hash可以放心设置长时间缓存location /assets/ { root /usr/share/nginx/html; expires 30d; }5.3 Vue、React、Angular 怎么选这个问题没有标准答案但可以根据团队情况和项目类型做判断。我的真实感受是中小型后台管理系统、内部工具、快速原型Vue 的上手速度和开发效率是明显优势大型复杂应用、需要高度可控性和生态完整性React 更稳Angular 适合团队本身有 Java/面向对象背景、需要强约束的大型企业级项目但学习曲线最陡。选技术栈不要只追热点。你所在的城市招什么岗位多、团队里的人熟悉什么、项目交付周期有多紧这些因素比“哪个框架更流行”重要得多。技术没有绝对的优劣适合团队和业务的才是最好的。5.4 开源项目参考若依框架的搭建价值如果有人想学 Vue Spring Boot 前后端分离的完整套路若依RuoYi是一个非常值得研究的开源项目。它把权限管理、用户管理、菜单管理、日志管理这些后台管理系统的基础设施都做好了前端基于 Vue Element Plus后端基于 Spring Boot。在 IDEA 里部署若依一般分三步导入后端工程配置application-druid.yml里的数据源信息执行项目里的 SQL 脚本。导入前端工程npm install之后npm run dev。后端启动后前端通过代理访问/dev-api转发的后端接口。跑通一遍若依的最大价值不是让你直接拿它改业务而是让你亲眼看到一套完整的前后端分离项目是怎么组织代码的权限拦截器怎么写、字典数据怎么管理、日志怎么记录、菜单和按钮权限怎么控制。把这些代码读一遍比看十篇零散教程收获都大。6. 打包部署与布局异常排查那些坑我替你踩过了热词里有一条“vue 打包后 布局异常”这个现象出现的频率比很多人想象中高得多。明明开发环境一切正常npm run build 之后部署到服务器上页面要么白屏要么样式全乱要么图标全部消失。这一节我把最可能的原因和排查思路全部列出来。6.1 绝对路径问题资源的 base 配置最常见的原因打包时资源路径写死了绝对路径导致部署到子目录时HTML 里引用的 JS/CSS 都指向了域名根目录结果 404页面自然白屏。Vite 打包默认base是/如果你的应用部署在域名根目录没问题。但如果部署在http://example.com/dashboard/这种子路径下就需要修改vite.config.tsexport default defineConfig({ base: /dashboard/ })打包之后 index.html 里引用的资源路径就会变成/dashboard/assets/xxx.js。如果前后端都在同一个域名下且放在不同子目录这个配置算是必填项。6.2 路由 history 模式下的刷新 404这个前面提到过刷新页面就 404 也是部署后常见问题。用 history 模式就必须配 nginx 的 try_files没有别的捷径。如果不愿意动 nginx 配置也可以把 vue-router 切成 hash 模式const router createRouter({ history: createWebHashHistory(), routes })但 hash 模式的 URL 会带一个#比如http://example.com/#/dashboard大部分人会觉得丑丑的。我的建议是能配 nginx 就配 nginx别为了省事牺牲 URL 体验。6.3 样式丢失与布局异常的完整排查链路样式异常的问题比白屏更难排查因为没有统一报错。我把排查链路整理成固定顺序遇到问题按这个顺序查基本都能定位。第一个检查项是浏览器控制台的网络请求。看 CSS 和字体文件是否请求失败。如果 CSS 文件状态是 404那就是 base 路径问题按 6.1 的方式处理。如果字体图标 404同样是因为 base 路径不对图片和字体的引用都是在打包时根据 base 生成的。第二个检查项是 Element Plus 等组件库的样式是否被正确加载。如果你用了组件的按需导入需要确保配置了对应的样式插件。手动导入组件时只导入了 JS 没有导入样式文件的情况很容易出现表现就是布局全部都错位像没穿衣服一样。第三个检查项是scoped样式的问题。如果一个组件里想覆盖子组件内部的样式尤其是想修改第三方组件库的内部样式scoped会导致选择器加上 data 属性而子组件内部的元素没有这个属性样式就不生效。解决办法是 Vue 提供的深度选择器:deep():deep(.el-table__header) { background-color: #f5f7fa; }编译后它会把 data 属性加到前面的父级选择器上子组件内部的选择器就正常生效了。这个功能是写 Vue 组件样式时必须掌握的否则你会一直在“样式不生效”的坑里打转。第四个检查项是浏览器缓存。部署新版本后用户浏览器可能还在用旧的 CSS 文件表现就是你改的样式完全没生效。解决方式是打包时开启文件名 hash这个 Vite 默认已经做了部署时注意 nginx 对静态资源不要设置过短的缓存时间。再不行就让用户强制刷新CtrlShiftR但这是兜底手段不能当作常规方案。6.4 打包体积优化的两个常用手段部署之后还要关注体积。首次打开页面如果加载了 1MB 以上的 JS体验会非常差。除了前面说的路由懒加载还有两个常用的处理办法。第一个是手动分包。Vite 支持在构建配置里把体积较大的库单独拆出来build: { rollupOptions: { output: { manualChunks: { echarts: [echarts], element-plus: [element-plus] } } } }把 ECharts、Element Plus 这类大而独立的库拆成单独 chunk浏览器可以并行加载而且只要版本不变chunk 的 hash 不变用户再次访问时可以直接命中缓存不用重新下载。第二个是使用unplugin-auto-import和unplugin-vue-components让组件和 API 按需导入。如果项目不大、团队比较小也可以先手动 import等发现体积确实大了再引入这两个插件没有必要一开始就把自动化配置全部铺开增加项目复杂度。如果部署后遇到布局异常我个人的建议是先在本地跑一遍npm run build npm run preview在本地起一个静态服务器模拟线上环境看能不能复现。如果能复现问题大概率出在打包配置如果不能复现那就要怀疑 nginx 配置和服务器环境比如 gzip 是否开启、MIME 类型是否正确。静态资源请求一直 404 的话这种问题看控制台就能定位到。从技术选型到环境搭建从路由到状态管理从功能开发到部署上线一个 Vue 项目真正走完一遍之后你会发现阻碍你的往往不是某个特别深奥的原理而是那些“书上没写、文档找不到、只有踩过坑才知道”的细节。我写这篇文章的目的就是把我踩过的这些坑直接摆在你面前让你能绕开它们把精力花在真正有价值的业务代码上。