本地一切正常一上线就白屏刷新一下404明明改了代码CDN还返回旧版本……这些事我前前后后都遇到过。前端部署听起来就是把文件夹丢到服务器上但真正跑一遍会发现从构建产物的处理到Nginx配置、HTTPS证书、CDN缓存任何一个环节没有理顺都会让“最后一公里”变成“终点前的连环坑”。这篇指南我会以一次完整的部署为主线把从本地到云端的每一步都拆开讲清楚内容包括静态托管与服务器托管怎么选、构建产物该检查哪些细节、Nginx和对象存储两条主线上线流程、以及CI/CD自动化和常见故障的排查思路。如果你正准备把项目从本地搬到线上或者部署完总出问题又不知道从哪里查这篇文章应该能帮你省下不少折腾时间。1. 部署前先想清楚你要的到底是“上传文件”还是“跑服务”1.1 前端部署的本质是“托管产物”而非“运行源码”很多人第一次部署前端项目时会下意识地以为部署就是把整个项目的源码传上去像后端项目那样在服务器上安装Node、执行启动命令。这个认知是很多问题的起点。前端工程经过打包之后最终产出的是dist目录也有叫build、public的里面是一堆静态文件HTML、CSS、JavaScript、图片、字体。它们不需要运行时去动态计算只需要一个能接收HTTP请求并把这些文件返回给浏览器的“托管环境”。你可以把打包流程理解成把食材加工成熟食而部署就是把熟食摆到餐台上客人浏览器点菜时餐厅HTTP服务把对应菜品端过去。所以部署真正的核心只有两件事把产物传到位让HTTP服务能正确访问它。至于源码里用的Vue还是React、业务逻辑如何复杂线上环境完全不关心服务器只需要知道dist目录在哪、怎么处理路径请求。这个认知一旦建立后续所有决策都会变得清晰为什么会有静态托管和服务器托管两条路线为什么有的部署只需要传文件为什么SPA应用还要额外处理刷新404问题。1.2 三条技术路线先判断自己属于哪一种部署方案很多但绝大多数前端项目主要集中在三条路线上它们的适用边界非常明确纯静态托管项目没有服务端逻辑或者接口完全由后端提供前端只是一组静态页面。最常见的形态就是把产物放到对象存储里再配合CDN加速分发。这条路配置最少、成本最低、抗并发能力最强适合绝大多数内容展示型站点和SPA应用。服务器 Nginx托管需要这台服务器同时负责静态文件服务、反向代理后端接口、处理复杂的响应头或路由规则。例如前端的API请求需要转发到不同的后端服务或者做了SSR就必须有一个真正的服务器来承载。容器/Node服务托管项目本身是一个Node服务比如Nuxt、Next的SSR模式部署的是服务进程而不是静态文件。此时需要安装Node运行时配置进程守护还要考虑端口管理和资源占用。我曾经负责过一个管理后台项目它当时的部署方案是分阶段演进的早期访问量不大直接一台云服务器跑Nginx托管静态文件后来加了多环境、动态配置需求就迁移到了对象存储加CDN再后来接口需要统一走网关又改成了Nginx反向代理结构。这说明选型不是一步到位的投入产出比会随着团队和项目变化而变化。这三者的关系可以用一个简单的判断标准来划分如果项目没有“服务端行为”选择纯静态托管是性价比最高的如果项目有接口代理、服务端渲染、独立响应头这类需求就要回到服务器方案如果项目本身就运行在Node层那就直接走进程托管路线。1.3 一个能直接在纸上打勾的部署准备清单在动手之前先把下面这些项检查一遍避免部署过程中频繁被打断域名是否已经购买并完成备案/实名认证取决于你的目标地区和域名服务商云厂商控制台都有指引域名DNS解析是否已指向目标服务器IP或CDN加速域名云服务器或对象存储资源是否已经开通HTTPS证书的获取方式托管平台自动签发、服务器上手动申请还是自定义上传本地是否安装好Node与包管理器确保构建环境稳定确认最终访问域名和路径因为这会直接影响资源引用路径和路由配置CI/CD是否需要接入如果当前只是验证流程可以暂时跳过2. 本地构建把 dist 当成“待发货包裹”发出去前必须验货2.1 构建过程到底做了什么先回到源头。执行npm run build或yarn build时打包器会做几件关键的事解析入口文件、编译模板和样式、把代码压缩混淆、生成带hash指纹的资源文件、按路由拆分包。最终输出的dist目录就是一份“与源码无关、只包含资源引用关系”的静态产物。这里要特别强调hash指纹的作用。打包器默认会给文件名加上内容hash例如app.8f3d2a.js。内容变了文件名就变浏览器和CDN看到新的文件名自然会把旧缓存当成失效资源重新拉取。这是前端缓存策略里最基础也最有效的机制理解它后面排查“改了代码不生效”会快很多。另一个常被忽略的点是包管理器的锁文件例如package-lock.json。只要安装了依赖这个文件就应该提交到代码仓库。锁文件保证了本地、CI流水线里安装到的依赖版本完全一致“本地构建成功但线上构建失败”这类问题多半是依赖版本漂移造成的。2.2 发出前的三处必查细节第一资源引用路径是否正确。打开dist/index.html看一下里面script和link的引用是绝对路径/assets/xxx.js还是相对路径./assets/xxx.js。如果项目部署在域名根路径绝对路径没问题但如果部署在子路径比如https://example.com/admin/就需要在构建时把基础路径配置成/admin/否则资源会去根路径找结果就是白屏。第二路由模式是否匹配托管环境。Vue、React这些框架默认的history模式URL里没有#号看起来很清爽但刷新页面时浏览器会向服务器请求这个真实的路径如果服务器没有配置对应的回退规则就会返回404。如果是hash模式URL里带#号服务器永远只收到首页请求反而没有这个问题。所以部署history项目前必须确保服务器能“找不到文件时回退到index.html”。第三PWA或Service Worker的缓存策略。如果一个项目注册了Service Worker旧版本可能在客户端悄悄缓存上一份资源长达数月导致用户以为自己访问的是新版本其实代码根本没更新。部署前检查一下配置里缓存版本号或手动跳过等待的逻辑会省掉大量线上问题。2.3 环境变量和运行时配置的坑前端环境变量有一个容易让新手踩坑的特性以VITE_、REACT_APP等前缀声明的变量在构建时会被打进产物里。也就是说构建时就确定了接口地址、请求域名这些信息线上无法在运行时“改配置”。如果你有不同环境的部署需求合理做法是分环境构建例如.env.production、.env.test然后为每个环境生成独立的产物并部署到对应站点。另一种思路是做一个运行时配置文件例如public/config.js页面在加载时先从该文件读取全局配置再用它拼接接口地址。这个文件不参与构建所以运维人员可以直接修改对应文件来完成配置变更而不需要重新打包。我自己的习惯是环境相关的域名、接口前缀、埋点ID走构建时注入可能随时调整的开关项、活动文案、渠道参数走运行时配置。这样既不暴露多余信息也保留了灵活性。3. 服务器部署实操Nginx 配置的完整链路3.1 服务器初始化和环境准备如果选择服务器部署这条路线我默认的实践是以一台Linux云服务器为例。登录服务器后第一件事是更新软件源然后安装Nginx。以Debian/Ubuntu系为例sudo apt update sudo apt install -y nginx安装完成后Nginx会以系统服务的方式自动启动。验证是否正常运行systemctl status nginx拿到服务器公网IP并做域名解析后先不要急着配项目直接访问一下这个IP。如果能看到Nginx的默认欢迎页说明网络链路、防火墙和Web服务都是通的接下来只需要把默认配置替换成你的项目配置。如果访问不了优先级最高的是排查云厂商安全组和服务器防火墙是否放行了80端口。很多部署卡在第一步不是Nginx配置问题而是安全组规则压根没放开HTTP端口。3.2 Nginx 托管 dist 目录的正确姿势Nginx的配置目录在不同发行版上有点小差别Debian系主要看/etc/nginx/sites-available和sites-enabled。推荐的做法是在sites-available下新建一个站点配置文件然后在sites-enabled下做软链接启用它。下面是一份可以直接套用的基础配置假设站点名为example.com产物放在/var/www/exampleserver { listen 80; server_name example.com; root /var/www/example; index index.html; # 关键SPA 路由回退 location / { try_files $uri $uri/ /index.html; } # 带 hash 的静态资源可以长缓存 location /assets/ { expires 7d; add_header Cache-Control public, immutable; } }解释一下几个核心指令root指定站点根目录所有请求都会在这个目录下寻找对应文件。try_files接收三个参数先按$uri找原始路径找不到再尝试作为目录访问都不行就返回/index.html。这就是SPA刷新不发404的关键。静态资源配了7天缓存由于文件名里带hash内容一旦变化文件名就会变旧缓存不会影响新版本所以“immutable”是安全的。注意配置里没有写location ~* .php这类动态语言处理因为前端托管场景根本不需要。写完配置后执行nginx -t检查语法看到“syntax is ok”后再用nginx -s reload让配置生效。如果出现403错误大概率是产物目录的文件权限不够排查顺序是Nginx运行用户默认www-data是否对/var/www/example有读取权限。如果您不确定这几个Nginx内置变量具体代表什么可以这么理解$uri表示“用户请求的地址在磁盘对应相对路径”try_files的取值数组是按顺序尝试直到遇到真实存在的文件或者被fallback规则接住。3.3 HTTPS 开通与自动续期这个时代HTTPS已经不是加分项而是必需品。就算不涉及登录信息浏览器地址栏的“不安全”三个字也会让用户信任感大打折扣。主流做法是用自动化证书工具来完成申请、安装和续期。以官方推荐的Certbot工具为例sudo apt install -y certbot python3-certbot-nginx sudo certbot --nginx -d example.com -d www.example.com执行过程中按提示指定webroot或nginx模式它会自动修改Nginx配置并启用443端口。证书有效期一般90天Certbot会自动添加续期定时任务不需要人工干预。验证自动续期是否正常工作sudo certbot renew --dry-run这里有一个经验提示如果配置了CDNHTTPS证书策略有“回源方式”的选择。通常建议使用“HTTP回源CDN边缘节点为用户提供HTTPS”这样证书只在CDN层维护源服务器压力更小。如果源服务器自己也配置了HTTPS证书CDN回源只能走HTTPS时反而要处理源站证书有效期、SSL握手性能等更多问题。3.4 部署后的验证清单把产物传上服务器这件事scp简单直接scp -r dist/* useryour-server:/var/www/example/如果文件较多rsync更合适因为可以增量同步rsync -avz --delete dist/ useryour-server:/var/www/example/--delete参数的意思是“以本地为唯一版本删掉服务器上多余的文件”避免旧版本残留。但这个参数使用时要注意如果服务器产物目录还有其他用途比如存放上传统计报表千万别加这个参数否则会被顺手清掉。部署完成后验证我的清单是curl -I https://example.com确认HTTP状态码为200刷新一个子路由比如/help确认不是404而是返回页面打开浏览器开发者工具Network面板确认JS和CSS资源响应状态、缓存头是否符合预期查看/var/log/nginx/error.log确认没有明显的权限或路径报错4. 静态托管的美妙方案对象存储 CDN 三步上线4.1 对象存储部署的七个步骤很多前端项目其实不需要一台“一直在运行”的服务器。把dist目录传到一个对象存储Bucket里再挂上一个CDN域名线上就通了。这是我从个人项目到高并发活动页都验证过的路线稳定性和速度都很能打。具体操作步骤大致如下以某个云厂商的控制台操作为例菜单名称各厂商略有差异但流程一致创建Bucket选择与用户群体地域相近的区域访问权限设为“公共读”把dist目录内所有文件按层级关系上传到Bucket根目录开启静态网站托管功能指定索引页面为index.html为Bucket绑定一个CDN加速域名并在CDN控制台完成源站指向把域名解析到CDN分配的CNAME地址上在CDN控制台为加速域名配置HTTPS证书设置缓存规则见下一节这套流程里面最容易漏掉的是静态网站托管的“错误文档”配置。对象存储默认碰到不存在的路径会返回404页面而SPA项目刷新子路由时路径在真实文件系统里是不存在的必须把错误文档或自定义回源规则指向index.html否则刷新404的问题又回来了。4.2 缓存策略姑且把 HTML 和静态资源区别对待对象存储和CDN结合的强大之处在于缓存控制灵活但这要求你明确区分两类文件index.html文件体积小更新频率高它是页面入口必须让浏览器每次都能拿到最新版本。缓存策略建议不缓存或者设一个很短的时间。JS、CSS、图片等静态资源文件名带hash指纹内容变了名称就变了可以放心地设置长时间缓存。对应的缓存规则可以按下面的方式设定文件/目录Cache-Control说明index.htmlno-cache 或 max-age0必须回源校验保证最新/assets/*max-age31536000, immutablehash变化产生新URL旧文件可长缓存/images/*max-age2592000一个月级别兼顾更新与性能其他无hash文件max-age3600短缓存防止改动后长时间不生效如果没有CDN而是在对象存储上直接配“重定向规则”原理也一样把404或子路由路径请求重写到/index.html然后根据文件扩展名区分缓存时长。4.3 两条路线的取舍原则聊完了两种部署方式直接给结论凡是纯前端的营销页、官网、用户中心、后台系统前端我优先推荐静态托管。它维护成本低、天然分布加速、天然抗流量尖峰费用通常比云服务器低一个量级。需要回到Nginx服务器的场景首先是服务端代理前端和后端域名不一致、需要规避跨域或合并多个后端地址时Nginx反代是更省事的方案。其次是服务端渲染Nuxt、Next的SSR模式必须跑Node进程对象存储没法承载这类服务。最后是对响应头有特殊要求包括自定义安全头、接口转发规则等。在我经历过的项目里正好有一个增量更新的案例一个后台系统的前端是静态部署在对象存储上的而后端接口部署在云服务器上两者域名不同通过接口网关做CORS授权和鉴权。这个组合跑得非常稳前端发布基本秒级生效后端再慢也影响不到页面。5. 从手动到自动CI/CD 接入与常见故障排查5.1 用一条流水线把“上传文件”自动化一开始可能是人工本地构建、再手动上传一旦项目变多、环境变多这套动作的容错率和时效性都会成为问题。接CI/CD的核心思路其实很统一代码推到远端分支后由代码托管平台提供的持续集成服务自动完成安装依赖、执行构建、上传产物这三个阶段。我不依赖特定平台的配置语法来展开直接把部署脚本的核心逻辑写出来你把它放到仓库里交由CI调用就行#!/bin/bash # deploy.sh set -e # 安装依赖并构建 npm ci npm run build # 上传到服务器产目录服务器地址从环境变量读取 rsync -avz --delete dist/ $DEPLOY_SERVER_PATH/ # 如果是对象存储这条命令换成对应工具上传命令安全方面只有一条铁律任何密钥都不允许写进仓库。把服务器SSH私钥、云服务商的SecretKey放到CI的密钥变量里构建时动态注入环境变量。流水线日志默认关闭敏感项回显这样既实现了自动化又不会把钥匙暴露给不该看到的人。一个实践中提高可靠性的技巧是划分发布与构建之间的依赖顺序。先上传带hash的静态资源最后替换index.html。原因很简单如果先更新入口文件用户加载新HTML后可能引用尚未上传完的新CSS和JS瞬间会白屏或报错。反过来无论CDN还是浏览器缓存只要index.html最后更新用户加载到的永远是“完整的新版本”或“完整的旧版本”不会夹在中间状态。5.2 故障排查清单白屏、404、旧缓存部署完出问题很多时候不是代码问题。把最常见的几个现象和排查路线整理成一张表现象优先排查点常见根因打开页面白屏浏览器Console的报错信息资源路径404JS运行时异常环境变量缺失导致接口报错刷新子路由404是否配置SPA回退Nginx丢失try_files对象存储缺少错误文档配置CSS样式错乱但HTML正常构建后的CSS引用地址部署在子路径时base路径没配CDN缓存旧文件代码更新了但线上不变查看JS文件名hashCDN或浏览器缓存了旧文件更新时index.html未最后替换接口请求跨域后端响应头CORS配置前端域名和后端域名不一致需要通过代理或网关解耦图片加载失败/403防盗链配置CDN和对象存储的Referer白名单配置不当接口域名指向localhost环境变量注入构建时使用的.env文件不正确运行时配置未修改5.3 真实项目里的三个排错片段第一个是history路由在对象存储上的404问题。现象是首页能访问点击菜单正常但只要手动刷新/xx/yy就跳到平台自带的404页面。当时的根因是对象存储的静态网站托管配置里没有指定错误文档到index.html。修改后路由刷新就正常了。第二个是CDN缓存导致的“假更新”。某次发版后我在手机上打开页面看到的是旧版本但PC上是新的。查了一圈发现是CDN的缓存过期时间设得太长index.html也被缓存了。最终调整了两件事入口文件不缓存带hash的资源长缓存。第三个是构建产物里的资源路径问题。项目之前一直部署在域名根路径后来为了测试临时挂在了二级路径上结果所有JS和CSS都404。那时才意识到构建配置里的基础路径是写死的根路径后来改成根据环境动态设置这个问题才彻底解决。5.4 把回滚和发布窗口也放进考虑范围自动化部署虽然快但也让失误的传播速度变得更快。建议在流程里增加一个回滚选项如果是Nginx服务器部署旧版本产物保留一个带时间戳的备份目录配置里改软链接指向就能快速回退如果是对象存储上一个版本的产物目录不覆盖删除CDN回源指回旧目录即可。更好的做法是把“回滚”想成一次普通的版本切换发布不是覆盖性操作而是“当前版本指针指向新版本”。这样出问题后按两下按钮就能回到上一个稳定版本而不是重新构建一套产物再手动上传。最后再说几条我自己踩出来的经验一个是发布顺序的问题。我在前面提过先传静态资源、最后替换index.html。这条规则帮我在好几次大版本更新里避免了大面积白屏值得反复强调。另一个是不要低估日志的价值。Nginx的access.log和error.log、CDN的访问日志、部署工具的执行日志都是排查问题的第一手线索。上线后如果发现异常先看日志再改代码顺序不要反。还有一个是关于“部署环境统一”的体会。我在本地跑得好好的一到CI却打包失败原因就是本地Node版本和流水线的Node版本不一致。后来养成了几乎每个项目都在仓库里带上.nvmrc文件指定Node版本的习惯CI和本地共用同一个版本约束这个坑就很难再踩到。前端部署不是什么玄学它就是把产物准确放到一个能对外服务的环境再把路径、缓存、安全这几件事理顺。你顺着这条链路走一遍就会发现之前觉得玄乎的地方都是确定性的问题每一条都能查、能改、能验证。
阅读完成 · 觉得有帮助?