资讯专栏INFORMATION COLUMN

vue:路由实现原理

mikasa / 3093人阅读

摘要:方法与方法不同之处在于,它并不是将新路由添加到浏览器访问历史栈顶,而是替换掉当前的路由可以看出,它与的实现结构基本相似,不同点它不是直接对进行赋值,而是调用方法将路由进行替换。

随着前端应用的业务功能起来越复杂,用户对于使用体验的要求越来越高,单面(SPA)成为前端应用的主流形式。大型单页应用最显著特点之一就是采用的前端路由系统,通过改变URL,在不重新请求页面的情况下,更新页面视图。

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

利用URL中的hash("#");

利用History interfaceHTML5中新增的方法;

vue-routerVue.js框架的路由插件,它是通过mode这一参数控制路由的实现模式的:

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

创建VueRouter的实例对象时,mode以构造参数的形式传入。

src/index.js

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

  init (app: any /* Vue component instance */) {
    
    const history = this.history

    // 根据history的类别执行相应的初始化操作和监听
    if (history instanceof HTML5History) {
      history.transitionTo(history.getCurrentLocation())
    } else if (history instanceof HashHistory) {
      const setupHashListener = () => {
        history.setupListeners()
      }
      history.transitionTo(
        history.getCurrentLocation(),
        setupHashListener,
        setupHashListener
      )
    }

    history.listen(route => {
      this.apps.forEach((app) => {
        app._route = route
      })
    })
  }

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

作为参数传入的字符串属性mode只是一个标记,用来指示实际起作用的对象属性history的实现类,两者对应关系:

    modehistory:
        "history":HTML5History;
        "hash":HashHistory;
        "abstract":AbstractHistory;

在初始化对应的history之前,会对mode做一些校验:若浏览器不支持HTML5History方式(通过supportsPushState变量判断),则mode设为hash;若不是在浏览器环境下运行,则mode设为abstract;

VueRouter类中的onReady(),push()等方法只是一个代理,实际是调用的具体history对象的对应方法,在init()方法中初始化时,也是根据history对象具体的类别执行不同操作

HashHistory

hash("#")符号的本来作用是加在URL指示网页中的位置:

http://www.example.com/index.html#print

#本身以及它后面的字符称之为hash可通过window.location.hash属性读取.

hash虽然出现在url中,但不会被包括在http请求中,它是用来指导浏览器动作的,对服务器端完全无用,因此,改变hash不会重新加载页面。

可以为hash的改变添加监听事件:

window.addEventListener("hashchange",funcRef,false)

每一次改变hash(window.location.hash),都会在浏览器访问历史中增加一个记录。

利用hash的以上特点,就可以来实现前端路由"更新视图但不重新请求页面"的功能了。

HashHistory.push()
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  this.transitionTo(location, route => {
    pushHash(route.fullPath)
    onComplete && onComplete(route)
  }, onAbort)
}

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

transitionTo()方法是父类中定义的是用来处理路由变化中的基础逻辑的,push()方法最主要的是对windowhash进行了直接赋值:

window.location.hash=route.fullPath

hash的改变会自动添加到浏览器的访问历史记录中。
那么视图的更新是怎么实现的呢,我们来看看父类History中的transitionTo()方法:

transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  const route = this.router.match(location, this.current)
  this.confirmTransition(route, () => {
    this.updateRoute(route)
    ...
  })
}

updateRoute (route: Route) {
  
  this.cb && this.cb(route)
  
}

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

可以看到,当路由变化时,调用了Hitory中的this.cb方法,而this.cb方法是通过History.listen(cb)进行设置的,回到VueRouter类定义中,找到了在init()中对其进行了设置:

init (app: any /* Vue component instance */) {
    
  this.apps.push(app)

  history.listen(route => {
    this.apps.forEach((app) => {
      app._route = route
    })
  })
}

appVue组件实例,但是Vue作为渐进式的前端框架,本身的组件定义中应该是没有有关路由内置属性_route,如果组件中要有这个属性,应该是在插件加载的地方,即VueRouterinstall()方法中混入Vue对象的,install.js的源码:

export function install (Vue) {
  
  Vue.mixin({
    beforeCreate () {
      if (isDef(this.$options.router)) {
        this._router = this.$options.router
        this._router.init(this)
        Vue.util.defineReactive(this, "_route", this._router.history.current)
      }
      registerInstance(this, this)
    },
  })
}

