前端路由是现代SPA应用必备的功能,每一个现代前端框架都有对应的实现,例如vue-router、react-router。javascript
咱们不想探究vue-router或者react-router们的实现,由于不论是哪一种路由无外乎用兼容性更好的hash实现或者是H5 History实现,与框架几个只须要作相应的封装便可。html
提早声明: 咱们没有对传入的参数进行及时判断而规避错误,也没有考虑兼容性问题,仅仅对核心方法进行了实现.前端
hash路由一个明显的标志是带有#
,咱们主要是经过监听url中的hash变化来进行路由跳转。vue
hash的优点就是兼容性更好,在老版IE中都有运行,问题在于url中一直存在#
不够美观,并且hash路由更像是Hack而非标准,相信随着发展更加标准化的History API会逐步蚕食掉hash路由的市场。 java
咱们用Class
关键字初始化一个路由.react
class Routers {
constructor() {
// 以键值对的形式储存路由
this.routes = {};
// 当前路由的URL
this.currentUrl = '';
}
}
复制代码
在初始化完毕后咱们须要思考两个问题:git
class Routers {
constructor() {
this.routes = {};
this.currentUrl = '';
}
// 将path路径与对应的callback函数储存
route(path, callback) {
this.routes[path] = callback || function() {};
}
// 刷新
refresh() {
// 获取当前URL中的hash路径
this.currentUrl = location.hash.slice(1) || '/';
// 执行当前hash路径的callback函数
this.routes[this.currentUrl]();
}
}
复制代码
那么咱们只须要在实例化Class
的时候监听上面的事件便可.github
class Routers {
constructor() {
this.routes = {};
this.currentUrl = '';
this.refresh = this.refresh.bind(this);
window.addEventListener('load', this.refresh, false);
window.addEventListener('hashchange', this.refresh, false);
}
route(path, callback) {
this.routes[path] = callback || function() {};
}
refresh() {
this.currentUrl = location.hash.slice(1) || '/';
this.routes[this.currentUrl]();
}
}
复制代码
对应效果以下: 面试
完整示例vue-router
点击这里 hash router by 寻找海蓝 (@xiaomuzhu) on CodePen.
上一节咱们只实现了简单的路由功能,没有咱们经常使用的回退与前进功能,因此咱们须要进行改造。
咱们在须要建立一个数组history
来储存过往的hash路由例如/blue
,而且建立一个指针currentIndex
来随着后退和前进功能移动来指向不一样的hash路由。
class Routers {
constructor() {
// 储存hash与callback键值对
this.routes = {};
// 当前hash
this.currentUrl = '';
// 记录出现过的hash
this.history = [];
// 做为指针,默认指向this.history的末尾,根据后退前进指向history中不一样的hash
this.currentIndex = this.history.length - 1;
this.refresh = this.refresh.bind(this);
this.backOff = this.backOff.bind(this);
window.addEventListener('load', this.refresh, false);
window.addEventListener('hashchange', this.refresh, false);
}
route(path, callback) {
this.routes[path] = callback || function() {};
}
refresh() {
this.currentUrl = location.hash.slice(1) || '/';
// 将当前hash路由推入数组储存
this.history.push(this.currentUrl);
// 指针向前移动
this.currentIndex++;
this.routes[this.currentUrl]();
}
// 后退功能
backOff() {
// 若是指针小于0的话就不存在对应hash路由了,所以锁定指针为0便可
this.currentIndex <= 0
? (this.currentIndex = 0)
: (this.currentIndex = this.currentIndex - 1);
// 随着后退,location.hash也应该随之变化
location.hash = `#${this.history[this.currentIndex]}`;
// 执行指针目前指向hash路由对应的callback
this.routes[this.history[this.currentIndex]]();
}
}
复制代码
咱们看起来实现的不错,但是出现了Bug,在后退的时候咱们每每须要点击两下。
点击查看Bug示例 hash router by 寻找海蓝 (@xiaomuzhu) on CodePen.
问题在于,咱们每次在后退都会执行相应的callback,这会触发refresh()
执行,所以每次咱们后退,history
中都会被push
新的路由hash,currentIndex
也会向前移动,这显然不是咱们想要的。
refresh() {
this.currentUrl = location.hash.slice(1) || '/';
// 将当前hash路由推入数组储存
this.history.push(this.currentUrl);
// 指针向前移动
this.currentIndex++;
this.routes[this.currentUrl]();
}
复制代码
如图所示,咱们每次点击后退,对应的指针位置和数组被打印出来
咱们必须作一个判断,若是是后退的话,咱们只须要执行回调函数,不须要添加数组和移动指针。
class Routers {
constructor() {
// 储存hash与callback键值对
this.routes = {};
// 当前hash
this.currentUrl = '';
// 记录出现过的hash
this.history = [];
// 做为指针,默认指向this.history的末尾,根据后退前进指向history中不一样的hash
this.currentIndex = this.history.length - 1;
this.refresh = this.refresh.bind(this);
this.backOff = this.backOff.bind(this);
// 默认不是后退操做
this.isBack = false;
window.addEventListener('load', this.refresh, false);
window.addEventListener('hashchange', this.refresh, false);
}
route(path, callback) {
this.routes[path] = callback || function() {};
}
refresh() {
this.currentUrl = location.hash.slice(1) || '/';
if (!this.isBack) {
// 若是不是后退操做,且当前指针小于数组总长度,直接截取指针以前的部分储存下来
// 此操做来避免当点击后退按钮以后,再进行正常跳转,指针会停留在原地,而数组添加新hash路由
// 避免再次形成指针的不匹配,咱们直接截取指针以前的数组
// 此操做同时与浏览器自带后退功能的行为保持一致
if (this.currentIndex < this.history.length - 1)
this.history = this.history.slice(0, this.currentIndex + 1);
this.history.push(this.currentUrl);
this.currentIndex++;
}
this.routes[this.currentUrl]();
console.log('指针:', this.currentIndex, 'history:', this.history);
this.isBack = false;
}
// 后退功能
backOff() {
// 后退操做设置为true
this.isBack = true;
this.currentIndex <= 0
? (this.currentIndex = 0)
: (this.currentIndex = this.currentIndex - 1);
location.hash = `#${this.history[this.currentIndex]}`;
this.routes[this.history[this.currentIndex]]();
}
}
复制代码
查看完整示例 Hash Router by 寻找海蓝 (@xiaomuzhu) on CodePen.
前进的部分就不实现了,思路咱们已经讲得比较清楚了,能够看出来,hash路由这种方式确实有点繁琐,因此HTML5标准提供了History API供咱们使用。
咱们能够直接在浏览器中查询出History API的方法和属性。
固然,咱们经常使用的方法实际上是有限的,若是想全面了解能够去MDN查询History API的资料。
咱们只简单看一下经常使用的API
window.history.back(); // 后退
window.history.forward(); // 前进
window.history.go(-3); // 后退三个页面
复制代码
history.pushState
用于在浏览历史中添加历史记录,可是并不触发跳转,此方法接受三个参数,依次为:
state
:一个与指定网址相关的状态对象,popstate
事件触发时,该对象会传入回调函数。若是不须要这个对象,此处能够填null
。
title
:新页面的标题,可是全部浏览器目前都忽略这个值,所以这里能够填null
。
url
:新的网址,必须与当前页面处在同一个域。浏览器的地址栏将显示这个网址。
history.replaceState
方法的参数与pushState
方法如出一辙,区别是它修改浏览历史中当前纪录,而非添加记录,一样不触发跳转。
popstate
事件,每当同一个文档的浏览历史(即history对象)出现变化时,就会触发popstate事件。
须要注意的是,仅仅调用pushState
方法或replaceState
方法 ,并不会触发该事件,只有用户点击浏览器倒退按钮和前进按钮,或者使用 JavaScript 调用back
、forward
、go
方法时才会触发。
另外,该事件只针对同一个文档,若是浏览历史的切换,致使加载不一样的文档,该事件也不会触发。
以上API介绍选自history对象,能够点击查看完整版,咱们不想占用过多篇幅来介绍API。
上一节咱们介绍了新标准的History API,相比于咱们在Hash 路由实现的那些操做,很显然新标准让咱们的实现更加方便和可读。
因此一个mini路由实现起来其实很简单
class Routers {
constructor() {
this.routes = {};
// 在初始化时监听popstate事件
this._bindPopState();
}
// 初始化路由
init(path) {
history.replaceState({path: path}, null, path);
this.routes[path] && this.routes[path]();
}
// 将路径和对应回调函数加入hashMap储存
route(path, callback) {
this.routes[path] = callback || function() {};
}
// 触发路由对应回调
go(path) {
history.pushState({path: path}, null, path);
this.routes[path] && this.routes[path]();
}
// 监听popstate事件
_bindPopState() {
window.addEventListener('popstate', e => {
const path = e.state && e.state.path;
this.routes[path] && this.routes[path]();
});
}
}
复制代码
点击查看H5路由 H5 Router by 寻找海蓝 (@xiaomuzhu) on CodePen.
咱们大体探究了前端路由的两种实现方法,在没有兼容性要求的状况下显然符合标准的History API实现的路由是更好的选择。
想更深刻了解前端路由实现能够阅读vue-router代码,除去开发模式代码、注释和类型检测代码,核心代码并很少,适合阅读。
下期准备一篇关于双向绑定的话题,由于许多人只知道Object.definedProperty
,禁不住深究:
Proxy
相比有何优劣?因为涉及的框架和知识点过多,我开了一个头已经小2000字了,,在考虑要不要分上下篇发出来,不过我相信它解决你对双向绑定全部的疑问。