手写 Hash Router,理解单页应用 SPA
前言
大家好!今天,我们来聊聊单页应用 SPA 中一个非常实用且在面试时常常被提及的主题 Hash Router。如果你正在学习前端开发,尤其是Vue.js或React,那么了解如何手动实现一个简单的Hash Router将会是必须要学会的。让我们一起深入探讨,一步步实现这个功能吧!
一、理解SPA与Hash Router
首先,我们要知道为什么需要Hash Router。在传统的网页中,每次点击链接都会向服务器发送一个新的HTTP请求,获取完整的HTML文档。但SPA则不同,它只加载一次基础页面,之后的页面变化都是通过JavaScript动态生成的。这样可以避免重复加载相同的资源,提高页面响应速度。
然而,问题来了,我们怎么告诉浏览器改变URL而不刷新页面呢?这就引出了“Hash Router”的概念。利用URL中的#
以及后面那部分也就是哈希,我们可以实现页面之间的导航而无需重新加载整个页面。
二、搭建简单页面
在构建一个基于Hash Router的单页应用 SPA 时,页面的结构设计也是至关重要的。让我们一起来看看一个典型的 SPA 页面布局,并探讨<div id="container">
在其中扮演的角色。
<nav id="nav">
<ul>
<li><a href="#/page1">page1</a></li>
<li><a href="#/page2">page2</a></li>
<li><a href="#/page3">page3</a></li>
</ul>
</nav>
<!-- <router-view></router-view> -->
<div id="container"></div>
在这个HTML片段中,<nav>
元素包含了导航链接,这些链接使用了哈希锚点,也就是#
后面的部分,来触发页面的不同视图。而<div id="container">
则是我们用来展示不同页面内容的容器。
在传统的多页面应用中,每次用户点击链接,整个页面都会重新加载,不同的HTML文件对应着不同的页面。然而,在SPA中,我们希望只替换页面的一部分,而不是整个页面。这就是<div id="container">
的用武之地。
三、初始化Router与监听hashchange
接下来,让我们开始构建我们的Hash Router。首先创建一个HashRouter
类,因为这个业务完全是可以复用的,并在构造函数中添加一些初始化逻辑。我们需要监听hashchange
事件,以便在URL的哈希值发生变化时做出响应。
class HashRouter {
constructor() {
this.routes = {};
window.addEventListener('hashchange', this.load.bind(this), false);
}
}
这里,我们创建了一个空的对象this.routes
用于存储不同的路由和对应的回调函数。然后,我们注册了hashchange
事件监听器,当URL的哈希值发生变化时,会触发load
方法。
注意理解
window.addEventListener('hashchange', this.load.bind(this), false)
。 这段代码是在HashRouter
的构造函数中设置的,其目的是为了让浏览器在URL的哈希值发生改变时,能够触发我们定义的load
方法。
-
'hashchange'
: 这是一个事件类型,表示URL中的哈希值发生了变化。每当用户手动更改URL中的哈希部分,或是通过JavaScript操作location.hash
属性导致变化时,浏览器就会触发这个事件。 -
this.load.bind(this)
: 这里使用了bind
方法来绑定load
方法的上下文。在JavaScript中,函数的this
关键字会根据函数的调用上下文而变化。在事件处理器中,this
通常指的是触发事件的DOM元素,但我们希望load
方法中的this
能够引用到HashRouter
实例本身,这样才能访问到如this.routes
这样的实例属性。因此,我们使用bind
方法显式地将this
绑定为HashRouter
实例。 -
false
: 这是可选的第三个参数,用来指定事件监听的捕获阶段或冒泡阶段。在这里,我们使用false
意味着在冒泡阶段触发事件处理器,这是大多数情况下的默认行为。
四、注册路由与加载内容
现在,我们来定义register
和load
方法。register
方法允许我们为特定的哈希值注册一个回调函数,而load
方法则会在哈希值改变时查找并执行相应的回调。
register(hash, callback = function() {}) {
this.routes[hash] = callback;
}
load() {
let hash = location.hash.slice(1);
let handler;
if (!hash) {
// 首页
handler = this.routes['index'] // 处理默认路由介绍
} else {
// 相应页面
handler = this.routes[hash]
}
handler && handler.call(this);
}
register
方法很简单,就是把传入的哈希值和回调函数存进routes
对象里。而load
方法则会根据当前的哈希值查找对应的处理函数,并调用它。
注意理解
handler && handler.call(this)
。 这一行代码是在load
方法中执行的,目的是调用与当前哈希值匹配的回调函数。
-
handler &&
: 这是一个逻辑与运算符,用于检查handler
是否为真值。在JavaScript中,任何非零数值、非空字符串、非null
或非undefined
的对象都被认为是真值。因此,如果handler
存在,即找到了一个与当前哈希值匹配的回调函数,那么这部分表达式的结果将是handler
本身;否则,结果将是false
。 -
handler.call(this)
: 如果handler
存在,那么这部分表达式将被执行。call
方法允许我们调用一个函数,并指定该函数内部的this
值。在这里,我们传递this
作为call
的第一个参数,这样在回调函数内部就可以访问到HashRouter
实例的属性和方法,例如this.routes
和this.load
。
五、处理默认路由
为了处理没有匹配的哈希值的情况,我们可以增加一个默认路由。如果用户访问的页面不存在,我们可以显示一个默认页面,比如首页。
registerIndex(callback = function() {}) {
this.routes['index'] = callback;
}
默认路由非常重要,因为在某些情况下,URL 的哈希值可能为空或者不匹配任何已定义的路由。例如,当用户第一次访问 SPA 时,或者当他们直接在地址栏输入了一个不存在的页面路径时,如果没有默认路由,应用可能会陷入无法识别的状态。通过定义默认路由,我们可以确保在这种情况下也有相应的处理器来处理UI的显示,避免出现空白页面或者错误信息。
六、实现与测试
最后,我们来看看如何使用这个HashRouter
类。在HTML中,我们创建了一些链接,并指向不同的哈希值。然后,在JavaScript中,我们实例化HashRouter
,并为每个哈希值注册一个回调函数。
let router = new HashRouter();
let container = document.querySelector('#container');
router.registerIndex(function() {
container.innerHTML = '首页'
})
router.register('/page1', function() {
container.innerHTML = 'page1'
})
router.register('/page2', function() {
container.innerHTML = 'page2'
})
router.register('/page3', function() {
container.innerHTML = 'page3'
})
// ...其他页面的注册
router.load();
当用户是通过好友给的链接直接跳转进页面时,会发现一开始并不会刷新页面到相应内容。所以别忘了,在页面首次加载时,我们也应该调用一次load
方法,以确保正确的初始内容显示。
结语
通过这篇文章,我们不仅了解了Hash Router的工作原理,还亲手实现了一个简单的版本。这不仅可以帮助我们在面试中应对相关问题,更重要的是,它加深了我们对SPA架构的理解。希望看完能对你起到帮助,我们一起加油!
转载自:https://juejin.cn/post/7393525123766747151