什么是前端路由

路由是根据不同的 url 地址展示不同的内容或页面。

早期的路由都是后端直接根据 url 来 reload 页面实现的,即后端控制路由。后来页面越来越复杂,服务器压力越来越大,随着 ajax(异步刷新技术) 的出现,页面实现非 reload 就能刷新数据,让前端也可以控制 url 自行管理,前端路由由此而生。

单页面应用的实现,就是因为有了前端路由这个概念。

前端路由的两种模式

Hash路由

hash就是指在 url 中看到 #, 以及其后面的字符。这个 # 有两种情况,一个是我们所谓的锚点,本身是用来做页面定位的,它可以使对应 id 的元素显示在可视区域内。而路由里的 # 不叫锚点,我们称之为 hash。hash满足以下几个特性,才使得其可以实现前端路由:

  1. url中hash值的变化并不会重新加载页面,因为hash是用来指导浏览器行为的,对服务端是无用的,所以不会包括在http请求中。
  2. hash值的改变,都会在浏览器的访问历史中增加一个记录,也就是能通过浏览器的回退、前进按钮控制hash的切换
  3. 我们可以通过hashchange事件,监听到hash值的变化,从而响应不同路径的逻辑处理。

触发hash值的变化有2种方法:

  1. 一种是通过a标签,设置href属性,当标签点击之后,地址栏会改变,同时会触发hashchange事件
1
<a href="#index">to index</a>
  1. 一种是通过js直接赋值给location.hash,也会改变url,触发hashchange事件。
1
window.location.hash="#index"

History路由

HTML5的 History API 为浏览器的全局history对象增加了扩展方法。

window对象提供了onpopstate事件来监听历史栈的改变,一旦历史栈信息发生改变,便会触发该事件。

特别注意的是,调用history.pushState()或history.replaceState()不会触发popstate事件。只有在做出浏览器动作时,才会触发该事件。

history提供了两个操作历史栈的API:history.pushState 和 history.replaceState,这两个API都接收三个参数:

1
2
window.history.pushState(null, null, "http://www.google.com");
window.history.replaceState(null, null, "http://www.google.com");

  • 状态对象(state object),一个JavaScript对象,与用pushState()方法创建的新历史记录条目关联。无论何时用户导航到新创建的状态,会触发popstate事件,并能在事件中使用该对象。
  • 标题(title) :传入一个短标题给当前state。现在大多数浏览器不支持或者会忽略此参数,最好传入null代替;
  • 地址(URL):新的历史记录条目的地址。浏览器不会在调用pushState()方法后加载该地址,但之后,可能会试图加载,例如用户重启浏览器。新的URL不一定是绝对路径;如果是相对路径,它将以当前URL为基准;传入的URL与当前URL应该是同源的,否则,pushState()会抛出异常。该参数是可选的;不指定的话则为文档当前URL。

这两个API的相同之处是都会操作浏览器的历史记录,而不会引起页面的刷新。不同之处在于,pushState会增加一条新的历史记录,而replaceState则会替换当前的历史记录。这两个api,加上state改变触发的popstate事件,提供了单页应该的另一种路由方式。

当我们使用history模式时,如果服务器没有进行配置,刷新页面会出现404。

原因是因为history模式的url是真实的url,服务器会对url的文件路径进行资源查找,找不到资源就会返回404。

解决方法就是对服务器进行配置,将所有向服务器请求的URL资源,都重定向到index.html返回给客户端。

  • 在nignx环境下:
1
2
3
4
5
location /{
root /data/nginx/html;
index index.html index.htm;
error_page 404 /index.html;
}
  • http-server下:

安装npm包 spa-http-server,启动时增加 –push-state 参数

1
http-server --push-state

  • webpack开发环境下:

使用webpack-dev-server的里的historyApiFallback属性来支持HTML5 History Mode。

1
2
3
4
5
6
7
{

devServer: {
//在开发单页应用时非常有用,它依赖于HTML5 history API,如果设置为true,所有的跳转将指向index.html (解决histroy mode 404)
historyApiFallback: true
}
}

兼容性

出现兼容性问题主要是在IE下面,hash路由在IE8及以上可用,而history路由在IE10及以上才可用。

实现一个简单的路由

