vue 路由的两种模式:hash与history

举报
SHQ5785 发表于 2024/04/25 09:04:41 2024/04/25
【摘要】 一、前言对于Vue 渐进式前端开发框架,为了构建SPA(单页面应用),需要引入前端路由系统,这也就是Vue-router存在的意义。前端路由的核心,就在于改变视图的同时不会向后端发出请求。一、为了达到这个目的,浏览器提供了 hash 与history 两种支持:为什么这两种模式实现了更新视图但不请求页面?hash ——即地址栏URL中的#符号(此hash 不是密码学里的散列运算)。比如这...

一、前言

对于Vue 渐进式前端开发框架,为了构建SPA(单页面应用),需要引入前端路由系统,这也就是Vue-router存在的意义。前端路由的核心,就在于改变视图的同时不会向后端发出请求。

一、为了达到这个目的,浏览器提供了 hashhistory 两种支持:

为什么这两种模式实现了更新视图但不请求页面

  • hash ——即地址栏URL中的#符号(此hash 不是密码学里的散列运算)。
    比如这个URL:http://www.abc.com/#/hello, hash 的值为#/hello。它的特点在于:hash 虽然出现URL中,但不会被包含在HTTP请求中,它是用来指导浏览器动作的,对后端完全没有影响,因此改变hash不会重新加载页面。

  • history ——利用了HTML5 History Interface 中新增的pushState()replaceState() 方法。(需要特定浏览器支持)

这两个方法应用于浏览器的历史记录栈,在当前已有的backforwardgo 基础上,它们提供了对历史记录进行修改的功能。只是当它们执行修改时,虽然改变了当前的URL,但浏览器不会立即向后端发送请求。

history模式,会出现404 的情况,需要后台配置。

二、404 错误

  • hash模式下,仅hash符号之前的内容会被包含在请求中,如 http://www.abc.com, 因此对于后端来说,即使没有做到对路由的全覆盖,也不会返回404错误;

  • history模式下,前端的url必须和实际向后端发起请求的url 一致,如http://www.abc.com/book/id 。如果后端缺少对/book/id 的路由处理,将返回404错误。

三、延伸阅读

vue-router实例化如下:

const  router = new VueRouter({
	mode: "XXX",
	routes: [...]
})

VueRouter实例化如下:

export default class VueRouter {
  
  mode: string; // 传入的字符串参数,指示history类别
  history: HashHistory | HTML5History | AbstractHistory; // 实际起作用的对象属性,必须是以上三个类的枚举
  fallback: boolean; // 如浏览器不支持,'history'模式需回滚为'hash'模式
  
