Vue3工程创建全流程:从create-vue脚手架到nginx部署实践
2026/9/14 20:51:43 网站建设 项目流程

开头部分:

我最早接触Vue3工程创建的时候,其实并没有太多可以抄作业的资料。那时候前端社区还在争论要不要上Vite,Vue3的生态还没完全铺开,很多教程还是老一套的Webpack配置方式。两年多过去,现在的Vue3工程创建已经完全不同了,官方脚手架create-vue基本成为事实标准,TypeScript默认开启,Vite早就把DevServer速度拉满了。这篇文章不打算只讲"敲哪个命令",我想把从零创建Vue3工程的全过程拆开讲清楚,包括工具链选型、交互式选项怎么选、目录结构怎么组织、请求层和状态管理怎么接入、工程化护栏怎么配、最后怎么部署到nginx。内容面向用过Vue2、刚切入Vue3的开发者,也适合准备从零搭一个Vue3后台管理系统的团队参考。

1. 为什么我劝你直接上官方脚手架,而不是手动搭或者守着老模板

先说结论:创建Vue3工程这件事,现在已经不需要自己操心了。但在实际问过不少开发者之后,我发现很多人还是不太清楚create-vuecreate vitevue-cli之间的区别,也不知道哪个方案最稳。这一节把这几个方案摊开来说清楚。

1.1 三种建工程方式的真实差异

vue-cli是Vue2时代的产物,底层基于Webpack。虽然它后来也支持Vue3,但用过的人都知道,创建完工程后DevServer冷启动可能要等十几秒甚至几十秒,热更新在稍微大一点的项目里也会有明显延迟。Webpack本质上是一层层递归地对模块做静态分析,大型项目构建链路特别长,再加上各种loader的配置,维护成本不低。

create-vite是Vite官方提供的通用脚手架,它创建的工程不带Router、不带Pinia、不带ESLint等Vue生态的东西,适合追求极简或者有特殊需求的场景。如果你只是想试一下Vite本身,用它没问题,但创建Vue3完整工程的话,后续还是要手动补一堆依赖和配置,不划算。

create-vue是Vue官方针对Vue3提供的工程脚手架,底层调用Vite进行构建,同时把TypeScript、Vue Router、Pinia、Vitest、ESLint、Prettier等选项全部做成交互式选择。它解决的问题直击痛点:创建出来的工程不仅构建速度快,而且目录结构、TS配置、代码规范都是Vue官方团队长期维护验证过的最佳实践组合。我们团队在3.0版本重写后台系统的时候,就是直接用create-vue生成的,后面基本没有做结构性调整。

如果你现在还在犹豫要不要把老工程从Webpack迁到Vite,我的建议是:新项目一律优先create-vue,老项目迁移则单独评估,不要为了追新而把正在稳定运行的系统推倒重来。

1.2 为什么Vite能比Webpack快这么多

这个问题的本质在于构建机制完全不同。Webpack在开发环境下必须从入口文件开始,把所有模块打包成一个bundle再启动服务;而Vite利用浏览器原生ESM的支持,开发者请求哪个模块,Vite才实时编译哪个模块。加上Vite预构建依赖用的是esbuild(Go语言实现),处理node_modules里的依赖包时不会因为项目膨胀而变慢。

具体到体感上:一个包含60多个页面路由的Vue3后台工程,用Vite启动开发服务器的耗时大概是400到800毫秒,页面切换秒开。这在以前Webpack时代是不可想象的。团队里新同学第一天接入项目,基本不用浪费时间等待构建。

1.3 一个反直觉的结论:模板越多越难维护

提到脚手架,很多人会倾向去GitHub上找一个开源的Vue3后台管理模板,因为模板通常自带权限系统、多级菜单、动态路由、大屏页面、表单设计器之类的功能,看起来省事。但我的实际体验是:开源模板的问题不在"功能少",而在"功能太多、耦合太深"。你要改一个请求超时时间,可能得先摸清楚它的axios封装在哪里、环境变量走哪个配置、mock数据怎么拦截;你要对UI框架降级或者换版本,可能牵一发动全身。

所以创建一个干净的Vue3工程,等于把底子握在自己手里。模板可以作为参考,但不建议当成项目的起点。自己创建工程省去后面无数debug时间。

