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 渲染相应的组件或页面内容。
所以,上述示例首次打开页面的流程应该是这样的:
- 浏览器打开
http://127.0.0.1:8080
,发送document
请求,服务器返回index.html
文件; - 浏览器解析 HTML 并加载其中的 JS 和 CSS 文件;
- 执行 JS,让
History API
接管路由,重定向到http://127.0.0.1:8080/login
; - 渲染
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.
-
按指定顺序检查文件是否存在,并使用第一个找到的文件进行请求处理,处理只在当前上下文中执行。
-
文件的路径是根据
root
和alias
两个指令,从文件参数中构造出来的。 -
如果名称末尾指定斜杠,则表示为一个目录,比如
$uri/
。此时会在该目录下查找由index
指令指定的初始页(默认初始页为index.html
) -
如果没有找到任何文件,则会对最后一个参数中指定的 uri 进行内部重定向。
请注意: 最后一个参数是回退 URI 且必须存在(命名 location 也可以当做最后一个参数使用),否则会出现内部 500 错误。
nginxlocation /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
。查找顺序如下:- 查找
/data/user/images/assets
文件; - 查找
/data/user/images/assets
文件夹里的index
指定文件,即index.gif
; - 查找
/data/user/images/default.gif
文件 - 都没找到,则对最后一个参数进行重定向,最终匹配到
@gif
location 模块。
- 查找
index
- 该指令后面可以跟多个文件,用空格隔开;
- 如果包括多个文件,Nginx 会根据文件的枚举顺序来检查,直到查找的文件存在;
- 文件可以是相对路径也可以是绝对路径,绝对路径需要放在最后;
- 文件可以使用变量
$
来命名; - 该指令默认值为
index index.html
。
index 实际工作方式:
- 如果文件存在,则使用文件作为路径,发起内部重定向。直观上看就像再一次从客户端发起请求,Nginx 再一次搜索 location 一样。
- 既然是内部重定向,域名 + 端口不发生变化,所以只会在同一个 server 下搜索。
- 同样,如果内部重定向发生在 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;
}
}
- 如果你使用
example.org
或www.example.org
直接发起请求,那么首先会访问到/
的location,结合root
与index
指令,会先判断/data/www/index.html
是否存在,如果不存在,则接着查看/data/www/index.php
。 - 如果
/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 的问题。