Nginx 子路径部署 React SPA 踩坑总结

背景

将 React 单页应用(SPA)部署在 Nginx 子路径(/company//company-admin/)下,与个人博客(/)共用同一台服务器,过程中遇到了三个典型问题。


问题一:alias + try_files 回退路径错误

现象

刷新页面(如 /company/products)或直接访问子路由时,返回 404 或跳转到根路径。

错误配置

location /company/ {
    alias /var/www/company-font/;
    index index.html;
    try_files $uri $uri/ /company/index.html;  # ❌ 错误
}

根因

alias 块中,try_files 的回退路径 /company/index.html 是相对于全局 root(如 /var/www/personal-website)解析的,而不是 alias 指定的目录。该路径不存在,导致 Nginx 触发异常跳转。

正确配置

location /company/ {
    alias /var/www/company-font/;
    index index.html;
    try_files $uri $uri/ /var/www/company-font/index.html;  # ✅ 使用绝对路径
}

结论

alias 块中,try_files 的 fallback 必须使用文件系统绝对路径。


问题二:无尾斜杠路径 /company 被错误路由

现象

访问 http://example.com/company(无尾斜杠)时,返回了个人博客首页而不是企业官网。

根因

Nginx 的 location /company/(带尾斜杠)不匹配 /company(无尾斜杠)请求,该请求被 location / 兜底处理,返回了 /var/www/personal-website/index.html

修复

新增精确匹配 location,将无尾斜杠请求 301 重定向到有尾斜杠路径:

location = /company {
    return 301 /company/;
}
location = /company-admin {
    return 301 /company-admin/;
}

问题三:React 登录后 navigate('/') 跳出子路径

现象

用户登录成功后,页面跳转到了 http://example.com/ 而不是 http://example.com/company/

根因分析

React Router v6 中,BrowserRouter 设置了 basename="/company"navigate('/') 理论上会跳转到 /company/。但实际触发跳出的原因是:

  1. 旧代码在 navigate('/') 之后紧接着调用了 window.location.reload()
  2. navigate 是异步的,reload() 在路由未完成切换时执行,此时 URL 还在 /company/login
  3. reload 触发浏览器刷新,命中了上述问题二(/company 无斜杠被个人博客兜底),最终落到了根路径

修复

移除 window.location.reload(),改用 navigatereplace 选项:

// ❌ 旧代码
navigate('/');
window.location.reload();

// ✅ 新代码
navigate('/', { replace: true });

问题四:登录后 Navbar 状态不同步

现象

登录成功跳转回首页后,Navbar 仍然显示"登录/注册"按钮,没有变成用户信息。

根因

Navbar 组件读取登录状态的 useEffect 依赖数组为空,只在组件挂载时执行一次。登录操作在 Login 组件中完成,写入 localStorage 后 Navbar 不会感知到变化。

// ❌ 问题代码:只在挂载时读一次
useEffect(() => {
  setLoggedIn(isAuthenticated());
  setUser(getUser());
}, []);

修复

登录时派发自定义事件,Navbar 监听该事件刷新状态:

Login.jsx:

setToken(data.token);
setUser({ username, nickname, role });
window.dispatchEvent(new Event('auth-change')); // 通知 Navbar
navigate('/', { replace: true });

Navbar.jsx:

const refreshAuth = useCallback(() => {
  setLoggedIn(isAuthenticated());
  setUser(getUser());
}, []);

useEffect(() => {
  refreshAuth();
  window.addEventListener('auth-change', refreshAuth);
  return () => window.removeEventListener('auth-change', refreshAuth);
}, [refreshAuth]);

最终完整 Nginx 配置

server {
    listen 80;
    server_name _;

    root /var/www/personal-website;
    index index.html;

    client_max_body_size 20m;

    # 无尾斜杠重定向,防止被 location / 兜底
    location = /company {
        return 301 /company/;
    }
    location = /company-admin {
        return 301 /company-admin/;
    }

    # 个人博客(根路径)
    location / {
        try_files $uri $uri/ /index.html;
    }

    # 企业官网前端
    location /company/ {
        alias /var/www/company-font/;
        index index.html;
        try_files $uri $uri/ /var/www/company-font/index.html;
    }

    # 管理后台前端
    location /company-admin/ {
        alias /var/www/company-admin/;
        index index.html;
        try_files $uri $uri/ /var/www/company-admin/index.html;
    }

    # 后端 API(去掉前缀后转发)
    location /company-api/ {
        rewrite ^/company-api/(.*)$ /api/$1 break;
        proxy_pass http://127.0.0.1:9080;
        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;
    }
}

总结

问题 根因 修复要点
alias + try_files 404 fallback 路径相对于 root 解析 使用文件系统绝对路径
无尾斜杠被根路径兜底 location /company/ 不匹配 /company 新增 location = /company 做 301 跳转
登录后跳出子路径 window.location.reload() 与异步 navigate 竞争 移除 reload,用 navigate('/', { replace: true })
登录后 Navbar 不更新 useEffect 空依赖只执行一次 自定义事件 auth-change 跨组件通知

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