谈谈前端路由

单页面应用与前端路由

在传统的 Web 开发中,浏览器根据地址栏的 URL 向服务器发送一个 HTTP 请求,服务器根据 URL 返回一个 HTML 页面。这种情况下,一个 URL 对应一个 HTML 页面,
一个 Web 应用包含很多 HTML 页面,这样的应用就是多页面应用;在多页面应用中,页面路由的控制由服务器负责,这种路由方式称为后端路由。

在多页面应用中,每次页面切换都需要向服务器发送一次请求,页面使用的静态资源也需要重新加载,存在一定的浪费。而且,页面的整体刷新对用户体验也有影响,因为不同页面
间往往存在共同的部分,例如导航栏、侧边栏等,页面整体刷新也会导致公共部分的刷新。

有没有一种方式让 Web 应用只是看起来像多页面应用,也就是说 URL 的变化可以引起页面内容的变化,但不会向服务器发送新的请求哪?满足这种条件的 Web 应用就是单页面
应用(Single Page Application,简称 SPA)。单页面应用虽然名为”单页“,但视觉上的感受仍然是多页面,因为 URL 发生变化,页面上的内容也会变化,但这只是逻辑上的多页面,实际上无论 URL 如何变化,对应的 HTML 文件都是同一个,这也是单页面应用名字的由来。在单页面应用中,URL 发生变化并不会向服务器发送新的请求,所以”逻辑页面“的变化只能由前端负责,这种方式称为前端路由。

前端路由的实现

路由就是 URL 到函数的映射,这个是前端路由的原理。如果做到在 URL 发生变化的时候不向服务器发送请求,而是去执行一个控制 UI 组件的函数哪?那就不得不说说 hash 和 history 这两种实现方案了。

基于 hash

在一个 URL 的组成中,#号包括#号后边的部分称为 hash。在浏览器中,可以通过location.hash获取到。#代表网页中的一个位置,其右边的字符,就是该位置的标识符。比如:

1
2
// #title 是 hash
http://www.example.com/index.html#title

#号是用来指导浏览器动作的,对服务器完全不起作用,HTTP 请求不会带上#号以及它后边的内容。单单改变#号后边的内容,浏览器只会滚动到指定的位置,不会重新加载网页。而且改变 hash 还会改变浏览器的历史记录。我们可以通过onhashchange监听到 hash 的改变来不刷新浏览器触发视图的更新。代码如下:

1
2
3
4
5
6
<ul>
<li><a href="#">white</a></li>
<li><a href="#yellow">yellow</a></li>
<li><a href="#green">green</a></li>
</ul>
这是页面的内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
function Router() {
this.routes = {};
this.currentUrl = "";
}

Router.prototype.route = function(path, callback) {
this.routes[path] =
callback ||
function() {
console.log("请为路由绑定处理方法");
};
};

Router.prototype.refresh = function() {
console.log("触发一次 hashchange,hash值为", location.hash);
this.currentUrl = "/" + location.hash.slice(1);
// 执行当前路由绑定的方法
this.routes[this.currentUrl]();
};

Router.prototype.init = function() {
window.addEventListener("DOMContentLoaded", this.refresh.bind(this), false);
window.addEventListener("hashchange", this.refresh.bind(this), false);
};

window.Router = new Router();
window.Router.init();

var content = document.querySelector("body");
function changeBgColor(color) {
content.style.backgroundColor = color;
}

Router.route("/", function() {
changeBgColor("white");
});
Router.route("/yellow", function() {
changeBgColor("yellow");
});
Router.route("/green", function() {
changeBgColor("green");
});

基于 history 模式

在 HTML5 规范中,history新增了一下几个 API:

1
2
3
history.pushState();    // 添加新的状态到历史状态栈
history.replaceState(); // 用新的状态代替当前状态
history.state // 返回当前状态对象

通过上面两个操作状态的 API,也能够做到:改变 url 的同时,不刷新页面。所以 history 也具备实现路由控制的潜力。仅仅是改变 url 不刷新页面还不够,还要能够监听到 url 的变化。对于 hash 来说,hash 的改变可以出发 onhashchange 事件,history 并没有这样的事件可以监听。然而,对于一个应用来说,改变一个 url 只有下面三种途径:

  • 点击浏览器的前进或者后退
  • 点击 a 标签
  • 在 JS 代码中直接修改路由

第 2 种和第 3 种途径可以看成是一种,因为 a 标签的默认事件可以被禁止,进而调用 js 方法。关键是第 1 种,HTML5 规范种新增了一个 onpopstate 事件,通过它便可以监听到前进或者后退的按钮点击。要特别注意的是:调用history.pushStatehistory.replaceState并不会触发 onpopstate 事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 页面加载完不会触发 hashchange,这里主动触发一次 hashchange 事件
window.addEventListener('DOMContentLoaded', onLoad)
// 监听路由变化
window.addEventListener('popstate', onPopState)

// 路由视图
var routerView = null

function onLoad () {
routerView = document.querySelector('#routeView')
onPopState()

// 拦截 <a> 标签点击事件默认行为, 点击时使用 pushState 修改 URL并更新手动 UI,从而实现点击链接更新 URL 和 UI 的效果。
var linkList = document.querySelectorAll('a[href]')
linkList.forEach(el => el.addEventListener('click', function (e) {
e.preventDefault()
history.pushState(null, '', el.getAttribute('href'))
onPopState()
}))
}

// 路由变化时,根据路由渲染对应 UI
function onPopState () {
switch (location.pathname) {
case '/home':
routerView.innerHTML = 'Home'
return
case '/about':
routerView.innerHTML = 'About'
return
default:
return
}
}

hash vs history

hash 模式下,每个 url 都会带有#号,看起来可能不太友好。但是,hash 模式兼容 IE8 及其以上的浏览器。history 模式使用了 HTML5 里边新的 API,看起来会比较友好。但是,仅仅有前端的参与还是不够的,需要后端进行配置,前端的路由要和后端的路由要匹配起来,在刷新浏览器的时候会给后端发送请求,这个时候后台需要对请求的 url,做一个捕捉,将后端不存在的 url,重定向到指定路由。

参考内容

Powered by Hexo and Hexo-theme-hiker

Copyright © 2018 - 2020 阿母工业前端组 All Rights Reserved.

UV : | PV :