Vue Router 源码之 hash 与 history 模式

本文深入分析Vue Router 3.5.1的源码,探讨在单页应用中,hash模式和history模式如何实现URL的改变而不发送服务器请求。通过监听hashchange和popstate事件,结合history.pushState和replaceState方法,解释了两种模式的区别和工作原理。同时,强调了history模式需要服务器配置以避免刷新时的404错误。
摘要由CSDN通过智能技术生成

相信用过 Vue 框架的朋友,一定听过 Vue Router。要想写一个复杂的单页应用,没有路由可不行。

Vue Router 默认开启的是 hash 模式,如果我们不想让浏览器中的 URL 包含影响美观的 #,我们可以开启 history 模式。不过这种 history 的模式稍微复杂一点,需要后端服务器配合。

学东西,最好是知其然,知其所以然。本文将从源码的角度分析 hash 与 history 这两种模式的实现,本文基于 Vue Router 3.5.1 版本。

单页应用

当我们在浏览器地址栏输入一个地址时,浏览器就会去服务端去请求内容。但每次点击一个链接,就去服务端请求,这样会有页面加载的等待。

后来慢慢就出现了单页应用,在第一次访问时,就把 html 文件,以及其他静态资源都请求到了客户端。之后的操作,只是利用 js 实现组件的展示和隐藏。除非需要刷新数据,才会利用 ajax 去请求。

但是纯粹的单页应用不方便管理,尤其是开发复杂应用的时候,需要有“多页面”的概念,并且很多用户习惯浏览器的前进后退的导航功能。

能不能有一种方法,可以在不向服务器发送请求的条件下,改变浏览器的 URL,以此来实现“多页面”概念?

答案是有,Vue Router 就是官方开发的一个插件,专门来做这件事。

URL 相关 API

最早改变 URL,但不向服务器发送请求的方式就是 hash。比如这种:

https://music.163.com/#/discover/toplist

同时浏览器也提供了一个事件来监听 hash 的改变,当 URL 的片段标识符更改时,将触发 hashchange 事件 (跟在#符号后面的URL部分,包括#符号)。

window.addEventListener('hashchange', function() {
   
  console.log('The hash has changed!')
}, false);

后来 HTML5 发布,history 对象又增加了两个方法,用来改变浏览器的 URL。只是改变浏览器的访问记录栈,但是不会向服务器发起请求。

  • history.pushState(state, title[, url]):该方法会向浏览器会话的历史堆栈中添加一个状态。
  • history.replaceState(stateObj, title[, url]):该方法与上一个方法类似,但区别是它会在历史堆栈中替换掉当前的记录。

下面给出一个方法示例,具体参数细节查看这里

const state = {
    'page_id': 1, 'user_id': 5 }
const title = ''
const url = 'hello-world.html'

history.pushState(state, title, url)

当我们调用 history 对象这两个方法时,会触发 popstate 事件,但不会触发 hashchange 事件。另外,在调用 pushState 方法后,我们点击浏览器的前进和后退按钮也会触发 popstate 事件。

不过 history 模式有个缺点,一旦我们点击了浏览器的刷新按钮,这时候会真正向服务器发起请求。当前处于首页还好,如果是其它页面,页面会报 404。所以,这种模式需要服务器端配置路由规则,防止出现 404 的情况。

源码中 hash 与 history 模式

Vue Router 源码中有一个 history 目录,有 4 个文件。

  • base.js:history 基类
  • hash.js:hash 模式
  • html5.js:history 模式
  • abstract.js:js 模拟历史堆栈,用于服务器端(这里不作分析)

为了帮助大家更好的理解 Vue Router,本文也会讲一些 hash 与 history 模式之外的内容。

我们先来看一看 VueRouter 对象,为了方便查看,这里只列了构造函数部分。

export default class VueRouter {
   
  constructor (options: RouterOptions = {
   }) {
   
    this.app = null
    this.apps = []
    this.options = options
    this.beforeHooks = []
    this.resolveHooks = []
    this.afterHooks = []
    this.matcher = createMatcher(options.routes || [], this)

    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}`)
        }
    }
  }
}

VueRouter 接受一个 options 参数,我们使用时,会把 routes 配置传递进去。

VueRouter 首先根据 routes 配置文件,创建一个 matcher 对象。然后根据 mode 模式实例化不同的 history 对象,这里也是本文关注的重点。

VueRouter 整个构造函数简洁清晰,阅读起来十分友好。

我们先来看一看 hash history 和 html5 history 对象前下,我们先看看两者的基类。

export class History {
   
  constructor (router: Router, base: ?string) {
   
    this.router = router
    this.base = normalizeBase(base)
    // start with a route object that stands for "nowhere"
    this.current = START
    this.pending = null
    this.ready = false
    this.readyCbs = []
    this.readyErrorCbs = []
    this.errorCbs = []
    this.listeners = []
  }

  listen (cb: Function) {
   
    this.cb = cb
  }

  transitionTo (
    location: RawLocation,
    onComplete?: Function,
    onAbort?: Function
  ) {
   
    let route
    // catch redirect option https://github.com/vuejs/vue-router/issues/3201
    try {
   
      route = this.router.match(location, this.current)
    } catch (e) {
   
      this.errorCbs.forEach(cb => {
   
        cb(e)
      })
      // Exception should still be thrown
      throw e
    }
    const prev = this.current
    this.confirmTransition(
      route,
      () => {
   
        this.updateRoute(route)
        onComplete && onComplete(route)
        this.ensureURL()
        this.router.afterHooks.forEach(hook => {
   
          hook && hook(route, prev)
        })

        // fire ready cbs once
        if (!this.ready) {
   
          this.ready = true
          this.readyCbs.forEach(cb => {
   
            cb(route)
          })
        }
      },
      err => {
   
        if (onAbort) {
   
          onAbort(err)
        }
        if (err && !this.ready) {
   
          // Initial redirection should not mark the history as ready yet
          // because it's triggered by the redirection instead
          // https://github.com/vuejs/vue-router/issues/3225
          // https://github.com/vuejs/vue-router/issues/3331
          if (!isNavigationFailure(err, NavigationFailureType.redirected) || prev !== START) {
   
            this.ready = true
            this.readyErrorCbs.forEach(cb => {
   
              cb(err)
            })
          }
        }
      }
    )
  }

  confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
   
    const current = 
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值