路由

后端路由

多页应用中,一个URL对应一个HTML页面,一个Web应用包含很多HTML页面,在多页应用中,页面路由控制由服务器端负责,每次页面切换都需要向服务器发送一次请求,页面使用到的静态资源也需要重新加载,存在一定的浪费

前端路由

在web前端单页应用SPA中,路由描述的是URL与UI之间的映射关系,这种映射是单向的,即URL变化引起UI更新(无需刷新页面)

如何实现前端路由

改变URL,不引起页面刷新

  1. hash实现:hash是URL中#及后面的部分,常用作锚点在页面内进行导航,改变URL中hash部分不会引起页面刷新
  2. history实现:history提供了pushState和replaceState中两个方法,这两个方法改变URL的path部分不会引起页面刷新

检测URL变化

  1. hash实现:通过hashChange事件监听URL的变化,以下改变URL的方式会触发hashChange:
  • 通过浏览器的前进后退
  • 通过a标签
  • 通过window.location
  1. history实现:history提供类似hashChange的popstate事件,通过以下几种方式监听URL变化
  • 浏览器前进后退
  • 拦截pushState/replaceState
  • 拦截a标签的点击事件

图解

An image

举例

hash demo

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
  </head>
  <body>
    <ul>
      <li><a href="#/">首页</a></li>
      <li><a href="#/list">列表页面</a></li>
      <li><a href="#/cart">购物车页面</a></li>
      <li><a href="#/home">我的</a></li>
    </ul>
    <div id="content"></div>
    <script src="./js/hashRouter.js"></script>
  </body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// hashRouter.js
class Router {
  constructor(routes) {
    this.routes = {}
    this.init(routes)
    this.onPopState()
  }
  init(routes) {
    let _this = this
    Object.keys(routes).forEach((item, index, array) => {
      _this.listen(item, routes[item])
    })
    window.addEventListener('hashchange', this.refresh.bind(this,routes))
  }
  listen(url, fn) {
    this.routes[url] = fn
  }
  refresh(routes) {
    this.curUrl = location.hash.slice(1) || '/'
    this.routes[this.curUrl] && this.routes[this.curUrl]()
  }
  onPopState() {
    let _this = this
    window.addEventListener('popstate', e => {
      let url = e.state && e.state.url
      _this.routes[url] && _this.routes[url]()
    })
  }
}
const route = new Router({
  '/': () => render('首页内容'),
  '/list': () => render('列表页内容'),
  '/cart': () => render('购物车内容'),
  '/home': () => render('我的内容')
})
function render (content) {
  document.getElementById('content').innerHTML = content
}
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

history demo

// index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>路由实现</title>
</head>
<body>
  <div class="nav" id="nav">
    <a href="javascript:void(null)" onclick="route.go('/')">首页</a>
    <a href="javascript:void(null)" onclick="route.go('/list')">列表页面</a>
    <a href="javascript:void(null)" onclick="route.go('/cart')">购物车页面</a>
    <a href="javascript:void(null)" onclick="route.go('/home')">我的</a>
  </div>
  <div id="content"></div>
  <script src="./js/router.js"></script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// router.js
class Router {
  constructor(routes) {
    this.routes = {}
    this.init(routes)
    this.onPopState()
  }
  init(routes) {
    let _this = this
    Object.keys(routes).forEach((item, index, array) => {
      _this.listen(item, routes[item])
    })
    this.go('/')
  }
  listen(url, fn) {
    this.routes[url] = fn
  }
  go(url) {
    history.pushState({url}, null, url)
    this.routes[url] && this.routes[url]()
  }
  onPopState() {
    let _this = this
    window.addEventListener('popstate', e => {
      let url = e.state && e.state.url
      _this.routes[url] && _this.routes[url]()
    })
  }
}
const route = new Router({
  '/': () => render('首页内容'),
  '/list': () => render('列表页内容'),
  '/cart': () => render('购物车内容'),
  '/home': () => render('我的内容')
})
function render (content) {
  document.getElementById('content').innerHTML = content
}
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