2. 开始之前:Node版本和包管理器这两个坑,先填平

创建Vue3工程之前,环境准备比想象中重要。很多人卡在安装报错上,实际不是命令的问题,而是Node版本或者包管理器的问题。这两块没有处理好,后面每一步都可能出各种莫名其妙的错。

2.1 Node版本怎么选:不是越新越好,但也不能太旧

Vite 5以上的版本对Node版本有硬性要求,package.json里通常写着"node": "^18.0.0 || >=20.0.0"。我在Node 16环境里跑Vite 5就遇到过一个经典报错:

Error: require() of ES Module ... not supported

说白了这个构建工具依赖的ESM特性,在旧版Node里不完整。解决方案也很直接,升级Node。现在新项目我基本推荐直接用Node 20 LTS,如果是Node 22,稳定版本也无妨。

检查当前Node版本的命令随手就能敲:

node -v npm -v

如果版本不满足,建议用nvm(Node Version Manager)管理Node版本。nvm可以同时装多个Node版本,随时切换,比如开发环境用20,某个老项目需要16的时候就立刻切回去。macOS和Linux下推荐用nvm,Windows下可以用nvm-windows或者fnm,fnm本身基于Rust实现,切换速度比nvm还快一些。

2.2 包管理器选型与切换源的实战细节

npm、yarn、pnpm三个包管理器在Vue3工程里都能用,但我的经验是优先用 pnpm。原因有两个:

第一,pnpm的依赖管理方式是全局存储 + 硬链接 + 符号链接,多个项目共享同一份依赖缓存,磁盘占用明显少。第二,pnpm默认不会扁平化node_modules,而是把依赖按内容寻址的方式存放,这样安装依赖更干净,也不会出现"幽灵依赖"问题。

实际切换仓库地址也要提前做。尤其在国内网络环境下,npm官方源安装依赖经常卡在npm install,等半天还可能失败。最省心的方式是直接把镜像仓库切到国内仓库:

npm config set registry https://registry.npmmirror.com

这个操作会写入全局配置文件~/.npmrc,以后所有npm命令都走国内镜像。如果不想改全局配置,也可以在项目根目录的.npmrc里单独指定:

registry=https://registry.npmmirror.com

pnpm的仓库源配置也是同理:

pnpm config set registry https://registry.npmmirror.com

注意:如果你用nvm管理Node,切Node版本后registry会保持之前的配置,不需要重新设置。

踩过一次坑:某次在CI环境里安装依赖时,因为CI只装了npm、没装pnpm,导致构建失败。后来我在package.json里加了"packageManager": "pnpm@9.x.x"字段,配合Corepack的corepack enable,CI里自动就会切到pnpm。这个字段在本地开发中也提醒团队不要混用包管理器。

3. 实际操作:create-vue交互式创建,每一步该怎么选

环境准备好之后,进入核心操作环节。create-vue的交互式问答一共包含10来个选项,很多第一次用的人会在这儿犹豫不决,尤其是TypeScript、JSX、Pinia、Vitest这种到底该不该勾选,对后面的工程结构影响很大。我逐个说清楚我的选法和理由。

3.1 完整创建命令及每个交互选项的推荐值

执行创建命令:

npm create vue@latest

也可以借助npm的--传参方式省去交互输入,不过第一次创建建议还是走一遍交互,能让你对工程生成的东西有数。

交互式问答涉及的主要选项如下表:

选项含义我的推荐
Project name项目名称,默认是 vue-project按实际项目命名,比如 admin-web
Add TypeScript?是否添加类型系统是,强烈建议
Add JSX Support?是否支持JSX语法组件不习惯写JSX就选No,需要的话可以后加
Add Vue Router?是否集成路由单页应用绝大部分需要,选Yes
Add Pinia?是否集成状态管理需要跨组件共享状态就选Yes
Add Vitest?是否集成单元测试框架团队有测试规范就选Yes,小项目可No
Add End-to-End Testing?是否集成E2E测试后台系统长期维护建议选Yes,否则No
Add ESLint?是否集成ESLint静态检查强烈建议选Yes
Add Prettier?是否集成Prettier格式化强烈建议选Yes
Add Vue DevTools 7?是否集成Vue官方浏览器调试工具选Yes,调试体验好很多

