Vue番外篇 -- vue-router浅析原理

近期被问到一个问题,在大家项目中使用的是Vue的SPA(单页面)仍是Vue的多页面设计?javascript

这篇文章主要围绕Vue的SPA单页面设计展开。 关于如何展开Vue多页面设计请点击查看html

官网vue-router文档前端

vue-router是什么?

首先咱们须要知道vue-router是什么,它是干什么的?vue

这里指的路由并非指咱们平时所说的硬件路由器,这里的路由就是SPA(单页应用)的路径管理器。 换句话说,vue-router就是WebApp的连接路径管理系统。java

vue-router是Vue.js官方的路由插件,它和vue.js是深度集成的,适合用于构建单页面应用。git

那与传统的页面跳转有什么区别呢?github

1.vue的单页面应用是基于路由和组件的,路由用于设定访问路径,并将路径和组件映射起来。vue-router

2.传统的页面应用,是用一些超连接来实现页面切换和跳转的。npm

在vue-router单页面应用中,则是路径之间的切换,也就是组件的切换。路由模块的本质 就是创建起url和页面之间的映射关系。后端

至于为啥不能用a标签,这是由于用Vue作的都是单页应用,就至关于只有一个主的index.html页面,因此你写的标签是不起做用的,必须使用vue-router来进行管理。

vue-router实现原理

SPA(single page application):单一页面应用程序,有且只有一个完整的页面;当它在加载页面的时候,不会加载整个页面的内容,而只更新某个指定的容器中内容。

单页面应用(SPA)的核心之一是:

1.更新视图而不从新请求页面;

2.vue-router在实现单页面前端路由时,提供了三种方式:Hash模式、History模式、abstract模式,根据mode参数来决定采用哪种方式。

路由模式

vue-router 提供了三种运行模式:

● hash: 使用 URL hash 值来做路由。默认模式。

● history: 依赖 HTML5 History API 和服务器配置。查看 HTML5 History 模式。

● abstract: 支持全部 JavaScript 运行环境,如 Node.js 服务器端。

Hash模式

vue-router 默认模式是 hash 模式 —— 使用 URL 的 hash 来模拟一个完整的 URL,当 URL 改变时,页面不会去从新加载。

