ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

React项目部署到服务器全指南:从Nginx配置到白屏排查

React项目部署到服务器全指南:从Nginx配置到白屏排查 1. 部署前必须想明白的几件事为什么你的React项目本地好好的一上服务器就白屏做React开发的朋友大多有过这种经历本地npm run dev跑得飞起页面丝滑流畅结果构建完丢到服务器上浏览器一打开——白屏、404、资源路径全错、接口调不通……然后开始怀疑人生。这套流程我前前后后部署过不下几十个项目从最简单的create-react-app到重型的Next.js SSR应用踩过的坑攒了一箩筐。这篇就把React项目部署到服务器的完整流程和避坑点一次性写清楚照着做基本能少走一半弯路。先说清楚部署到服务器到底是在干什么。React项目本身是纯前端应用开发模式下你访问的http://localhost:3000是Webpack/Vite启动的开发服务在实时编译代码。但服务器上不可能给你跑一套开发环境生产环境里的React项目本质上是把源码通过构建工具打包成静态HTML、CSS、JS文件然后由一个Web服务器Nginx、Apache等把这些静态文件当作网站对外提供服务。这句话里藏着三个最常见的坑路由问题React做的是SPA单页应用路由是前端JS控制的。你要是不懂Nginx需要配置try_files回退一刷新浏览器就404了。资源路径问题构建产物里的JS/CSS资源默认是用绝对路径/static/js/main.js引用的如果你的项目部署在子目录比如http://ip/react-app/下不调整publicPath照样一片白屏。环境变量问题env.development和.env.production不分开API地址硬编码部署后接口全部调不通。所以说部署不是把文件夹扔上去就完事而是构建、环境、服务器配置、域名与HTTPS这一套组合拳。下面我按一个完整的部署流程从零到一展开讲。2. 本地构建与产物检查打包这一步决定了服务器上80%的坑2.1 构建命令与产物结构不同脚手架构建命令略有差异但核心思路一致。常见几种脚手架/框架构建命令产物目录create-react-appnpm run buildbuild/Vitenpm run builddist/Next.js纯静态导出next build next exportout/UmiJSnpm run builddist/我在部署主机的第一步永远是在本机或CI环境执行构建而不是在服务器上执行。为什么因为服务器环境往往和本地不一致Node版本不同、npm依赖没装全、系统架构不同都可能让构建莫名其妙失败。更关键的是构建过程会消耗服务器CPU和内存小内存服务器在构建时直接被OOM内存溢出杀死也见过好几次。执行完构建后马上检查几个关键点# 以Vite项目为例构建并查看产物 npm run build ls -la dist/ cat dist/index.html打开index.html看里面的资源引用路径script typemodule src/assets/index-abc123.js/script注意这个/assets/...最前面有一个斜杠这是根路径。如果你的站点部署在域名根路径比如https://example.com/下没问题但如果你打算部署到子路径比如https://example.com/react-app/就需要在vite.config.ts里设置base: /react-app/在create-react-app里设置package.json中的homepage: /react-app。这个base路径配置是部署后白屏的最高频原因之一。很多人本地预览好好的——因为本地开发服务器也是部署在根路径下但一旦放到服务器子目录资源全部404。检查产物中JS/CSS是否404用浏览器F12打开Network面板一眼就能看出来。2.2 环境变量拆分让开发和生产的API地址不再混淆第二个要提前处理的是环境变量。React项目里.env文件支持在不同环境下加载不同配置这个机制很多人忽略了。默认情况下React构建时会加载这几类环境文件.env所有情况下都会加载.env.development仅npm start时加载.env.production仅npm run build时加载你可以在项目根目录创建两个文件// .env.development VITE_API_BASE_URL http://localhost:8080/api // .env.production VITE_API_BASE_URL https://api.example.com/api这样代码里统一用import.meta.env.VITE_API_BASE_URLVite或process.env.REACT_APP_API_BASE_URLcreate-react-app来拼接接口地址构建时自动选对应的值。省的每次上线前手动改接口地址改完忘记改回来下次开发接口全崩。这里有个血的教训所有在.env中声明的自定义环境变量必须带特定前缀才能被React暴露给前端代码。Vite要求是VITE_前缀create-react-app要求是REACT_APP_前缀。我遇到过同事把变量写成API_URL怎么访问都是undefined找了半天才意识到是前缀问题。2.3 构建产物验证的独门技巧构建完成先别急着传服务器。我习惯在本地起一个静态服务器验证产物是否正常cd dist # 用npx起一个静态服务模拟服务器环境 npx serve -s . -l 8080打开http://localhost:8080重点检查三件事页面是否正常渲染——不是白屏点击页面里的链接刷新浏览器——刷新后是否404本地静态服务器可能复现不了Nginx的try_files配置但至少能看出路由模式是否正常F12看Console——有没有红色报错特别是资源加载失败、跨域错误如果你之前配置了history路由模式并且不带hash本地npx serve刷新子路由也可能会404这不算意外正式在Nginx上配置了try_files就能解决。但如果页面加载就白屏一定要先在这步解决不要上传后再排查。3. 服务器初始化不是有一台机器就能直接用的3.1 服务器选型与基础配置国内的话阿里云、腾讯云、华为云是主流选择海外有AWS、Vultr、DigitalOcean等。作为一个个人博客或中小型React项目的部署目标2核4G的配置绰绰有余带宽按需买一般5Mbps起步够用。系统我建议选Debian或Ubuntu LTS版——用的人多教程通用软件源干净不像某些系统的包管理器会让你在安装Nginx时多折腾半小时。如果你的服务器是全新的到手第一件事不是装Node和Nginx而是先做基础安全加固# 创建新用户避免直接用root操作 adduser deploy usermod -aG sudo deploy # 修改SSH端口可选但建议 # 配置SSH密钥登录关闭密码登录 # 安装并启用防火墙 sudo apt update sudo apt install -y ufw sudo ufw allow 22/tcp sudo ufw allow 80/tcp sudo ufw allow 443/tcp sudo ufw enable这些操作虽然不是React部署的核心但在实际生产服务器上不做的话过不了几天你就能在日志里看到大量暴力破解SSH的记录。另外服务器安全组云控制台那边的规则也要放行80和443端口光改服务器内部防火墙没用云平台自带的安全组会先拦一道。3.2 安装Node.js和Nginx服务器上需要Node吗过去我直接回答不需要因为产物是纯静态文件。但后来发现很多人在服务器上可能还要重新构建、或者要跑一些脚本比如配合CI的部署钩子所以建议还是装一个。注意安装LTS版本即可不要装最新版免得遇到奇奇怪怪的兼容问题。# 使用NodeSource安装指定版本的Node以18.x为例 curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - sudo apt install -y nodejs node -v npm -vNginx也一并装上sudo apt install -y nginx sudo systemctl enable nginx sudo systemctl start nginx装完在浏览器访问服务器IP如果看到Nginx默认欢迎页说明80端口通了、Nginx在正常工作。看到这一步基本就放心一半了。3.3 连接服务器的方式从密码到密钥在部署过程中你需要频繁往服务器上传文件、执行命令连接工具建议用VS Code的Remote-SSH插件。这个插件真的省事——打开VS Code装好插件配置好SSH连接就能像在本地一样编辑服务器上的文件、直接使用终端。VS Code连远程服务器需要先配置~/.ssh/config文件Host my-server HostName 你的服务器IP或域名 User deploy Port 22 IdentityFile ~/.ssh/id_rsa配置好后在VS Code命令面板执行Remote-SSH: Connect to Host选my-server就能连上。这个方式是纯SSH协议安全性没问题。如果你需要高速传输文件可以用scp命令或者装个rsync后面讲到自动化部署时会细说。4. 上传与目录组织别把项目源码扔服务器上4.1 目录结构设计很多新手部署完服务器上还留着源码、node_modules、package.json这种习惯不好。你上传到服务器的应该只有构建产物build或dist目录里的东西源码只存在代码仓库里就够了。我个人的标准目录结构是这样的/var/www/ └── my-react-app/ ├── build/ # 静态文件构建产物 ├── deploy.sh # 部署脚本可选 └── nginx.conf # 参考配置可选上传方式直接用scp# 在本地执行 scp -r ./dist/* deploy你的服务器IP:/var/www/my-react-app/build/如果文件很多、更新频繁scp每次全量上传会比较慢。更好的方式是rsync只同步有变更的文件rsync -avz --delete ./dist/ deploy你的服务器IP:/var/www/my-react-app/build/这里有几个参数可以解释一下-a归档模式保留文件权限和时间戳-v显示详细输出-z传输时压缩减少流量--delete删除服务器上产物目录里本地已不存在的文件保证服务器是构建产物的精确镜像4.2 权限问题为什么Nginx 403 Forbidden总是找上你上传完文件后经常会遇到403 Forbidden错误。罪魁祸首绝大多数是目录权限和Nginx运行用户不一致。Nginx默认以www-data用户运行。如果你的/var/www/my-react-app目录是deploy用户创建的默认权限可能是755www-data用户没有读取权限。解决办法# 把项目目录所属修改为www-data用户组 sudo chown -R www-data:www-data /var/www/my-react-app # 目录需要读和执行权限 sudo chmod -R 755 /var/www/my-react-app顺便说一句如果你用了root用户上传文件目录权限是700那Nginx完全没权限访问403就来了。自己的服务器上文件权限尽量保持755目录和644文件就对了。5. Nginx配置部署React项目的核心关卡5.1 一份能直接用的Nginx配置详解这是整个部署过程中最关键的一步。我直接给出一份供参考的Nginx站点配置React单页应用按这个改一下域名和路径基本就能跑server { listen 80; server_name example.com; # 改成你的域名或IP # 开启gzip压缩减少传输体积 gzip on; gzip_types text/plain text/css application/javascript application/json image/svgxml; gzip_min_length 1024; root /var/www/my-react-app/build; index index.html; # 关键配置处理前端路由 location / { try_files $uri $uri/ /index.html; } # 静态资源缓存提升二次访问速度 location ~* \.(js|css|png|jpg|jpeg|gif|svg|webp|woff2?)$ { expires 30d; add_header Cache-Control public, no-transform; } # 禁止访问隐藏文件 location ~ /\. { deny all; } }这个配置里的灵魂是这一行try_files $uri $uri/ /index.html;这行的作用当用户访问https://example.com/about时Nginx先去找/var/www/my-react-app/build/about这个文件不存在再找/about/目录还是找不到就把请求重写到/index.html。这样React的前端路由就接管了页面组件根据URL渲染对应视图。少了这一行刷新子路由就404这是React部署最经典的一个坑。5.2 从HTTP到HTTPSLets Encrypt证书配置现在部署新站点我默认直接上HTTPS。原因很简单浏览器对HTTP站点的限制越来越多比如获取地理位置、麦克风权限、PWA以及navigator.serviceWorker等API都要求安全上下文另外如果API接口是HTTPS的从HTTP页面去请求会直接报跨域或Mixed Content错误。既然部署别给自己留隐患。用Certbot申请Lets Encrypt证书非常快# 安装certbot和nginx插件 sudo apt install -y certbot python3-certbot-nginx # 自动获取证书并改Nginx配置 sudo certbot --nginx -d example.comCertbot会自动帮你改Nginx配置加好证书路径、自动跳转。证书每90天过期但Certbot会装一个定时任务自动续期不需要你操心。需要补充一个场景如果你只有IP、没有域名也别急着放弃HTTPS。Lets Encrypt不签IP证书但可以通过自签名证书配合acme.sh等方式处理不过对个人项目来说用IP访问时直接用HTTP问题也不大——局域网内部署、开发环境测试HTTP就够用了。5.3 Nginx配置修改后的生效与检查改完配置文件别直接完事执行一下检查再重载# 测试配置语法 sudo nginx -t # 平滑重载配置 sudo systemctl reload nginxnginx -t这个检查很重要我曾经手抖在配置里少写了一个分号直接reload就把Nginx搞崩了。每次改完配置先nginx -t确认没有语法错误再重载已经成了肌肉记忆。6. 踩坑实录白屏、404、代理404、端口不通6.1 完整排查链路从白屏到定位问题部署完后最常见的问题就是白屏。我把完整的排查链路写一下遇到问题按这个顺序来基本不会漏。第一步看页面源代码和Network面板打开浏览器按F12先看Console和Network如果index.html都返回不了看Nginx日志sudo tail -f /var/log/nginx/error.log如果index.html返回了但JS/CSS加载404看资源的路径和服务器上的真实路径是否对应如果JS加载成功了但页面白屏看Console有没有报错信息第二步确认构建产物的资源路径举个例子之前有个项目用Vite构建完的index.html里是/assets/index-xxxx.js我在本地用npx serve打开正常但传到服务器的/var/www/app下后浏览器访问http://IP/它去请求的是http://IP/assets/index-xxxx.js但我的文件实际放在了/var/www/app/assets/Nginx root配置的就是/var/www/app。这里看起来应该没问题但检查后才发现项目里配置了base: ./导致资源路径变成了相对路径在某些嵌套路由下就凑出了404路径。这种问题就要通过把base调成/或者按部署子路径来配。第三步查看Nginx配置和物理路径是否对得上用curl直接在本机测试curl -I http://127.0.0.1/ curl -I http://127.0.0.1/assets/index-abc123.js如果JS返回404看看这个文件在服务器上到底存不存在、路径是否多了或少了一层。这一步能快速定位是Nginx配置问题还是文件没传对。6.2 子路径部署的坑base路径引发的连锁反应如果你确定要把React项目部署到某个子路径下比如http://example.com/react-appNginx配置要改成server { listen 80; server_name example.com; location /react-app/ { alias /var/www/my-react-app/build/; try_files $uri $uri/ /react-app/index.html; } }注意这里用的是alias而不是root区别在于root /var/www/my-react-app/build;location /react-app/实际访问路径为/var/www/my-react-app/build/react-app/index.html会把location路径拼在后面alias /var/www/my-react-app/build/;实际访问路径为/var/www/my-react-app/build/index.html直接替换location路径同时构建时要在vite.config.ts里设置base: /react-app/或在create-react-app里设置homepage否则JS/CSS资源全按绝对路径请求还是会404。子路径部署的坑在于构建时的base、路由器的basename、Nginx的location三者必须保持一致漏一个页面就废。6.3 接口代理配置如何在生产环境解决/API跨域很多React项目在开发时靠Vite的proxy或者WebpackDevServer的proxy把/api代理到后端解决了跨域问题。但生产环境这些代理全部失效——你的Nginx才是那个代理。后端接口单独在另一台服务器或另一个端口时Nginx里加一个反向代理配置server { listen 80; server_name example.com; root /var/www/my-react-app/build; index index.html; location / { try_files $uri $uri/ /index.html; } # 代理/api到后端服务 location /api/ { proxy_pass http://127.0.0.1:8080/api/; 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_read_timeout。如果后端的接口处理比较慢默认的60秒超时时间可能不够建议设置成proxy_read_timeout 300;避免长时间接口还没处理完Nginx先给你断了。6.4 端口无法访问的排查顺序某个项目的页面打不开但你在本地curl一切正常这种时候极大概率是端口没开放。按顺序排查云服务器安全组/防火墙规则登录云控制台看80/443端口是否放行。这一步最容易被忽略——很多人配好了服务器里的ufw但忘了控制台安全组。服务器内部防火墙sudo ufw status确保80/443端口是allow状态。Nginx监听端口sudo netstat -tlnp | grep nginx确认Nginx确实监听了80/443。本地网络测试telnet 你的IP 80从本地看端口是否通。这四步走完端口问题基本能定位出来。注意腾讯云和阿里云在轻量应用服务器上还有一层防火墙设置和ECS的安全组是两个概念轻量服务器要单独去轻量控制台检查。7. 版本更新与自动化部署从手动到脚本化7.1 手动更新流程的标准操作项目上线后每次发版就涉及到更新服务器上的文件。最简单的手动流程是# 本地执行 npm run build rsync -avz --delete ./dist/ deploy你的服务器IP:/var/www/my-react-app/build/这里注意--delete参数在删除旧版本中已不存在的文件时很有用但也会把服务器上其他手工放进去的文件一并删除所以使用前一定要确认目录结构。真正线上的项目在更新前还应该考虑备份——把旧版本复制一份cp -r /var/www/my-react-app/build /var/www/my-react-app/build_backup_date %Y%m%d%H%M这样新版本出问题了能快速回滚改Nginx的root指向或直接恢复目录。7.2 用脚本一键完成构建上传重载每次手动敲命令虽然不复杂但次数多了总会漏步骤。我个人的做法是写一个简单的部署脚本放在本地项目根目录#!/bin/bash # deploy.sh - 本地构建并部署到服务器 set -e # 任何一步失败立即终止脚本 SERVERdeploy你的服务器IP REMOTE_DIR/var/www/my-react-app echo 1. 本地构建 npm run build echo 2. 同步文件到服务器 rsync -avz --delete ./dist/ $SERVER:$REMOTE_DIR/build/ echo 3. 设置权限 ssh $SERVER sudo chown -R www-data:www-data $REMOTE_DIR sudo chmod -R 755 $REMOTE_DIR echo 部署完成 在本地执行bash deploy.sh即可。脚本里用到了set -e保证构建失败时不会继续往服务器上传坏文件——这个小细节帮我避免过好几次事故。7.3 更进一步用Docker和CI/CD彻底解放双手如果项目要继续迭代、需要多人协作脚本化还不够更理想的方式是CI/CD Docker。Docker部署React项目的核心是多阶段构建让镜像只包含最终的静态文件FROM node:18-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build FROM nginx:alpine COPY --frombuilder /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 CMD [nginx, -g, daemon off;]配合GitHub Actions在push代码后自动构建并推送到服务器name: Deploy to Server on: push: branches: [ main ] jobs: build-and-deploy: runs-on: ubuntu-latest steps: - uses: actions/checkoutv3 - uses: actions/setup-nodev3 with: node-version: 18 - run: npm ci - run: npm run build - name: Deploy to server uses: appleboy/scp-actionv0.1.4 with: host: ${{ secrets.SERVER_HOST }} username: ${{ secrets.SERVER_USER }} key: ${{ secrets.SSH_PRIVATE_KEY }} source: dist/* target: /var/www/my-react-app/build strip_components: 1服务器上的/var/www/my-react-app目录挂载给一个Nginx容器或直接用宿主机Nginx。这套流程一旦跑通以后每发一次版push完代码就完事页面自动更新。不过如果你只是个人项目、更新频率一周一次脚本化部署已经够用没必要为了自动化而自动化。工具服务于人部署流程越简单越不容易出错。8. 最后的经验安全和性能优化8.1 隐藏服务器版本信息与安全响应头Nginx默认会在HTTP响应头里暴露版本号这个信息对攻击者来说是有用线索。隐藏掉它server_tokens off; # 添加基础安全响应头 add_header X-Frame-Options SAMEORIGIN always; add_header X-Content-Type-Options nosniff always; add_header X-XSS-Protection 1; modeblock always;项目上线后用curl -I http://你的域名看看响应头如果没有明显的版本泄露安全这块算基础分拿到了。8.2 静态资源缓存策略React项目构建后JS/CSS文件通常带有hash指纹比如index-abc123.js。文件内容变了hash就变这意味着你可以放心对静态资源设置很长的缓存时间。我上面的配置里给了30天如果你认为项目迭代很频繁也可以只给7天。index.html本身不要设置缓存或缓存几秒否则用户访问的始终是旧版页面——这个问题可能比你想象中更容易踩到。之前有朋友就是因为给index.html也设置了expires 30d导致发版后所有用户都要强刷才能看到新页面体验非常糟糕。正确的做法是给带hash的静态资源设置长缓存给index.html设置no-cachelocation /index.html { add_header Cache-Control no-cache, no-store, must-revalidate; }8.3 内存和负载的简单考量React静态站点的服务端压力其实不大2核4G跑个Nginx托管静态文件完全没有问题。但如果同一台服务器上还跑了Node后端、数据库、Redis等一堆服务就要注意整体的资源占用。用htop或free -h看一眼内存长期在90%以上就该想想是不是要升级配置或者优化服务数量了。云服务器厂商一般都有监控报警设一个CPU和内存的告警阈值比如CPU超过80%持续5分钟就发短信通知这样不用每天盯服务器有问题早点知道。这个小设置非常值得花两分钟搞定。
返回列表