这里重点说TypeScript与ESLint这两个单选Yes的原因。

TypeScript对Vue3工程的意义不只是"类型安全"这种抽象好处,而是实打实地提升了协作效率。比如接口返回的列表数据,以前在JavaScript里你不清楚某个字段有没有,得反复console.log;有了类型定义,编辑器直接给出字段提示,字段拼写错了编译阶段就报错。Vue3的组合式API和defineProps、defineEmits这些宏,与TS结合得很好,写起来不会觉得别扭。实际上Vue3官方文档里把TypeScript作为一等公民推荐,整个类型的推导能力做得比Vue2时代好了不止一个档次。

ESLint选Yes的原因更直接:你不想在Code Review时为了"这里多了一个分号""那里用了双引号"和同事争论。ESLint把这些规则自动化,不符合规范根本提交不上去。

我一般在TypeScript、Vue Router、Pinia、ESLint、Prettier这几个都选Yes,其他根据项目需要决定。在开发后台管理系统这类中大型项目时,Vitest和E2E也会打开。

3.2 创建完成后的目录结构到底有什么门道

创建完成后,工程根目录大致长这样:

├── .vscode/ # 编辑器统一设置 ├── public/ # 纯静态资源,不会被打包处理 ├── src/ │ ├── assets/ # 需要走构建管线的静态资源 │ ├── components/ # 公共组件 │ ├── router/ # 路由配置 │ ├── stores/ # Pinia 状态管理 │ ├── views/ # 页面级组件 │ ├── App.vue # 根组件 │ └── main.ts # 应用入口 ├── .gitignore ├── .npmrc # 项目级npm配置 ├── env.d.ts # 类型声明文件 ├── index.html # Vite的入口HTML文件 ├── package.json ├── tsconfig.json # TypeScript 配置总入口 ├── tsconfig.app.json # 应用代码的TS配置 └── vite.config.ts # Vite 配置

说一下容易忽略的几个点。

tsconfig.app.jsontsconfig.node.jsoncreate-vue自动生成的分层配置。tsconfig.app.json管理 src 目录下的应用代码,tsconfig.node.json管理 vite.config.ts 这种Node环境下的脚本文件。这样做的好处是不同类型代码的编译目标、模块解析策略不同,混在一个配置里反而容易出问题。

public目录和src/assets目录容易混。public里的文件会原封不动拷贝到构建输出目录,引用时直接以根路径/开头,比如/favicon.icosrc/assets下的文件都会被Webpack/Vite处理,支持压缩、指纹、字体、图片的雪碧图之类。经验是:logo文件、favicon、外链资源之类的放public,组件里要用的小图片、自定义字体走src/assets

初始目录里的HelloWorld.vueTheWelcome.vue这些示例组件,删掉就好。它们解决的问题只有一个:验证脚手架真的能跑。留着会干扰实际开发。

3.3 启动项目前必须做的三个调整

创建完、装完依赖,默认工程虽然能启动,但直接在这个基础上开发会有点别扭。我一般会先做三件事。

第一,重置默认样式。很多项目会引入normalize.css或者reset.css,把浏览器默认的margin、padding、字体大小统一掉。我在工程里通常直接用一个新的全局样式文件src/styles/index.scss,在里面做变量定义和基础样式重置。

第二,配置路径别名。默认的src路径在每个组件里写../../../utils这种简直痛苦。在vite.config.ts里加配置:

import { fileURLToPath, URL } from 'node:url' import { defineConfig } from 'vite' export default defineConfig({ resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } } })

配上之后,组件里import request from '@/utils/request'就优雅多了。注意这一步对应tsconfig.app.json里的paths也要同步修改,不然TypeScript不认@别名,会一直报红。

第三,配置env.d.ts。如果后端接口请求、环境变量等要用到import.meta.env这种Vite暴露的对象,建议在env.d.ts里加上类型声明,编辑器才能有提示。

4. 创建完工程的第一件事:把请求层、路由和状态管理串起来

脚手架生成的工程是一张白纸。真正开始写业务之前,我会先把请求层、路由、状态管理这三根"脊椎"搭好,否则后面每写一个页面都要回头补齐基础设施。

4.1 环境变量按环境拆分,不要相信"改一处生效全环境"

