SPA 与 History 模式下的 404 问题

SPA(单页面应用)是一种基于前端路由的应用架构,它在加载初始页面后,通过 JavaScript 动态地更新页面内容,而不是通过传统的页面刷新。因此,当你在 SPA 中进行页面刷新时,可能会导致出现 404 错误的情况。

SPA 是如何渲染页面的

请看以下示例:

请仔细看 Network 中的第一个 127.0.0.1, 这是一个 document 类型的请求。当你第一次打开 SPA 时,浏览器会发送该请求到服务器,服务器会返回 index.html 文件作为初始页面。这是因为在服务器配置中,通常会将所有的请求都指向 index.html 文件,无论是刷新页面还是直接访问某个 URL。(比如 Nginx 中的 try_files 或 Webpack 中的 historyApiFallback)。

打开这条请求,我们可以清楚的看到 html 的内容:

然后,浏览器会解析该 HTML 文件,并加载其中的 JS 和 CSS 文件。通过 JS 的执行,SPA 应用会初始化并根据当前的 URL 渲染相应的组件或页面内容。

所以,上述示例首次打开页面的流程应该是这样的:

  1. 浏览器打开 http://127.0.0.1:8080,发送 document 请求,服务器返回 index.html 文件;
  2. 浏览器解析 HTML 并加载其中的 JS 和 CSS 文件;
  3. 执行 JS,让 History API 接管路由,重定向到 http://127.0.0.1:8080/login
  4. 渲染 login 页的内容。

刷新后的 404

当我们第二次刷新浏览器时,页面丢失,可以看到请求 404。

这是因为在 SPA 中进行页面刷新时,服务器同样会尝试根据刷新的 URL 返回对应的页面,但是服务器上压根就没有与 /login 相对应的实际文件。

你可以理解为这个自定义的 /login 是给 JS 的 router 用的(不管是 vue-router 还是 react-router),并不是给服务器用的。

配置 Webapck

在本地开发中,你可以启用 devServer.historyApiFallback 来防止 404 的发生。

js 复制代码
module.exports = {
  //...
  devServer: {
    historyApiFallback: true,
  },
};

也可以提供一个对象,通过 rewrites 这样的配置项进一步控制:

js 复制代码
historyApiFallback: {
    rewrites: [
        {
            from: /\//,
            // 注意! 如果设置了publicPath, 则需要: `${publicPath}index.html`
            // 名字与 HtmlWebpackPlugin 中的 filename 一致
            to: '/index.html',
        },
    ],
},

配置 Nginx

这里我们稍微介绍下与解决 404 问题相关的几个配置项。

try_files

