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


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

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

    官网vue-router文档

    vue-router是什么?

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

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

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

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

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

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

    在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
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 相关阅读:
    jmeter 数据库类型的脚本
    jmeter 协议到脚本编写
    python socket粘包
    微信公众号开发
    常用windows和office激活工具
    分辨率等概念
    设置单元格同高或同宽
    单元格内容前或后增加内容
    单元格内数字复制和递增
    excel单元格内容换行
  • 原文地址:https://www.cnblogs.com/mmzuo-798/p/10791315.html
Copyright © 2020-2023  润新知