项目一接入后端,马上就会遇到不同环境对应不同API地址的问题:本地开发用http://localhost:8080,测试环境用https://test-api.example.com,生产环境用https://api.example.com。如果把这个值硬编码在axios封装里,每次部署都得改代码,极其容易出事。

Vite对多环境配置文件有明确的约定,我习惯在根目录新建三个文件:

.env # 所有环境下都会加载的基础变量 .env.development # 开发环境 .env.production # 生产环境

.env.development示例:

VITE_API_BASE_URL=https://test-api.example.com VITE_APP_TITLE=后台管理系统(测试环境)

.env.production示例:

VITE_API_BASE_URL=https://api.example.com VITE_APP_TITLE=后台管理系统

变量名必须以VITE_开头,Vite才会把这几个变量暴露到客户端代码中,这是官方约定的安全边界,防止服务端密钥之类的变量被不小心打进前端包。后面代码里通过import.meta.env.VITE_API_BASE_URL就能拿到。

还需要在env.d.ts中做类型声明:

/// <reference types="vite/client" /> interface ImportMetaEnv { readonly VITE_API_BASE_URL: string readonly VITE_APP_TITLE: string } interface ImportMeta { readonly env: ImportMetaEnv }

好几个项目都遇到过一个隐蔽的坑:改了.env文件之后,热更新不会自动触发,必须手动停掉DevServer再重新跑npm run dev,这个变量才会重新加载。我自己就因为在环境变量文件里加了新变量但忘了重启,导致后端请求地址一直走的旧值,排查了半天。

4.2 axios 封装:拦截器里只放通用逻辑

请求层一般基于axios封装一个可复用的实例。我们要做的不是把代码写得多花哨,而是保证两点:请求出去时能自动带上token;返回回来时能统一处理错误码。

src/utils/request.ts里一个比较通用的封装形式:

import axios from 'axios' import { ElMessage } from 'element-plus' const service = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 10000 }) // 请求拦截 service.interceptors.request.use( (config) => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }, (error) => Promise.reject(error) ) // 响应拦截 service.interceptors.response.use( (response) => { const res = response.data // 这里根据后端约定的code字段做统一处理 if (res.code !== 0) { ElMessage.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) } return res }, (error) => { ElMessage.error(error.message || '网络异常') return Promise.reject(error) } ) export default service

代码看着简单,但有几个细节要说清楚:

响应拦截器里处理的是后端业务错误,不是HTTP层错误。比如登录过期、无权限、参数校验失败,这些本质上HTTP状态码可能都是200,但业务code是不同的值。我们统一判断res.code,错误提示也在这层一次性做完,业务组件里就不用到处try...catch再弹message了。

请求拦截器里处理的是鉴权信息。token从哪拿、过期了怎么跳登录页,不同项目有差异。我习惯把token放在localStorage,路由守卫里再统一判断有没有token,没有就重定向到/login

为什么不在拦截器里塞一堆和业务强相关的东西?比如某个页面特殊要传额外header,某个接口不需要带token。强行在拦截器里写判断,代码会越来越乱。更好的方式是通过axios配置文件或者请求参数里加一个自定义配置项,比如config.headers.noAuth = true,然后在拦截器里根据这个字段跳过写token的逻辑。

4.3 路由和Pinia的接入顺序

路由的创建在Vue3里很简单,只需要在src/router/index.ts里配置路由表:

import { createRouter, createWebHistory } from 'vue-router' import { useUserStore } from '@/stores/user' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: '/login', name: 'login', component: () => import('@/views/login/index.vue'), meta: { title: '登录' } }, { path: '/', component: () => import('@/layout/index.vue'), redirect: '/dashboard', children: [ { path: 'dashboard', name: 'dashboard', component: () => import('@/views/dashboard/index.vue'), meta: { title: '工作台', requiresAuth: true } } ] } ] })

路由懒加载是必须的,这个很好理解:不懒加载的话,用户首屏进来会把所有页面组件全部拉下来,项目页面一多,首屏JS体积直接爆炸。component: () => import('@/views/xxx/index.vue')会把对应路由的组件拆成单独chunk,只有访问到该路由时才加载。

全局前置守卫的思路也很固定:

router.beforeEach((to) => { const userStore = useUserStore() if (to.meta.requiresAuth && !userStore.token) { return { name: 'login', query: { redirect: to.fullPath } } } return true })

