源码学习之vue-router

4 篇文章 0 订阅
1 篇文章 0 订阅

使用回顾

通常我们使用vue-router会单独创建一个js来写vue-router的逻辑,如 src/router/index.js,并在main.js中引入,例如。

//  src/router/index.js
Vue.use(Router) // 启动路由
// 路由实例
let router=new Router({
    routes:[
        {
        //路由信息
        }
    ]
})


//  src/main.js
   new Vue({
        router,
        render: h => h(App)
    }).$mount('#app')

源码位置

两个地方可以找到:

1、Github源码

2、打开自己使用的项目,进入node_modules目录,找到vue-router下的src目录,index.js就是vue-router的源码入口。

index.js: 主代码逻辑

install.js: vue插件注册

history目录:提供不同路由模式下的方法

util目录:提供各种方法,路径处理、query、route、scroll、push-state、location处理等等

components目录:提供router-view和router-link组件

 

插件的加载

首先,install.js中定义一个install方法,可以供Vue加载插件。index.js中定义了一个叫做VueRouter的类(后面再讲),并将install方法赋值给VueRouter.install。

当我们import Router from 'vue-router',并使用Vue.use(Router)的时候,Vue就调用install方法进行插件注册。install方法中的内容如下(已省略可以不关心的内容):

export function install (Vue) {

  Vue.mixin({
    beforeCreate () {
      if (isDef(this.$options.router)) {
        this._routerRoot = this
        this._router = this.$options.router
        this._router.init(this)  //初始化路由
        Vue.util.defineReactive(this, '_route', this._router.history.current) //定义了响应式的_route属性
      } else {
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
    }
  })

  Object.defineProperty(Vue.prototype, '$router', {  //在vue原型上定义$router
    get () { return this._routerRoot._router }
  })

  Object.defineProperty(Vue.prototype, '$route', {  //在vue原型上定义$route
    get () { return this._routerRoot._route }
  })

  Vue.component('RouterView', View)   //注册router-view 组件
  Vue.component('RouterLink', Link)   //注册router-link 组件

}

install方法做了如下事情:

  1. 通过vue.mixin混入的方式在组件生命周期钩子beforeCreate函数中注入初始化方法,并执行this._router.init(this)。
  2. 通过Vue.util.defineReactive(this, '_route', this._router.history.current),定义了响应式的_route属性。
  3. 这里在全局注册的好处是,影响之后创建的每一个Vue实例。
  4. 通过Object.defineProperty在全局定义$router方法和$route属性,并且$route其实就是响应式的_route,因此我们也可以在watch中监听$route的变化。
  5. 这里也全局注册了router-view和router-link组件,在任何地方都可以直接使用。
  6. 关于install函数与vue的关系,或者想自己开发插件,可参考 官方插件文档

index.js中VueRouter做了什么?

我们还是先来看看简化后的源码:

export default class VueRouter {
  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}`)
        }
    }
  }

  init (app: any /* Vue component instance */) {
    this.apps.push(app)
    // 不用重新创建 history 的监听
    if (this.app) {
      return
    }

    this.app = app
    const history = this.history
    if (history instanceof HTML5History || history instanceof HashHistory) {
      const setupListeners = routeOrError => {
        history.setupListeners()
        handleInitialScroll(routeOrError) //处理滚动
      }
      history.transitionTo(
        history.getCurrentLocation(),
        setupListeners,
        setupListeners
      )
    }

    history.listen(route => {
      this.apps.forEach(app => {
        app._route = route
      })
    })
  }  
  push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
      return new Promise((resolve, reject) => {
        this.history.push(location, resolve, reject)
      })
    } else {
      this.history.push(location, onComplete, onAbort)
    }
  }

  replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    // $flow-disable-line
    if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
      return new Promise((resolve, reject) => {
        this.history.replace(location, resolve, reject)
      })
    } else {
      this.history.replace(location, onComplete, onAbort)
    }
  }
  //各种钩子函数的定义,这里省略了 beforeEach go back forward 等
}

这里代码可以分为四个部分:一是:constructor构造函数;二是:init函数;三是:push和replace方法;四是:其他方法,如生命周期钩子、go、back等。

  • constructor构造函数:这里面的操作就是看我们的页面应当采用什么mode。 
  1. 根据传入的options对象判断是否传入了mode参数(默认是‘hash’),mode支持 hash/history/abstract 三种模式,
  2. 三种模式并分别对应:HashHistory / HTML5Histroy /  AbstractHistory 三种类,这三种类都继承了base类,代码在history目录,这里面定义了各自的push、replace、监听器、transitionTo等方法。
  3. 对mode参数对应的类该进行实例化,this.history = new 三个类中的一个(this, options.base)
  • init函数:
  1. 根据this.history进行监听器的初始化,在组件beforeCreate的时候将会调用。
  2. history.listen中 app._route = route,又遇到了这个响应式的_route,修改_route后会触发render函数切换组件的渲染,从而更新页面内容。
  • push/replace函数:也是调用this.history封装的方法。
  • 其他方法:这里忽略不计了。

前端路由

前端路由,是把不同路由对应不同的内容或页面的任务交给前端来做,以前是通过服务端根据 url 不同返回不同的页面来实现。

前端路由方案 :

hash:可能是大多数人了解的模式,主要是基于锚点的原理实现,简单易用。

history:即使用 html5 标准中的 history api 通过监听 popstate 事件来对 dom 进行操作。每次路由变化都会引起重定向

Hash路由原理

hash(#)是URL 的锚点,代表的是网页中的一个位置,单单改变#后的部分,浏览器只会滚动到相应位置,不会重新加载网页,也就是说 #是用来指导浏览器动作的,对服务器端完全无用,HTTP请求中也不会不包括#;同时每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用”后退”按钮,就可以回到上一个位置。

通过以下原理实现更新视图而不用重新请求页面

  1. hash虽然出现在URL中,但不会被包括在HTTP请求中。改变hash不会重新加载页面。
  2. 可通过window.history.hash获取hash值。
  3. 可以监听hash值的变化: window.addEventListener("hashchange", funcRef, false)。
  4. 每次hash改变就会在浏览器中增加一个访问记录。

我们来看看代码:

export class HashHistory extends History {
  constructor (router: Router, base: ?string, fallback: boolean) {
    super(router, base)
    //  如果是回退hash的情况,并且判断当前路径是否有/#/。如果没有将会添加'/#/'
    if (fallback && checkFallback(this.base)) {
      return
    }
    // 获取hash值,判断其最前面是否有/,没有则添加(通过history.replace)
    ensureSlash()
  }

  // 延迟到app mounts后才设置的监听器
  setupListeners () {
    const handleRoutingEvent = () => {
      this.transitionTo(getHash(), route => {
        if (!supportsPushState) {
          replaceHash(route.fullPath)
        }
      })
    }
    const eventType = supportsPushState ? 'popstate' : 'hashchange'
    window.addEventListener(
      eventType,
      handleRoutingEvent
    )
    this.listeners.push(() => {
      window.removeEventListener(eventType, handleRoutingEvent)
    })
  }

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

  replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(
      location,
      route => {
        replaceHash(route.fullPath)
        onComplete && onComplete(route)
      },
      onAbort
    )
  }
}


function pushHash (path) {
  if (supportsPushState) {
    pushState(getUrl(path))
  } else {
    window.location.hash = path
  }
}

function replaceHash (path) {
  if (supportsPushState) {
    replaceState(getUrl(path))
  } else {
    window.location.replace(getUrl(path))
  }
}

HashHistory类定义了监听器、push、replace方法等。

  1. 监听方法:我们的监听器会在init函数中进行设置,在hash模式下调用 history.transitionTo(url,setupListeners,setupListeners)。不管transitionTo成功或失败都会执行setupListeners,然后通过 window.addEventListener 监听hashChange事件,在支持pushState的环境中会监听popstate事件。
  2. 手动触发:当我们调用this.$router.push的时候,执行this.transitonTo函数,transitionTo执行成功后的回调中执行pushHash(),并执行window.location.hash=pash方法,实现链接地址的修改。同理this.$router.replace执行window.location.replace(path)。当然,这里会根据当前环境是否支持pushState,如果支持则会分别调用window.history.pushState()和window.history.replaceState()。
  3. transitionTo是base类中定义的方法,主要是实现了导航守卫的功能和更新触发。transitionTo中执行了一个confirmTransition的方法,其中会执行一系列的事件队列,其中包括beforeHooks,然后在confirmTransition的onComplete回调中依次执行updateRoute和 afterHooks。 updateRoute中执行this.cb && this.cb(route) ,也就是更新响应式的_route,触发render函数更新组件。

总结: hash路由改变到试图更新流程如下:

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

History路由原理 

History interface是浏览器历史记录栈提供的接口,通过back(), forward(), go()等方法,我们可以读取浏览器历史记录栈的信息,进行各种跳转操作。

从HTML5开始,History interface中pushState(), replaceState() 不仅可以读取,还可以修改浏览器历史记录栈,例如:

window.history.pushState(stateObject, title,URL)
window.history.replaceState(stateObject, title, URL)
//stateObject参数的副本
//title: 所添加记录的标题
//URL: 所添加记录的UR
  • push:与hash模式类似,只是将window.hash改为history.pushState
  • replace:与hash模式类似,只是将window.replace改为history.replaceState
  • 监听地址:在HTML5History的构造函数中监听popState(window.onpopstate)
  • 这两个方法有个共同的特点:当调用他们修改浏览器历史记录栈后,虽然当前URL改变了,但浏览器不会立即发送请求该URL(the browser won't attempt to load this URL after a call to pushState()),这就为单页应用前端路由“更新视图但不重新请求页面”提供了基础。

HTML5 History API提供了一种功能,能让开发人员在不刷新整个页面的情况下修改站点的URL,就是利用 history.pushState API 来完成 URL 跳转而无须重新加载页面;通常情况下,我们会选择使用History模式,原因就是Hash模式下URL带着‘#’会显得不美观;但实际上,这样选择一不小心也会出问题,例如:链接中带路由参数,如果后端没有对应的路由处理,就会返回404错误;

为解决这一问题,vue-router提供的方法是:在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。

源码:几乎与hash模式的一致,唯独只能使用HTML5的几个apid

history路由改变到试图更新流程如下:

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

Abstrack路由原理 

支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式。其原理为用一个数组stack模拟出浏览器历史记录栈的功能。

如带所示,用stack作为路由栈,push的时候推入一个路由,replace的时候替换最后一个路由。它不会向前面两种路由调用location.hash、location.history、pushState等等浏览器的api。

constructor (router: Router, base: ?string) {
    super(router, base)
    this.stack = []
    this.index = -1
  }

  push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    this.transitionTo(
      location,
      route => {
        this.stack = this.stack.slice(0, this.index + 1).concat(route)
        this.index++
        onComplete && onComplete(route)
      },
      onAbort
    )
  }

  replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    this.transitionTo(
      location,
      route => {
        this.stack = this.stack.slice(0, this.index).concat(route)
        onComplete && onComplete(route)
      },
      onAbort
    )
  }

 

源码中还有很多细节待完善!

例如:

router-view的实现及组件更新

transitionTo中导航守卫的逻辑、路由切换中做了些什么处理等

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值