你知道实现"前端路由"有多简单吗?这篇详解,让你面试考到完全不慌
前言
在前端开发的世界里,"路由"是一个让很多人头疼的概念。
特别是在面试中,如果被问到如何实现一个简单的路由功能,很多人可能会手足无措。然而,其实实现一个基础的路由功能比你想象的要简单得多。今天我们就以一个简单的 HashRouter
为例,带你一步一步实现这个功能,让你在面试中从容应对。
什么是路由?
简单来说,路由就是根据 URL 的不同来显示不同的页面内容。前端路由就是根据 URL 指引用户去不同的页面。常见的前端路由有两种实现方式:
HashRouter
和HistoryRouter
。今天我们主要讲解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 />
。
接下来,我们来看一下 HashRouter
的 JavaScript 实现:
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
调用obj
的getName
方法,输出 [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)中视图切换的一种机制。它允许用户在不同的视图之间导航,而无需重新加载整个页面。常见的前端路由实现方式有两种:HashHistory
和 WebHistory
。
问题 2:什么是 HashRouter?它的工作原理是什么?
回答: HashRouter
使用 URL 中的 hash 部分(即 #
及其后面的部分)来模拟不同的路径。当 hash 部分变化时,不会导致页面重新加载,但会触发 hashchange
事件,从而实现视图切换。
问题 3:为什么使用 HashRouter 而不是传统的多页应用?
回答: HashRouter
使得 SPA 的实现更加高效和用户友好。它允许在不重新加载整个页面的情况下切换视图,提供更快的响应和更流畅的用户体验。
问题 4:如何实现一个简单的 HashRouter?
- 初始化路由对象
首先,需要一个对象来存储不同的路由及其对应的回调函数。
class HashRouter {
constructor() {
this.routers = {
'/': function() {
container.innerHTML = 'index';
},
'/page1': function() {
container.innerHTML = 'page1';
},
'/page2': function() {
container.innerHTML = 'page2';
},
'/page3': function() {
container.innerHTML = 'page3';
}
};
}
}
- 添加事件监听器
需要监听 URL hash 的变化,以便在 hash 变化时触发路由的处理。
window.addEventListener('hashchange', this.load.bind(this), false);
- 注册路由
提供一个方法来注册新的路由和对应的回调函数,这样可以在需要时动态添加路由。
register(url, callback = function() {}) {
this.routers[url] = callback;
}
- 处理路由
实现一个方法,根据当前 URL 的 hash 值来加载对应的路由回调函数。
load() {
let hash = location.hash.slice(1);
this.routers[hash] ? this.routers[hash]() : this.routers['/']();
}
- 初始化路由
在页面加载时调用路由处理方法,以处理用户直接通过 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