# 前端路由

在前端技术早期,一个url对应一个页面,如果你要从A页面切换到B页面,那么必然伴随着页面的刷新。后面Ajax出现了允许人们不刷新页面情况下发起请求,还有不刷新页面可以更新页面内容。在这样的背景下出现了单页应用SPA。单页应用并不知道页面进展到哪一步了,换言之刷新界面就又清零了,所以前端路由出现了。

实现前端路由需要解决两个核心:

  • 如何改变URL却不引起页面刷新?
  • 如何检测URL变化?捕捉到 url 的变化,以便执行页面替换逻辑

从实现方式来回答上面两个问题

# hash实现

  • hashURLhash#)及后面的那部分,常用作锚点在页面内进行导航,改变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 = '';
  }
}
1
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]();
  }
}
1
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]();
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# hash相关API

  1. location.href:返回完整的URL
  2. location.hash: 返回URL的锚部分
  3. location.pathname:返回URL路径名
  4. hashchange事件:当location.hash发生改变时,将触发这个事件。

# history实现

  • window.history提供了pushStatereplaceState两个方法,这两个方法改变URLpath部分不会引起页面刷新。
  • history 提供类似 hashchange 事件的 popstate 事件,但 popstate 事件有些不同:通过浏览器前进后退改变 URL 时会触发 popstate 事件,通过pushState/replaceState<a>标签改变 URL 不会触发popstate 事件。 好在我们可以拦截 pushState/replaceState的调用和<a>标签的点击事件来检测 URL 变化,所以监听 URL 变化可以实现,只是没有 hashchange 那么方便。
  • 因为没有 # 号,所以当用户刷新页面之类的操作时,浏览器还是会给服务器发送请求。为了避免出现这种情况,所以这个实现需要服务器的支持,需要把所有路由都重定向到根页面。

# window.history相关API

  1. history.go(n)::路由跳转,比如n2 是往前移动2个页面,n-2 是向后移动2个页面,n0是刷新页面
  2. history.back():路由后退,相当于 history.go(-1)
  3. history.forward():路由前进,相当于 history.go(1)
  4. history.pushState():添加一条路由历史记录,如果设置跨域网址则报错,回退按钮点击回退到上个页面。

TIP

history.pushState用于在浏览历史中添加历史记录,但是并不触发跳转,此方法接受三个参数,依次为:

  • state:一个与指定网址相关的状态对象,popstate事件触发时,该对象会传入回调函数。如果不需要这个对象,此处可以填null
  • title:新页面的标题,但是所有浏览器目前都忽略这个值,因此这里可以填null
  • url:新的网址,必须与当前页面处在同一个域。浏览器的地址栏将显示这个网址。
  1. history.replaceState():替换当前页在路由历史记录的信息,不会在路由history栈中记录,回退按钮点击回退到上上个页面。
  2. popstate 事件:当活动的历史记录发生变化,就会触发 popstate 事件。

WARNING

需要注意的是,仅仅调用pushState方法或replaceState方法 ,并不会触发该事件,只有用户点击浏览器倒退按钮和前进按钮,或者使用JavaScript调用back、forward、go方法时才会触发。

pushStatereplaceState 不同,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]();
    });
  }
}
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

# 两种路由模式总结

Hash 模式是使用 URLHash 来模拟一个完整的 URL,因此当 URL 改变的时候页面并不会重载。History 模式则会直接改变 URL,所以在路由跳转的时候会丢失一些地址信息,在刷新或直接访问路由地址的时候会匹配不到静态资源。因此需要在服务器上配置一些信息,让服务器增加一个覆盖所有情况的候选资源,比如跳转 index.html 什么的,一般来说是你的 app 依赖的页面,事实上 vue-router 等库也是这么推荐的,还提供了常见的服务器配置。