凌晨3点,女同桌发来消息:这次发版用户再骂娘,我就真要嫁人了!

142 阅读6分钟

解决前端发版后用户访问资源出现404的问题!


💼 故事开始

为什么会写这一篇文章呢,主要是因为看了一下好像还没有过类似的问题的解决。那天凌晨6点,我刚准备关机下班,微信突然弹出一条消息:

“前端仔在吗?这次发版如果再出问题,我可能真的撑不下去了...”

发消息的是小美,我们团队的产品经理。平时雷厉风行的她,此刻头像都透着疲惫。

我赶紧回了个电话,对面传来带着哭腔的声音:

“为什么每次发版都会收到用户的投诉,用户投诉又炸了!老板在群里@我,说如果下次发版还不能解决这个发版后用户使用出现资源加载出错的问题,就让我考虑转岗了,还说你这个前端仔也要收拾东西滚蛋了...”

“用户反馈说跳转页面突然卡死、资源加载404、懒加载的路由直接报错。最可怕的是,这些反馈的都是我们的资深学员的!(我们做的主要是学员直播看录播系统)”

她越说越激动:“你知道用户怎么说吗?‘每次起早看录播都会出现这个问题(刚好卡在我们的发版时间),任永远不知道下一步操作是什么问题引起的卡死!’”


🔍 问题根源

我让她冷静下来,仔细分析了现状:

原来他们的发版流程是这样的:

  1. npm run build
  2. 直接覆盖线上静态资源 ❌
  3. 用户正在访问的页面突然资源404 ❌
  4. 懒加载组件版本不匹配 ❌
  5. 全员火葬场 ❌❌❌

“所以...” 我顿了顿,“你们是在用最危险的方式玩俄罗斯轮盘赌,每次都有用户中枪。”

小美苦笑:“那怎么办?总不能为了发版让全站停机吧?”


💡 转折时刻

“当然不用!” 首先我提出了发版后提示用户刷新操作不就好了吗这是普遍的操作。小美根领导说了我的办法得到的领导回复是:你这样也没解决基本的问题啊,如果用户正在答题答到一半你叫他刷新吗,瞎扯淡吧(就跟你拉屎到一半叫你缩回去重新拉你能接受的了?), 我说那行给我涨工资我来解决。然后我就自信地说,“让我给你介绍一套零停机、用户无感知的灰度发版方案...”

🚀 技术方案预告

接下来的内容,你将学到:

如何让新老版本完美共存
用户无感知的平滑迁移方案
懒加载路由的版本一致性保证
CI/CD全自动化部署脚本
生产环境验证的最佳实践

最重要的是:这套方案已经在十万级用户产品中稳定运行1年,发版时用户投诉降为0!


🚀 前言:

《前端多版本无缝发布实战:用 Nginx + 软链接实现旧版不中断,新版秒切换(Windows & Linux 全讲解)》

最终效果

后续有机会上传


一、前言

前端发版最怕什么?
👉 用户正在访问旧页面,而你刚好发了新版本。

这时候新旧版本资源不匹配,比如懒加载的 JS 文件路径变了,
旧页面请求新的资源时就会报错 404,造成“白屏”或功能异常。

本文将教你一个稳定实用的解决方案:
使用 Nginx + 软链接(符号链接 / junction) ,实现 多版本共存 + 平滑切换
无论用户是否刷新页面,都能保持访问稳定。


二、核心思路

  1. 每次打包时,不覆盖旧文件,而是输出到独立目录,例如:

    一般情况下是根据打包的时间日期来做文件目录如 **V20251019131452**
    dist/V1/
    dist/V2/
    
  2. 使用一个软链接 current 指向当前线上版本:

    dist/current → dist/V2
    
  3. Nginx 永远服务 dist/current 目录下的资源。
    发布新版本时,只需把软链接改成新的目标(V3)即可。

  4. 老用户继续访问旧的懒加载资源(因为缓存里是旧 URL),
    所以我们提供一个“旧版本兜底目录”:

    dist/previous → dist/V1
    

    一旦找不到新资源,就从旧目录里查。


三、目录结构示例

dist/
 ├─ V1/
 ├─ V2/
 ├─ current/   -> 软链接,指向当前版本(例如 V2)
 └─ previous/  -> 软链接,指向旧版本(例如 V1)

四、Nginx 配置(核心版)

server {
    listen       8000;
    server_name  localhost;

    # 1️⃣ 静态资源:优先从 current 找,找不到走 fallback
    location ~ ^/(assets|img|js|css|fonts)/ {
        root D:/AllProject/monorepo/apps/frontend/nginx_build/dist/current; //这里填写部署的路径
        try_files $uri $uri/ @fallback;

        access_log off;
        add_header Cache-Control "public, max-age=31536000, immutable";
    }

    # 2️⃣ 兜底目录(previous)
    location @fallback {
        root D:/AllProject/monorepo/apps/frontend/nginx_build/dist/previous;//这里填写部署的上一个版本的路径
        try_files $uri =404;
    }

    # 3️⃣ 前端入口(SPA)
    location / {
        root D:/AllProject/monorepo/apps/frontend/nginx_build/dist/current;//这里填写部署的路径
        index index.html;
        try_files $uri $uri/ /index.html;
        add_header Cache-Control "no-cache, must-revalidate";
    }

    # 4️⃣ 调试辅助
    error_page 404 = @debug;
    location @debug {
        internal;
        return 200 "root=$document_root\nuri=$uri\nfilename=$request_filename\n";
    }
}