通过Vue.mixin()方法,全局注册一个混合,影响注册之后所有创建的每个Vue实例,该混合在beforeCreate钩子中通过Vue.util.defineReactive()定义了响应式的_route属性。所谓响应式属性,即当_route值改变时,会自动调用Vue实例的render()方法,更新视图。

$router.push()-->HashHistory.push()-->History.transitionTo()-->History.updateRoute()-->{app._route=route}-->vm.render()
HashHistory.replace()

replace()方法与push()方法不同之处在于,它并不是将新路由添加到浏览器访问历史栈顶,而是替换掉当前的路由:

replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  this.transitionTo(location, route => {
    replaceHash(route.fullPath)
    onComplete && onComplete(route)
  }, onAbort)
}
  
function replaceHash (path) {
  const i = window.location.href.indexOf("#")
  window.location.replace(
    window.location.href.slice(0, i >= 0 ? i : 0) + "#" + path
  )
}

可以看出,它与push()的实现结构基本相似,不同点它不是直接对window.location.hash进行赋值,而是调用window.location.replace方法将路由进行替换。

监听地址栏

上面的VueRouter.push()VueRouter.replace()是可以在vue组件的逻辑代码中直接调用的,除此之外在浏览器中,用户还可以直接在浏览器地址栏中输入改变路由,因此还需要监听浏览器地址栏中路由的变化 ,并具有与通过代码调用相同的响应行为,在HashHistory中这一功能通过setupListeners监听hashchange实现:

setupListeners () {
  window.addEventListener("hashchange", () => {
    if (!ensureSlash()) {
      return
    }
    this.transitionTo(getHash(), route => {
      replaceHash(route.fullPath)
    })
  })
}

该方法设置监听了浏览器事件hashchange,调用的函数为replaceHash,即在浏览器地址栏中直接输入路由相当于代码调用了replace()方法。

HTML5History

History interface是浏览器历史记录栈提供的接口,通过back(),forward(),go()等方法,我们可以读取浏览器历史记录栈的信息,进行各种跳转操作。
HTML5开始,History interface提供了2个新的方法:pushState(),replaceState()使得我们可以对浏览器历史记录栈进行修改:

window.history.pushState(stateObject,title,url)
window.history,replaceState(stateObject,title,url)

stateObject:当浏览器跳转到新的状态时,将触发popState事件,该事件将携带这个stateObject参数的副本

title:所添加记录的标题

url:所添加记录的url

2个方法有个共同的特点:当调用他们修改浏览器历史栈后,虽然当前url改变了,但浏览器不会立即发送请求该url,这就为单页应用前端路由,更新视图但不重新请求页面提供了基础。

push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  const { current: fromRoute } = this
  this.transitionTo(location, route => {
    pushState(cleanPath(this.base + route.fullPath))
    handleScroll(this.router, route, fromRoute, false)
    onComplete && onComplete(route)
  }, onAbort)
}

replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  const { current: fromRoute } = this
  this.transitionTo(location, route => {
    replaceState(cleanPath(this.base + route.fullPath))
    handleScroll(this.router, route, fromRoute, false)
    onComplete && onComplete(route)
  }, onAbort)
}

// src/util/push-state.js
export function pushState (url?: string, replace?: boolean) {
  saveScrollPosition()
  // try...catch the pushState call to get around Safari
  // DOM Exception 18 where it limits to 100 pushState calls
  const history = window.history
  try {
    if (replace) {
      history.replaceState({ key: _key }, "", url)
    } else {
      _key = genKey()
      history.pushState({ key: _key }, "", url)
    }
  } catch (e) {
    window.location[replace ? "replace" : "assign"](url)
  }
}

export function replaceState (url?: string) {
  pushState(url, true)
}

代码结构以及更新视图的逻辑与hash模式基本类似,只不过将对window.location.hash()直接进行赋值window.location.replace()改为了调用history.pushState()history.replaceState()方法。

HTML5History中添加对修改浏览器地址栏URL的监听popstate是直接在构造函数中执行的:

constructor (router: Router, base: ?string) {
  
  window.addEventListener("popstate", e => {
    const current = this.current
    this.transitionTo(getLocation(this.base), route => {
      if (expectScroll) {
        handleScroll(router, route, current, true)
      }
    })
  })
}

HTML5History用到了HTML5的新特性,需要浏版本的支持,通过supportsPushState来检查:

src/util/push-state.js

