剖析前端路由原理

192 阅读5分钟

前言

前端三大框架 Angular、React、Vue 他们都有自己的路由方案,都是基于前端路由原理进行封装实现的,我们在实际项目开发中会遇到一些坑,弄明白其中的实现原理才能在开发中游刃有余。

什么是路由

路由的概念起源于服务端,在以前前后端没有分离的时候,由后端来控制路由,当接收到客户端发来的HTTP请求,就会根据所请求的URL,来查找对应的映射函数,然后执行该函数,并将函数的返回值发送给客户端。

对于最简单的静态资源服务器,可以认为,所有的URL的映射函数就是一个文件读取操作,对于动态资源,映射函数可能是一个数据库读取操作,也可能是进行一些数据的处理等等,然后根据这些数据渲染模板,再将渲染好的页面返回。

这样处理的缺点和有优点都很明显:

  • 好处:安全性好、SEO好;
  • 缺点:加大了服务器的压力,不利于用户体验,代码比较冗余不好维护

因为服务端路由有自己的局限性,前端路由才能找到了自己的发挥空间,对于前端路由来说,路由的映射函数,通常是一些DOM的显示和隐藏操作,这样,当访问不同的路径的时候,会显示不同的页面组件,在现在的单页面应用中,前端路由描述的URL和UI之间的单向映射关系,即URL变化引起的UI页面的更新。

前端路由的核心问题

  • 如何检测到URL的变化?
  • 如何改变URL却不引起页面的刷新?

前端路由主要有以下两种实现方案:

  • 1、Hash
  • 2、History

Hash方式中,我们可以通过hashchange事件监听URL的变化,以下的场景会触发hashchange事件:通过浏览器前进后退改变URL、通过标签改变URL、通过window.location改变URL Hash 是 URL中 # 以及后面的部分,改变URL中的hash部分不会引起页面的刷新。

History方式中,我们可以通过popstate事件监听 URL的变化, 我们可以调用pushStatereplaceState两个方法,改变URL而不引起页面的刷新,值得注意的是,通过浏览器前进后退改变时候回触发popState事件,而通过pushState、replaceState或标签改变 URL 并不会触发popstate事件,因此我们需要手动拦截监听。

当然,前端路由也存在缺陷:使用浏览器的前进、后退的时候重新发送了请求,重新获取数据,没有合理的利用缓存,但是总的来说,现在前端路由已经是实现路由的主要方式了,前端三大框架 Angular、React、Vue,它们的路由解决方案 angular/router、react-router、vue-router都是基于前端路由进行开发的,因此将前端路由进行了解和掌握是很有必要的,下面我们分别对两种常见的前端路由模式 Hash 和 History 进行讲解。

前端路由的两种实现

Hash 模式

实现原理

早期的前端路由实现就是基于location.hash来实现的,其实现原理也很简单。location.hash的值就是url中 # 后面的内容,比如这个网址,它的location.hash的值为 '#/search'

https://www.word.com#search

此外,hash也存在下面几个特性

  • URL中hash值只是客户端的一种状态,也就是说当向服务器发出请求的时候,hash部分不会被发送。
  • hash值的改变,都会在浏览器的访问历史中添加一个记录,因此我们能够通过浏览器的回退、前进控制hash的切换
  • 我们可以使用hashChange来监听hash的变化
两种方式触发hash

a标签: 通过a标签设置 href属性,当用户点击这个标签后,URL就会发生改变,也就会触发 hashchange 事件了;

<a href="#search">search</a>

js控制实现:

另外一种就是js控制实现,从而改变URL

location.hash="#search"

js实现

我们先定义一个父类 BaseRouter,用于实现 Hash路由和 History 一些公共方法

export class BaseRouter {
  // list 表示路由表
  constructor(list) {
    this.list = list
  }
  // 页面渲染函数
  render() {
    let ele = this.list.find(ele => ele.path === state);
    ele ? ele: this.list.find( ele => ele.path === "*");
    ELEMENT.innerText = ele.component; 
  }
}

我们简单实现了 push 压入功能,go 前进后退功能

export class HashRouter extends BaseRouter {
  constructor(list) {
    super(list);
    this.handler();
    // 监听 hashchange 事件
    window.addEventListener('hashchange', e => {
      this.handler()
    })
  }

  // hash改变的时候,重新渲染页面
  handler() {
    this.render(this.getState());
  }
  // 获取hash值
  getState() {
    const hash = window.location.hash;
    return hash ? hash.slice(1): '/'
  }

  // push 新的页面
  push(path) {
    window.location.hash = path;
  }
  // 获取默认页面的url
  getUrl(path) {
    const href = window.location.href;
    const i = href.indexOf('#');
    const base = i >= 0 href.slice(0,i) : href;
    return base + '#' + path;
  }
  // 替换页面
  replace(path) {
    window.location.replace(this.getUrl(path));
  }
  // 前进 or 后退浏览历史
  go(n){
    window.history.go(n)
  }
}

History 模式

实现原理

前面的hash 虽然也很不错,但是使用上都需要加上 # ,并不是很美观,因此到了HTML5 又提供了 History API 来实现URL变化。其中做最主要的api有两个:

history.pushState() 和 history.replaceState() 这两个api可以在不进行刷新的状态下操作浏览器的历史记录,唯一不同的是,前者是新增加一个历史记录,后者是直接替换掉当前的历史记录。

window.history.pushState(null,null,path);
window.history.repalceState(null,null,path);

此外,history 还存在几个特性:

  • pushState 和 replaceState 的标题(title)一般浏览器会忽略,最好传入null
  • 我们可以使用 popstate 事件来监听 url 变化
  • history.pushState() 或者 history.repalceState() 不会触发 popstate 事件,这个时候我们应该手动触发页面渲染。

js实现

同样简单实现了push压入功能、 go 前进/后退功能

export class HistoryRouter extends BaseRouter {
  constructor() {
    super(list) {
      this.list = list;
    }
    this.handler();
    // 监听 popState 事件
    window.addEventListener('popstate', e => {
      console.log('触发 popstate。。。');
      this.handler();
    })

    // 渲染页面
    handler() {
      this.render(this.getState());
    }
    // 获取url
    getState() {
      const path = window.location.pathname;
      return path ? path : '/';
    }

    // push 页面
    push(path) {
      history.replaceState(null,null,path);
      this.handler();
    }
    // 前进 or 后退
    go(n) {
      window.history.go(n)
    }
  }
}

总结

本文介绍了什么是路由,前端路由的起源,以及分析了两种前端路由: Hash模式和History模式原理以及简单的实现。 通过对于本文对前端路由的原理的掌握,这个时候就可以基于原理去阅读react-router和 vue-router的源码实现了。