hash(#)是URL 的锚点,表明的是网页中的一个位置,单单改变#后的部分(/#/..),浏览器只会加载相应位置的内容,不会从新加载网页,也就是说 #是用来指导浏览器动做的,对服务器端彻底无用,HTTP请求中不包括#;同时每一次改变#后的部分,都会在浏览器的访问历史中增长一个记录,使用”后退”按钮,就能够回到上一个位置;因此说Hash模式经过锚点值的改变,根据不一样的值,渲染指定DOM位置的不一样数据。

History模式

HTML5 History API提供了一种功能,能让开发人员在不刷新整个页面的状况下修改站点的URL,就是利用 history.pushState API 来完成 URL 跳转而无须从新加载页面;

因为hash模式会在url中自带#,若是不想要很丑的 hash,咱们能够用路由的 history 模式,只须要在配置路由规则时,加入"mode: 'history'",这种模式充分利用 history.pushState API 来完成 URL 跳转而无须从新加载页面。

//main.js文件中
 
const router = new VueRouter({
 
mode: 'history',
 
routes: [...]
 
})

  

当使用 history 模式时,URL 就像正常的 url,例如 yoursite.com/user/id,比较好… 不过这种模式要玩好,还须要后台配置支持。由于咱们的应用是个单页客户端应用,若是后台没有正确的配置,当用户在浏览器直接访问

因此呢,你要在服务端增长一个覆盖全部状况的候选资源:若是 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。

export const routes = [
 
{path: "/", name: "homeLink", component:Home}
 
{path: "/register", name: "registerLink", component: Register},
 
{path: "/login", name: "loginLink", component: Login},
 
{path: "*", redirect: "/"}]

  

此处就设置若是URL输入错误或者是URL 匹配不到任何静态资源,就自动跳到到Home页面。

abstract模式

abstract模式是使用一个不依赖于浏览器的浏览历史虚拟管理后端。

根据平台差别能够看出,在 Weex 环境中只支持使用 abstract 模式。 不过,vue-router 自身会对环境作校验,若是发现没有浏览器的 API,vue-router 会自动强制进入 abstract 模式,因此 在使用 vue-router 时只要不写 mode 配置便可,默认会在浏览器环境中使用 hash 模式,在移动端原生环境中使用 abstract 模式。 (固然,你也能够明确指定在全部状况下都使用 abstract 模式)

vue-router使用方式

1:下载 npm i vue-router -S

**2:在main.js中引入 ** import VueRouter from 'vue-router';

3:安装插件 Vue.use(VueRouter);

4:建立路由对象并配置路由规则

let router = new VueRouter({routes:[{path:'/home',component:Home}]});

5:将其路由对象传递给Vue的实例,options中加入 router:router

6:在app.vue中留坑

<router-view></router-view>

  

具体实现请看以下代码:

  1. //main.js文件中引入
     
    import Vue from 'vue';
     
    import VueRouter from 'vue-router';
     
    //主体
     
    import App from './components/app.vue';
     
    import index from './components/index.vue'
     
    //安装插件
     
    Vue.use(VueRouter); //挂载属性
     
    //建立路由对象并配置路由规则
     
    let router = new VueRouter({
     
    routes: [
     
    //一个个对象
     
    { path: '/index', component: index }
     
    ]
     
    });
     
    //new Vue 启动
     
    new Vue({
     
    el: '#app',
     
    //让vue知道咱们的路由规则
     
    router: router, //能够简写router
     
    render: c => c(App),
     
    })
     
    复制代码

      

最后记得在在app.vue中“留坑”

    1. //app.vue中
       
      <template>
       
      <div>
       
      <!-- 留坑,很是重要 -->
       
      <router-view></router-view>
       
      </div>
       
      </template>
       
      <script>
       
      export default {
       
      data(){
       
       return {}
       
        }
       
      }
       
      </script>
       
      复制代码

        

vue-router源码分析

咱们先来看看vue的实现路径。

 

 

 

在入口文件中须要实例化一个 VueRouter 的实例对象 ,而后将其传入 Vue 实例的 options 中。

  1.  1 export default class VueRouter {  2   static install: () => void;  3  static version: string;  4  
     5  app: any;  6   apps: Array<any>;  7   ready: boolean;  8   readyCbs: Array<Function>;  9  options: RouterOptions;  10  mode: string;  11   history: HashHistory | HTML5History | AbstractHistory;  12  matcher: Matcher;  13   fallback: boolean;  14   beforeHooks: Array<?NavigationGuard>;  15   resolveHooks: Array<?NavigationGuard>;  16   afterHooks: Array<?AfterNavigationHook>;  17  
     18   constructor (options: RouterOptions = {}) {  19     this.app = null
     20     this.apps = []  21     this.options = options  22     this.beforeHooks = []  23     this.resolveHooks = []  24     this.afterHooks = []  25     // 建立 matcher 匹配函数
     26     this.matcher = createMatcher(options.routes || [], this)  27     // 根据 mode 实例化具体的 History,默认为'hash'模式
     28     let mode = options.mode || 'hash'
     29     // 经过 supportsPushState 判断浏览器是否支持'history'模式
     30     // 若是设置的是'history'可是若是浏览器不支持的话,'history'模式会退回到'hash'模式
     31     // fallback 是当浏览器不支持 history.pushState 控制路由是否应该回退到 hash 模式。默认值为 true。
     32     this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
     33     if (this.fallback) {  34       mode = 'hash'
     35  }  36     // 不在浏览器内部的话,就会变成'abstract'模式
     37     if (!inBrowser) {  38       mode = 'abstract'
     39  }  40     this.mode = mode  41      // 根据不一样模式选择实例化对应的 History 类
     42     switch (mode) {  43       case 'history':  44         this.history = new HTML5History(this, options.base)  45         break
     46       case 'hash':  47         this.history = new HashHistory(this, options.base, this.fallback)  48         break
     49       case 'abstract':  50         this.history = new AbstractHistory(this, options.base)  51         break
     52       default:  53         if (process.env.NODE_ENV !== 'production') {  54           assert(false, `invalid mode: ${mode}`)  55  }  56  }  57  }  58  
     59  match (  60  raw: RawLocation,  61     current?: Route,  62     redirectedFrom?: Location  63  ): Route {  64     return this.matcher.match(raw, current, redirectedFrom)  65  }  66  
     67   get currentRoute (): ?Route {  68     return this.history && this.history.current  69  }  70  
     71   init (app: any /* Vue component instance */) {  72     process.env.NODE_ENV !== 'production' && assert(  73  install.installed,  74       `not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
     75  `before creating root instance.`  76  )  77  
     78     this.apps.push(app)  79  
     80     // main app already initialized.
     81     if (this.app) {  82       return
     83  }  84  
     85     this.app = app  86  
     87     const history = this.history  88     // 根据history的类别执行相应的初始化操做和监听
     89     if (history instanceof HTML5History) {  90  history.transitionTo(history.getCurrentLocation())  91     } else if (history instanceof HashHistory) {  92       const setupHashListener = () => {  93  history.setupListeners()  94  }  95  history.transitionTo(  96  history.getCurrentLocation(),  97  setupHashListener,  98  setupHashListener  99  ) 100  } 101  
    102     history.listen(route => { 103       this.apps.forEach((app) => { 104         app._route = route 105  }) 106  }) 107  } 108   // 路由跳转以前
    109  beforeEach (fn: Function): Function { 110     return registerHook(this.beforeHooks, fn) 111  } 112   // 路由导航被确认之间前
    113  beforeResolve (fn: Function): Function { 114     return registerHook(this.resolveHooks, fn) 115  } 116   // 路由跳转以后
    117  afterEach (fn: Function): Function { 118     return registerHook(this.afterHooks, fn) 119  } 120   // 第一次路由跳转完成时被调用的回调函数
    121   onReady (cb: Function, errorCb?: Function) { 122     this.history.onReady(cb, errorCb) 123  } 124   // 路由报错
    125  onError (errorCb: Function) { 126     this.history.onError(errorCb) 127  } 128   // 路由添加,这个方法会向history栈添加一个记录,点击后退会返回到上一个页面。
    129   push (location: RawLocation, onComplete?: Function, onAbort?: Function) { 130     this.history.push(location, onComplete, onAbort) 131  } 132   // 这个方法不会向history里面添加新的记录,点击返回,会跳转到上上一个页面。上一个记录是不存在的。
    133   replace (location: RawLocation, onComplete?: Function, onAbort?: Function) { 134     this.history.replace(location, onComplete, onAbort) 135  } 136   // 相对于当前页面向前或向后跳转多少个页面,相似 window.history.go(n)。n可为正数可为负数。正数返回上一个页面
    137  go (n: number) { 138     this.history.go(n) 139  } 140   // 后退到上一个页面
    141  back () { 142     this.go(-1) 143  } 144   // 前进到下一个页面
    145  forward () { 146     this.go(1) 147  } 148  
    149   getMatchedComponents (to?: RawLocation | Route): Array<any> { 150     const route: any = to 151       ? to.matched 152         ? to 153         : this.resolve(to).route 154       : this.currentRoute 155     if (!route) { 156       return [] 157  } 158     return [].concat.apply([], route.matched.map(m => { 159       return Object.keys(m.components).map(key => { 160         return m.components[key] 161  }) 162  })) 163  } 164  
    165  resolve ( 166  to: RawLocation, 167     current?: Route, 168     append?: boolean
    169  ): { 170  location: Location, 171  route: Route, 172  href: string, 173     // for backwards compat
    174  normalizedTo: Location, 175  resolved: Route 176  } { 177     const location = normalizeLocation( 178  to, 179       current || this.history.current, 180  append, 181       this
    182  ) 183     const route = this.match(location, current) 184     const fullPath = route.redirectedFrom || route.fullPath 185     const base = this.history.base 186     const href = createHref(base, fullPath, this.mode) 187     return { 188  location, 189  route, 190  href, 191       // for backwards compat
    192  normalizedTo: location, 193  resolved: route 194  } 195  } 196  
    197   addRoutes (routes: Array<RouteConfig>) { 198     this.matcher.addRoutes(routes) 199     if (this.history.current !== START) { 200       this.history.transitionTo(this.history.getCurrentLocation()) 201  } 202  } 203 }

     

      

HashHistory

• hash虽然出如今url中,但不会被包括在http请求中,它是用来指导浏览器动做的,对服务器端没影响,所以,改变hash不会从新加载页面。

• 能够为hash的改变添加监听事件:

  1.  
    window.addEventListener("hashchange",funcRef,false)
  2.  
    复制代码

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

  1. export class HashHistory extends History { constructor (router: Router, base: ?string, fallback: boolean) { super(router, base) // check history fallback deeplinking
        // 若是是从history模式降级来的,须要作降级检查
        if (fallback && checkFallback(this.base)) { // 若是降级且作了降级处理,则返回
          return } ensureSlash() } .......

     

    function checkFallback (base) { const location = getLocation(base) // 获得除去base的真正的 location 值
      if (!/^\/#/.test(location)) { // 若是此时地址不是以 /# 开头的
      // 须要作一次降级处理,降为 hash 模式下应有的 /# 开头
     window.location.replace( cleanPath(base + '/#' + location) ) return true } } function ensureSlash (): boolean { // 获得 hash 值
      const path = getHash() if (path.charAt(0) === '/') { // 若是是以 / 开头的,直接返回便可
        return true } // 不是的话,须要手动保证一次 替换 hash 值
      replaceHash('/' + path) return false } export function getHash (): string { // We can't use window.location.hash here because it's not
      // consistent across browsers - Firefox will pre-decode it!
      // 由于兼容性的问题,这里没有直接使用 window.location.hash
      // 由于 Firefox decode hash 值
      const href = window.location.href const index = href.indexOf('#') return index === -1 ? '' : decodeURI(href.slice(index + 1)) } // 获得hash以前的url地址
    function getUrl (path) { const href = window.location.href const i = href.indexOf('#') const base = i >= 0 ? href.slice(0, i) : href return `${base}#${path}` } // 添加一个hash
    function pushHash (path) { if (supportsPushState) { pushState(getUrl(path)) } else { window.location.hash = path } } // 替代hash
    function replaceHash (path) { if (supportsPushState) { replaceState(getUrl(path)) } else { window.location.replace(getUrl(path)) } }

     

     

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

  1. transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) { const route = this.router.match(location, this.current) //找到匹配路由
        this.confirmTransition(route, () => { //确认是否转化
          this.updateRoute(route) //更新route
          onComplete && onComplete(route) this.ensureURL() // 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) { this.ready = true
            this.readyErrorCbs.forEach(cb => { cb(err) }) } }) } //更新路由
    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) }) } }

     

      

