前端项目部署到阿里云服务器:nginx、PM2与history路由配置全流程 部署这件事说难不难说简单也不简单。尤其是现在vue、react这类单页应用SPA已经成为前端主流的背景下很多同学本地npm run dev跑得飞起接口通了、样式正常了、交互也没毛病可一到“上服务器”这一步就开始手足无措不知道代码怎么传上去、不知道nginx该怎么配、不知道为什么页面刷新就404、更搞不清楚pm2到底是干什么的。我见过太多项目在本地跑得好好的上了服务器之后被各种问题折磨到怀疑人生。这篇文章我就从自己多年部署前端项目的实操经验出发把一套完整的“vue / react / web前端项目部署到阿里云服务器”流程整理出来。内容会覆盖部署前的准备、服务器环境搭建、nginx配置、pm2管理Node服务、常见问题排查等等既适合第一次接触部署的前端新人也能给已经会部署但想系统梳理一遍的工程师做参考。我会尽量把每一个步骤背后“为什么要这么做”也讲清楚而不只是丢给你一堆命令。1. 部署这件事先想清楚再动手很多人一上来就问“nginx怎么配”“pm2怎么启动”但我建议先别急着操作。部署其实是一个系统工程你得先搞清楚自己项目属于哪种类型因为不同类型的项目上线方式差别非常大。1.1 静态部署与Node服务两种场景别混为一谈前端项目部署通常分两种情况第一种是纯静态托管。vue和react项目执行build之后会生成一堆静态文件HTML、CSS、JS、图片这些文件不需要服务器端做任何计算只需要一个web服务器把它们“托管”出来用户访问时把文件返回给浏览器即可。这种情况用nginx就足够了配置简单性能也极好。第二种是带有Node服务的项目。这类项目可能是SSR服务端渲染比如Nuxt、Next.js也可能是前端工程里内置了一个Node中间层服务器用来做接口代理、SSR渲染或者Mock服务还有可能是纯粹的Node后端API服务前端只是作为一个静态站点同时部署在上面。这种情况下你必须保证Node进程能在服务器上长时间运行而且即使崩溃了也要自动重启这时候就轮到pm2登场了。你可以把nginx理解成前台接待负责把访问者引导到正确的房间而pm2像一个保姆专门负责看好Node进程这个“孩子”哭了要哄、病了要治、断电了要让他自己爬起来。1.2 部署前必须想清楚的几件事在动手之前先回答下面几个问题能省掉你后面80%的麻烦项目到底是纯静态还是带Node服务这决定了你后续的整套部署方案。接口地址是走同一个域名下的反向代理还是直接请求另一个域名的API这关系到nginx的location配置。路由模式是history还是hash如果是history刷新页面会404nginx必须配try_files兜底。有没有使用环境变量本地开发用的环境变量和线上环境的值很可能不一样打包时是否注入了正确的值服务器上准备用哪个端口80端口要留给nginxNode服务习惯上用3000、8080或者其他自定义端口。这些问题在本地开发时通常不会暴露因为本地有webpack-dev-server帮我们处理了路由回退、接口代理这些问题但到了服务器上一切都是原始状态任何一个环节没考虑到都会导致上线后白屏或者接口报错。1.3 目录与端口规划建议提前定好标准我个人的习惯是不管项目大小在服务器上都遵循一套固定的目录结构。这样做的好处是换服务器、换项目、交接给同事的时候大家都能很快找到东西。/var/www/ my-project/ # 每个项目一个独立目录 dist/ # 前端构建产物vue/react build后生成 server/ # Node服务代码如果有 logs/ # 运行日志 ecosystem.config.js # pm2配置文件端口规划上我一般把80端口留给nginx做入口Node服务统一使用3000以上的端口并且在一个项目一个端口的原则上做隔离。这样nginx反向代理的时候指向非常明确不会出现端口冲突这种糟心事。2. 项目改造部署前的准备都在这很多同学部署失败的根源不在服务器而在项目本身。本地开发环境和线上环境差异巨大先把项目调整好部署才会顺利。2.1 构建配置publicPath、路由模式、环境变量先说publicPath。vue项目在vue.config.js里react项目在vite.config.ts或webpack.config.js里都能配置。默认情况下构建出的静态资源路径是/开头如果你的页面是部署在域名根路径下这没有任何问题。但如果部署在子路径下比如https://example.com/my-app/就必须把publicPath设置成相对路径或者/my-app/否则所有JS、CSS都会去域名根路径下找结果当然是404。再说路由。vue-router和react-router都支持history和hash两种模式。hash模式会在URL里带#虽然丑但好处是刷新不会404history模式的URL干净美观但要求服务器必须把所有路由都指向index.html。部署到线上的项目如果不是特殊原因建议用history模式然后通过在nginx里加一行try_files来解决刷新404的问题。这个配置后面会详细说。环境变量是另一个大坑。很多人项目里写着const API_BASE process.env.VUE_APP_API_URL或者const API_BASE import.meta.env.VITE_API_BASE本地开发时.env.development文件里写的是http://localhost:8080到了线上环境如果没有对应的.env.production文件或者构建机器上没有设置对应的环境变量最终打包出来的代码里接口地址还是本地的localhost那用户访问时自然会全部请求失败。所以部署前一定要到构建产物里搜一下localhost或者127.0.0.1这种字样确认环境变量被正确替换了。2.2 本地先跑一遍“模拟上线”在把代码传到服务器之前我强烈建议先在本地模拟一遍生产环境。方法很简单本地执行构建然后用一个静态服务器把dist目录跑起来打开页面点点看。npm run build npx serve dist这里有个容易踩的坑如果你用serve这类工具跑history路由会正常回退但如果你用python -m http.server这种纯静态服务器跑刷新页面就会404。这一点恰恰能帮你提前暴露路由问题如果刷新404了说明你得先检查nginx的try_files。另外一个值得做的检查是直接打开构建产物里的index.html看源代码里引用的JS/CSS路径是否是绝对路径/assets/xxx.js。如果你的项目部署在子路径下这里就应该改成相对路径或者带前缀的路径。这一步检查虽然枯燥但能提前拦截掉一大批“部署后白屏”的问题。3. 服务器环境搭建nginx、PM2、Node.js前面准备工作做得差不多了现在开始真正动手操作服务器。以阿里云服务器为例整个环境搭建流程基本是固定的。3.1 购买服务器后的第一件事当你从阿里云控制台买到一台新的ECS服务器时系统会给你分配一个公网IP和一个默认的root账号密码。第一件事是登录服务器把系统更新一下顺便看看基本环境。ssh root你的服务器IP apt update apt upgrade -y # Ubuntu/Debian系 # 或者 yum update -y # CentOS系注意阿里云服务器默认的安全组规则通常只开放了22端口SSH。你后续要用80端口跑nginx、用3000端口跑Node服务必须先在阿里云控制台的安全组里添加入方向规则把这些端口放行否则即使服务器内部服务正常运行外部也无法访问。有一个细节我经常提醒别人安全组入方向放行80端口时授权对象一般写0.0.0.0/0。如果你只想让特定IP访问可以填具体IP段但对于Web服务来说通常必须对所有IP开放否则用户没法访问。3.2 安装Node.js与PM2前端部署Node环境我建议直接使用nvm来管理Node版本。虽然服务器上不像本地开发那样频繁切换版本但nvm的好处是安装方便而且能避免系统自带的Node版本过旧的问题。# 安装nvm curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash source ~/.bashrc # 安装Node.js LTS版本 nvm install 18 nvm alias default 18 # 验证 node -v npm -vNode装好之后直接全局安装pm2。npm install -g pm2 pm2 -v这里说明一下为什么需要pm2而不是直接用node server.js启动因为直接启动的Node进程一旦你关掉SSH连接进程就会被系统杀掉而且如果进程因为异常崩溃了没有人会去把它重新拉起来。pm2能解决这几个核心问题后台运行、崩溃自动重启、日志持久化、开机自启。对于线上Node服务来说pm2不是可选项而是必需品。3.3 安装并配置Nginxnginx在Ubuntu和CentOS下的安装方式略有不同。# Ubuntu/Debian apt install nginx -y # CentOS yum install nginx -y安装完成后先别急着改配置先确认一下它能不能正常启动systemctl start nginx systemctl enable nginx # 设置开机自启 curl http://localhost # 应该有nginx默认页面nginx的配置文件位置在Ubuntu下是/etc/nginx/nginx.conf但真正写站点配置的地方是/etc/nginx/conf.d/或/etc/nginx/sites-available/。我习惯的做法是直接在/etc/nginx/conf.d/下新建一个项目配置文件这样每个项目一套配置互不干扰。后续需要改nginx配置的时候记住一个口诀改完先nginx -t检查语法再systemctl reload nginx重载配置。千万不要直接nginx -s stop再启动那样会造成短暂的访问中断。4. 代码上传从本地到服务器的常规姿势环境搭好了现在把代码弄到服务器上去。这一步的方法很多我推荐两种。4.1 scp还是git我推荐的方式如果你是一个人在维护项目最简单的办法是用scp把构建产物直接传到服务器。本地执行npm run build scp -r dist/* root你的服务器IP:/var/www/my-project/dist/这种方式的优点是快缺点是每次部署都要手动执行而且如果忘了--delete参数服务器上旧的残留文件会和新文件混在一起有时候会出现一些莫名其妙的缓存问题。所以如果你用scp建议配合rsyncrsync -avz --delete dist/ root你的服务器IP:/var/www/my-project/dist/--delete参数会在同步时删除本地不存在的文件确保服务器上的内容和本地构建产物完全一致。如果是团队协作项目或者项目有完整的CI/CD流程更推荐在服务器上直接git clone或git pull代码然后在服务器上执行安装依赖和构建。这样做的好处是代码版本完全可控回滚也方便——只需要git checkout到上一个tag重新构建即可。4.2 进入服务器安装依赖并构建把项目代码放到服务器指定目录后接下来就是安装依赖和构建。这一步建议用npm ci而不是npm install。npm ci会严格按照package-lock.json来安装依赖速度更快而且不会因为依赖版本浮动导致生产环境和本地构建结果不一致。cd /var/www/my-project # 前端项目构建 npm ci npm run build # 如果有Node服务也需要安装依赖并启动 cd server npm ci构建完成之后确认一下dist目录是否生成文件大小和数量是否正常。这一步如果构建失败了大概率是服务器上的Node版本和本地不一致导致的检查一下nvm list有没有切换对版本。5. Nginx核心配置静态托管、history路由、反向代理nginx是整个部署架构里的门面配置是否正确直接决定了用户能不能正常访问你的网站。这里我给出一份完整可用的配置然后逐个拆解关键点。5.1 一个完整可用的server配置在/etc/nginx/conf.d/下新建文件my-project.conf写入以下内容server { listen 80; server_name your-domain.com; gzip on; gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svgxml; gzip_min_length 1k; root /var/www/my-project/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:3000; 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; } location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?|ttf|eot)$ { expires 30d; add_header Cache-Control public, immutable; access_log off; } }这份配置基本覆盖了静态站点最常见的需求静态资源托管、history路由回退、接口反向代理、静态缓存优化。5.2 try_files与location /api/细节try_files $uri $uri/ /index.html;这行是整个history路由配置的灵魂。它的意思是当一个请求进来先尝试直接访问这个URI对应的文件如果文件不存在尝试访问这个URI对应的目录如果目录也不存在就返回index.html。为什么这样能解决刷新404因为前端路由跳转是在浏览器端完成的刷新时浏览器会向服务器请求当前的完整路径比如/user/detail。服务器上根本没有user/detail这个文件如果不做处理就会返回404。有了try_files这一兜底所有不存在的路径都会返回index.html浏览器加载index.html之后再执行JS根据当前URL重新渲染对应页面。这就是history路由在nginx下的标准解法。再看location /api/反代。前端项目里接口请求通常是/api/login、/api/user/list这种形式。nginx把这部分请求转发给本机的Node服务去处理Node服务跑在3000端口。注意这里的proxy_pass结尾写的是http://127.0.0.1:3000;没有带路径。这里有个很多人搞不懂的知识点如果proxy_pass的URL不带路径那么nginx会把原始请求的完整路径原样转发。也就是说前端请求/api/login后端收到的也是/api/login。如果proxy_pass后面带了路径比如http://127.0.0.1:3000/那么nginx会用location匹配到的部分替换掉前缀。这个细节非常容易踩坑一旦写错后台接口会全部404而且问题很难排查。5.3 配置校验与优雅重启每次改完nginx配置执行以下命令nginx -t如果输出syntax is ok和test is successful说明配置没问题然后systemctl reload nginxreload是平滑重载不会中断正在进行的请求。我见过不少人改完配置直接systemctl restart nginx其实没必要restart会造成一瞬间的连接中断对于线上服务来说能避免尽量避免。如果你配置了域名别忘了解析到服务器IP然后在服务器上用curl测试一下是否能正常访问curl -I http://your-domain.com返回200 OK就说明静态站点已经通了。6. PM2管理Node进程从启动到守护如果你的项目像1.1里说的那种情况需要Node服务常驻那pm2这一步逃不掉。6.1 为什么前端项目也需要pm2有人会问“我这就是个vue项目构建完是纯静态文件nginx托管一下就行用得着pm2吗”正常情况下确实用不着。但当你有后台Node服务——无论是SSR、接口中间层还是独立的API服务——pm2就是刚需。举例来说如果你的项目里有个server.js它负责给前端提供接口用node server.js启动后测试一下接口正常但一关SSH窗口服务就断了。原因是这个进程是当前SSH会话的子进程会话关闭系统会把子进程一并清理掉。就算你改成后台启动nohup node server.js 进程崩溃了还得手动去拉起非常不优雅。pm2解决的就是这些事。它是Node进程管理器负责守护你的进程让你可以做到后台运行不依赖SSH会话进程崩溃自动重启统一管理日志输出设置开机自启多实例负载均衡6.2 ecosystem.config.js一份完整可用的配置在项目根目录建一个ecosystem.config.js把Node服务的启动参数集中管理起来module.exports { apps: [ { name: my-project-server, script: ./server/index.js, instances: 1, autorestart: true, watch: false, max_memory_restart: 1G, env: { NODE_ENV: production, PORT: 3000 }, error_file: /var/www/my-project/logs/err.log, out_file: /var/www/my-project/logs/out.log, log_date_format: YYYY-MM-DD HH:mm:ss } ] };然后通过pm2启动cd /var/www/my-project pm2 start ecosystem.config.js pm2 save这里几个配置项解释一下instances: 1表示单实例。如果你的服务器CPU核数多可以设置成maxpm2会按CPU核数开启集群模式。max_memory_restart: 1G表示内存占用超过1G就自动重启。Node服务内存泄漏是常见问题这个配置能起一个兜底作用。watch: false表示不监听文件变化自动重启。生产环境一般不建议开watch因为文件一变化就重启不稳定。启动后执行pm2 status看到status为online就算成功了。6.3 日志、监控与开机自启pm2的日志管理是我最喜欢的功能。以前用nohup的时候日志都写在同一个文件里想找到某条报错信息跟大海捞针一样。pm2可以分别查看错误日志和输出日志pm2 logs my-project-server # 实时查看日志 pm2 logs --lines 200 # 查看最近200行日志文件路径就是ecosystem.config.js里配置的error_file和out_file也可以直接tail -f看。还有一个不能忘的操作是设置开机自启。服务器重启后如果pm2没有自动拉起你的Node进程服务就挂了pm2 startuppm2会返回一条需要你执行的命令复制到终端执行一下然后pm2 save这样之后即使服务器宕机重启pm2也会自动把保存的进程列表拉起来。这个步骤太容易被遗漏了我有一次就因为这个疏忽凌晨服务器自动重启后网站挂了一上午才被发现。7. 上线后的常见问题与排障手法该踩的坑我都帮你踩过了下面整理一份高频问题速查表遇到问题直接对照排查效率翻倍。7.1 问题速查表403/404/502/504现象可能原因排查方法解决方案访问站点403 Forbiddennginx没有读取目录的权限ls -l /var/www/my-project查看目录权限给目录执行权限chmod -R 755 /var/www/my-project确保用户有读取权限页面刷新404history路由没配置try_files查看nginx配置中location块添加try_files $uri $uri/ /index.html;静态资源404publicPath配置错误查看HTML中JS/CSS路径调整publicPath为/或相对路径接口502 Bad GatewayNode服务未启动或者端口不对ss -tlnpgrep 3000查看端口pm2 status 查看进程接口504 Gateway Timeout后端处理超时查看后端日志增加proxy_read_timeout 300s;或优化后端性能页面能开但接口全挂环境变量没有正确注入在浏览器Network看接口请求地址统一使用相对路径/api通过nginx反代避免跨域7.2 排查套路三个curl两行日志遇到部署问题我有一套自己的排查流程百试不爽。记住这套“三个curl两行日志”基本能定位90%以上问题。第一个curl测试nginx本身是否在工作curl -I http://your-domain.com如果返回200说明nginx和静态资源托管正常问题大概率在路由或者后端。如果返回403/404去查nginx配置和目录权限。第二个curl测试静态资源是否可访问curl -I http://your-domain.com/assets/index.js如果JS文件返回404去检查构建配置里的publicPath和文件是否真的上传成功了。第三个curl测试后端服务是否正常curl http://127.0.0.1:3000/api/health这是在服务器本机直接请求后端不走nginx。如果返回正常数据说明后端没问题要去查nginx反代配置如果连接拒绝或超时说明Node服务没起来去看pm2 logs。“两行日志”指的是tail -f /var/log/nginx/error.log pm2 logsnginx错误日志会明确告诉你请求在nginx这一层发生了什么pm2日志会告诉你Node进程内部报了什么错。两边日志一对照问题边界立刻清晰。排障有一个很重要的原则先在服务器本机验证再走外部链路。本机验证能最快把问题锁定在“服务器内部”还是“网络/安全组”这个范围内。比如外网访问超时但本机curl localhost正常那一定是安全组、防火墙或者DNS解析的问题而不是代码问题。最后说一个我个人的小习惯。每次部署之前我会在本地重新build一次然后把dist目录的index.html打开看一眼引用的资源路径再传到服务器。这个习惯帮我拦截掉了至少三次“publicPath没改导致线上白屏”的隐患。部署这件事细心比技巧重要流程固化下来之后每次上线就像流水线作业一样稳定、快速、不慌。如果你的部署流程还停留在“每次靠百度、靠猜”的阶段把这篇文章里的流程走一遍相信你以后再也不用为上线发愁了。