这里有个容易忽略的细节:useUserStore()一定要在router.beforeEach回调函数内部调用,不能在模块顶部调用。因为createPinia的实例还没挂载到app上时,直接调用会报getActivePinia()的错误。这个坑我在第一次写路由守卫时踩过一次,报错信息还不太直观。

Pinia的 store 定义则很贴近Vue3组合式API的风格:

import { ref } from 'vue' import { defineStore } from 'pinia' import { loginApi, getUserInfoApi } from '@/api/user' export const useUserStore = defineStore('user', () => { const token = ref(localStorage.getItem('token') || '') const userInfo = ref<UserInfo>() async function login(loginForm: LoginForm) { const data = await loginApi(loginForm) token.value = data.token localStorage.setItem('token', data.token) } async function getUserInfo() { userInfo.value = await getUserInfoApi() } function logout() { token.value = '' userInfo.value = undefined localStorage.removeItem('token') } return { token, userInfo, login, getUserInfo, logout } })

这种setup写法比旧版options模式直观很多。你把它理解成"把一组响应式数据和操作方法聚在一起,哪个组件要用就去store里拿",基本就抓住了Pinia的本质。

4.4 跑通一个从登录页到列表页的最小闭环

搭完上面这些基础设施,我建议不要马上写业务代码,先按下面这个顺序跑通最小闭环,验证系统各部分真的联通:

  1. 登录页调用loginApi,成功拿到token,存入Pinia和localStorage。
  2. 路由跳转到首页/工作台,触发全局路由守卫,读到token放行。
  3. 列表页在onMounted里调用listApi,请求拦截器自动带上token,后端校验通过返回数据。
  4. 故意让token失效,测试响应拦截器是否正确弹错和跳转。

整个闭环里,前端请求数据的链路是:组件方法 -> API模块 -> axios instance -> 拦截器 -> 后端,返回数据再原路返回。任何一个环节断了,在这个最小闭环里都能立刻暴露出来,好过写了几十个页面之后才回头排查"为什么接口全部401"。

5. 工程化护栏:ESLint、Prettier、husky 一次性配到位

很多人觉得ESLint和Prettier是"浪费时间配置的东西",直到在联调阶段因为代码风格问题被队友连续review打回,才开始重视。在这个部分我会讲清楚create-vue生成的ESLint和Prettier应该如何理解,以及如何用husky把代码检查做成提交前的硬性门槛。

5.1 ESLint 9 的扁平化配置,与传统eslintrc的区别

create-vue现在生成的ESLint配置已经是ESLint 9的扁平化格式,在eslint.config.ts文件里定义,不再是老的.eslintrc.cjs。这一点要注意,网上很多教程还是旧的extends写法,直接复制过来大概率不生效。

Vue3工程的eslint.config.ts主要做的事情可以理解为三部分:加上对JS、TS、Vue文件的默认规则集,加上对Vue单文件组件的代码规范解析,加上TypeScript的类型感知规则。

我用create-vue生成的默认配置就够用,不用自己去网上找一堆"最佳实践"配置堆上去。默认配置里已经包含了 vue 官方推荐的 essential 级别规则,再配合Prettier把格式类问题收走,基本就覆盖了日常需求。

有的团队喜欢再加一些高阶规则,比如禁止any、强制导入顺序、强制组件的name和文件名一致等,这些可以等团队形成规范后慢慢加。一开始就把规则拉满,会让新同学特别难受,容易产生抵触情绪。

5.2 Prettier 配置:依赖默认值也是一种选择

Prettier的理念是"有主见但无需讨论的代码格式化工具"。很多团队为了守住自己的代码风格,会在.prettierrc.json里配一堆选项,比如semi: falsesingleQuote: trueprintWidth: 100之类。

我的建议是:不要为了"顺手"去改这些配置,如果你没有特别强烈的审美偏好,直接保持create-vue默认生成的Prettier配置就很好。默认值本身经过了大部分项目的验证,一致性才是格式化的真正价值,具体是双引号还是单引号,并没有那么重要。

如果配置了"editor.formatOnSave": true和默认格式化工具指向Prettier,那么每次保存文件,代码自动格式化,团队所有成员风格一致:

