# 前端路由
在前端技术早期,一个
url对应一个页面,如果你要从A页面切换到B页面,那么必然伴随着页面的刷新。后面Ajax出现了允许人们不刷新页面情况下发起请求,还有不刷新页面可以更新页面内容。在这样的背景下出现了单页应用SPA。单页应用并不知道页面进展到哪一步了,换言之刷新界面就又清零了,所以前端路由出现了。
实现前端路由需要解决两个核心:
- 如何改变
URL却不引起页面刷新? - 如何检测
URL变化?捕捉到url的变化,以便执行页面替换逻辑
从实现方式来回答上面两个问题
# hash实现
hash是URL中hash(#)及后面的那部分,常用作锚点在页面内进行导航,改变URL中的hash部分不会引起页面刷新- 通过
hashchange事件监听URL的变化,改变URL的方式:通过浏览器前进后退改变URL,通过a标签改变,通过window.location改变URL,这几种情况都会触发该事件。 - 改变
hash会改变浏览器历史浏览记录
TIP
在第一个#后面出现的任何字符,都会被浏览器解读为位置标识符。这意味着这些字符都不会被发送到服务器端。因此后面 hash 值的变化,并不会导致浏览器向服务器发出请求,浏览器不发出请求,也就不会刷新页面。另外每次 hash 值的变化,还会触发 hashchange 这个事件,通过这个事件我们就可以知道 hash 值发生了哪些变化。
- 改变
hash值方式:a标签使锚点变化- 通过设置
window.location.hash的值 - 浏览器前进键,后退键
# 初始化class
初始化一个路由。
class Routers {
constructor() {
// 以键值对的形式储存路由
this.routes = {};
// 当前路由的URL
this.currentUrl = '';
}
}
2
3
4
5
6
7
8
# 实现路由hash存储和执行
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]();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 监听对应事件
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]();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# hash相关API
location.href:返回完整的URLlocation.hash: 返回URL的锚部分location.pathname:返回URL路径名hashchange事件:当location.hash发生改变时,将触发这个事件。
# history实现
window.history提供了pushState和replaceState两个方法,这两个方法改变URL的path部分不会引起页面刷新。history提供类似hashchange事件的popstate事件,但popstate事件有些不同:通过浏览器前进后退改变URL时会触发popstate事件,通过pushState/replaceState或<a>标签改变URL不会触发popstate事件。 好在我们可以拦截pushState/replaceState的调用和<a>标签的点击事件来检测URL变化,所以监听URL变化可以实现,只是没有hashchange那么方便。- 因为没有
#号,所以当用户刷新页面之类的操作时,浏览器还是会给服务器发送请求。为了避免出现这种情况,所以这个实现需要服务器的支持,需要把所有路由都重定向到根页面。
# window.history相关API
history.go(n)::路由跳转,比如n为2是往前移动2个页面,n为-2是向后移动2个页面,n为0是刷新页面history.back():路由后退,相当于history.go(-1)history.forward():路由前进,相当于history.go(1)history.pushState():添加一条路由历史记录,如果设置跨域网址则报错,回退按钮点击回退到上个页面。
TIP
history.pushState用于在浏览历史中添加历史记录,但是并不触发跳转,此方法接受三个参数,依次为:
state:一个与指定网址相关的状态对象,popstate事件触发时,该对象会传入回调函数。如果不需要这个对象,此处可以填null。title:新页面的标题,但是所有浏览器目前都忽略这个值,因此这里可以填null。url:新的网址,必须与当前页面处在同一个域。浏览器的地址栏将显示这个网址。
history.replaceState():替换当前页在路由历史记录的信息,不会在路由history栈中记录,回退按钮点击回退到上上个页面。popstate事件:当活动的历史记录发生变化,就会触发popstate事件。
WARNING
需要注意的是,仅仅调用pushState方法或replaceState方法 ,并不会触发该事件,只有用户点击浏览器倒退按钮和前进按钮,或者使用JavaScript调用back、forward、go方法时才会触发。
与 pushState 和 replaceState 不同,a 标签锚点的变化会立即触发 popstate 事件。这里我们扩展一下思路,a 标签做的事情就是改变了 hash 值,那通过 window.location 改变 hash 值是不是也是能立即触发 popstate。答案是肯定的,也会立即触发 popstate。
**hash 值的改变会触发 hashchange 事件,所以,hash 值的改变会同时触发 popstate 事件与 hashchange 事件。但如果改变的 hash 值与当前 hash 值一样的话,hashchange事件不触发,popstate 事件触发。**之前我们说过,window.location 设置的 hash 值必须与当前 hash 值不一样才能新建一条历史记录,而 pushState 却可以。
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]();
});
}
}
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
# 两种路由模式总结
Hash模式是使用URL的Hash来模拟一个完整的URL,因此当URL改变的时候页面并不会重载。History模式则会直接改变URL,所以在路由跳转的时候会丢失一些地址信息,在刷新或直接访问路由地址的时候会匹配不到静态资源。因此需要在服务器上配置一些信息,让服务器增加一个覆盖所有情况的候选资源,比如跳转index.html什么的,一般来说是你的app依赖的页面,事实上vue-router等库也是这么推荐的,还提供了常见的服务器配置。