Checks the existence of files in the specified order and uses the first found file for request processing; the processing is performed in the current context. The path to a file is constructed from the file parameter according to the root and alias directives. It is possible to check directory's existence by specifying a slash at the end of a name, e.g. "$uri/". If none of the files were found, an internal redirect to the uri specified in the last parameter is made.

  • 按指定顺序检查文件是否存在,并使用第一个找到的文件进行请求处理,处理只在当前上下文中执行。

  • 文件的路径是根据 rootalias 两个指令,从文件参数中构造出来的。

  • 如果名称末尾指定斜杠,则表示为一个目录,比如 $uri/。此时会在该目录下查找由 index 指令指定的初始页(默认初始页为 index.html

  • 如果没有找到任何文件,则会对最后一个参数中指定的 uri 进行内部重定向。

    请注意: 最后一个参数是回退 URI 且必须存在(命名 location 也可以当做最后一个参数使用),否则会出现内部 500 错误。

    nginx 复制代码
    location /images/ {
        root /data/user/;
        index index.gif;
        try_files $uri $uri/ /images/default.gif @gif;
    }
    
    location @gif {
        expires 30s;
    }

    当请求 http://localhost:8080/images/assets 时,$uri/images/assets。查找顺序如下:

    1. 查找 /data/user/images/assets文件;
    2. 查找 /data/user/images/assets文件夹里的 index 指定文件,即 index.gif
    3. 查找 /data/user/images/default.gif 文件
    4. 都没找到,则对最后一个参数进行重定向,最终匹配到 @gif location 模块。

index

  • 该指令后面可以跟多个文件,用空格隔开;
  • 如果包括多个文件,Nginx 会根据文件的枚举顺序来检查,直到查找的文件存在;
  • 文件可以是相对路径也可以是绝对路径,绝对路径需要放在最后;
  • 文件可以使用变量$来命名;
  • 该指令默认值为 index index.html

index 实际工作方式:

  1. 如果文件存在,则使用文件作为路径,发起内部重定向。直观上看就像再一次从客户端发起请求,Nginx 再一次搜索 location 一样。
  2. 既然是内部重定向,域名 + 端口不发生变化,所以只会在同一个 server 下搜索。
  3. 同样,如果内部重定向发生在 proxy_pass 反向代理后,那么重定向只会发生在代理配置中的同一个server。

示例:

nginx 复制代码
server {
    listen      80;
    server_name example.org www.example.org;    
    
    location / {
        root    /data/www;
        index   index.html index.php;
    }
    
    location ~ \.php$ {
        root    /data/www/test;
    }
}
  1. 如果你使用 example.orgwww.example.org 直接发起请求,那么首先会访问到 / 的location,结合rootindex 指令,会先判断 /data/www/index.html 是否存在,如果不存在,则接着查看 /data/www/index.php
  2. 如果 /data/www/index.php 存在,则使用 /index.php 发起内部重定向,就像从客户端再一次发起请求一样,Nginx 会再一次搜索 location,毫无疑问匹配到第二个 ~ \.php$,从而访问到 /data/www/test/index.php

rewrite

语法:rewrite regex replacement [flag];

regex:正则表达式;

replacement:替换内容;

flag:命令执行模式,有两个值 [break | last]

rewrite主要功能就是使用 Nginx 提供的全局变量或自己设置的变量,结合正则表达式和标志位实现 url 重写以及重定向。

  • 对 url 进行重写指的是重写真实请求路径,如果是同域内,浏览器不会发生跳转(302),如果是非同域浏览器会发生跳转(307)。

  • 只能对域名后边的除去查询字符串的部分起作用,例如 seanlook.com/a/we/index.... 只对 /a/we/index.php 重写。

  • break 表示重写后停止不再匹配,一般用于接口重定向;

    例如将 http://127.0.0.1/down/123.xls 冲重定向到 http://192.168.0.1:8080/file/123.xls 来解决跨域下载。

  • last 表示重写后跳到 server 块再次用重写后的地址匹配,用于请求路径发生改变的常规需求。

    例如将 http://127.0.0.1/request/getlist 的请求代理到 http://127.0.0.1/api/getlist 上。

本地 Nginx 部署测试

有了前置基础,我们可以使用 build 打包本地项目,将打包后的 dist 文件夹整个丢进 Nginx 的 html 文件夹内,然后进行以下配置。

yml 复制代码
    http {
        # 本地打包部署测试
        server {
        # 配置完 server_name, 记得配置 host, 然后访问 http://server_name:listen
        server_name demo.com;
        listen 8080;
        proxy_set_header Host $host;
        charset koi8-r;
        access_log logs/host.access.log format;
    
        # 打包部署
        location / {
            root html/dist;
            index index.html index.htm;
            try_files $uri $uri/ @router;
        }

        # 主要原因是路由的路径资源并不是一个真实的路径,所以无法找到具体的文件
        # 因此需要 rewrite 到 index.html 中,然后交给前端路由再处理请求资源
        location @router {
            rewrite ^.*$ /index.html last;
        }
    }

这样,我们就能在本地部署 Nginx 测试打包后的项目能否正常工作了,并且不用担心刷新后遇到 404 的问题。

参考资料

try_files | Nginx

index | Nginx

nginx 配置选项 try_files 详解

Nginx之坑:完全理解location中的index

nginx 部署 React 项目

相关推荐
崔庆才丨静觅3 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60614 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了4 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅4 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅4 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅5 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment5 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅5 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊5 小时前
jwt介绍
前端
爱敲代码的小鱼5 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax