路由
hash
history
概念
mp.weixin.qq.com/s/2o7VvwyPI...
www.jianshu.com/p/b6684ef7c...
在 Web 开发过程中,经常遇到路由 的概念。那么到底什么是路由呢?简单来说,路由就是 URL 到函数的映射。
路由这个概念本来是由后端提出来的,在以前用模板引擎开发页面的时候,是使用路由返回不同的页面,大致流程是这样的:
- 浏览器发出请求;
- 服务器监听到 80 或者 443 端口有请求过来,并解析 UR L路径;
- 服务端根据路由设置,查询相应的资源,可能是 html 文件,也可能是图片资源......,然后将这些资源处理并返回给浏览器;
- 浏览器接收到数据,通过
content-type
决定如何解析数据
简单来说,路由就是用来跟后端服务器交互的一种方式,通过不同的路径来请求不同的资源,请求HTML
页面只是路由的其中一项功能。
-
服务端路由
当服务端接收到客户端发来的 HTTP 请求时,会根据请求的 URL,找到相应的映射函数,然后执行该函数,并将函数的返回值发送给客户端。
- 对于最简单的静态资源服务器,可以认为,所有 URL 的映射函数就是一个文件读取操作。
- 对于动态资源,映射函数可能是一个数据库读取操作,也可能进行一些数据处理,
-
客户端路由
服务端路由会造成服务器压力比较大,而且用户访问速度也比较慢。在这种情况下,出现了单页应用。
单页应用,就是只有一个页面,用户访问网址,服务器返回的页面始终只有一个,不管用户改变了浏览器地址栏的内容或者在页面发生了跳转,服务器不会重新返回新的页面,而是通过相应的js操作来实现页面的更改。
前端路由的优点:
- 前端路由可以让前端自己维护路由与页面展示的逻辑,每次页面改动不需要通知服务端。
- 更好的交互体验:不用每次从服务端拉取资源。
区别
模式的区别
hash
模式来说, 它虽然看着是改变了 url
,但不会被包括在 http
请求中,所以这种模式不利于 SEO
优化。即所有页面的跳转都是在客户端进行操作。所以,它算是被用来指导浏览器的动作,并不影响服务器端。
使用 history
模式时,在对当前的页面进行刷新时,此时浏览器会重新发起请求。如果 nginx
没有匹配得到当前的 url
,就会出现 404
的页面。
history 模式需要服务端的支持,服务端在接受到所有页面请求后,都指向index.html文件,或设置404页面为index.html。不然刷新时页面会出现404。
router 和 route 的区别
- router 可以理解为一个容器,或者说一种机制,它管理了一组 route。
- route 就是一条路由,它将一个 URL 路径和一个函数进行映射。
概括为:route 只是进行了 URL 和函数的映射,在当接收到一个 URL 后,需要去路由映射表中查找相应的函数(组件函数),这个过程是由 router 来处理的。
实现原理
实现前端路由,需要解决两个核心:
- 如何改变 URL 却不引起页面刷新?
- 如何检测 URL 变化了?
hash 实现
hash 是 URL 中 hash (#) 及后面的那部分,常用作锚点在页面内进行导航,改变 URL 中的 hash 部分不会引起页面刷新
改变 URL 会触发 hashchange 事件:
- 浏览器前进后退改变URL
- 通过a标签锚点方式改变URL。
- 通过window.location.hash改变URL
- 调用history的back、go、forward方法
不能触发事件的方式
- pushState和replaceState
javascript
window.addEventListener(
'hashchange',
function (event) {
const oldURL = event.oldURL; // 上一个URL
const newURL = event.newURL; // 当前的URL
console.log(newURL, oldURL);
},
false
);
简单
xml
<ul>
<!-- 定义路由 -->
<li><a href="#/home">home</a></li>
<li><a href="#/about">about</a></li>
<!-- 渲染路由对应的 UI -->
<div id="routeView"></div>
</ul>
<script>
// 页面加载完不会触发 hashchange,这里主动触发一次 hashchange 事件
window.addEventListener('DOMContentLoaded', onLoad)
// 监听路由变化
window.addEventListener('hashchange', onHashChange)
// 路由视图
var routerView = null
function onLoad () {
routerView = document.querySelector('#routeView')
onHashChange()
}
// 路由变化时,根据路由渲染对应 UI
function onHashChange () {
switch (location.hash) {
case '#/home':
routerView.innerHTML = 'Home'
return
case '#/about':
routerView.innerHTML = 'About'
return
default:
return
}
}
</script>
详细
对 oldURL 和 newURL 进行拆分后,就能获取到更详细的 hash 值。我们这里从创建一个 HashRouter 的 class 开始一步步写起:
ini
class HashRouter {
currentUrl = ''; // 当前的URL
handlers = {};
getHashPath(url) {
const index = url.indexOf('#');
if (index >= 0) {
return url.slice(index + 1);
}
return '/';
}
}
事件hashchange
只会在 hash 发生变化时才能触发,而第一次进入到页面时并不会触发这个事件 ,因此我们还需要监听load
事件。这里要注意的是,两个事件的 event 是不一样的:hashchange 事件中的 event 对象有 oldURL 和 newURL 两个属性,但 load 事件中的 event 没有这两个属性,不过我们可以通过 location.hash 来获取到当前的 hash 路由
javascript
class HashRouter {
currentUrl = ""; // 当前的URL
handlers = {};
constructor() {
this.refresh = this.refresh.bind(this);
window.addEventListener(
"load",
(curURL, oldURL) => {
console.log("load");
this.emit("change", curURL, oldURL);
},
false
);
window.addEventListener(
"hashchange",
(curURL, oldURL) => {
console.log("hashchange");
this.emit("change", curURL, oldURL);
},
false
);
}
getHashPath(url) {
const index = url.indexOf("#");
if (index >= 0) {
return url.slice(index + 1);
}
return "/";
}
on(evName, listener) {
this.handlers[evName] = listener;
}
emit(evName, ...args) {
const handler = this.handlers[evName];
if (handler) {
handler(...args);
}
}
refresh(event) {
let curURL = "",
oldURL = null;
if (event.newURL) {
oldURL = this.getHashPath(event.oldURL || "");
curURL = this.getHashPath(event.newURL || "");
} else {
curURL = this.getHashPath(window.location.hash);
}
this.currentUrl = curURL;
}
}
调用
ini
// 先定义几个路由
const routes = [
{
path: "/",
name: "home",
component: "<h1>home</h1>",
// component: <Home />
},
{
path: "/about",
name: "about",
// component: <About />
component: "<h1>1</h1>",
},
{
path: "*",
name: "404",
component: "<h1>404</h1>",
// component: <NotFound404 />
},
];
const router = new HashRouter();
// 监听change事件
router.on("change", (currentUrl, lastUrl) => {
let route = null;
// 匹配路由
for (let i = 0, len = routes.length; i < len; i++) {
const item = routes[i];
if (router.getHashPath(currentUrl.target.location.hash) === item.path) {
route = item;
break;
}
}
// 若没有匹配到,则使用最后一个路由
if (!route) {
route = routes[routes.length - 1];
}
// 渲染当前的组件
document.getElementById("app").innerHTML = route.component;
});
history实现
history 提供了 pushState 和 replaceState 两个方法,这两个方法改变 URL 的 path 部分不会引起页面刷新。
history 提供类似 hashchange 事件的 popstate 事件,但 popstate 事件有些不同:a标签改变 URL 不会触发 popstate 事件
history提供了popstate监听事件,但是只有以下两种情况会触发该事件
- 点击浏览器前进后退的按钮
- 显示调用history的back、go、forward方法
不能触发事件的方式
-
pushState和replaceState (可以使用
window.dispatchEvent
重写事件,实现对popstate监听)iniconst listener = function (type) { var orig = history[type]; return function () { var rv = orig.apply(this, arguments); var e = new Event(type); e.arguments = arguments; window.dispatchEvent(e); return rv; }; }; window.history.pushState = listener('pushState'); window.history.replaceState = listener('replaceState');
-
a标签不能触发,因为非锚点模式直接跳转了页面。
详细
xml
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app"></div>
<a
href="http://127.0.0.1:5500/front/%E6%BA%90%E7%A0%81demo/router/history.html/"
>home</a
>
<a
href="http://127.0.0.1:5500/front/%E6%BA%90%E7%A0%81demo/router/history.html/about"
>about</a
>
<a
href="http://127.0.0.1:5500/front/%E6%BA%90%E7%A0%81demo/router/history.html/404"
>404</a
>
<button>location</button>
<button>pushState</button>
</body>
<script>
document.getElementsByTagName("button")[0].addEventListener("click", () => {
location.href =
"http://127.0.0.1:5500/front/%E6%BA%90%E7%A0%81demo/router/history.html/404";
});
document
.getElementsByTagName("button")[1]
.addEventListener("click", (e) => {
window.history.pushState("a", "", "about");
});
// 先定义几个路由
const routes = [
{
path: "/",
name: "home",
component: "<h1>home</h1>",
// component: <Home />
},
{
path: "/about",
name: "about",
// component: <About />
component: "<h1>about</h1>",
},
{
path: "*",
name: "404",
component: "<h1>404</h1>",
// component: <NotFound404 />
},
];
class HistoryRouter {
currentUrl = "";
handlers = {};
constructor() {
this.refresh = this.refresh.bind(this);
this.addStateListener();
window.addEventListener("load", this.refresh, false);
window.addEventListener("popstate", this.refresh, false);
window.addEventListener("pushState", this.refresh, false);
window.addEventListener("replaceState", this.refresh, false);
}
addStateListener() {
const listener = function (type) {
var orig = history[type];
return function () {
console.log(orig, type, arguments);
var rv = orig.apply(this, arguments);
var e = new Event(type);
e.arguments = arguments;
window.dispatchEvent(e);//使用window.dispatchEvent添加事件
return rv;
};
};
window.history.pushState = listener("pushState");
window.history.replaceState = listener("replaceState");
}
refresh(event) {
this.currentUrl = location.pathname;
console.log(location);
this.emit("change", location.pathname);
// document.querySelector("#app span").innerHTML = location.pathname;
}
on(evName, listener) {
this.handlers[evName] = listener;
}
emit(evName, ...args) {
const handler = this.handlers[evName];
if (handler) {
handler(...args);
}
}
}
const router = new HistoryRouter();
// 监听change事件
router.on("change", (currentUrl) => {
let route = null;
// 匹配路由
for (let i = 0, len = routes.length; i < len; i++) {
const item = routes[i];
console.log("/" + currentUrl.split("/").pop(), item.path);
if ("/" + currentUrl.split("/").pop() === item.path) {
route = item;
break;
}
}
if (currentUrl.split("/").pop().includes("html")) {
route = routes[0];
} else if (!route) {
// 若没有匹配到,则使用最后一个路由
route = routes[routes.length - 1];
}
// 渲染当前的组件
document.getElementById("app").innerHTML = route.component;
});
</script>
</html>