export const supportsPushState = inBrowser && (function () {
  const ua = window.navigator.userAgent

  if (
    (ua.indexOf("Android 2.") !== -1 || ua.indexOf("Android 4.0") !== -1) &&
    ua.indexOf("Mobile Safari") !== -1 &&
    ua.indexOf("Chrome") === -1 &&
    ua.indexOf("Windows Phone") === -1
  ) {
    return false
  }

  return window.history && "pushState" in window.history
})()

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

两种模式比较

一般的需求场景中,hash模式与history模式是差不多的,根据MDN的介绍,调用history.pushState()相比于直接修改hash主要有以下优势:

pushState设置的新url可以是与当前url同源的任意url,而hash只可修改#后面的部分,故只可设置与当前同文档的url

pushState设置的新url可以与当前url一模一样,这样也会把记录添加到栈中,而hash设置的新值必须与原来不一样才会触发记录添加到栈中

pushState通过stateObject可以添加任意类型的数据记录中,而hash只可添加短字符串

pushState可额外设置title属性供后续使用

history模式的问题

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

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

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

所以hash模式下遇到根据url请求页面不会有问题

history模式则将url修改的就和正常请求后端的url一样(history不带#)

http://oursite.com/user/id

如果这种向后端发送请求的话,后端没有配置对应/user/idget路由处理,会返回404错误。

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/94890.html

相关文章

  • 前端路由简介以及vue-router实现原理

    摘要:后端路由简介路由这个概念最先是后端出现的。前端路由模式随着的流行,异步数据请求交互运行在不刷新浏览器的情况下进行。通过这些就能用另一种方式来实现前端路由了,但原理都是跟实现相同的。 后端路由简介 路由这个概念最先是后端出现的。在以前用模板引擎开发页面时,经常会看到这样 http://www.xxx.com/login 大致流程可以看成这样: 浏览器发出请求 服务器监听到80端口(或4...

    tuomao 评论0 收藏0
  • 前端路由原理解析和实现

    摘要:如何实现前端路由要实现前端路由,需要解决两个核心如何改变却不引起页面刷新如何检测变化了下面分别使用和两种实现方式回答上面的两个核心问题。 原文链接:github.com/whinc/blog/… 在单页应用如此流行的今天,曾经令人惊叹的前端路由已经成为各大框架的基础标配,每个框架都提供了强大的路由功能,导致路由实现变的复杂。想要搞懂路由内部实现还是有些困难的,但是如果只想了解路由实现基本...

    lavor 评论0 收藏0
  • vue-router实现原理

    摘要:我们知道是的核心插件,而当前项目一般都是单页面应用,也就是说是应用在单页面应用中的。原理是传统的页面应用,是用一些超链接来实现页面切换和跳转的其实刚才单页面应用跳转原理即实现原理实现原理原理核心就是更新视图但不重新请求页面。 近期面试,遇到关于vue-router实现原理的问题,在查阅了相关资料后,根据自己理解,来记录下。我们知道vue-router是vue的核心插件,而当前vue项目...

    vibiu 评论0 收藏0
  • 面试官常问——vue

    摘要:如果要相应状态改变,通常最好使用计算属性或取而代之。那解决问题的思路便是在改变的情况下,保证页面的不刷新。后面值的变化,并不会导致浏览器向服务器发出请求,浏览器不发出请求,也就不会刷新页面。 1.vue生命周期2.vue 双向绑定原理3.vue router原理4.vue router动态路由 1.vue 生命周期钩子 showImg(https://segmentfault.com/...

    BlackMass 评论0 收藏0
  • 面试官常问——vue

    摘要:如果要相应状态改变,通常最好使用计算属性或取而代之。那解决问题的思路便是在改变的情况下,保证页面的不刷新。后面值的变化,并不会导致浏览器向服务器发出请求,浏览器不发出请求,也就不会刷新页面。 1.vue生命周期2.vue 双向绑定原理3.vue router原理4.vue router动态路由 1.vue 生命周期钩子 showImg(https://segmentfault.com/...

    xingqiba 评论0 收藏0
  • 面试官常问——vue

    摘要:如果要相应状态改变,通常最好使用计算属性或取而代之。那解决问题的思路便是在改变的情况下,保证页面的不刷新。后面值的变化,并不会导致浏览器向服务器发出请求,浏览器不发出请求,也就不会刷新页面。 1.vue生命周期2.vue 双向绑定原理3.vue router原理4.vue router动态路由 1.vue 生命周期钩子 showImg(https://segmentfault.com/...

    quietin 评论0 收藏0

发表评论

0条评论

mikasa

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<