# 👉 前端路由实现
# 什么是前端路由
在 SPA 当中,路由描述了 URL 和对应页面的映射关系,单向映射。
现在单页面应用能够模拟多页面应用的效果,归功于其前端路由机制。现在前端路由有两种形式:Hash / History。
实现核心:
- 如何改变 URL 却不引起页面刷新?
- 如何检测 URL 变化了?
# hash
#,原来常用于锚点导航至页面具体元素。hash 是基于监听 hashchange 事件实现。通过监听 url 变化,可以触发相应 hashchange 回调函数。
# hash 实现思路:
- 监听 load 事件,第一次进入页面/刷新时,需要主动触发一次 onHashChange 事件,保证页面能够正常显示;
- 监听 hashChange,触发 onHashChange,更新视图;
<ul>
<li><a href="#/">home</a></li>
<li><a href="#/about">about</a></li>
<li><a href="#/topics">topics</a></li>
</ul>
<div id="routeView"></div>
<script type="text/javascript">
function myRouter() {
this.routersMap = {};
this.currentUrl = "";
this.add = function(path, callback) {
this.routersMap[path] = callback;
};
this.init = function() {
window.addEventListener(
"DOMContentLoaded",
this.updateView(),
false
);
window.addEventListener("hashchange", this.updateView(), false);
};
this.updateView = function() {
this.currentUrl = location.hash.slice(1) || "/";
this.routersMap[this.currentUrl] &&
this.routersMap[this.currentUrl]();
};
}
let routerView = document.querySelector("#routeView");
const router = new myRouter();
router.add("/about", function() {
routerView.innerText = "about";
});
router.add("/topics", function() {
routerView.innerText = "topics";
});
router.add("/", function() {
routerView.innerText = "home";
});
router.init();
</script>
# history
HTML5 的 History API 为浏览器的全局 history 对象增加的扩展方法。
History 路由是基于 HTML5 规范,在 HTML5 规范中提供了history.pushState
和history.replaceState
来进行路由控制。
# history 的属性:
history.state:获取当页 state 信息(页面间可以通过 state 传递信息)
hstory.length:会话历史记录数目
# history 的方法:
history.back
/history.forward
/history.go(0/1)
0 代表刷新半页,-1 后退一页,以上三个方法能触发 popStatehistory.pushState(state [,title] [,url])
:向历史记录里增加一条记录history.replaceState(state [,title] [,url])
:替换当前页在历史的记录这两个方法不会刷新页面,也不会触发 popState 回调
参数说明如下:
state:存储 JSON 字符串,可以用在 popstate 事件中
title:现在大多浏览器忽略这个参数,直接用 null 代替
url:任意有效的 URL,用于更新浏览器的地址栏history.pushState({}, null, '/about') 时候,页面 url 会从 http://xxxx/ 跳转到 http://xxxx/about 可以在改变 url 的同时,并不会刷新页面。
# history 实现思路
监听 window.popstate,触发 handlePopstate,更新视图(点击浏览器按钮和 js 调用 foward/go/back 会触发 popstate)
监听 load 事件,第一次进入页面/刷新时,需要主动触发 handlLoad 事件;
handlLoad 事件中,对所有有 href 属性的 a 标签进行 click 事件监听,拦截原来 default 事件(跳转链接并刷新页面)
调用 window.history.pushState 更新路由(不刷新页面)和 handlePopstate 更新路由对应的试图。
<script type="text/javascript">
function myRouter() {
this.routersMap = {};
this.currentUrl = "";
this.add = function(path, callback) {
this.routersMap[path] = callback;
};
// 不同的地方在于 init 初始化函数,首先需要获取所有特殊的链接标签,然后监听点击事件,并阻止其默认事件,触发 history.pushState 以及更新相应的视图。
this.init = function() {
window.addEventListener("DOMContentLoaded", this.load(), false);
window.addEventListener("popstate", this.updateView(), false);
};
this.load = function() {
this.updateView();
const _this = this;
// 拦截 <a> 标签点击事件默认行为,点击时使用 pushState 修改URL并更新手动 UI,从而实现点击链接更新 URL 和 UI 的效果。
const aLinks = document.querySelectorAll("a[href]");
for (let i = 0; i < aLinks.length; i++) {
const link = aLinks[i];
link.addEventListener("click", function(e) {
e.preventDefault();
window.history.pushState(
{ state: link.getAttribute("href") },
"",
link.getAttribute("href")
);
_this.updateView();
});
}
};
this.updateView = function() {
console.log(1, location.pathname);
this.currentUrl = location.pathname || "/";
this.routersMap[this.currentUrl] &&
this.routersMap[this.currentUrl]();
};
this.getRouterPath = function() {
return window.location.pathname || "/";
};
this.pushRoute = function(path) {
// 因为pushState不会触发popState,所以需要手动updateView
window.history.pushState(null, null, path);
this.updateView();
};
this.go = function(delta) {
window.history.go(delta);
};
this.push = function() {
window.history.go(1);
};
this.back = function() {
window.history.back();
};
this.replace = function(path) {
// 因为replaceState不会触发popState,所以需要手动updateView
window.history.replaceState(null, null, path);
this.updateView();
};
}
const router = new myRouter();
router.init();
router.add("/about", function() {
document.getElementById("content").innerHTML = "about";
});
router.add("/topics", function() {
document.getElementById("content").innerHTML = "topics";
});
router.add("/", function() {
document.getElementById("content").innerHTML = "home";
});
</script>