pushState

  1. 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
      // 加了 try...catch 是由于 Safari 有调用 pushState 100 次限制
      // 一旦达到就会抛出 DOM Exception 18 错误
      const history = window.history try { if (replace) { // replace 的话 key 仍是当前的 key 不必生成新的
          history.replaceState({ key: _key }, '', url) } else { // 从新生成 key
          _key = genKey() // 带入新的 key 值
          history.pushState({ key: _key }, '', url) } } catch (e) { // 达到限制了 则从新指定新的地址
        window.location[replace ? 'replace' : 'assign'](url) } }

     

      

replaceState

  1. // 直接调用 pushState 传入 replace 为 true
     
    export function replaceState (url?: string) {
     
        pushState(url, true)
     
    }
     
    复制代码

      

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

supportsPushState

  1. 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 })()

     

      

其实所谓响应式属性,即当_route值改变时,会自动调用Vue实例的render()方法,更新视图。 $router.push()-->HashHistory.push()-->History.transitionTo()-->History.updateRoute()-->{app._route=route}-->vm.render()

监听地址栏

在浏览器中,用户能够直接在浏览器地址栏中输入改变路由,所以还须要监听浏览器地址栏中路由的变化 ,并具备与经过代码调用相同的响应行为,在HashHistory中这一功能经过setupListeners监听hashchange实现:

  

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

 

  

