likes
comments
collection
share

你知道实现"前端路由"有多简单吗?这篇详解,让你面试考到完全不慌

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

前言

在前端开发的世界里,"路由"是一个让很多人头疼的概念。

特别是在面试中,如果被问到如何实现一个简单的路由功能,很多人可能会手足无措。然而,其实实现一个基础的路由功能比你想象的要简单得多。今天我们就以一个简单的 HashRouter 为例,带你一步一步实现这个功能,让你在面试中从容应对。

什么是路由?

简单来说,路由就是根据 URL 的不同来显示不同的页面内容。前端路由就是根据 URL 指引用户去不同的页面。常见的前端路由有两种实现方式:HashRouterHistoryRouter。今天我们主要讲解 HashRouter,因为它相对简单且适合初学者理解

技术原理

实现路由的关键是在 hash 路由的机制

一般来说,URL的改变会引起浏览器发送新的请求到服务器,服务器返回新的HTML页面给浏览器。这种方式就会导致页面重新加载。

但有个例外:在浏览器中,URL 中 # 其后面的部分称为 hash部分

例如: http://127.0.0.1:5500/index.html#/page1 中的 #/page1 就是 hash部分

hash部分 与传统的 URL 路由不同,其发生改变 不会发送新的请求 到服务器,这意味着页面不会重新加载。只会触发 hashchange事件 并将 hash部分 存放在 window对象 中。

hash 部分可以通过 window对象的 location 属性(对象)中的 hash 属性读取到。

console.log(location.hash) 可获得 当前URL # 及其后面的内容

所以我们就可以利用 hashchange事件 结合特定的 DOM操作(如innerHTML等),实现页面的局部切换或更新

开始动手实现 HashRouter

首先,我们建立一个 基本的 HTML 结构 用于之后演示我们的路由功能:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>手写Hash Router</title>
</head>
<body>
    <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>
            <li><a href="#/page114514">unknow page</a></li>
            <li><a href="#/pageReg">register router</a></li>
            <li><a href="#/">back to index</a></li>
        </ul>
    </nav>
    <div id="container"></div>
</body>
<script src="router.js"></script>
</html>

我们用简单的<li>标签模拟了一个基本的导航栏,每个导航链接都指向一个不同的 hash 路由

因为href的指向带有#,改变的是URL的hash部分。所以当你点击这些链接时,URL 会发生变化,但不会导致页面刷新

后面则是一个 id="container"div 容器。它是我们路由的挂载点。路由对应的不同内容会在这个容器中更新变换

如果你用过 Vue 的话,那你可以将这个 div 理解为 <router-view />

接下来,我们来看一下 HashRouterJavaScript 实现

class HashRouter {
    constructor() {
        this.routers = {
            '/': function() {
                container.innerHTML = 'index';
            },
            '/page1': function() {
                container.innerHTML = 'page1';
            },
            '/page2': function() {
                container.innerHTML = 'page2';
            },
            '/page3': function() {
                container.innerHTML = 'page3';
            }
        }; // 初始化路由表
        window.addEventListener('hashchange', this.load.bind(this), false); // 监听hash变化
    }

    register(url, callback = function() {}) {
        this.routers[url] = callback; // 注册新的路由
    }

    load() {
        let hash = location.hash.slice(1); // 获取当前hash值
        this.routers[hash] ? this.routers[hash]() : this.routers['/'](); // 根据hash值加载对应内容
    }
}

const hashRouter = new HashRouter();
const container = document.getElementById('container');

hashRouter.register('/pageReg', function() {
    container.innerHTML = 'router registed';
});

hashRouter.load(); // 初始化加载

不着急,我们逐块拆解这整个实现过程。实现过程可以分为以下步骤

1. 初始化路由表

首先,我们在 HashRouter构造函数中初始化了一个 routers 对象,用来保存路由地址和对应的回调函数。这就像是我们为每个路由设置了一张地图,告诉程序每个路由应该显示什么内容。

this.routers = {
    '/': function() {
        container.innerHTML = 'index';
    },
    '/page1': function() {
        container.innerHTML = 'page1';
    },
    '/page2': function() {
        container.innerHTML = 'page2';
    },
    '/page3': function() {
        container.innerHTML = 'page3';
    }
};

