序言
在前端世界中,我们经常听到关于路由
的讨论,而服务器端的大佬们似乎早已熟知"路径"的奥秘,用其来描述路径。不过,在前端领域,我们借用了这个名词,并用它来描述 URL
和组件
的那种特殊的映射关系。今天,让我们揭开前端路由的神秘面纱,看看如何在浏览器中实现路由的魔法变换。
实现路由需要解决的问题
在解决问题之前,我们首先要了解问题是什么。实现路由,其实就是在不引起页面刷新的情况下,巧妙地改变 URL,并且需要有一种方法来知道 URL 发生了变化从而可以将特定的组件渲染到相应的页面中。 听起来容易,但细细琢磨,里面还是有点门道的。
实现路由的"大问题":
-
如何修改URL,还不引起页面的刷新
-
如何知道URL变化了
如何解决这两个大问题?
Hash
你有没有注意到,有些网址最后面会跟着一个 #
加上一串字符?这个字符其实是 URL 的"小尾巴",也就是 hash
。
不同于引起页面刷新的完整 URL 改变,只修改 hash
部分是不会刷新页面的。 这就好比是在 URL 后面加了一条小小的船尾,船在航行,但船身却依然安稳。
html
<ul>
<li><a href="#/home">首页</a></li>
<li><a href="#/about">关于</a></li>
</ul>
如果我们在页面中放两个超链接,并且在路径前加一个#
会发生什么呢?
当我们点击这两个超链接时,页面的URL会发生改变但是不会并刷新页面。
我们解决了第一个问题,但是我们如何知道页面的URL发生了变化呢?
欸,有朋友可能会说了,既然我们是因为点击触发的页面跳转,那我们监听点击事件不就行了?
其实呢,也不是不行,但是!!! 如果我们页面中有一百多个类似于a
标签这样的跳转链接,你是不是一个个加逻辑处理?
没有其他办法了吗? 有!
在原生浏览器中有一个事件叫做hashchange
:当页面的 hash
发生变化时,浏览器会触发 hashchange
事件,从而允许我们添加相关的操作。
到这里我们就通过Hash解决了这两个问题。
History
而另一位帮手则是 history
。这位大神提供了一个 pushState
方法,可以轻松修改 URL 而不引起页面刷新。有点像是在 URL 上粘贴一张贴纸,页面依然是原来的页面,只是 URL 发生了点小改变。
另外,history
还贴心地提供了一个 popState
事件,专门用于捕捉浏览器前进和后退的时候,告诉我们 URL 又发生了变化。这就好比是浏览器在推一下,告诉你:"快,看看 URL 变啦!"
Hash vs History
在上面我们已经了解到解决问题的两种实现方法,接下来让我们开启实战,从下面这两段代码中分别深入了解
Hash vs History
他们是如何实现路由的,并细致分析。
用Hash实现路由
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hash</title>
</head>
<body>
<ul>
<li><a href="#/home">首页</a></li>
<li><a href="#/about">关于</a></li>
</ul>
<div id="routeView">
<!-- 放一个代码片段 -->
</div>
<script>
const routes = [
{
path: '#/home',
component: '首页页面内容'
},
{
path: '#/about',
component: 'about page'
}
]
const routeView = document.getElementById('routeView')
window.addEventListener('DOMContentLoaded', onHashChange)
window.addEventListener('hashchange', onHashChange)
function onHashChange() {
console.log(location.hash);
routes.forEach( item => {
if (item.path === location.hash) {
routeView.innerHTML = item.component
}
})
}
</script>
</body>
</html>
页面效果如下
解释一下代码: 类似于VUE中的路由,我们在JS里定义了一个数组routes
,并在数组中添加多个对象,存放多个页面的URL和组件内容,这里我们简单一点就给个字符串。所以我们现在要做的就是捕获页面发生跳转同时将相应的组件映射到页面中去。
1. 首先我们获取到页面div容器的DOM结构
js
const routeView = document.getElementById('routeView')
2. 当页面初次加载或者URL发生改变时将对应的URL映射的组件添加到页面中
js
window.addEventListener('DOMContentLoaded', onHashChange)
window.addEventListener('hashchange', onHashChange)
DOMContentLoaded
事件表示文档已经完全加载和解析,不包括外部资源如图片和样式表。我们这行代码添加了一个事件监听器,当整个HTML文档加载完成时触发DOMContentLoaded
事件,然后调用onHashChange
函数。hashchange
事件表示浏览器地址栏中的哈希部分发生变化。这行代码添加了另一个事件监听器,当浏览器的URL中的哈希部分(即#
后面的部分)发生变化时触发hashchange
事件,同样调用onHashChange
函数。
onHashChange函数
js
function onHashChange() {
console.log(location.hash);
routes.forEach( item => {
if (item.path === location.hash) {
routeView.innerHTML = item.component
}
})
}
首先我们遍历route拿到里面每一项,我们在浏览器里面可以看到每一项里面有一个属性叫做path就代表其URL ,欸这不就是我们需要用到的吗,所以我们就拿出这个属性与页面的URL对比,如果相同则将这一item中的component添加到容器中就OK啦~
用History实现路由
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>History</title>
</head>
<body>
<ul>
<li><a href="/home">首页</a></li>
<li><a href="/about">关于</a></li>
</ul>
<div id="routeView">
<!-- 放一个代码片段 -->
</div>
<script>
const routes = [
{
path: '/home',
component: '<h2>首页页面内容</h2>'
},
{
path: '/about',
component: '<h3>about page</h3>'
}
]
const routeView = document.getElementById('routeView')
window.addEventListener('DOMContentLoaded', onLoad)
window.addEventListener('popstate', onPopState)
function onLoad() {
onPopState()
const links = document.querySelectorAll('li a')
links.forEach(a => {
a.addEventListener('click', (e) =>{
e.preventDefault() // 阻止了a标签的默认跳转行为
// 添加一个可以修改url又不造成页面刷新
history.pushState(null, '', a.getAttribute('href'))
// 映射对应的dom
onPopState()
})
})
}
function onPopState() {
console.log(location.pathname);
routes.forEach(item => {
if (item.path === location.pathname) {
routeView.innerHTML = item.component
}
})
}
</script>
</body>
</html>
页面效果如下
我们发现我不添加#
也可以实现同样的效果,那接下来让我们分析一下这份代码吧:
1. 首先我们获取到页面div容器的DOM结构
js
const routeView = document.getElementById('routeView')
2. 当页面初次加载或者URL发生改变时触发事件监听
js
window.addEventListener('DOMContentLoaded', onLoad)
window.addEventListener('popstate', onPopState)
-
window.addEventListener('DOMContentLoaded', onLoad)
:与之前的类似,在页面加载完毕时,将触发"DOMContentLoaded"
事件,执行onLoad
函数。 -
window.addEventListener('popstate', onPopState)
:history提供了一个popState事件,仅当浏览器前进后退时生效。 而在浏览器的URL发生变化时(例如用户点击了后退或前进按钮),将触发"popstate"
事件,执行onPopState
函数。
3. onPopState函数
js
function onPopState() {
console.log(location.pathname);
routes.forEach(item => {
if (item.path === location.pathname) {
routeView.innerHTML = item.component
}
})
}
我们首先打印console.log(location.pathname);
从结果可以看出他是跳转页面的URL。
欸!这个元素不就是我们需要的吗。
所以onPopState
函数遍历已定义的路由,将当前location.pathname
与路由路径进行比较。当匹配到路径时,我们更新routeView
的内容为对应路由的组件。
4. onLoad函数
js
function onLoad() {
onPopState()
const links = document.querySelectorAll('li a')
links.forEach(a => {
a.addEventListener('click', (e) =>{
e.preventDefault() // 阻止了a标签的默认跳转行为
// 添加一个可以修改url又不造成页面刷新
history.pushState(null, '', a.getAttribute('href'))
// 映射对应的dom
onPopState()
})
})
}
在onLoad
函数内,我们首先执行onPopState()
渲染对应的页面。然后为所有的锚点(<a>
)元素添加了事件监听器。因为我们不能让锚点(<a>
)元素发生刷新页面的行为,所以这里我们阻止这些监听器锚点的默认行为(跳转到新页面) ,而是使用history.pushState()
方法更新URL
,这个方法可以修改URL且不引起页面刷新。
5. History API的使用
js
history.pushState(null, '', a.getAttribute('href'))
通过history.pushState
方法,我们能够向浏览器的会话历史添加新条目,从而改变地址栏中显示的URL,而无需触发完整的页面刷新。 不清楚的小伙伴可以去了解MDN文档中对History:pushState() 方法的说明。一言以蔽之,history 提供了一个pushState方法可以修改URL且不引起页面刷新。