HTML5History

History interface是浏览器历史记录栈提供的接口,经过back(),forward(),go()等方法,咱们能够读取浏览器历史记录栈的信息,进行各类跳转操做。

  1.  
    export class HTML5History extends History { constructor (router: Router, base: ?string) { super(router, base) const expectScroll = router.options.scrollBehavior //指回滚方式
        const supportsScroll = supportsPushState && expectScroll if (supportsScroll) { setupScroll() } const initLocation = getLocation(this.base) //监控popstate事件
        window.addEventListener('popstate', e => { const current = this.current // Avoiding first `popstate` event dispatched in some browsers but first
          // history route not updated since async guard at the same time.
          // 避免在某些浏览器中首次发出“popstate”事件
          // 因为同一时间异步监听,history路由没有同时更新。
          const location = getLocation(this.base) if (this.current === START && location === initLocation) { return } this.transitionTo(location, route => { if (supportsScroll) { handleScroll(router, route, current, true) } }) }) }

     

      

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

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

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

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

oursite.com/user/id

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

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

两种模式比较

通常的需求场景中,hash模式与history模式是差很少的,根据MDN的介绍,调用history.pushState()相比于直接修改hash主要有如下优点:

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

• pushState设置的新url能够与当前url如出一辙,这样也会把记录添加到栈中,而hash设置的新值必须与原来不同才会触发记录添加到栈中

• pushState经过stateObject能够添加任意类型的数据记录中,而hash只可添加短字符串 pushState可额外设置title属性供后续使用

AbstractHistory

'abstract'模式,不涉及和浏览器地址的相关记录,流程跟'HashHistory'是同样的,其原理是经过数组模拟浏览器历史记录栈的功能

  1.  

    //abstract.js实现,这里经过栈的数据结构来模拟路由路径
    export class AbstractHistory extends History { index: number; stack: Array<Route>; constructor (router: Router, base: ?string) { super(router, base) this.stack = [] this.index = -1 } // 对于 go 的模拟
     go (n: number) { // 新的历史记录位置
        const targetIndex = this.index + n // 小于或大于超出则返回
        if (targetIndex < 0 || targetIndex >= this.stack.length) { return } // 取得新的 route 对象
        // 由于是和浏览器无关的 这里获得的必定是已经访问过的
        const route = this.stack[targetIndex] // 因此这里直接调用 confirmTransition 了
        // 而不是调用 transitionTo 还要走一遍 match 逻辑
        this.confirmTransition(route, () => { this.index = targetIndex this.updateRoute(route) }) }
    //确认是否转化路由
      confirmTransition (route: Route, onComplete: Function, onAbort?: Function) { const current = this.current const abort = err => { if (isError(err)) { if (this.errorCbs.length) { this.errorCbs.forEach(cb => { cb(err) }) } else { warn(false, 'uncaught error during route navigation:') console.error(err) } } onAbort && onAbort(err) } //判断若是先后是同一个路由,不进行操做
        if ( isSameRoute(route, current) && route.matched.length === current.matched.length ) { this.ensureURL() return abort() } //下面是各种钩子函数的处理
        //*********************
     }) }

     

看到这里你已经对vue-router的路由基本掌握的差很少了,要是喜欢看源码能够点击查

要是喜欢能够给我一个star,github

感谢Aine_潔CaiBoBo两位老师提供的思路。

做者:DIVI连接:https://juejin.im/post/5bc6eb875188255c9c755df2来源:掘金著做权归做者全部。商业转载请联系做者得到受权,非商业转载请注明出处。

相关文章
相关标签/搜索