vue hash模式和history模式的本质区别
Table of Contents
hash模式与history模式url对比
以下为hash模式和history模式的区别
# hash模式(带#号)
https://www.example.com/#/user/manage
# history模式(不带#号)
https://www.example.com/user/manage
从url上来看,has模式多了/#
,但hash模式和history模式的本质区别,并不是有无#
号的区别,因为有无#
号最多算是“表面上的区别”。
出现两种模式的原因
出现这两种模式的原因有两个:
- 1、一是因为地址栏要有路径才能让使用者能感觉到这是一个正常的网页(否则,如果点哪儿路径都不变,但是页面数据又在变,会让人感觉怪怪的,也不方便查看参数);
- 2、二是因为要实现浏览器的“返回”操作(主要原因)。
其实对于vue开发的uniapp,你也看不到它的地址,所以这两种模式其实主要是用于支持浏览器“返回”操作的,如果不需要支持浏览器返回操作,那么点击按钮我局部更新数据就行了,根本不需要修改url。
返回操作:对于一个普通网页,我们点击链接可跳转到新页面(这里指在本页直接跳转,而不是打开新窗口或新标签),此时如果我们想返回上一个页面,那么我们点击浏览器左上角的“返回”按钮即可返回到上一个页面。
而能实现这一功能的本质,是因为当浏览器访问某个url时,会把该url入栈到浏览历史记录中。
可是vue实现的页面是单页面应用,页面对于浏览器来说,始终是不刷新的,而且也不建议刷新,因为一刷新,浏览器就会向刷新后的url发出请求,这个请求虽然不会导致报错,但它是不必要的,既影响使用体验,又发出了不必要的请求增加服务器压力。
比如如果点击某个链接刷新到以下url
https://www.example.com/user/manage
那么浏览器就会向该url发出请求,服务器端nginx收到这个请求,最终还是会返回index.html,这是由以下nginx配置决定的(try-files指令),但这个请求却是不必要的
location / {
try_files $uri $uri/ /index.html$is_args$args =404;
}
结论:浏览器不能使用“刷新”的方式来记录历史记录,可是我们使用vue编写的后台时,明明可以点“返回”来返回上一个页面,这到底是怎么实现的呢?
这里分两种情况
- 1、当使用hash模式时(浏览器url带
#
号时),是怎么实现返回上一个页面的? - 2、当使用history模式时(浏览器url不带
#
时),又是怎么实现返回上一个页面的?
另外,这两种模式实现的原理相同吗?答案当然是:不相同!
浏览器显示url
以下是Chrome(或chrome内核)浏览器显示百度和gogole主页的url的样子
但是,实际的url真的是你看到的这样的吗?其实不是的,你可以自己访问这两个url,再复制下来,看看你看到的跟复制下来的,是不是一样?
以下两个是复制下来的url,可以看到,它们的最后都多了一个斜杠(/
)
https://www.baidu.com/
https://www.google.com/
这到底是怎么回事呢?事实上url最后是一定有一个斜杠(/
)的,如果你看到没有,那只是浏览器为了好看,把它隐藏不显示而已,事实上它们发起请求时,请求的地址后面都是有一个斜杠(/
)的,如下图所示
有斜杠和没有斜杠到底有什么区别呢?拿google首页来说,它的完整的url,其实是这样的https://www.google.com/:443
,其中https://
表示协议,www.google.com
表示域名,/
表示请求的资源路径(path),:443
表示请求的资源所在的服务器的端口。
其中/
就代表它请求的“路径(path)”,在js中可以用window.location.pathname
来获取。
由于一个url必须要有一个路径,所以,如果你没有具体的路径时,那就是请求根目录,斜杠(/
)表示根目录。
所以根本就不存在“没有斜杠”的url,一个url它至少也是带有一个斜杠的。所以,实际上是不存在没有斜杠的链接的,你在浏览器上没看到,那是因为浏览器为了好看把它隐藏了而已。
hash模式
hash就是指浏览器url最后带有一个#
号,并且#
号后面跟着一个路径。
假设我们有一个页面url为链接A,链接A中有一个链接B和链接C,并且页面足够长,那么当点击链接B时,页面会滚动到id=test1
的位置,同理点击链接C,会滚动到id=test2
位置
# 链接A:
https://www.example.com/
# 链接B
https://www.example.com/#test1
# 链接C
https://www.example.com/#test2
也就是说,url后面的#
部分,其实是与页面中的某个id值相对应的,如果页面中不存在对应的id值,那么上面点击B链接时,就会没反应。
总之,无论页面中有没有对应的id值,以上从链接A切换到链接B的操作并不会导致页面被刷新(因为只有url中的“path”改变了页面才会刷新),而从链接A切换到链接B后,如果我们在链接B位置刚好放置链接C,此时我们占一下链接C又会滚动到链接C所以位置。那么此时如果我点击浏览器左上角的“返回”按钮,是能返回到链接B位置的,也就是说,锚点的改变是会被浏览器记录到历史记录中的。
由以上我们可以得出两个重要的知识点:
- 1、锚点值(hash)改变时,整个url(连同hash值)会被记录到浏览器历史记录中;
- 2、锚点值(hash)改变时,不会刷新页面,只会滚动页面(如果没有对应id值,连滚动也不会滚动,直接没有任何反应,但是整个url,包括锚点值也是会被记录到浏览器历史记录中的)。
这就是hash模式的原理,通过把path作为hash值,来避免浏览器刷新,但又能记录到历史。
比如以下url中,其实/user/manage
这整个就只是一个hash值,你不要看它有斜杠(/
)就认为它是路径,它其实只是个hash值而已,只不过vue获取到这个值后,会用js去处理这个值所对应的方法而已。
https://www.example.com/#/user/manage
当点击浏览器返回按钮时,会触发hashchange
事件,vue正是通过这个事件来获取到上一页的url中的hash值,并进行相应处理的
addEventListener("hashchange", (event) => {});
onhashchange = (event) => {};
history模式
从文档开头我们可知,history模式就是正常的url模式,它是“会改变path值”的,也就是说,理论上改变了这个值,浏览器应该是会刷新当前页面,并向新的url发起请求的,但我们实际使用的时候,发现浏览器并不会刷新当前页面,这是为什么呢?
因为history模式是通过以下js方法来修改url的
window.history.pushState('', '', url)
通过这种方式修改url,浏览器是不会刷新当前页面的,但修改的值同样会被浏览器记录到它的历史浏览记录里,从而实现:既能保存历史记录,又不刷新页面。
当点击浏览器返回按钮时,会触发popstate
事件,从而使用js从url中获取到path值,然后再根据具体的path来做出具体的动作(一般都是用js通过ajax请求path所对应的api)
addEventListener("popstate", (event) => {});
onpopstate = (event) => {};
hash模式与history模式区别
hash模式兼容性更强,因为这是早就有的功能,而history模式,由于要用到window.history.pushState()
函数,相比hash来说,它支持的更晚一点,但也早就支持了,只要不是老掉牙的浏览器,都不会不支持pushState()
。
我个人认为现在根本没必要搞hash模式,hash模式应该是一个历史遗留的功能,可能是在vue最初出现的时候(2014年),那时候浏览器不兼容pushState()
的情况可能比较常见吧,但现在都2023年了,压根没必要用hash这种老掉牙的方式了。
当然这只是我个人的猜测,我并没有深入了解vue源码,也没有详细的查资料,我只是从浏览器保存浏览历史的角度来分析的。
两种模式nginx配置区别
两种模式的nginx配置没有任何区别,nginx配置中只有以下配置为关键配置,其它配置都是通用的与项目无关(比如你要配置https,配置日志文件位置,开启gzip,配置防盗链,静态资源过期时间等等)
root /path/to/dist;
index index.html;
location / {
try_files $uri $uri/ /index.html$is_args$args =404;
}
对于以下两种方式来说,无论哪种方式,只有首次访问或自己主动刷新页面时,会向nginx服务器请求一次
# hash模式(带#号)
https://www.example.com/#/user/manage
# history模式(不带#号)
https://www.example.com/user/manage
对于hash模式来说:由于它的path为根目录/
,所以浏览器就会去访问根目录下的index.html文件,最终把index.html返回给浏览器,浏览器解析出index.html的内容,我们就看到了首页。并且由于把路径放在了#
后面,所以hash模式的path永远为根目录/
。
对于history模式来说:由于它的path为/user/manage
,所以nginx的location会收到访问/user/manage
的请求,可是文件并不存在,不存在会怎样呢?返回404吗?在这这边肯定不是404。
因为我的nginx配置为,注意我使用了try-files指令
location / {
try_files $uri $uri/ /index.html$is_args$args =404;
}
try-files指令后面跟着多个参数,每个参数都是一个文件路径,try_files会按顺序测试它后面每个参数所指定的文件是否存在,存在则返回给浏览器,不存在则测试下一个。
- 1、首先它会测试
$uri
是否存在,$uri
是nginx的一个变量,它的值就是url中的path值,本例为/user/manage
,所以它会测试root下是否存在/user/manage
,root是/path/to/dist
,所以它实际就是去测试以下文件是否存在,结果肯定是不存在/path/to/dist/user/manage
- 2、前面测试
$uri
不存在,它会往下测试$uri/
,实际上就是去找以下文件夹(最后多加了一个/
),但是nginx只能找文件,找不了文件夹,可是你又没有指定找这个文件夹下的哪个文件,于是它会找默认的文件(index index.html
指定默认文件为index.html)/path/to/dist/user/manage/
所以它会去找以下文件,结果很明显,这文件根本不存在
/path/to/dist/user/manage/index.html
- 3、上面找了
$uri/
不存在,它会继承往下找,往下就是这个参数了/index.html$is_args$args
它在root下找这个,肯定能找到,最终拼出来的路径是
/path/to/dist/index.html$is_args$args
$is_args
:如果path后面有参数,则$is_args
的值为?
号,否则为空字符串;$args
:如果path后面有参数,则$args
就是path后面的参数,否则为空字符串;- 由于本例的path为
/user/manage
,后面没有参数,所以这两个变量都为空,最终nginx会去找以下文件/path/to/dist/index.html
你说找不找的到?dist文件夹下明显有一个index.html呀,所以肯定找的到呀,所以就返回给浏览器了,这就是它不报错的原因。
实际nginx日志:
172.70.214.18 – – [17/May/2023:12:42:25 +0800] “GET /user/manage HTTP/2.0” 200 663 “-” “Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36”
[17/May/2023:12:39:46 +0800] “GET /user/manage HTTP/2.0” 304 0 “-” “Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36”
第一条日志状态码为200,即首次返回index.html时的状态码,而第二条就是你再次刷新时,由于返回的页面内容没有改变(还是index.html),所以会报304(Not Modified,即未修改)。
