先说实话搭建一个 Vue 项目难度从来不在那些npm create vue之类的命令上。命令五分钟就跑完了真正让新手卡住的地方是命令跑完之后目录该长什么样、路由怎么分、样式怎么隔离、依赖报错怎么查、项目怎么交给别人。这些才是“从 0 到 1”里那个“1”的真相。这篇文章我不想写成一个教程的复读机而是按我这些年从零起项目的实际操作顺序把环境、依赖、目录、路由、第三方接入、交付协作这几件事一件件拆开讲中间穿插大量真实踩过的坑和排查思路适合刚入门前端、准备独立搭建 Vue 3 项目的读者也适合团队里负责初始化工程的同学用来对照查漏。1. 先把地基打对Node 环境、包管理器与脚手架选型1.1 Node 版本为什么它经常是第一道坎很多人搭 Vue 项目遇到的第一句报错不是来自代码而是来自 Node 版本不兼容。Vue 3 的官方脚手架 create-vue 底层依赖 Vite而 Vite 5 及以上版本对 Node 版本有硬性要求通常要求 18 或 20。如果你机器上还挂着 Node 16执行创建命令的时候要么直接报错要么项目生成后一跑npm run dev就提示Unsupported engine。我建议在开始之前先检查两件事打开终端执行node -v和npm -v确认版本号。如果你的机器上有多个项目同时存在强烈建议装一个 Node 版本管理器用 nvm 或者 fnm 都行。不要嫌麻烦等你同时维护一个老 Vue 2 项目和一个新 Vue 3 项目的时候就知道版本切换有多救命了。一个比较稳妥的版本策略是长期稳定用 Node 20 LTS。Vue 3、Vite、主流组件库在这个版本下兼容性都很好也不至于像 Node 22 那样偶尔需要在某个边缘依赖上做额外处理。1.2 create-vue 与 vue-cli新老脚手架的选型逻辑现在创建一个新的 Vue 项目主流工具已经和几年前完全不同了。create-vueVue 官方推出的新一代脚手架底层基于 Vite默认支持 Vue 3、TypeScript、JSX、Router、Pinia、Vitest、ESLint 等全功能选配。它是目前官方推荐的方式。vue/clivue-cli老牌脚手架基于 Webpack。如果你没有历史包袱我不建议新项目再选它。Webpack 的配置复杂度、冷启动速度和热更新体验对比 Vite 都有明显差距。还有一个更灵活的玩法你完全不需要脚手架自己拿package.json加上vit、vitejs/plugin-vue和vue三个依赖也能手搓一个 Vue 项目。脚手架的价值在于把 TS 配置、路径别名、环境变量、ESLint 这些“基础设施”一次性给你配好省去大量初始配置。新人不要一上来就手搓先理解脚手架的生成结果之后再逐步拆解也不迟。1.3 一个能跑起来的项目到底要执行哪些命令以 create-vue 为例完整的最小操作序列是这样的我假设你已经在目标目录下# 1. 全局安装 pnpm如果你还没装 npm install -g pnpm # 2. 创建项目my-vue-app 换成你的项目名 pnpm create vuelatest my-vue-app # 3. 根据交互提示选择功能 # 我一般会开启TypeScript、Router、Pinia、ESLint、Prettier # 不需要的Vitest单测后续按需加、CypressE2E 后续按需加 # 4. 进入目录并安装依赖 cd my-vue-app pnpm install # 5. 启动开发服务器 pnpm run dev到这一步浏览器打开http://localhost:5173看到 Vite 的欢迎页项目就“跑起来”了。但注意这只是从 0 到 0.1真正决定项目能不能稳健走下去的是后面依赖管理、目录拆分、路由设计和工程化规范这些细节。提示包管理器我优先选 pnpm。原因很简单——它通过全局内容寻址存储来复用依赖多个项目共用同一份包文件磁盘占用少、安装速度快而且默认的符号链接机制能更好地约束依赖的“幽灵依赖”问题。团队协作时只需要统一在 package.json 里声明packageManager: pnpm9.x.x即可。2. 依赖装完不是终点从 tsconfig 报错谈依赖管理的隐性坑2.1 “failed to load tsconfig vue/tsconfig/tsconfig.web.json”一次完整排查这个报错几乎是个网红问题随便一搜就是一片。报错原文大致是failed to load tsconfig vue/tsconfig/tsconfig.web.json: tsconfig not found第一次遇到它的人很容易懵因为明明项目刚生成什么代码都还没写。我复盘一下这类问题的完整排查链路供你参考第一步先看现象。报错发生在pnpm run dev或者 IDE 的 TypeScript 服务启动时。Vite 在启动过程中会去读项目中tsconfig.app.json或tsconfig.web.json里引用的配置扩展而这个扩展指向的是node_modules/vue/tsconfig这个包。第二步检查 node_modules。打开node_modules/vue/目录看有没有tsconfig包。如果目录不存在说明依赖安装阶段就没有把这个包装进来如果目录存在但版本和你 package.json 里的声明不一致多半是 lockfile 和实际安装结果不同步。第三步重新安装。是的很多所谓“玄学报错”最有效的处理方式就是干净重装rm -rf node_modules pnpm-lock.yaml pnpm install但重装之前必须想一件事为什么会“没装上”如果每次都要靠删锁文件来绕过项目越到后面越危险。第四步追溯根因。根据我的经验这种报错高频出现在几类场景包管理器混用有人之前用 npm 装过生成了package-lock.json后来又改用 pnpm 或 yarn 安装导致依赖树混乱。此时要清理掉所有锁文件只保留一个。pnpm 的符号链接机制pnpm 默认把依赖放在全局存储里node_modules下是符号链接。如果你用了某些不兼容的配置比如node-linkerhoisted的旧工程改造可能导致 tsconfig 解析不到包。仓库缓存损坏公司的统一 npm 缓存或 CI 机器上的缓存如果和 lockfile 校验不一致也会出现这类问题。第五步也是我最推荐的做法统一工具、统一锁文件。项目里只保留pnpm-lock.yaml所有成员和 CI 都用 pnpm并在 package.json 里声明packageManager字段。这能消灭一大类“我这能跑你那不能跑”的协作问题。2.2 依赖管理最容易踩的三个隐性坑除了 tsconfig 报错我整理了一下依赖管理里最容易在项目中期爆发的三个问题第一个坑直接改 node_modules 里的东西。有些同学为了解决某个临时问题直接把node_modules里某个包的源码改了。改完当时是好使的但下次pnpm install一跑所有改动全部蒸发最后线上出了 bug 都查不到原因。正确做法是要么 fork 这个包要么用patch-package这类工具对依赖打补丁并且把补丁文件提交到仓库里。Vue 生态里遇到组件库特定 bug 时patch-package是真的能救场。第二个坑依赖版本不锁定。Vue 3 的生态更新速度很快vue-router、pinia、vite的 minor 版本更新时偶尔会带出一些不兼容变更。用vue-router: ^4.4.0这种写法下一次 install 可能就会拉到较新的 4.x 版本表面上没问题实际行为可能有细微差异。团队项目必须提交 lockfile并且不要在没需求的时候频繁pnpm update。第三个坑只装了包没装对包。Vue 3 项目里有个经典场景很多组件库比如 Element Plus 的按需引入需要额外安装unplugin-vue-components和unplugin-auto-import两个 Vite 插件。少装任何一个页面能渲染出来但组件可能是全量打包体积直接起飞或者 IDE 里报一堆类型错误。装依赖前多看一眼官方文档的安装章节能省很多事。Vue 3 项目里的依赖锁文件本质上是团队协作的“契约”。谁破坏了这份契约谁就要承担浪费别人一小时以上的代价。这是我踩过足够多坑之后最深的体会。3. 目录结构决定你能走多远组件、插槽与样式隔离3.1 标准目录结构别把业务都塞进 App.vue项目能跑起来之后第一件重要的事就是定目录规范。Vue 3 的 create-vue 默认生成的结构已经有基础分层但实际业务项目几乎都要再扩展。我习惯的目录结构大致是这样src/ api/ # 接口请求层按业务模块拆分文件 assets/ # 静态资源图片、全局样式 components/ # 通用组件按钮、弹窗、卡片等 composables/ # 组合式函数useTable、useForm 等 layouts/ # 布局组件侧边栏、顶栏、内容区 router/ # 路由配置静态路由 动态路由逻辑 stores/ # Pinia 状态模块 utils/ # 工具函数日期格式化、文件下载等 views/ # 页面级组件一个路由对应一个文件这里的核心原则是views 里只放页面级组件components 里只放可复用组件api 层统一收口所有请求。我见过很多项目业务写了两周之后views下面每个目录里堆了一堆.vue、.ts、.d.ts连谁依赖谁都说不清楚。尽早把 api 请求从组件里拆出去是项目保持可维护性的最低成本操作。3.2 插槽slot是组件复用的灵魂Vue 的插槽机制看着简单但用好了组件的复用性会有质的提升。很多人写组件时喜欢用props传一堆配置项比如传一个type字段来切换标题区样式结果组件内部全是v-if分支越写越臃肿。插槽的思路完全不同组件只负责结构框架内容由使用者决定。一个典型的例子是卡片组件template div classbase-card header v-if$slots.header classbase-card__header slot nameheader / /header main classbase-card__body slot / /main footer v-if$slots.footer classbase-card__footer slot namefooter / /footer /div /template使用的时候BaseCard template #header标题区域想放什么放什么/template 正文内容完全自定义 template #footer el-button typeprimary确认/el-button /template /BaseCard还有一种更容易被忽略的是作用域插槽也就是插槽内容需要拿到子组件内部的数据。比如一个表格组件内部维护了分页和数据请求逻辑需要让使用者自定义每一行的操作列这时就可以在插槽里把当前行的数据传出去!-- TableWrapper 内部 -- slot nameaction :rowrowData :indexrowIndex /使用方template #action{ row } el-button clickedit(row)编辑/el-button /template插槽的核心理念是“反向控制”。组件把控制权交还给使用方既保持了框架统一又不限制内容形态。这一点在写低代码平台、动态表单这类需要高度可配置的场景里尤其重要。3.3 样式冲突scoped 的边界和 :deep() 的正确用法样式冲突是 Vue 项目里特别常见、又特别容易被忽视的问题。Vue 单文件组件的style scoped会在编译时给选择器加一个>style scoped :deep(.el-dialog__header) { padding: 20px; } /style:deep()会把父组件的 scoped 属性作用在子组件内部元素的上层选择器上从而穿透到子组件内部。它解决的是“我需要覆盖第三方组件内部样式”的合理需求而不是让你用它把整个组件库的样式全部重写一遍。重写样式这种事情优先考虑用 CSS 变量或者组件库的主题配置:deep()只是最后手段。如果项目中全局样式和 scoped 样式同时存在我建议约定一个顺序全局样式统一放在src/assets/styles目录里页面级布局类样式写在 views 的 scoped 里组件内部的样式尽量收敛在组件自身。谁写谁负责不要越级。4. 从 hello world 到真实业务路由参数、动态菜单与第三方能力接入4.1 路由参数三种传参方式怎么选Vue Router 4 里路由传参有三种主要方式很多人弄不清到底该用哪个第一种query查询参数。URL 长这样/detail?id1typevideo。使用方式router.push({ path: /detail, query: { id: 1, type: video } })读取route.query.id。query 参数会出现在 URL 中可分享、可收藏、刷新不丢失。适合列表筛选条件、分页参数这类“状态可被外部感知”的场景。第二种路径参数。URL 长这样/detail/1。需要路由配置里定义path: /detail/:id使用方式router.push({ path: /detail/${id} })读取route.params.id。路径参数语义清晰适合详情页、编辑页这种“资源标识”性质的参数。第三种params name 组合。使用方式router.push({ name: detail, params: { id: 1 } })这里有一个很容易踩的坑如果路由配置是path: /detail没有:id占位用 params 传参后参数不会体现在 URL 里刷新页面参数就会丢失。所以在 Vue Router 4 中params 只推荐配合路径参数使用name params这种写法除非路由 path 里明确写了占位符否则要谨慎。我的选型经验很简单能放路径参数就放路径参数其次是 query。优先保证 URL 的可读性和可分享性避免把状态藏在内存里。4.2 动态路由权限菜单的后台驱动实现真实项目中菜单和权限几乎不可能靠前端写死。通常后端会根据登录用户的角色返回一个菜单配置前端拿到配置后动态注册路由。这就是动态路由的核心场景用一个 addRoute 接口把静态路由和动态路由分开管理。一个典型的实现链路静态路由只保留固定页面登录页/login、404 页、根布局/。用户登录后前端请求/user/menus拿到菜单树。前端把菜单树转换成 Vue Router 的RouteRecordRaw数组逐个调用router.addRoute(layout, route)挂到根布局下面。每次动态挂载完成后调用router.replace(router.currentRoute.value.fullPath)重新进入当前地址让新增路由生效。同时要在全局前置守卫里处理页面刷新时的还原逻辑router.beforeEach(async (to) { const userStore useUserStore() if (!userStore.menusLoaded) { await userStore.loadMenus() // 重新拉取并 addRoute return { ...to, replace: true } } })这里有个细节很多人会忽略动态路由匹配不到时应该让 404 页兜底。有些项目随便写了一个通配符/:pathMatch(.*)*指向 404结果权限外的页面也直接展示了 404没有做“无权限”的区分。更好的做法是 404 路由保持静态由后端菜单决定“这个路由到底存不存在”菜单里没有的路由在前端层面直接拦截跳转 403而不是依赖 404 兜底。动态路由说起来不复杂但它和状态管理、权限控制、菜单渲染常常纠缠在一起是项目里逻辑复杂度最高的模块之一值得花心思专门维护。4.3 第三方能力接入腾讯地图与 m3u8 视频播放业务项目里最常见的第三方接入一个是地图一个是视频流。这两个我都踩过不少坑简单说说 Vue 3 里的处理方式。腾讯地图接入。腾讯位置服务提供的是传统 JS API不是现成的 Vue 组件所以需要封装。核心思路是在组件 mounted 时动态加载脚本等window.TMap就绪后再初始化地图实例。function loadTMapScript() { return new Promise((resolve, reject) { if (window.TMap) { resolve(window.TMap) return } const script document.createElement(script) script.src https://map.qq.com/api/gljs?v1.expkey你的Key script.onload () resolve(window.TMap) script.onerror reject document.head.appendChild(script) }) }拿到实例后设置中心点、添加标记、绑定事件。要特别注意两点第一key 一定要通过环境变量配置不能写死在代码里第二组件销毁时一定要调用map.destroy()清理地图实例否则在列表页和详情页来回切换时会报“Map container is already initialized”之类的错。m3u8 视频播放。这个需求在直播、监控回放、课程点播里很常见。m3u8 本质是一个播放列表文件里面列出一堆视频分片.ts 文件的 URL。浏览器原生不直接支持 HLS 播放除了 Safari所以常用的方案是用hls.jsimport Hls from hls.js function playM3u8(videoEl, url) { if (Hls.isSupported()) { const hls new Hls() hls.loadSource(url) hls.attachMedia(videoEl) // 监听错误直播场景下做断线重连 hls.on(Hls.Events.ERROR, (_event, data) { if (data.fatal) { hls.recoverMediaError() // 或者重新 loadSource } }) } else if (videoEl.canPlayType(application/vnd.apple.mpegurl)) { videoEl.src url // Safari 直接设 src } }封装成播放器组件后组件onUnmounted时要调用hls.destroy()否则切换路由后播放器会继续请求分片白耗流量和内存。另外m3u8 播放的跨域问题比普通视频更麻烦因为分片请求是异步的服务端必须正确设置 CORS 头否则首片加载就会失败。5. 项目交付与协作源码打包、依赖锁定与 IDEA 高频操作5.1 发项目给别人为什么压缩源码不是简单 zip 一下“Vue 项目源码怎么发给别人”这个问题其实是一个协作问题。最常见的新手操作是把整个项目文件夹 zip 打包右键压缩后发过去。结果对方一解压发现node_modules就有几百 MB发文件慢到崩溃对方还得删掉重装。正确的源码交付方式是只交付“源码 依赖清单 构建配置”src/目录所有源码package.json依赖清单和脚本命令pnpm-lock.yaml/package-lock.json锁文件vite.config.ts、tsconfig*.json、index.html工程配置.env.example环境变量样例真实密钥绝对不能发收到源码的人只需要pnpm install pnpm run dev所以发源码之前永远检查两件事一是node_modules不要在里面二是把.gitignore配置好。.gitignore里至少要有node_modules/、dist/、.env.local、.idea/、.vscode/这些基础项。如果你用 Git 协作直接让对端git clone是最干净的交付方式比压缩包可靠一万倍。还有一个细节锁定 Node 版本和包管理器版本。在 package.json 里加上{ engines: { node: 20 }, packageManager: pnpm9.12.0 }对方跑pnpm install时如果包管理器版本不对pnpm 自己会提示。这一步能避免大量“我这跑得好好的你那怎么报错”的扯皮。5.2 用 IDEA 开发 Vue 项目值得配置的三件事先说清楚IDEA 是 JetBrains 家的 IDE很多后端同学的主力工具就是它所以写 Vue 项目时也习惯顺手用 IDEA。用它开发 Vue 3 项目有三件事值得在项目启动时一次性配好否则体验会非常打折扣。第一件事装对插件。Vue 3 必须使用 Volar 插件而不是老旧的 Vetur。IDEA 集成了 Vue.js 插件支持 Volar在插件市场里直接安装启用即可。配好后.vue单文件组件里模板、脚本、样式三段的语法高亮和自动补全才会正常。第二件事配置运行与调试。不要让 IDEA 里每次都手动敲pnpm run dev。在 Run/Debug Configurations 里新增一个 npm 类型的配置选择pnpm和dev脚本指定好工作目录。之后一键启动、一键停止IDE 的日志面板里还能看到 Vite 的完整输出。调试 Vue 3 Vite 项目时用 IDE 的 JavaScript Debug 配置指向http://localhost:5173然后在代码里打断点。Vite 的 source map 默认配置就能映射回源码断点不会跑偏。如果你发现断点不生效多半是 vite.config 里build.sourcemap没开或者 IDE 的调试端口设置不对。第三件事ESLint 自动修复。IDEA 里安装 ESLint 插件后在 Settings 里把它和项目的 eslint.config 关联起来开启Run eslint --fix on save。这样每次保存文件时IDE 会自动修复可自动修复的 lint 问题比如分号、引号、缩进团队风格统一这件事就基本不用人工操心了。我个人的体会是项目越早期配置好 ESLint 和 Prettier 的自动格式化后面代码 review 走神的概率越低。还有一个 IDEA 用户容易纠结的问题要不要用 Vue 官方推荐的 VS Code其实没必要把 IDE 上升到信仰问题。工具只是入口关键是工程规范能不能约束团队所有人。IDEA 也好 VS Code 也好只要 Volar、ESLint、Prettier 这三件套齐了写 Vue 3 的手感不会有质的差别。最后借这个机会说一个我自己的习惯新项目初始化之后我会立刻把三件事补齐——目录规范写进 README、ESLint Prettier husky 的提交检查跑通、.env环境变量样例整理好。这三点不花多少时间但都是在项目还没“写满业务”时最容易低成本布局的。等项目上了规模再回头补代价就完全不一样了。搭建 Vue 项目这件事从 0 到 1 真正难的从来不是把页面跑起来而是让这个项目在交付、协作、扩展的每一个环节都别掉链子。
阅读完成 · 觉得有帮助?