这很像 Vue 中我们都会在src/router/index.js中配置一个这样的文件:

import { createRouter, createWebHashHistory } from 'vue-router'

const router = createRouter({
  history: createWebHashHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'home',
      component: () => import('../views/Home.vue')

    },
    {
      path: '/about',
      name: 'about',
      component: () => import('../views/About.vue')
    }
  ]
});

export default router;

其中,routes就保存了每个路由对应的组件,和我们实现的routers非常相近。

当然,在我们实现 Hash Router 的过程中,也可以不填写 routers 数组中的内容,而是使用我们后面要编写的 register方法注册路由规则。这种方式就更加面向对象

2. 监听 hash 变化

接着,我们在构造函数中通过 window.addEventListener 监听 hashchange 事件。这样在 HashRouter实例化之后,每当 URL 的 hash部分 发生变化时,就会触发 load 方法。

window.addEventListener('hashchange', this.load.bind(this), false);

事件监听函数中的 this 指向的是触发事件的元素,因此不一定是 HashRouter实例。所以这里的 bind(this) 是为了确保 load 方法中的 this 指向 HashRouter 实例,而不是 window 对象。

这里的 this指向修正 是一个重点,建议继续阅读本文的 “扩展知识点:修正 this 的指向” 部分。

最后的 false 是指是否启用事件冒泡(默认为false,因此可以不填)。

3. 编写register()方法,注册新路由

我们还可以通过 register 方法来注册新的路由,方便扩展:

register(url, callback = function() {}) {
    this.routers[url] = callback;
}

4. 加载路由

最后,load 方法根据当前的 hash 值来加载对应的内容。如果找不到对应的路由,就会跳转到首页。

load() {
    let hash = location.hash.slice(1);
    this.routers[hash] ? this.routers[hash]() : this.routers['/']();
}

扩展知识点:修正 this 的指向

其实面试官在让实现Hash路由的时候,也是在旁敲侧击地考察 能否注意到 this的指向问题。所以这无疑是一个面试重点,相当易错。

在 JavaScript 中,函数的 this 值在调用时是动态确定的。尤其像这里事件监听函数中的 this 指向的是触发事件的元素,而不是HashRouter实例。

为了确保在事件回调中 this 仍然指向我们期望的对象,可以使用 bind 方法。这就像给函数打了个“保镖”,保证它不会在执行时被其他对象“绑架”

window.addEventListener('hashchange', this.load.bind(this), false);

下面我们用一个小案例,了解一下有哪些方式修正this的指向:

let obj = {
    name: 'dikkoo',
    getName: function(...args) { // 使用rest参数
        console.log(args); // []
        console.log(this.name);
    }
}

obj.getName(1, 2); // [1,2] dikkoo

let obj2 = {
    name: 'bob'
}

问题: 如何让 obj2 调用 objgetName 方法,输出 [1, 2] bob

方法1:使用 call() 方法

call()方法的第一个参数是要绑定的 this对象,后面的参数(不限个数)是传入的参数。

obj.getName.call(obj2, 1, 2);

方法2:使用 apply() 方法

apply()方法的第一个参数是要绑定的 this对象,第二个参数是传入的参数数组,数组中的每个元素都会作为参数传入。

obj.getName.apply(obj2, [1, 2]);

方法3:使用 bind() 方法

bind() 方法会创建一个新函数,当这个新函数被调用时,它的 this值是传递给 bind() 的值。

let getName = obj.getName.bind(obj2);
getName(1, 2);
// 或者:obj.getName.bind(obj2)(1, 2);

bind() 适用于需要创建新的函数并在后续调用的情况,而 call()apply() 则适用于需要立即调用函数并指定 this 值和参数的情况。

因此在设计“路由”监听器的时候,应该属于后续调用的情况,使用bind()更加合适。

本文总结

面试自查

问题 1:什么是前端路由?

回答: 前端路由是管理单页应用(SPA)中视图切换的一种机制。它允许用户在不同的视图之间导航,而无需重新加载整个页面。常见的前端路由实现方式有两种:HashHistoryWebHistory

问题 2:什么是 HashRouter?它的工作原理是什么?

