我们使用 vue 搭建的 SPA(single page web application) 网站,之所以能够做到只加载单个 HTML 页面就可以在后续用户与网页交互时动态更新页面的内容,比如点击某个导航,浏览器的 url 和页面的内容就会相应地改变,看起来就像是页面发生了跳转一样,但事实上页面并没有重载,借助的就是前端路由。
前端路由,简单地说就是 url 与内容的映射,有 2 种实现方式:hash 和 history,分别对应着 Vue Router 中的 Hash 模式和 HTML5 模式,本篇文章就对它们进行个介绍。
URL 的 hash
在一个很长的页面中,想实现点击某个导航让页面滚动到指定位置,我们就可以使用到 url 的 hash:
html
<!-- 代码片段 1 -->
<a href="#1">1</a>
<a href="#2">2</a>
<div style="height: 100vh">---</div>
<div id="1">1</div>
<div style="height: 100vh">---</div>
<div id="2">2</div>
如下图所示,点击 <a href="#1">1</a>
,页面就会滚动到 <div id="1">1</div>
。可以看到,url 后面多出了 "#1" ,页面也没有重载。
利用 url 的 hash 的特性,我们就可以实现个简单的前端路由了:
html
<!-- 代码片段 2 -->
<a href="#home">首页</a>
<a href="#profile">个人中心</a>
<div id="router-view">首页内容</div>
<script>
const routerView = document.getElementById('router-view')
window.addEventListener('hashchange', () => {
switch (location.hash) {
case '#home':
routerView.innerHTML = '首页内容'
break
case '#profile':
routerView.innerHTML = '个人中心'
break
default:
routerView.innerHTML = '首页内容'
break
}
})
</script>
a 标签就相当于 Vue Router 中的 <router-link>
,而 <div id="router-view">
就相当于是 <router-view>
。当 url 中的 hash 值发生改变时,就会触发 hashchange
事件,然后我们可以通过 location.hash
获取到当前的 url 中的 hash 值,接着就可以根据不同的 hash,往 id 为 router-view 的 div 内插入不同的内容了:
hash 模式的优点是兼容性好,但有个不好的地方在于 url 中会出现一个 "#",对于某些对 url 有执念的人来说可能就会觉得很难受。所幸,HTML5 新增的 history 接口可以解决这个问题。
HTML5 的 history
先看看 MDN 的介绍:
History 接口允许操作浏览器的曾经在标签页或者框架里访问的会话历史记录。
history 有 5 个方法可以来改变 url 而不刷新页面。下面还是以点击 a 标签改变 <div id="router-view">
里的内容为例来做介绍:
html
<!-- 代码片段 3 -->
<a href="home">首页</a>
<a href="profile">个人中心</a>
<div id="router-view">首页内容</div>
如果直接像上面这样写,那么点击 <a href="home">首页</a>
, 页面会跳转到 http://xxx/home
,浏览器会去我们起动的服务器的根目录下寻找 home 文件,重新加载资源,我们需要阻止这一默认行为:
javascript
// 代码片段 4
const aEls = document.getElementsByTagName('a')
for (const item of aEls) {
item.addEventListener('click', e => {
e.preventDefault()
})
pushState()
现在点击 a 标签,浏览器就不会去请求资源了,但 url 也不会有变化,我们可以通过 history.pushState()
来让 url 发生改变,其接收 3 个参数,第一个为状态对象 state,我们可以传一个空对象;第二个参数为 title,当前大多数浏览器都忽略此参数,我们直接传 ''
,第三个参数就是 url,可以由 a 标签的 href
属性获取:
javascript
// 代码片段 5
for (const item of aEls) {
item.addEventListener('click', e => {
e.preventDefault()
const href = item.getAttribute('href')
history.pushState({}, '', href)
})
}
在 url 发生改变后通过 location.pathname
获取到当前的路径,再去改变 <div id="router-view">
的内容:
javascript
// 代码片段 6,接在代码片段 5 history.pushState({}, '', href) 之后
switch (location.pathname) {
case '/home':
routerView.innerHTML = '首页内容'
break
case '/profile':
routerView.innerHTML = '个人中心'
break
default:
routerView.innerHTML = '首页内容'
break
}
效果如下:
可以看到,现在 url 中就没有 "#" 号了。但是请注意,history 模式下,我们的项目需要通过服务器运行,如果是直接通过文件在浏览器打开,点击 a 标签时是会报错的:
而 hash 模式不会有这问题。
popstate 事件
再仔细看上面的动图,可以发现,当我们点击浏览器的后退按钮(左箭头)或前进按钮(右箭头)时,url 虽然变化了,但是 <div id="router-view">
内的内容没有变化,如果想正确地显示,我们可以监听 popstate
事件:
javascript
// 代码片段 7
window.addEventListener('popstate', () => {
switch (location.pathname) {
case '/home':
routerView.innerHTML = '首页内容'
break
// ... 省略,同代码片段 6 一样
}
})
现在点击前进后退,内容就会对应地改变了:
replaceState()
但是,如果在代码片段 5 中,我们使用的不是 pushState(
) 而是 replaceState()
,则不会在历史堆栈中添加 一个状态,而是修改当前历史记录实体,这就导致我们点击 a 标签后,后退按钮仍然不可用:
其余方法
history 还有 back()
、forward()
和 go()
方法,比较简单,见名知意,可参见 MDN 文档,就不多做介绍了。