likes
comments
collection
share

手写 Hash Router,理解单页应用 SPA

作者站长头像
站长
· 阅读数 22

前言

大家好!今天,我们来聊聊单页应用 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意味着在冒泡阶段触发事件处理器,这是大多数情况下的默认行为。

四、注册路由与加载内容

现在,我们来定义registerload方法。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.routesthis.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
评论
请登录