{ "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true }

5.3 husky + lint-staged:把检查卡在提交之前

光有ESLint和Prettier还不够,因为开发者可能不在编辑器里开保存自动格式化,直接git commit就把代码推上去了。所以要在提交之前加一道关卡。

新版husky的初始化比旧版简单很多:

npx husky init

这个命令会在项目根目录生成.husky/文件夹,并注册pre-commit钩子。然后安装并配置lint-staged,让它只对暂存区的文件跑检查,而不是每次commit全量检查整个项目,后者在大型工程里慢到让人崩溃。

package.json里添加配置:

{ "lint-staged": { "*.{ts,tsx,vue}": ["eslint --fix", "prettier --write"], "*.{js,jsx,json,md}": ["prettier --write"] } }

.husky/pre-commit文件里写上:

npx lint-staged

从此每次git commit,只会针对本次改动的文件做格式化和代码检查,有问题就拦截在提交前,明显减少"CI跑了十分钟发现ESLint报错"的尴尬。

注意:如果团队里有人用Windows开发,建议在提交信息里让lint-staged处理换行符问题,不要把Windows默认的CRLF带进仓库。通常Prettier格式化后默认统一为LF,能省去很多文件被误判为更名/删除的烦恼。

有一个我实际碰到的坑:在某个项目初始化husky时,.husky/pre-commit没有执行权限,导致提交不触发检查。macOS/Linux下可以给钩子加执行权限:

chmod +x .husky/pre-commit

这个问题在Windows下反而不容易出现,但在Linux服务器集中部署CI时特别常见。

6. 部署上线:nginx 配置里的三个关键点,以及 build 前的检查

本地开发跑通、代码规范也都配置好之后,最终目标是上线到服务器。Vue3工程构建出来的dist目录就是所有静态资源,部署到nginx上比较简单,但有不少细节会影响成败,尤其是前端路由、静态资源路径、接口反向代理这几块。

6.1 构建配置:base路径和路由模式要一起考虑

默认情况下,npm run build生成的静态资源都是绝对路径,比如/assets/index-xxxx.js。如果应用部署在域名根路径下,没问题;但如果部署在子路径下,比如https://example.com/admin/,就会出现找不到资源的问题。

两种处置办法:

  • 方法一:保持相对路径,修改vite.config.ts里的base: './',构建产物里的资源路径会变成相对路径,对部署在任意子路径都更友好。
  • 方法二:配合部署的实际路径,用nginxlocation /admin/做映射,并设置base: '/admin/'

我更推荐第二个方法,因为静态资源全用相对路径有时会遇到二级路由刷新后资源404的问题。把实际部署路径明确告诉Vite,让它按绝对路径生成资源,配合nginx,逻辑更清晰。

路由模式也需要一起决策。Vue Router默认的history模式(createWebHistory)没有URL中的#,但要求服务器对所有前端路由都做回退到index.html的处理;如果用了hash模式(createWebHashHistory),服务器压力小一些,URL里带#会让后端获取URL参数时变得麻烦。后台管理系统如果更看重美观和标准URL,建议用history模式并配好nginx回退。

6.2 nginx 配置的通用写法与每一个指令的作用

我给出一个生产环境常用的nginx站点配置,直接把注释写在指令边上:

server { listen 80; server_name example.com; gzip on; gzip_min_length 1k; gzip_comp_level 5; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss image/svg+xml; location / { root /var/www/admin/dist; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location ~* \.(js|css|png|jpg|jpeg|gif|svg|woff2?)$ { root /var/www/admin/dist; expires 30d; add_header Cache-Control "public, no-transform"; } }

逐条解释为什么这么配:

  • try_files $uri $uri/ /index.html;是history模式路由能正常刷新的关键。用户直接访问https://example.com/dashboard或者刷新页面时,nginx首先尝试找$uri这个文件,如果不存在就尝试作为目录,都找不到就回退到/index.html,由前端路由接管。
  • /api/做反向代理:前端代码里的请求地址直接写/api/...,nginx把请求转发到后端的http://127.0.0.1:8080,这样解决了跨域问题。注意proxy_pass如果写成http://127.0.0.1:8080/(带斜杠),会把/api前缀去掉再转发;如果不带斜杠,则保留完整路径。这个细节极容易踩坑,很多人发现部署后接口404,多半就是这里的问题。
  • 静态资源做缓存:JS、CSS、图片这类带哈希指纹的文件,直接设置过期时间,浏览器第二次访问时命中缓存,明显提升加载速度。no-transform是防止CDN或代理擅自压缩、转化静态资源。

6.3 build 前的检查清单

部署前花10分钟检查这几项,能避免不少上线事故:

  1. vite.config.tsbase是否匹配实际部署路径。
  2. .env.production中的VITE_API_BASE_URL是否正确指向生产环境后端。
  3. 后端接口是否已经允许生产环境域名跨域访问(如果走nginx反代则通常不需要)。
  4. 控制台是否残留console.log或者debugger调试代码(生产构建时部分项目会用插件删除)。
  5. 本地执行npm run build,确认构建产物正常生成,且没有TS类型报错或ESLint错误。
  6. dist目录传到服务器,先在服务器本机访问http://127.0.0.1验证服务和静态资源路径。

把整个dist目录直接放到nginx配置的root路径下,然后执行nginx -t检查配置语法,通过后nginx -s reload让配置生效。如果浏览器打开还是404,大概率是try_files没生效或者nginx进程没有reload成功。

7. 我在实际创建和开发Vue3工程过程中踩过的几个高频坑

这部分更像是闲聊,但遇到的人不少。我捡三四个最常见的展开,给你提前预警。

7.1npm create vue@latest命令在旧终端里选不了选项

如果你用Windows自带的cmd窗口跑交互式命令,方向键上下选择时可能错乱。这不是create-vue的问题,是cmd对ANSI转义序列支持不好。解决方式很简单:用PowerShell、Windows Terminal或者VS Code集成终端来跑命令。macOS用户直接用系统自带Terminal或者iTerm2都没问题。

7.2 修改了vite.config.ts之后热更新不生效

Vite对vite.config.ts的修改会自动重启服务,但偶尔有缓存残留的情况。比如改完别名或代理配置后,DevServer没有真正重启成功。遇见的处理方式通常是Ctrl+C停掉服务,再重新npm run dev。不要在那儿等它自己刷新,大概率等不到。

7.3 create-vue 生成的文件里,.vscode/extensions.json的作用

编辑器扩展统一,在团队协作里很重要。create-vue 会自动生成.vscode/extensions.json,里面声明了Vue官方推荐的扩展,比如Vue.volardbaeumer.vscode-eslintesbenp.prettier-vscode。VS Code打开项目时会弹出"是否安装推荐的扩展",建议全部安装。特别是Vue开发,Volar是新的Vue3语言工具,不是原来Vue2时代的Vetur。没有装Volar,Vue3单文件组件的类型推导、模板表达式补全基本等于摆设。

7.4 关于Vue3嵌套iframe影响外层点击事件,一个老生常谈的坑

有时项目里需要嵌入第三方页面,比如地图、报表、可视化面板,会用到iframe。iframe天然是独立的文档上下文,外部页面无法直接捕获iframe内部的事件。但更严重的问题是:鼠标在iframe区域内交互时,外层容器的click等事件完全接收不到。

我的处理思路是:如果能控制被嵌入的iframe页面,用postMessage双向通信,跨域也能用;如果不能控制,就在iframe外层加一个透明的遮罩层,通过遮罩层捕获鼠标事件再手动触发外部交互。这个方法在业务中很常见,并不管iframe内部是什么内容。

7.5 Vue3 + 若依/JeecgBoot 这类低代码平台迁移时的TS报错

后端框架通常配套了前端工程,典型的有若依的RuoYi-Vue3和JeecgBoot的Vue3版本。遇到迁移或者从旧版升级到新版Vue3 + TS的时候,报错集中在组件参数类型不一致、Ref解包后类型不匹配、第三方UI组件缺少类型声明这几类。处理顺序一般是:先升级依赖版本统一,再补全局类型声明文件,最后逐个解决报错。如果还在用defineComponent扩组件并出现defineComponent is not defined,通常是语法解析配置没跟上,检查vite.config.ts中的@vitejs/plugin-vue和TSConfig的jsx配置。

这些坑在我刚切Vue3的那一两年里基本都遇到过,有的花了大半天才排查出来。希望你在动手创建Vue3工程之前先看到这些,能少走不少弯路。

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

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

立即咨询