VueRouter的hash和history模式 vue-router
在实现单页面前端路由时,提供了两种方式:Hash
模式和 History
模式
vue2 是根据 mode
参数来决定采用哪一种方式
vue3 则是 history
参数
hash 简述 localhost:8080/#/home
vue-router
默认 hash 模式 —— 使用 URL 的 hash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载。 hash
(#)是 URL
的锚点,代表的是网页中的一个位置,单单改变 #
后的部分,浏览器只会滚动到相应位置,不会重新加载网页,也就是说 #
是用来指导浏览器动作的,对服务器端完全无用,HTTP
请求中也不会不包括 #
,同时每一次改变 #
后的部分,都会在浏览器的访问历史中增加一个记录,使用 “后退” 按钮,就可以回到上一个位置,所以说 hash
模式通过锚点值的改变,根据不同的值,渲染指定 DOM
位置的不同数据。
#
符号本身以及它后面的字符称之为 hash
,可通过 window.location.hash
属性读取。
对 SEO 不友好
实现 ==hash
通过 window.onhashchange
的方式,来监听 hash
的改变,根据 hash 的改变值来渲染对应路由。 ==
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta http-equiv ="X-UA-Compatible" content ="IE=edge" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > Document</title > </head > <body > <ul > <li > <a href ="#/home" > 首页</a > </li > <li > <a href ="#/about" > 关于</a > </li > </ul > <div id ="routerView" > </div > <script > let routerView = document .getElementById ('routerView' ) window .addEventListener ('hashchange' , onHashChange) function onHashChange ( ) { switch (location.hash ) { case '#/home' : routerView.innerHTML = '首页' break case '#/about' : routerView.innerHTML = '关于' break default : return } } </script > </body > </html >
稍复杂一些的实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" /> <meta http-equiv ="X-UA-Compatible" content ="IE=edge" /> <meta content ="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover" name ="viewport" /> <title > 实现简单的hash路由</title > <style > * { margin : 0 ; padding : 0 ; box-sizing : border-box; } html , body { height : 100% ; } #content { height : calc (100vh - 50px ); display : flex; align-items : center; justify-content : center; font-size : 3em ; } #nav { height : 50px ; position : fixed; left : 0 ; bottom : 0 ; width : 100% ; display : flex; } #nav a { width : 25% ; display : flex; justify-content : center; align-items : center; border : 1px solid black; } #nav a :not (:last-of-type ) { border-right : none; } </style > </head > <body > <main id ="content" > </main > <nav id ="nav" > <a href ="#/" > 首页</a > <a href ="#/shop" > 商城</a > <a href ="#/shopping-cart" > 购物车</a > <a href ="#/mine" > 我的</a > </nav > </body > <script > class VueRouter { constructor (routes = [] ) { this .routes = routes; this .currentHash = "" ; this .refresh = this .refresh .bind (this ); window .addEventListener ("load" , this .refresh , false ); window .addEventListener ("hashchange" , this .refresh , false ); } getUrlPath (url ) { return url.indexOf ("#" ) >= 0 ? url.slice (url.indexOf ("#" ) + 1 ) : "/" ; } refresh (event ) { let newHash = "" , oldHash = null ; if (event.newURL ) { oldHash = this .getUrlPath (event.oldURL || "" ); newHash = this .getUrlPath (event.newURL || "" ); } else { newHash = this .getUrlPath (window .location .hash ); } this .currentHash = newHash; this .matchComponent (); } matchComponent ( ) { let curRoute = this .routes .find ( (route ) => route.path === this .currentHash ); if (!curRoute) { curRoute = this .routes .find ((route ) => route.path === "/" ); } const { component } = curRoute; document .querySelector ("#content" ).innerHTML = component; } } const router = new VueRouter ([ { path : "/" , name : "home" , component : "<div>首页内容</div>" }, { path : "/shop" , name : "shop" , component : "<div>商城内容</div>" }, { path : "/shopping-cart" , name : "shopping-cart" , component : "<div>购物车内容</div>" }, { path : "/mine" , name : "mine" , component : "<div>我的内容</div>" } ]); </script > </html >
设置hash模式 vue2 中默认为 mode:hash
vue3 中使用 createWebHashHistory
1 2 3 4 5 6 import { createWebHashHistory } from 'vue-router' const router = createRouter({ history: createWebHashHistory(), routes });
history 简述 localhost:8080/home
history
是路由的另一种模式,由于 hash 模式会在 url 中带#,如果不想要带 #的话,我们可以使用路由的 history
模式,只需要在响应的 router
配置规则时加上即可,vue
的路由默认是 hash
模式。
利用了HTML5 History Interface
中新增的 pushState()
和 replaceState()
方法。
这两个方法应用于浏览器的历史记录栈,在当前已有的 back、forward、go
的基础之上,它们提供了对历史记录进行修改的功能。只是当它们执行修改时,虽然改变了当前的 URL,但浏览器不会立即向后端发送请求。
history api
window.history.go 可以跳转到浏览器会话历史中的指定的某一个记录页
window.history.forward 指向浏览器会话历史中的下一页,跟浏览器的前进按钮相同
window.history.back 返回浏览器会话历史中的上一页,跟浏览器的回退按钮功能相同
window.history.pushState 可以将给定的数据压入到浏览器会话历史栈中
window.history.replaceState 将当前的会话页面的url替换成指定的数据
优势
美观,没有#
pushState() 相比于直接修改 hash 的优势
pushState()设置的新 URL 可以是与当前 URL 同源的任意 URL;而hash
只可修改#
后面的部分,因此只能设置与当前 URL 同文档的 URL;
pushState()设置的新 URL 可以与当前 URL 一模一样,这样也会把记录添加到栈中; 而hash
设置的新值必须与原来不一样才会触发动作将记录添加到栈中;
pushState()通过stateObject
参数可以添加任意类型的数据到记录中; 而hash
只可添加短字符串;
pushState()可额外设置title
属性供后续使用。
缺陷 需要后端配置(nginx代理转发,找不到就重定向到项目首页)
否则刷新页面会报404,因为会重新发请求,而此时的 url 已经改变。
具体的说:当我们把 history 项目部署到服务器中后,此时我们在浏览器输入一个网址(比如是 www.test.com ), 此时会经过 dns 解析,拿到 ip 地址后根据 ip 地址向该服务器发起请求,服务器接受到请求后,然后返回相应的结果(html,css,js)。如果我们在前端设置了重定向,此时页面会进行跳转到 www.test.com/home ,在前端会进行匹配对应的组件然后将其渲染到页面上。此时如果我们刷新页面的话,浏览器会发送新的请求 www.test.com/home , 如果后端服务器没有 /home 对应的接口,那么就会返回404。
hash模式不会404,因为 #及之后的内容并不会放入请求
注意:开发环境没有这个问题,因为 webpack 处理好了。生产环境才会有问题。
实现
HTML5提供了History API来实现URL的变化,其中最主要的两个API有以下两个 history.pushState()和history.replaceState()。这两个API可以在不进行刷新的情况下,操作浏览器的历史记录。唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录
而 history 对比 hash 不同的是通过阻塞浏览器 url 地址栏更改之后的跳转,然后将其 pathname 拼接上去,实现路由,具体代码实现如下:
==每当激活的历史记录发生变化时,都会触发popstate事件。==
理论上我们可以通过监听popstate来控制路由渲染。
但是pushstate和replacestate是不会触发popstate事件的;go/back/forward可以触发.
所以可以重写 pushstate 和 replaceState
1 2 3 4 5 6 7 8 9 10 11 12 13 let _wr = function (type ) { let orig = history[type] return function ( ) { let rv = orig.apply (this , arguments ) let e = new Event (type) e.arguments = arguments window .dispatchEvent (e) return rv } } history.pushState = _wr ('pushState' ) history.replaceState = _wr ('replaceState' )
或者手动触发?
页面加载成功时触发一次popstate,并给所有可能的路由跳转按钮加上点击事件,点击事件,调用pushState,后调用一次popstate
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta http-equiv ="X-UA-Compatible" content ="IE=edge" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > Document</title > </head > <body > <ul > <li > <a href ="#/home" > 首页</a > </li > <li > <a href ="#/about" > 关于</a > </li > </ul > <div id ="routerView" > </div > <script > let routerView = document .getElementById ('routerView' ) window .addEventListener ('DOMContentLoaded' , onLoad) function onLoad ( ) { PoPState () let links = document .querySelectorAll ('li a[href]' ) links.forEach (a => { a.addEventListener ('click' , (e ) => { e.preventDefault () history.pushState (null , '' , a.getAttribute ('href' )) PoPState () }) }) } onLoad () function PoPState ( ) { console .log (location.pathname ) switch (location.pathname ) { case '/home' : routerView.innerHTML = '<h2>home page</h2>' return case '/about' : routerView.innerHTML = '<h2>about page</h2>' return default : return } } </script > </body > </html >
设置history模式 vue2 中设置 mode:history
vue3 中设置 createWebHistory
1 2 3 4 5 6 import { createWebHistory } from 'vue-router' const router = createRouter({ history: createWebHistory(), routes });
实际项目中对两种模式的使用
我们看 vue3 的官网
点击左侧的章节,中间部分会被整体替换,使用的是 history 模式。url 如下
https://cn.vuejs.org/guide/introduction.html
点击右侧的本页目录,中间部分不会替换,而是滚动到对应位置,使用的是 hash 模式。url 如下
https://cn.vuejs.org/guide/typescript/composition-api.html#typing-component-props
注意:上面写的是错的,这里其实根本不是 hash,而是 html 自带的锚点。
现在感觉大部分的网页都使用的是 history,可能是因为hash对 SEO 不友好?
参考 https://juejin.cn/post/7116336664540086286#heading-7
https://juejin.cn/post/7096034733649297421
https://juejin.cn/post/7127143415879303204#heading-5