手写前端路由要解决的问题
在学习之前,我们要先知道我们要解决什么问题。
实现前端路由需要解决哪些问题?
- 如何修改url,还不引起页面的刷新?
- 如何知道url发生了变化?
着两个问题,就是我们手写前端路由需要解决的问题!
什么是路由?
前端本无路由!就像鲁迅先生说的:世界上本没有路,走的人多了也就成了路!
路由最早是服务端,服务器上的概念,和前端没有什么关系,当我们想要从服务器上读取某个盘的文件,拿到文件的路径就是我们所熟知的路由!路由本来是服务器端用于描述文件路径的一个概念
因为前端单页面的情景,前端也借鉴了路由这样一个概念,前端也需要这样一个映射关系,只要浏览器的url地址栏变化了,我就需要展示对应的代码片段!也就是展示相应的组件,实现一个url对应组件一一映射的关系!总结来说:前端借鉴路由的称呼来描述 url 和 组件(代码片段) 的映射关系
哈希Hash概念
什么是哈希?:哈希(Hash)是一种算法,它可以将任意长度的输入(也称为预映射或pre-image)通过特定的散列算法转换成固定长度的输出,这个输出就是散列值。这种转换过程是一种压缩映射,因为散列值的空间通常远小于输入的空间。这也意味着不同的输入可能会产生相同的输出,因此不可能通过散列值来唯一确定输入值。
通俗一点来说:哈希是按照某种规则,生成的一串值。通常可以用来代表一个唯一的文件,在文件名后面接一个哈希值,可以用于判断这个文件是否被修改过。
在浏览器中,也有哈希这个概念,在url中,接一个#
,在#
后方接的任意值,这个值都是哈希值,而哈希值的变更,不会引起浏览器的刷新!
这样在hash模式下,我们就解决了第一个问题!
通过修改url的哈希值,并且不会引起页面的刷新
下面,我们开始着手实现前端路由Hash模式
手写前端路由Hash模式
准备工作:新建一个hash.html
,放置两个a
标签,在a
标签的href
有值时,我们点击a
标签必定会引起页面的刷新,但是如果我们在href
中添加一个#
,将url改为哈希值就不会引起页面的刷新!
html
<!-- 加 # 号 就是hash模式 -->
<ul>
<li>
<a href="#/home">首页</a>
</li>
<li>
<a href="#/about">关于</a>
</li>
</ul>
<!-- 放一个代码片段,展示点击页面的内容 -->
<div id="routeView">
</div>
接下来,我们要实现的效果就是,通过点击首页 ,在routeView
容器中展示首页页面的内容,点击关于,让容器展示关于页面的内容。实现一个单页应用!
思路:当我们点击首页时,将首页的代码片段塞到这个容器当中,点击关于时,清空容器当中的代码,再把关于页面的代码片段塞到这个容器当中,就可以实现这样一个效果!
我们先封装js,让代码片段进行一一映射。
html
<script>
const routes = [
{
path:'#/home',
component:'首页页面内容'
},
{
path:'#/about',
component:'About page'
}
]
</script>
实现了这样一个映射关系之后,点击首页,展示首页页面内容
,点击关于,展示About page
接下来,我们就是获取url的变更,给a
标签添加点击事件显然不合理。
这时候,js就自带了一个事件hashchange
,可以自动监听哈希值的变更,在我们点击一个页面,哈希值发生变化,这个事件就会触发!
js
window.addEventListener('hashchange', () => {
console.log('changed')
})
这个问题解决之后,我们可以自行封装一个onHashChange
函数,我们现在要拿到变化后的哈希值,我们可以这样尝试一下!location
是js中的一个方法,本地的意思。
js
window.addEventListener('hashchange', onHashChange)
function onHashChange() {
console.log(location)
}
我们到浏览器中看看打印结果!
我们可以看到,location
是一个对象,描述的就是整个浏览器url地址栏中的详情!
我们可以看到在location
对象里面有一个hash
属性,这个不就是我们想要拿到的hash值(哈希值)吗?
这个时候,我们就可以直接去数据中对path
与这个hash
属性进行遍历。进行匹配!
js
function onHashChange(){
console.log(location)
const routeView = document.getElementById('routeView')
routes.forEach((item,index)=>{
if(item.path==location.hash){
//innerHTML会重置容器内的内容,重新赋值
routeView.innerHTML=item.component
}
})
}
- 这段代码,我们首先
const routeView = document.getElementById('routeView')
拿到页面中的容器 - 通过
forEach
遍历routes
数组中的一条条数据,item.path==location.hash
进行匹配! - 最后通过
routeView.innerHTML=item.component
将代码片段装载到容器中进行展示!
innerHTML
会重置容器内的内容,将新的值赋给这个容器
这样,其实点击页面的效果展示对应页面的单页应用!我们就实现了!
但是有一个问题的是:当我们初始刷新页面的时候,我们的页面不会默认展示,所以我们在页面初次加载的时候,命中了哪个路由就展示哪个页面。现在我们只能hashchange
事件发生的时候才能展示页面。
所以我们可以通过一个事件DOMContentLoaded
事件! DOMContentLoaded 会在页面加载完毕之后就会自动触发一次
所以我们再添加一个语句:
js
window.addEventListener('DOMContentLoaded',onHashChange)
这样hash路由主要的功能我们就实现了!
最终,我们的hash.html
代码如下:
html
<body>
<!-- 加 # 号 就是hash模式 -->
<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'
}
]
// js有一个hashChange事件 解决了如何知道url改变了
window.addEventListener('DOMContentLoaded',onHashChange)// DOMContentLoaded 会在页面加载完毕之后就会自动触发一次
window.addEventListener('hashchange',onHashChange)
function onHashChange(){
// console.log(location)
const routeView = document.getElementById('routeView')
console.log(location.hash);//location就是描述整个url路径里面的情况 hostname就是域名
routes.forEach((item,index)=>{
if(item.path==location.hash){
//innerHTML会重置容器内的内容,重新赋值
routeView.innerHTML=item.component
}
})
}
</script>
</body>
我们来看看效果!
hash模式路由修改地址栏
- a标签
- 浏览器的前进和后退
- 修改windows.location
这三种方式都会导致url的变更而触发hashchange事件!这是我们实现hash路由的主要手段!
接下来,我们实现路由的history模式,在这个模式下,不是通过修改url的哈希值来展示页面的,那我们不引起页面的刷新该怎么解决呢??
注意!hash模式和history没有本质上的区别,仅仅url路径上是否有#,与美观度有关。
手写前端路由History模式
我们先来看一个history是一个什么东西?
来到我们的mdn字典的介绍:History - Web API 接口参考 | MDN (mozilla.org)
这个接口上具有一个pushState()
方法!
pushState()方法
按指定的名称和 URL(如果提供该参数)将数据 push 进会话历史栈,数据被 DOM 进行不透明处理;你可以指定任何可以被序列化的 javascript 对象。请注意,除了 Safari 所有浏览器现在都忽略了 title 参数。更多的信息,请看使用 History API。
History:pushState() 方法
在 HTML 文档中,
history.pushState()
方法向浏览器的会话历史栈增加了一个条目。
这个方法可以修改url而不引起页面刷新!
浏览器维护了一个历史栈,这个历史栈可以维护我们的访问路径,通过这个栈,可以让我们按照栈的进出顺序进行前进和回退!
在pushState()
方法中,有一个popstate
事件,可以通过监听popstate
事件,来感知url的改变!当浏览器发生前进后退时,这个事件触发!
接下来,我们就可以开始实现了
同样的,我们还是准备两个li
和一个容器routeView
html
<ul>
<li>
<a href="/home">首页</a>
</li>
<li>
<a href="/about">关于</a>
</li>
</ul>
<!-- 放一个代码片段 -->
<div id="routerView">
</div>
接下来,我还是声明一个映射关系数组routes
,不过此时的path
不要再使用hash模式了!
html
<script>
const routes = [
{
path: '/home',
component: '<h2>首页页面内容</h2>'
},
{
path: '/about',
component: 'About page'
}
]
</script>
既然我们用的不是hash模式,a
标签默认会有跳转刷新的效果,所以我们可以通过一段js把这个效果给它干掉!
js
const links = document.querySelectorAll('li a')
links.forEach(a => {
a.addEventListener('click', (e) => {
console.log(e);
e.preventDefault()//阻止了a标签的默认跳转行为
})
})
在事件参数e
中有一个方法preventDefault
,可以禁用a
标签的默认跳转行为!
我们阻止了跳转行为之后,我们还要解决如何修改url而不引起刷新的问题,这里我们就需要使用到pushState
语法:
js
pushState(state, unused)
pushState(state, unused, url)
接收三个参数
state:state
对象是一个 JavaScript 对象,其与通过 pushState()
创建的新历史条目相关联。每当用户导航到新的 state
,都会触发 popstate
事件,并且该事件的 state
属性包含历史条目 state
对象的副本。(一般我们不需要,可以放一个null)
unused:由于历史原因,该参数存在且不能忽略;传递一个空字符串是安全的,以防将来对该方法进行更改。
url:新历史条目的 url。
请注意,浏览器不会在调用
pushState()
之后尝试加载该 URL,但是它可能会在以后尝试加载该 URL,例如,在用户重启浏览器之后。新 URL 可以不是绝对路径;如果它是相对的,它将相对于当前的 URL 进行解析。新的 URL 必须与当前 URL 同源;否则,pushState()
将抛出异常。如果该参数没有指定,则将其设置为当前文档的 URL。
因为url是我们需要根据不同的点击,而拿到的url值,我们需要拿到用户点击的a
标签href
中的地址!
这里我们可以通过
js
a.getAttribute('href')
来获取a
标签的href
属性!
走到这里,我们就掌握了一个核心方法
js
history.pushState(null, '', a.getAttribute('href'))
拿到用户点击的url地址!同时修改url地址,又不会引起刷新,就实现和哈希一样的效果!
代码就写成这样
js
const links = document.querySelectorAll('li a')
links.forEach(a => {
a.addEventListener('click', (e) => {
console.log(e);
e.preventDefault()//阻止了a标签的默认跳转行为
history.pushState(null, '', a.getAttribute('href'))
})
})
我们再优化一下代码,用一个函数onLoad
来封装我们的主函数
js
function onLoad() {
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()
})
})
}
接下来,我们要实现的效果是去根据url地址,来映射对应的代码片段,也就是展示对应的组件!
我们可以自己封装一个函数,来映射对应的dom
这个函数我们命名为onPopState()
,并且在onLoad
中调用!
js
const routerView = document.getElementById('routerView')
function onPopState() {
//拿到当前浏览器的地址
console.log(location.pathname);
routes.forEach(item => {
if (item.path == location.pathname) {
routerView.innerHTML = item.component
}
})
}
我们知道location
描述的就是整个浏览器url地址栏中的详情,它其中有一个属性pathname
展示就是当前浏览器的地址。
拿到之后,我们还是同样的操作forEach遍历routes
数组,找到url匹配的path
,我们再拿到routerView
的dom结构,使用innerHTML
装载对应的代码片段(组件)
我们可以在初次进入页面时,就加载一次onLoad
函数!
js
window.addEventListener('DOMContentLoaded',onLoad)
但是,我们不点击还是不会展示页面,并且刷新,这个url地址不会被浏览器承认!因为现在不是hash模式下的hash值,现在url是真实的路径,只有当我们点击浏览器才会认为这个路径是存在的!这个是live server存在的一个现象!
并且前进后退,不会触发我们的方法!所以,这里我们就可以用到上面提到的官方事件,popstate
刚好可以补全这个不足。
所以我们可以再添加一个事件监听!
js
window.addEventListener('popstate',onPopState)
只要popstate
这个事件触发,就会调用一次onPopState
,就是把浏览器的前进和后退包括再内,
所以最终history.html代码如下:
html
<body>
<!-- 加 # 号 就是hash模式 -->
<ul>
<li>
<a href="/home">首页</a>
</li>
<li>
<a href="/about">关于</a>
</li>
</ul>
<!-- 放一个代码片段 -->
<div id="routerView">
</div>
<script>
const routerView = document.getElementById('routerView')
window.addEventListener('DOMContentLoaded',onLoad)
window.addEventListener('popstate',onPopState)
const routes = [
{
path: '/home',
component: '<h2>首页页面内容</h2>'
},
{
path: '/about',
component: 'About page'
}
]
function onLoad() {
const links = document.querySelectorAll('li a')
console.log(links);
links.forEach(a => {
a.addEventListener('click', (e) => {
// console.log(e);
e.preventDefault()// 阻止了a标签的默认跳转行为
//添加一种,可以修改url 又不造成页面刷新的
history.pushState(null, '', a.getAttribute('href'))//核心方法 a.getAttribute('href')获取a标签下面的href属性 获取dom结构的属性
//用函数映射对应的dom
onPopState()
})
})
}
function onPopState() {
//拿到当前浏览器的地址
console.log(location.pathname);
routes.forEach(item => {
if (item.path == location.pathname) {
routerView.innerHTML = item.component
}
})
}
</script>
</body>
最终效果!
最后
今天,我们针对实现前端路由的两个问题
- 如何修改url,还不引起页面的刷新?
- 如何知道url发生了变化?
分别手搓了一下前端路由的两种模式:hash模式和history模式!
假如你也和我一样,在准备冲刺春招,欢迎大家加微信shunwuyun,我们这里有好多小伙伴和各种大佬可以相互鼓励,分析信息,模拟面试,共读源码,齐刷算法,手撕面经。加油!友友们!
如果,你觉得这篇文章有帮助的话,可以帮博主点赞+评论+收藏,三连一波!感谢!
往后,我还会持续输出vue相关的文章,如node相关的内置模块,koa的使用等等文章,感兴趣的小伙伴可以关注一波!
代码已经上传至个人Github:一个修远君的github之手写前端路由