Vite打包Vue项目部署到Nginx:从配置到避坑全指南
2026/9/9 19:25:19 网站建设 项目流程

项目开发完只是第一步,让它跑在公网上被别人正常访问,才算真正交付。我见过不少前端同学,本地npm run dev熟练得很,一到打包部署就发怵,甚至有人直接把 dist 文件夹拖到服务器上就完事,结果各种 404、白屏、接口跨域问题接踵而至。Vite 作为 Vue 项目的新一代构建工具,打包速度和生产构建体验确实比 Webpack 时代舒服太多,但如果你没搞清楚 Vite 的产物结构和 Nginx 的配置逻辑,部署这关照样会卡得你怀疑人生。这篇文章我把 Vite 打包 Vue 项目到 Nginx 这件事拆开揉碎,从方案选型到配置细节,从打包优化到问题排查,一条龙讲透,适合刚接触部署、或者部署过但老出问题的朋友照着操作。

1. 部署方案选型:为什么是 Vite + Vue + Nginx 这套组合

1.1 三件套各自解决什么问题

先理清楚这三个角色各管哪一段。Vue 是你用的前端框架,负责页面的组件化开发;Vite 是构建工具,负责把.vue单文件组件、ES Module、TypeScript、SCSS 这些东西编译成浏览器能直接识别的静态文件;Nginx 是高性能 Web 服务器,负责把编译后的静态文件通过 HTTP 协议吐给用户浏览器,同时还能做反向代理、负载均衡、Gzip 压缩、缓存控制这些脏活累活。

这套组合之所以主流,是因为每一个环节都踩在了点上。Vite 开发服务器基于原生 ESM,冷启动速度比 Webpack 快一个量级,热更新也是毫秒级响应,开发体验非常舒服;生产构建底层用的是 Rollup,产物干净、Tree Shaking 彻底,打包出来的 JS 文件体积在同配置下通常比 Webpack 小。而 Nginx 作为静态文件服务器,性能极其强劲,单机处理几万并发连接毫无压力,配置文件可读性也高,不像 Apache 那一堆复杂的指令让人头晕。

1.2 部署方案对比:为什么不直接用 Node 托管或纯静态服务器

你可能会问:我能不能不装 Nginx,直接在服务器上跑个node server.js,或者用 Python 的http.server来托管 dist 文件夹?能,但都有明显短板。用 Node 原生写静态服务器,你需要自己处理 MIME 类型、Gzip、缓存头、history 路由回退,这些 Nginx 一个配置块就搞定的事,用 Node 手写既重复又容易漏。用python3 -m http.server这类玩具级方案,就只能做最基本的文件传输,生产环境完全不够看,尤其不支持配置 history 路由回退,Vue Router 一用 history 模式刷新就 404。

相比之下 Nginx 的优势非常明确:

  • 轻量高效,资源占用极低,一个默认配置的 Nginx 进程内存占用不到 10MB
  • 静态文件服务、反向代理、负载均衡、SSL 终止一气呵成
  • 配置语法简单,改完nginx -s reload即可生效,不需要重启操作系统

另一个常见的替代方案是 Docker 部署,把 Nginx 打成镜像跑在容器里,适合需要标准化交付、多环境迁移的团队。但 Docker 本质上也还是 Nginx,只不过多了一层容器封装。我个人的建议是:如果你只是一个人维护一个小项目,先把裸机部署跑通,理解清楚静态文件、反向代理、缓存这几个核心概念,再去碰 Docker、Jenkins 那些自动化东西,会顺畅得多。

2. 环境准备与 Vite 打包核心配置

2.1 服务器安装 Nginx,搞懂目录结构

部署的前提是服务器上得有 Nginx。以 Linux 服务器为例,Ubuntu 和 Debian 系可以直接用:

sudo apt update sudo apt install nginx -y

CentOS、Rocky Linux 这类 RedHat 系用:

sudo yum install nginx -y

安装完成后,Nginx 的几个关键目录你一定要心里有数:

路径作用
/etc/nginx/nginx.conf主配置文件,全局配置,一般不建议直接改这里
/etc/nginx/conf.d/自定义站点配置目录,每个站点一个.conf文件,推荐在这里写
/usr/share/nginx/html/默认站点根目录,刚装完时里面的 index.html 就是欢迎页
/var/log/nginx/access.log访问日志,谁请求了什么资源都记录在这里
/var/log/nginx/error.log错误日志,出问题第一个要看的地方

启动和检查状态:

sudo systemctl start nginx sudo systemctl status nginx

改完配置以后一定要用nginx -t检查语法,看到syntax is oknginx -s reload,千万不要改完直接 reload,语法错了会直接把服务搞挂。

2.2 Vite 打包的三大关键配置

Vite 项目打包,核心配置都在项目根目录的vite.config.js里。很多新手把npm run build一跑,dist 出来就直接丢服务器,结果样式找不到、图片裂了、路由刷新白屏,问题都出在这几个配置上没搞对。

第一个是base。这是 Vite 生成资源路径的基准路径,默认是/。如果你的站点部署在域名根路径(比如https://example.com/),用/没问题。但如果你要部署到子路径(比如https://example.com/admin/),就必须把base改成/admin/,否则打包出来的 HTML 里引用的 JS、CSS 路径全都带/assets/...前缀,浏览器去域名根目录找资源,自然 404。这个坑我见过太多次。

第二个是build配置块。outDir控制产物输出目录,默认是dist,一般不用改。assetsDir控制静态资源子目录,默认assetschunkSizeWarningLimit是 chunk 大小警告阈值,默认 500KB,项目引了 Element Plus、ECharts 之类的库很容易触发警告,可以调到 1500 消除告警,但注意这只是一个提示,不影响构建结果。

第三个是路由模式引发的连锁反应。Vue Router 有两种模式:createWebHistory()的 history 模式和createWebHashHistory()的 hash 模式。hash 模式地址栏会有个#,比如example.com/#/about,这种模式下 Nginx 不需要额外配置,因为#后面的内容不会被发到服务器。history 模式地址美观,example.com/about,但服务器必须配置try_files $uri $uri/ /index.html;做回退,否则用户在某一个子路径刷新页面就 404。这两者的取舍要在项目开发初期就定好,部署配置完全不一样。

一个比较典型的vite.config.js

import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ base: '/', plugins: [vue()], build: { outDir: 'dist', assetsDir: 'assets', chunkSizeWarningLimit: 1500, rollupOptions: { output: { manualChunks: { 'vue-vendor': ['vue', 'vue-router', 'pinia'], 'ui-vendor': ['element-plus'] } } } }, server: { port: 3000, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })

注意server.proxy只对开发环境有效,它解决的是本地开发时前端调接口的跨域问题。生产环境这套代理根本不会生效,接口转发必须靠 Nginx,这是很多新人容易误解的地方。

3. Nginx 部署配置实战

3.1 最基础的静态站点配置

假设打包产物在项目根目录的dist文件夹里,你把它整个上传到服务器/usr/share/nginx/html/下。最简单的 Nginx 配置长这样:

server { listen 80; server_name example.com; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } }

这里的root指向站点根目录,index是默认首页文件。try_files那一行是 history 路由的核心:当一个请求进来,Nginx 先去找这个 URI 对应的文件,找不到就找同名的目录,再找不到就回退到/index.html。这样 Vue Router 才能接管前端路由,刷新example.com/about时不会返回 404。

上传文件的方式我习惯用scp,本地终端直接跑:

scp -r dist/* user@你的服务器IP:/usr/share/nginx/html/

如果你用的是宝塔面板之类的图形化工具,把 dist 内容上传到对应目录也行,本质都是一样的。

3.2 history 路由模式下的 try_files 回退

这里需要展开一下try_files的执行逻辑,它是 Nginx 里比较容易让人犯晕的指令。

try_files $uri $uri/ /index.html;由三个参数组成,Nginx 会按顺序检查:

  • $uri:当前的请求 URI,比如/about,Nginx 会在 root 目录里找有没有about这个文件
  • $uri/:如果$uri不是文件,再看是不是目录,是就返回目录索引
  • /index.html:前两个都找不到,就把请求重写到/index.html,重新走一遍静态文件查找

这个机制对前端单页应用来说就是保命条款:无论浏览器请求什么路径,最终都会落到index.html,然后 Vue 的 JS 读地址栏、渲染对应路由组件。如果没有这行配置,用户在/about刷新,Nginx 去找/about文件找不到,就直接 404 了。

有几种情况需要调整。如果你部署在子路径,比如base: '/admin/',那么root应该指向 dist 内容的上一级目录,而不是直接指向 dist,同时try_files的路径也要做适配:

location /admin/ { alias /usr/share/nginx/html/; try_files $uri $uri/ /admin/index.html; }

这个场景相对少见,但一旦碰到,对路径的理解要求会高不少。建议刚开始部署的朋友都用根路径部署,把这个基础流程跑通以后再去折腾子路径。

3.3 API 反向代理配置:前端接口跨域的关键

Vue 项目通常需要请求后端接口,比如/api/user/list,后端服务跑在http://localhost:8080。如果前端直接发请求给 Nginx 域名的/api/...,Nginx 默认只会去静态目录里找同名文件,找不到就 404。要解决这个问题,必须配置反向代理:

server { listen 80; server_name example.com; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://localhost: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; proxy_set_header X-Forwarded-Proto $scheme; } }

这一段配置的关键点是proxy_pass的写法。当proxy_pass后面不带路径时,请求会原样转发,浏览器请求/api/user/list,Nginx 就转发给后端的/api/user/list。如果后端接口本来就带/api前缀,这样写最简单直接。

但有时候后端接口不带前缀,比如后端路由是/user/list,希望前端请求/api/user/list时,剥掉/api再转发。这时候proxy_pass后面就要加一个斜杠:

location /api/ { proxy_pass http://localhost:8080/; }

带不带结尾斜杠,转发结果完全不同:

proxy_pass 写法前端请求后端实际收到
http://localhost:8080/api/user/list/api/user/list
http://localhost:8080//api/user/list/user/list

我每次写这段都下意识确认一下,这个坑太经典了。另外proxy_set_header X-Forwarded-For这些头信息是用来传递客户端真实 IP 的,后端如果做了日志分析、风控或者限流,这些头必须有,否则后端看到的所有请求都来自 Nginx 的本机 IP,直接炸掉。

3.4 Gzip 压缩与静态资源缓存策略

前面的配置已经把站点跑起来了,接下来是性能优化。Vite 构建出来的 JS、CSS 文件都比较大,如果不做压缩,用户首次访问要下载几百 KB 甚至几 MB 的资源,在弱网环境下体验非常差。Nginx 开启 Gzip 压缩很方便:

gzip on; gzip_min_length 1k; gzip_comp_level 5; gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml; gzip_vary on;

gzip_min_length的意思是小于 1KB 的文件不压缩,因为压缩这类小文件反而有额外开销。gzip_comp_level 5是个平衡点,压缩级别从 1 到 9,9 压得最小但耗 CPU,实际生产用 5 左右就够了。

缓存策略同样重要。Vite 打包后的文件名自带 hash,比如assets/index-7f9c4e2b.js,这个 hash 是内容级别的,代码一改 hash 就变。对这类带 hash 的静态资源,可以设置长期缓存,浏览器下次访问直接走本地缓存,不打服务器:

location /assets/ { expires 365d; add_header Cache-Control "public, immutable"; }

但是index.html绝对不能这样设置。它是页面入口,每一次发布都要保证用户能拿到最新的。给index.html配置 no-cache:

location = /index.html { add_header Cache-Control "no-cache, no-store, must-revalidate"; }

这一对配置组合起来,即资源强缓存、入口不缓存,就能兼顾部署更新和访问速度。如果不设置,浏览器可能把旧的index.html缓存住,你更新了代码用户却还在跑旧页面,排查起来非常费劲。

4. 部署过程中的常见问题排查实录

4.1 页面刷新 404 或白屏

部署完以后访问首页正常,但路由跳转某个子页面后一刷新就 404,或者直接白屏,这是 history 路由模式最典型的问题。原因非常简单:刷新时浏览器向服务器发请求,请求路径是/about,Nginx 在静态目录里找不到about文件,就直接返回 404 了。只要没有配置try_files $uri $uri/ /index.html;,这个 404 就会存在。

解决方法就是前面说的回退配置。另外一个容易忽略的点是,如果你用 hash 模式,这个问题天然不存在。但 hash 模式的 URL 不好看,我建议正经项目直接用 history 模式,然后配好 try_files。

白屏还有一种情况:刷新后页面渲染出来了,但 JS 报错,比如Uncaught SyntaxError: Unexpected token '<'。这个通常是服务器对 JS 请求返回了 HTML 导致的,常见于try_files配错,JS 文件被错误回退到了 index.html。检查一下location /assets/有没有单独处理,以及在浏览器 Network 面板看 JS 的响应体。

我实测下来的排查顺序是:先看 Network 面板确认静态资源是否 404,再看 Console 报错,最后看 Nginx 的error.log。多数前端部署问题,三步以内就能定位。

4.2 静态资源 404:base 路径的坑

如果页面能打开,但 CSS、JS 全部加载失败,控制台一堆 404,先去看 HTML 里的资源引用路径。打开浏览器按 F12,在 Elements 面板看<script>标签的src是什么。

正常的应该是/assets/index-xxx.js。如果看到/assets/index-xxx.js带上了一层奇怪的子路径,或者请求打了别的站点,那多半是vite.config.js里的base配置跟你实际部署的路径不一致。

具体来说:

  • 项目部署在https://example.com/base设置成/,正确
  • 项目部署在https://example.com/admin/base设置成/admin/,正确
  • 项目部署在https://example.com/admin/base还是默认的/,错误,资源全部从根目录找,404
  • 项目部署在https://example.com/base设置成./,大部分场景能用,但要注意带嵌套路由时可能出问题

base改成相对路径./是不少人用来绕坑的方法,因为这样打包出来的资源路径是相对 HTML 文件的。但对于深层路由页面刷新这种情况,相对路径可能会指错位置,所以我一般不推荐,还是老老实实配绝对路径的base最稳。

4.3 接口跨域与代理失效

前端页面正常出来了,但所有请求都报跨域错误Access-Control-Allow-Origin,或者请求直接 404。前者通常是没有配置proxy_pass,或者后端服务没有开 CORS;后者多半是proxy_pass路径拼接不对,走了/api前缀,后端实际不认这个路由。

如果后端还没加 CORS,优先用 Nginx 代理方案解决,也就是我前面写的配置。后端服务不需要感知前端的域名,所有请求都走同源,从根源上避免跨域问题。修改了 Nginx 配置后记得先nginx -t再 reload,我用过一次nginx -s reload之前忘了检查语法,结果配置文件里少了一个分号,整套服务直接崩了。

4.4 代码更新了但用户访问还是旧版

这个问题大概率是缓存惹的祸。用户浏览器缓存了旧的index.html,而新的静态资源文件名虽然变了,但 HTML 不会主动去拉取新版本。所以在生产环境,index.html必须配置禁用缓存或强校验缓存,带 hash 的资源则放心地设置长期缓存。

如果已经改了配置但用户还是看到旧版,可以让用户强制刷新试试(Ctrl+Shift+R),或者清除浏览器缓存。更彻底的做法是在 Nginx 配置里对index.html设置Cache-Control: no-cache,这样浏览器每次都会去服务器验证一下文件有没有变化,有变化就拉新,没变化就用缓存,兼顾速度和更新。

另外还有个隐藏点:静态资源长期缓存是建立在文件名带 hash 的前提下的。如果你用的是自定义的rollupOptions输出固定文件名,比如output: { entryFileNames: 'main.js' },那每次发布同名文件就会被浏览器强缓存劫持,这是个大坑,建议避免。

4.5 打包内存溢出和 Vite 热更新失效

打包过程中偶尔会遇到FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory,尤其项目较大、依赖较多时。这是 Node.js 默认堆内存不够用了。解决方案是调整 Node 内存上限:

export NODE_OPTIONS=--max-old-space-size=4096 npm run build

或者更直接一点,把启动脚本改成:

{ "scripts": { "build": "node --max-old-space-size=4096 node_modules/vite/bin/vite.js build" } }

这里 4096 表示 4GB,具体数值根据服务器内存调整,别调到比服务器物理内存大,否则 OOM Killer 会把进程杀掉,构建照样失败。

Vite 热更新失效的问题,如果你遇到改 vue 文件后浏览器不刷新,大概率是缓存坏了。删掉项目根目录下的node_modules/.vite再重启开发服务器,90% 的情况都能恢复。如果还不行,检查一下你的文件系统是否被某些同步工具干扰,以及系统文件监听上限,macOS 和 Linux 上文件监听数超过限制也会导致热更新失效。

4.6 常见问题速查表

问题现象最可能原因解决方向
刷新子路径 404缺少 try_files 回退配置location / { try_files $uri $uri/ /index.html; }
页面打开但没有样式base 路径不对导致 CSS 404检查并修正 Vite base 配置,重新构建
接口跨域没有配置反向代理在 Nginx 中配置proxy_pass
接口代理 404proxy_pass 结尾斜杠处理不当根据后端实际路由调整 proxy_pass 是否带/
用户看到旧版本index.html 被缓存对 index.html 设置 no-cache
构建内存溢出Node 堆内存不足配置--max-old-space-size或 NODE_OPTIONS
热更新失效Vite 缓存损坏删除node_modules/.vite重启开发服务
Nginx 配置语法错误配置文件有错误nginx -t检查,修正后 reload

5. 部署上线后的几点个人经验

部署这套东西,一次成功的背后必须有系统性思维。我的经验是先本地验证再上服务器。打包完成后,先用npx serve dist在本地起一个静态服务看一眼,确认构建产物没问题了再往服务器传。很多人在服务器上抓瞎半天,最后才发现是本地打包就有问题,白白浪费通信时间。

正式上线前一定要检查 Nginx 的 error log。配置没问题不代表运行没问题,权限、磁盘、端口占用这些系统层面的问题才是真杀手。比如我遇到过403 Forbidden,排查到头是 Nginx 的user没有读取站点目录的权限,改一下目录权限就解决。

还有一个小技巧,如果你在 Nginx 的location块里写了代理,但又不确定代理是否生效,可以直接在服务器上用curl http://localhost:8080/api/test测试后端接口是否通,再curl -H "Host: example.com" http://localhost/api/test测试 Nginx 转发是否通,两步就能把问题圈定在具体环节。

关于自动化部署,很多团队后面会引入 Jenkins、GitHub Actions 之类的 CI/CD,但不管工具多花哨,核心链路都是一样的:拉代码 → 安装依赖 → 构建 → 上传产物 → reload Nginx。把裸机部署流程吃透,自动化只是把这几步串起来的体力活。刚接触的朋友不用急着上自动化,先手动部署成功两次,理解每一行的意义,比盲目抄一个流水线配置有价值得多。

最后再分享一个实际操作中的体会:Nginx 配置文件和代码一样,也要做版本管理。把conf.d下面的配置文件存进 Git,每次修改都留痕,出问题能回滚。虽然听起来有点小题大做,但当你被一个隐藏的配置问题折磨到半夜的时候,就知道版本化配置有多香了。

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

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

立即咨询