五、原理讲解(必须看懂的知识点)

1️⃣ $uriroot 的拼接规则

Nginx 实际访问文件时,会执行类似:

$document_root + $uri

比如:

root D:/dist/current;
访问 /assets/app.jsD:/dist/current/assets/app.js

如果写错路径、或者多 /,就可能拼成:

D:/nginx/htmlindex.html

从而导致下载文件或 404。


2️⃣ try_files 的查找逻辑
try_files $uri $uri/ /index.html;

意思是:

  1. 先找是否存在文件 $uri
  2. 再找是否存在目录 $uri/
  3. 如果都不存在,就回退到 /index.html(用于 SPA 前端路由)

⚠️ 注意:
/index.html 前面的 斜杠不能省略,但如果导致循环(Windows 下常见),可以写成 index.html


3️⃣ alias vs root 的区别
指令路径拼接方式常见用途
rootroot + uri 拼接一般用于静态资源或 SPA
alias完全替换 uri用于映射非同名目录

示例:

location /app/ {
  alias D:/project/dist/;
}

访问 /app/main.jsD:/project/dist/main.js


六、Windows 下软链接命令

1️⃣ 创建软链接(符号链接)
mklink /D current V2

什么是软链接 软链接就跟快捷方式一样吧,通过当面目录链接到目标目录这里的current就是链到了V1目录

image.png

image.png

image.png

⚠️ 某些 Nginx 无法识别软链接(会触发下载问题)

2️⃣ 推荐用目录联接(junction)
mklink /J current V2
mklink /J previous V1

这种方式兼容性最好,Nginx 能直接当普通目录使用。


七、Linux 下软链接命令

ln -s /www/dist/V2 /www/dist/current
ln -s /www/dist/V1 /www/dist/previous

验证软链接是否正确:

ls -l /www/dist

输出类似:

current -> /www/dist/V2
previous -> /www/dist/V1

八、自动发布脚本示例(Linux + Windows 双版本)

💻 Linux 版 deploy.sh

readlink $APP_DIR/current 这是获取这个软链接的指向 如得到软链接目标路径,比如 "/home/webapp/dist/V2" 管道符 | 表示 把前一个命令的输出作为后一个命令的输入。 xargs basename 拆解 xargs:从标准输入(stdin)读取数据并作为参数传给后面的命令; basename:从一个路径中提取出“最后一层目录名”

#!/bin/bash
set -e

APP_DIR=/www/dist
NEW_VERSION=V3
 
OLD_VERSION=$(readlink $APP_DIR/current | xargs basename)

echo "📦 旧版本: $OLD_VERSION"
echo "🚀 新版本: $NEW_VERSION"

# 1️⃣ 更新软链接
ln -snf $APP_DIR/$NEW_VERSION $APP_DIR/current
ln -snf $APP_DIR/$OLD_VERSION $APP_DIR/previous

# 2️⃣ 重载 Nginx
nginx -t && nginx -s reload  好像不重装也不影响吧

echo "✅ 发布完成!current => $NEW_VERSION"

🪟 Windows 版 deploy.bat
@echo off
setlocal enabledelayedexpansion

set DIST=D:\AllProject\monorepo\apps\frontend\nginx_build\dist
set NEW_VERSION=V2

:: 获取当前版本
for /f "tokens=2 delims==>" %%i in ('dir /al "%DIST%" ^| find "current"') do set OLD_VERSION=%%i
set OLD_VERSION=%OLD_VERSION: =%
echo 📦 旧版本: %OLD_VERSION%
echo 🚀 新版本: %NEW_VERSION%

:: 删除旧链接并创建新链接
rmdir "%DIST%\previous"
mklink /J "%DIST%\previous" "%DIST%%OLD_VERSION%"
rmdir "%DIST%\current"
mklink /J "%DIST%\current" "%DIST%%NEW_VERSION%"

:: 重载 Nginx
nginx -t
nginx -s reload

echo ✅ 发布完成!
pause

九、发布流程总结

步骤操作
1️⃣打包新版本 → 输出到 dist/V3
2️⃣执行自动脚本(更新软链接)
3️⃣Nginx reload
老用户继续访问旧版,新用户自动访问新版

🔚 十、结语

通过这种方式,我们实现了:

✅ 新旧版本共存、懒加载资源无 404
✅ 一键切换版本,0 停机
✅ 兼容 Windows 与 Linux
✅ 资源缓存与入口缓存策略清晰

这是前端发布中非常实用的一种灰度替换策略,
尤其适合单页应用(SPA)项目使用。


也许你们也发现了,如果这样子的话不会出现每次发版都会多一个目录吗,是的但是呢可以根据情况写一个脚本打包的时候如果有超过两个的目录情况可以自行删掉避免占过多的内存。

最后

转载请注明出处,非常感谢!