谈谈vue-router的实现原理

x33g5p2x  于2021-09-27 转载在 Vue.js  
字(5.9k)|赞(0)|评价(0)|浏览(343)

前言

相信不少伙伴都听过SPA(单页面应用),SPA指的是在一个应用中只有一个主的index.html页面,区别于多页面应用(多个index.html页面)。SPA的优点如下:
1、交互体验良好
单页应用的内容的改变不需要重新加载整个页面,获取数据也是通过Ajax异步获取,没有页面之间的切换,就不会出现“白屏现象”,也不会出现假死并有“闪烁”现象,页面显示流畅,用户的交互体验得到了提升和改善。
2、前后端分离
良好的前后端分离机制,后段无需负责模板渲染、输出页面工作,后端API通用化,即同一套后端程序代码,不用修改就可以用于Web界面、手机、平板等多种客户端。
3、服务器压力减小,节约服务器资源
单页应用相对服务器压力小,服务器只用出数据就可以,不用管展示逻辑和页面合成,所以服务器的吞吐能力会得到大幅的提高,吞吐量可提升几倍甚至几十倍以上。
而对于单页面应用,它是如何在一个页面中实现视图更新的呢? 其中最重要的一个概念就是前端路由,那么接下来就做一个简单的介绍和分析:

vue-router 介绍

vue-router是Vue.js官方提供的路由插件,专门配合vue实现路由跳转功能,需要使用Vue.use()进行安装。区别于传统的通过超链接的形式跳转,实现了在页面无刷新的状态下更新页面视图,用户体验感更好。

vue-router底层原理

vue-router的实现流程主要由如下几个步骤:
1、url发生改变
2、 触发监听事件
3、改变vue-router里面的current变量
4、监视current变量(变量的监视者)
5、获取对应的组件
6、render新组件
7、页面重新渲染

更为详细的底层原理这里便不再赘述,可以直接去看vue-router源码,纸上得来终觉浅,绝知此事要躬行,自己去翻源码捋一捋,会有不一样的收获和成长。

路由的三种模式

...
export default class VueRouter {
  ...
  static install: () => void;
  constructor(options: RouterOptions = {}) {
    let mode = options.mode || "hash";
    this.fallback =
      mode === "history" && !supportsPushState && options.fallback !== false;
    if (this.fallback) {
      mode = "hash";
    }
    if (!inBrowser) {
      mode = "abstract";
    }
    this.mode = mode;

    switch (mode) {
      case "history":
        this.history = new HTML5History(this, options.base);
        break;
      case "hash":
        this.history = new HashHistory(this, options.base, this.fallback);
        break;
      case "abstract":
        this.history = new AbstractHistory(this, options.base);
        break;
      default:
        if (process.env.NODE_ENV !== "production") {
          assert(false, `invalid mode: ${mode}`);
        }
    }
  }
  ...
}

上面是vue-router源码片段,由此可知vue-router 有三种路由模式:hash模式、history模式和abstract模式,比较常用的是hash模式和history模式。

一般场景下,hash 和 history 都可以用,除非你更在意颜值或者有强迫症,毕竟/# 符号夹杂在 URL 里我是觉得看起来确实有些不太美丽。如果不想浏览器url里显示很丑的 hash路由网址,我们可以用路由的 history 模式,这种模式利用 history.pushState API 来完成,URL 跳转而无须重新加载页面(后文会详解)。

hash模式与history模式都是通过浏览器接口实现的,除此之外vue-router还为非浏览器环境准备了一个abstract模式,其原理为用一个数组stack模拟出浏览器历史记录栈的功能。

看到这里,小伙伴可以思考一下,在前端里面如何不刷新页面实现跳转?

目前浏览器中有两种方式可以不刷新页面,实现跳转:1.HTML5的historyAPI,2.通过/#。

mode参数作用

我们从上面的vue-router源码里看到一个非常重要的参数–mode,从代码逻辑里可以解读出mode的作用

  1. 作为参数传入的字符串属性mode只是一个标记,用来指示实际起作用的对象属性history的实现类:HashHistory,HTML5History和AbstractHistory。
  2. 在初始化对应的history之前,会对mode做一些校验:若浏览器不支持HTML5History方式(通过supportsPushState变量判断),则mode强制设为’hash’;若不是在浏览器环境下运行(node环境),则mode强制设为’abstract’。
  3. VueRouter类中的onReady(), push()等方法只是一个代理,实际是调用的具体history对象的对应方法,在init()方法中初始化时,也是根据history对象具体的类别执行不同操作。

注:如果浏览器不支持,'history’模式会回滚为’hash’模式。不在浏览器环境下(node环境)运行会强制为’abstract’模式。

三种模式源码调用逻辑

history模式

  • history -> HTML5History -> window.history.pushState()、window.history.go(n)、window.history.replaceState()

app是Vue根实例,以下是源码调用逻辑。

this.$router.push() -> VueRouter.prototype.push() -> HTML5History.prototype.push() -> History.prototype.transitionTo() -> History.prototype.confirmTransition() -> History.prototype.updateRoute() -> { app._route = route } -> defineReactive(_route) setter -> dep.notify() -> Dep.prototype.notify() -> Watcher.prototype.update() -> Vue.prototype.$nextTick() ->

hash模式

  • hash -> HashHistory ->

  • 支持window.history -> window.history.pushState()、window.history.go(n)、window.history.replaceState()

  • 不支持window.history -> window.location.hash = path、window.history.go(n)、window.location.replace(url)

this.$router.push() -> VueRouter.prototype.push() -> HashHistory.prototype.push() -> History.prototype.transitionTo() -> History.prototype.confirmTransition() -> History.prototype.updateRoute() -> { app._route = route } -> defineReactive(_route) setter -> dep.notify() -> Dep.prototype.notify() -> Watcher.prototype.update() -> vm.render()

abstract模式

  • abstract -> AbstractHistory -> node环境下

history模式优点

history最大的优点就是长得美观,除此之外还有如下优势:

pushState设置的新URL可以是与当前URL同源的任意URL;而hash只可修改/#后面的部分,故只可设置与当前同文档的URL
*
pushState通过stateObject可以添加任意类型的数据到记录中;而hash只可添加短字符串;
*
pushState可额外设置title属性供后续使用;
*
history模式则会将URL修改得就和正常请求后端的URL一样,如后端没有配置对应/user/id的路由处理,则会返回404错误。

大型单页应用最显著特点之一就是采用前端路由系统,通过改变URL,在不重新请求页面的情况下,更新页面视图。

更新视图但不重新请求页面”是前端路由原理的核心之一,目前在浏览器环境中这一功能的实现主要有两种方式:

利用URL中的hash(“/#”),修改/#后面的地址,不会重新请求页面,但是修改/#前面的地址会重新请求页面。

//在浏览器的访问历史中增加一个记录,并且不会重新请求页面,只能修改#后面的部分
window.location.hash='#/b/c'

利用History API在 HTML5中新增的方法

//在浏览器的访问历史中增加一个记录,并且不会重新请求页面,只能同源跳转
window.history.pushState({name:'哈哈哈哈'},'首页','http://localhost:8080/s/b')

r o u t e r 和 router和router和route通过Object.defineProperty实现代理,当调用this.$router.push()的时候,会拦截vue原型上的get操作,返回对应的Vue._routerRoot._router的值。

Object.defineProperty(Vue.prototype, "$router", {
  get() {
    return this._routerRoot._router;
  },
});
Object.defineProperty(Vue.prototype, "$route", {
  get() {
    return this._routerRoot._route;
  },
});
Vue.component("RouterView", View);
Vue.component("RouterLink", Link);

之所以视图可以更新,是因为_route是响应式的,即当 _route变化时,会拦截set操作,调用dep.notify()更新视图。

history模式缺点

我们知道对于单页应用来讲,理想的使用场景是仅在进入应用时加载index.html,后续在的网络操作通过Ajax完成,不会根据URL重新请求页面,但是难免遇到特殊情况,比如用户直接在地址栏中输入并回车,浏览器重启重新加载应用等。

hash模式仅改变hash部分的内容,而hash部分是不会包含在HTTP请求中的:

http://oursite.com/#/user/id   // 如重新请求只会发送http://oursite.com/

故在hash模式下遇到根据URL请求页面的情况不会有问题。

而history模式则会将URL修改得就和正常请求后端的URL一样

http://oursite.com/user/id

在此情况下重新向后端发送请求,如后端没有配置对应/user/id的路由处理,则会返回404错误。

解决方案:在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。同时这么做以后,服务器就不再返回 404 错误页面,因为对于所有路径都会返回 index.html 文件。为了避免这种情况,在 Vue 应用里面覆盖所有的路由情况,然后在给出一个 404 页面。或者,如果是用 Node.js 作后台,可以使用服务端的路由来匹配 URL,当没有匹配到路由的时候返回 404,从而实现 fallback。

从总结起来,history的缺点如下:

  • 在用户手动输入地址或刷新页面时会发起url请求,后端需要配置index.html页面用户匹配不到静态资源的情况,否则会出现404错误,但是有解决方案。
  • 兼容性比较差,pushState() 和 replaceState() 方法,需要特定浏览器的支持。

从辩证法的角度说,任何事物都具有两面性,要全面客观的看待事物。history模式和hash模式都有自己的优缺点,项目中采用哪种模式要根据实际情况来定,具体问题具体分析,鞋子合不合穿自己最清楚。

从文件系统直接加载Vue单页应用

要想从文件系统直接加载Vue单页应用而不借助后端服务器,除了打包后的一些路径设置外,还需确保vue-router使用的是hash模式。

vue-router路由优缺点

  • 优点:
    用户体验好良好,不需要每次都从服务器全部获取数据,快速展现给用户,并节省了服务器资源。
  • 缺点:
    不利于网页的SEO。
    使用浏览器的前进,后退键的时候会重新发送请求,降低了缓存的使用率
    单页面无法记住之前滚动的位置,无法在前进,后退的时候记住滚动的位置,需要指定位置跳转等需求时困难。

总结与体会

vue-router是vue开发中常见到不能再常见的的一个组件技术了,但是并不是每个人都愿意去花时间去深究它的原理和使用。在工作之余,我花了一些时间去学习它,研究它,加深了对vue-router的理解。每项小的技术都有它的独特之美,我们需要去发现这种美,毕竟万丈高楼平地起,需要牢固的根基也需要每一块砖的堆砌。对于工作和生活,我们也应保持同样的态度,事情不管多小,我们应尽力做好每一件事,做好每一件小事,才能做好能做好更大的事。不积跬步无以至千里。不积小流,无以成江海。

下一步计划:手写vue-router。

相关文章