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 项目

相关推荐
qq_3901617714 分钟前
防抖函数--应用场景及示例
前端·javascript
雨雪飘零34 分钟前
Windows系统使用OpenSSL生成自签名证书
nginx·证书·openssl
John.liu_Test44 分钟前
js下载excel示例demo
前端·javascript·excel
Yaml41 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
PleaSure乐事1 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro
哟哟耶耶1 小时前
js-将JavaScript对象或值转换为JSON字符串 JSON.stringify(this.SelectDataListCourse)
前端·javascript·json
getaxiosluo1 小时前
react jsx基本语法,脚手架,父子传参,refs等详解
前端·vue.js·react.js·前端框架·hook·jsx
理想不理想v1 小时前
vue种ref跟reactive的区别?
前端·javascript·vue.js·webpack·前端框架·node.js·ecmascript
知孤云出岫1 小时前
web 渗透学习指南——初学者防入狱篇
前端·网络安全·渗透·web
yanwushu1 小时前
Xserver v1.4.2发布,支持自动重载 nginx 配置
mysql·nginx·php·个人开发·composer