  constructor (options: RouterOptions = {}) {    
    let mode = options.mode || 'hash' // 默认为'hash'模式
    this.fallback = mode === 'history' && !supportsPushState // 通过supportsPushState判断浏览器是否支持'history'模式
    if (this.fallback) {
      mode = 'hash'
    }
    if (!inBrowser) {
      mode = 'abstract' // 不在浏览器环境下运行需强制为'abstract'模式
    }
    this.mode = mode

    // 根据mode确定history实际的类并实例化
    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}`)
        }
    }
  }
  // 初始化操作, app表示组件实例 
  init (app: any /* Vue component instance */) {
    // ...
    this.apps.push(app)

    // main app already initialized.
    if (this.app) {
      return
    }

    this.app = app
    const history = this.history

    // 根据history的类别执行相应的初始化操作和监听
    if (history instanceof HTML5History) {
   	  // html5模式
      history.transitionTo(history.getCurrentLocation())
    } else if (history instanceof HashHistory) {
      // hash模式 
      const setupHashListener = () => {
        history.setupListeners()
      }
      history.transitionTo(
        history.getCurrentLocation(),
        setupHashListener,
        setupHashListener
      )
    }
	
	// 切换路由
    history.listen(route => {
      this.apps.forEach((app) => {
        app._route = route
      })
    })
  }
  // 一些钩子函数 beforeEach、 afterEach等
  // ...
  // ...
  

  // VueRouter类暴露的以下方法实际是调用具体history对象的方法
  push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    this.history.push(location, onComplete, onAbort)
  }

  replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    this.history.replace(location, onComplete, onAbort)
  }
}
 // 相对于当前页面向前或向后跳转多少个页面,类似 window.history.go(n)。n可为正数可为负数。正数返回上一个页面
  go (n: number) {
    this.history.go(n)
  }
  // 后退到上一个页面
  back () {
    this.go(-1)
  }
  // 前进到下一个页面
  forward () {
    this.go(1)
  }

 // ...
 // ...

代码解读:

  • 浏览器默认是hash模式,当浏览器不支持html5的history模式时,也会强制为hash模式;当环境不是浏览器时,强制为abstract模式。
  • 当创建VueRouter实例后,VueRouter构造函数会通过传入对象optionsmode参数,来调用对应的(HashHistory / HTML5History / AbstractHistory)构造函数,进而创建对应的history实例对象。
  • 创建相应的history实例后,可以看到init函数里面会有对应的初始化操作。

$router实例有两个常见的跳转方法:pushreplace方法,源码的最下面,就暴露出来这两个方法。很显然这两种方法都是对不同模式下的方法的封装,本质还是执行的对应模式上的方法。

HashHistory中的push()方法:

push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  // 执行transitionTo函数
  this.transitionTo(location, route => {
  	// 改变hash值
    pushHash(route.fullPath)
    onComplete && onComplete(route)
  }, onAbort)
}

function pushHash (path) {
  window.location.hash = path
}

pushHash直接对window.location.hash赋值,hash值变化之后,浏览器访问历史中就会增加一个记录。

记录增加了,如何更新视图呢?接着看父类History中transitionTo函数是如何实现的。

transtitionTo函数具体实现如下:

transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
	//找到匹配路由(match函数)
    const route = this.router.match(location, this.current) 
    this.confirmTransition(route, () => { //确认是否转化
      this.updateRoute(route) //更新route
      // ...
    })
  }
  
//更新路由
updateRoute (route: Route) {
    const prev = this.current // 跳转前路由
    this.current = route // 准备跳转路由
    this.cb && this.cb(route) // 回调函数,这一步很重要,这个回调函数在index文件中注册,会更新被劫持的数据 _router
    this.router.afterHooks.forEach(hook => {
      hook && hook(route, prev)
    })
  }
}

   // this.cb函数的定义 
   listen (cb: Function) {
 	this.cb = cb	
}

transitionTo函数中,执行了updateRoute更新路由函数,这个函数中执行了cb这个函数,cb这个函数是在VueRouter实例中的init函数中通过history.listen传入的。

init (app: any /* Vue component instance */) {  
   // ...  
  this.apps.push(app)
  // ...
  history.listen(route => {
    this.apps.forEach((app) => {
      app._route = route
    })
  })
}

上面的代码,从$router.push 分析到了cb()函数, 再接着看cb函数,这个函数中有一个_route属性,并且将匹配的路由route赋给了app._route。(这里app是组件实例,apps是所有组件实例一个数组) 。

那么_route属性是什么,组件本身是没有定义这个属性的,那么这个属性从哪里来的呢?源码中在install()方法中使用Vue.mixin()方法添加一个全局的混合对象:

install.js代码里面做了四件事:

  1. 混入beforeCreate函数,在里面定义_route这个响应式属性。
  2. 混入destroyed函数。
  3. Vue.prototype上定义$router$route这两个对象,以便每个组件都可以获得。
  4. 在Vue注册router-linkrouter-view这两个组件。
// install.js代码:
export function install (Vue) {
  if (install.installed && _Vue === Vue) return
  install.installed = true

  _Vue = Vue

  const isDef = v => v !== undefined

  const registerInstance = (vm, callVal) => {
    let i = vm.$options._parentVnode
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
      i(vm, callVal)
    }
  }
  
 // 使用mixin会在每个.vue文件中进行beforeCreate和destroyed 
  Vue.mixin({
    //对每个Vue实例混入beforeCreate钩子操作
    //验证Vue实例是否有router对象了,如果有,就不再初始化了
    beforeCreate () {
    // 如果没有router对象(this.$option.router来自于VueRouter实例router对象)
      if (isDef(this.$options.router)) { 
       // 将_routerRoot指向当前组件
        this._routerRoot = this
       // 将router对象挂载到根组件的_router上
        this._router = this.$options.router
        //  调用VueRouter实例的init方法
        this._router.init(this)
        // 劫持数据_route,一旦_route数据变化之后,通知router-view执行render方法
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
		// 如果有router对象,则将每一个组件的_routerRoot指向根Vue实例
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      // 注册vuecomponent实例
      registerInstance(this, this)
    },
    destroyed () {
     // 销毁vuecomponent实例
      registerInstance(this)
    }
  })
  //通过Vue.prototype定义$router、$route 属性(方便所有组件可以获取这两个属性)
  Object.defineProperty(Vue.prototype, '$router', {
    get () { return this._routerRoot._router }
  })

  Object.defineProperty(Vue.prototype, '$route', {
    get () { return this._routerRoot._route }
  })
  //Vue上注册router-link和router-view两个组件
  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)

  const strats = Vue.config.optionMergeStrategies
  // use the same hook merging strategy for route hooks
  strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}

beforeCreate钩子中,通过Vue.util.defineReactive()创建一个响应式_route属性。router-view组件挂载时,会生成一个watcher,当hash值变化时,匹配到新的路由route后,_route属性跟着改变,触发_route属性的getter,进行依赖收集watcher,最后触发_routesetter,执行watcher的更新函数,进而触发<router-view>render函数,更新视图。

大致流程为:

$router.push-> history.push -> transitionTo->updateRoute->cb(即 app._route = route) -> render
  1. 触发$router.push()
  2. 触发 HashHistory.push()
  3. 触发transitionTo() ,根据传入的location找到匹配的route,触发updateRoute()
  4. 触发updateRoute(), 将匹配的route赋给实例app_route响应式属性, 当_route属性变化时,就会触发实例的render函数,更新视图。

四、拓展阅读

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。