回答HashRouter 使用 URL 中的 hash 部分(即 # 及其后面的部分)来模拟不同的路径。当 hash 部分变化时,不会导致页面重新加载,但会触发 hashchange 事件,从而实现视图切换。

问题 3:为什么使用 HashRouter 而不是传统的多页应用?

回答HashRouter 使得 SPA 的实现更加高效和用户友好。它允许在不重新加载整个页面的情况下切换视图,提供更快的响应和更流畅的用户体验。

问题 4:如何实现一个简单的 HashRouter?

  1. 初始化路由对象

首先,需要一个对象来存储不同的路由及其对应的回调函数。

class HashRouter {
    constructor() {
        this.routers = {
            '/': function() {
                container.innerHTML = 'index';
            },
            '/page1': function() {
                container.innerHTML = 'page1';
            },
            '/page2': function() {
                container.innerHTML = 'page2';
            },
            '/page3': function() {
                container.innerHTML = 'page3';
            }
        };
    }
}
  1. 添加事件监听器

需要监听 URL hash 的变化,以便在 hash 变化时触发路由的处理。

window.addEventListener('hashchange', this.load.bind(this), false);
  1. 注册路由

提供一个方法来注册新的路由和对应的回调函数,这样可以在需要时动态添加路由。

register(url, callback = function() {}) {
    this.routers[url] = callback;
}
  1. 处理路由

实现一个方法,根据当前 URL 的 hash 值来加载对应的路由回调函数。

load() {
    let hash = location.hash.slice(1);
    this.routers[hash] ? this.routers[hash]() : this.routers['/']();
}
  1. 初始化路由

在页面加载时调用路由处理方法,以处理用户直接通过 URL 访问某个路由的情况。

hashRouter.load();

问题 5:代码中的 window.addEventListener('hashchange', this.load.bind(this), false) 有什么作用?

回答: 这段代码添加了一个事件监听器,用于监听 URL 中 hash 部分的变化。当 hash 变化时,会调用 load 方法。bind(this) 确保 load 方法中的 this 指向 HashRouter 实例,而不是事件触发的元素。

问题 6:register 方法的作用是什么?

回答register 方法用于注册新的路由和对应的回调函数。调用 register 方法时,会将路由和回调函数存储在 this.routers 对象中,以便在路由变化时调用相应的回调函数。

问题 7:如何初始化和加载路由?

回答: 在实例化 HashRouter 后,调用 hashRouter.load() 方法。这是为了处理用户直接通过 URL 访问某个路由的情况,确保在页面加载时能够显示正确的内容。

问题 8:为什么 load 方法中使用 this.routers[hash] ? this.routers[hash]() : this.routers['/']();

回答: 这段代码检查当前的 hash 是否在 this.routers 对象中。如果存在,则调用对应的回调函数;否则,加载默认路由(通常是首页)。

完整代码

最后附上带有完整注释的全部代码,供大家参考~

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>手写Hash Router</title>
</head>
<body>
    <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>
            <li><a href="#/page114514">unknow page</a></li>
            <li><a href="#/pageReg">register router</a></li>
            <li><a href="#/">back to index</a></li>
        </ul>
    </nav>
    <div id="container"></div>
</body>
<script>
    class HashRouter {
        constructor() {
            this.routers = {
                '/': function() {
                    container.innerHTML = 'index';
                },
                '/page1': function() {
                    container.innerHTML = 'page1';
                },
                '/page2': function() {
                    container.innerHTML = 'page2';
                },
                '/page3': function() {
                    container.innerHTML = 'page3';
                }
            };
            window.addEventListener('hashchange', this.load.bind(this), false);
        }

        register(url, callback = function() {}) {
            this.routers[url] = callback;
        }

        load() {
            let hash = location.hash.slice(1);
            this.routers[hash] ? this.routers[hash]() : this.routers['/']();
        }
    }

    const hashRouter = new HashRouter();
    const container = document.getElementById('container');

    hashRouter.register('/pageReg', function() {
        container.innerHTML = 'router registed';
    });

    hashRouter.load();
</script>
</html>
转载自:https://juejin.cn/post/7389278362242023461
评论
请登录