先构造一个myRouter类,根据不同的mode参数,分别为这两种方式创建对应的类,并进行实例化,完成myRouter类的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class myRouter {
constructor(opts) {
this.router = opts.mode !== 'hash' ? new HistoryRouter(opts) : new HashRouter(opts)
this.router.init()
}
push(path) {
this.router.push(path)
}
replace(path) {
this.router.replace(path)
}
go(num) {
this.router.go(num)
}
}

hashRouter

  • 初始化

插件在被调用的时候进行初始化,作用是注册路由以及绑定对应的路由切换事件的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
init() {
var that = this
// 注册路由
this.initRouter()
// debugger
// 页面加载匹配路由
window.addEventListener('load', function () {
that.urlChange()
})

// 路由切换
window.addEventListener('hashchange', function () {
that.urlChange()
})
}

  • 路由注册

将路由对象数组参数在初始化的时候就做好路由匹配。this.routers用来存储路由对象,执行每一个路由的callback函数就是加载对应的js文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
initRouter() {
// debugger
(this.opts.routes || []).forEach((item, index) => {
this.map(item)
})
}

// 单个路由注册
map(item) {
let path = item.path.replace(/\s*/g, '')// 过滤空格

this.routers[path] = {
callback: (state) => {
return this.asyncFun(item.url, state)
} // 回调
}
}

  • asyncFun函数

异步加载目标js文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 路由异步懒加载js文件
asyncFun(file, transition) {
var that = this

var _body = document.getElementsByTagName('body')[0]
var scriptEle = document.createElement('script')
scriptEle.type = 'text/javascript'
scriptEle.src = file
scriptEle.async = true

scriptEle.onload = function () {
that.opts.afterFun && that.opts.afterFun(transition)
}
_body.appendChild(scriptEle)
}
  • render函数

作用就是渲染页面,在这里也就是执行加载路由对应的js文件。如果存在beforeFun钩子的话,则由beforeFun钩子触发render函数。

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
// 渲染视图(执行匹配到的js代码)
render(currentPath) {
this.currentPath = currentPath
// 全局路由守护
let pathObj = this.routers[currentPath.path]
if(!pathObj) {
alert('404')
return
}

if (this.opts.beforeFun) {
this.opts.beforeFun({
to: {
path: currentPath.path,
query: currentPath.query
},
next() {
// 执行目标路由对应的js代码(相当于是组件渲染)
pathObj.callback(currentPath)
}
})
} else {
pathObj.callback(currentPath)
}
}

historyRouter

historyRouter的实现与HashRouter的实现也是很类似的,下面只写下不同之处:

  • 路由监听
  1. historyRouter
1
2
3
window.addEventListener('popstate', function () {
that.urlChange()
})
  1. HashRouter
1
2
3
window.addEventListener('hashchange', function () {
that.urlChange()
})
  • 获取当前路由和参数
  1. historyRouter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//获取当前hash
getPath() {
var hash = window.location.pathname
return hash
}
//获取参数
getParams() {
var paramsStr = window.location.search
var index = paramsStr.indexOf('?')
var params = {}

if(index !== -1) {
let arr = paramsStr.slice(1).split('&')
for(let i = 0; i < arr.length; i++){
let data = arr[i].split("=")
if(data.length == 2){
params[data[0]] = data[1]
}
}
}
return params
}
  1. HashRouter
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
getHash() {
var hash = window.location.hash.slice(1)
var index = hash.indexOf('?')
if(hash === '') return '/'

if(index !== -1) {
return hash.slice(0, index)
}

return hash
}
//获取参数
getParams() {
var hash = window.location.hash
var index = hash.indexOf('?')
var params = {}

if(index !== -1) {
let arr = hash.slice(index + 1).split('&')
for(let i = 0; i < arr.length; i++){
let data = arr[i].split("=")
if(data.length == 2){
params[data[0]] = data[1]
}
}
}
return params
}
  • push方法,压入history栈,进行路由跳转
  1. historyRouter
1
2
3
4
push(path) {
window.history.pushState(null, null, path)
this.urlChange() //手动触发
}
  1. HashRouter
1
2
3
push(path) {
window.location.hash = path
}

完整代码

router

参考资料