# 前端路由
在前端技术早期,一个
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
:返回完整的URL
location.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
等库也是这么推荐的,还提供了常见的服务器配置。