最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 从路由到 vue-router 源码,带你吃透前端路由

    正文概述 掘金(sunshine小小倩)   2021-03-23   659

    起因是因为我们团队内部在进行发布系统迁移的时候,遇到了规格路由相关的基础问题。当时隐约知道是为什么,但是对于路由 因为我们平时过于熟悉,以至于忘了其很多基础特性,并没有第一时间快速的排查问题。对此深感惭愧,于是就找个时间补一补路由相关的基础知识,并做一个整理~~~

    路由

    我们既然是要聊一下前端路由,那么首先应该知道什么是路由。

    路由这个概念本来是后端提出来的。很早的时候,都是服务端渲染,那时候前后端还没有分离,服务端将整个页面返回,响应过程基本都是这样的:

    1. 浏览器发出请求
    2. 服务器的 80 或者 443 接口监听到浏览器发过来的请求,解析 URL 路径
    3. 服务端根据 URL 的内容,查询相应的资源,可能是 html 资源,可能是图片资源....,然后将对应的资源处理并返回给浏览器
    4. 浏览器接收到数据,然后根据 content-type 来判断如何解析资源

    那么什么是路由呢?我们可以简单理解为和服务器交互的一种方式,通过不同的路由我们去请求不同的资源(HTML 资源只是其中的一种方式)

    后端路由

    我们上面介绍的其实就是后端路由。

    后端路由又可称之为服务器端路由,因为对于服务器来说,当接收到客户端发来的HTTP请求,就会根据所请求的URL,来找到相应的映射函数,然后执行该函数,并将函数的返回值发送给客户端。

    对于最简单的静态资源服务器,可以认为,所有URL的映射函数就是一个文件读取操作。 对于动态资源,映射函数可能是一个数据库读取操作,也可能是进行一些数据的处理,等等。

    然后根据这些读取的数据,在服务器端就使用相应的模板来对页面进行渲染后,再返回渲染完毕的页面。

    前端路由

    前端路由是由于 ajax 的崛起而诞生的,我们大家都知道 ajax 是浏览器为了实现异步加载的一种技术方案,刚刚也介绍了,在前后端没有分离的时候,服务端都是直接将整个 HTML 返回,用户每次一个很小的操作都会引起页面的整个刷新(再加上之前的网速还很慢,所以用户体验可想而知)

    在 90年代末的时候,微软首先实现了 ajax(Asynchronous JavaScript And XML) 这个技术,这样用户每次的操作就可以不用刷新整个页面了,用户体验就大大提升了。

    又随着技术的发展,慢慢三大框架称霸了前端圈,成为前端开发的主力军。前端也可以做更多的事情了,陆陆续续也有了模块化和组件化的概念。

    当然还有单页应用、MVVM也陆陆续续出现在了前端er的视野。

    至此,前端开发者能够开发出更加大型的应用,职能也变得更加强大了,那么这和前端路由有什么关系呢?

    异步交互体验的更高级版本就是 SPA —— 单页应用。单页应用不仅仅是在页面交互是无刷新的,连页面跳转都是无刷新的。既然页面的跳转是无刷新的,也就是不再向后端请求返回 html。

    那么,一个大型应用通常会有几十个页面(url 地址)相互跳转,怎么前端怎么知道 url 对应展示什么内容呢?

    答案就是 —— 前端路由

    可以理解为,前端路由就是将之前服务端根据 url 的不同返回不同的页面的任务交给前端来做。

    从路由到 vue-router 源码,带你吃透前端路由 优点:用户体验好,不需要每次都从服务器全部获取,快速展现给用户 缺点:使用浏览器的前进,后退键的时候会重新发送请求,没有合理地利用缓存,单页面无法记住之前滚动的位置,无法在前进,后退的时候记住滚动的位置。

    前端路由解决了什么问题

    • 前端路由可以让前端自己维护路由和页面展示的逻辑。每次页面的改动不需要通知服务端。
    • 更好的交互体验:不用每次都从服务端拉取资源,快速展现给用户

    前端路由有哪些缺点?

    • 最让人诟病的就是不利于 SEO
    • 使用浏览器的前进,后退键时会重新发送请求,来获取数据,没有合理地利用缓存。

    前端路由实现的原理是什么

    在了解了什么是前端路由和前端路由解决了什么问题之后,我们再来深入了解下前端路由实现的原理

    前端路由的实现原理其实很简单,本质上就是检测 URL 的变化,通过拦截 URL然后解析匹配路由规则。

    hash 路由

    之前,大家都是通过 hash 来实现实现路由的,hash 路由的方式就和 <a> 链接的锚点是一样的,在地址后面增加 # ,例如我的个人博客 https://cherryblog.site/#/  # 及后面的内容,我们称之为 location 的 hash 从路由到 vue-router 源码,带你吃透前端路由

    然后我们再点开其他的 tab 页面,发现虽然浏览器地址栏的 url 改变了,但是页面却没有刷新。打开控制台,我们可以看到切换 tab 只是向服务端发送了请求接口数据的接口,并没有重新请求 html 的资源。 从路由到 vue-router 源码,带你吃透前端路由 这是因为 hash 的变化不会导致浏览器向服务端发送请求,所以也就不会刷新页面。但是每次 hash 的变化,都会触发 haschange 事件。所以我们就可以通过监听 haschange 的变化来做出响应。

    在我们现在(2021)的前端开发中,通常都是会有一个根节点 <div id="root"></div> ,然后将所要展示的内容插入到这个根节点之中。然后根据路由的不同,更换插入的内容组件。 从路由到 vue-router 源码,带你吃透前端路由

    history 路由

    hash 路由有一个问题就是因为有 #  所以不是那么“好看”

    14年后,因为 HTML5 标准发布。多了两个 API, pushState  和 replaceState ,通过这两个 API 可以改变 url 地址且不会发送请求。同时还有 onpopstate  事件。通过这些就能用另一种方式来实现前端路由了,但原理都是跟 hash 实现相同的。

    用了 HTML5 的实现,单页路由的 url 就不会多出一个 # ,变得更加美观。但因为没有 # 号,所以当用户刷新页面之类的操作时,浏览器还是会给服务器发送请求。为了避免出现这种情况,所以这个实现需要服务器的支持,需要把所有路由都重定向到根页面。具体可以见:[HTML5 histroy 模式](HTML5 History 模式)

    注意,直接调用 history.popState() 和 history.poshState() 并不会触发 popState 。只有在做出浏览器的行为才会调用 popState ,比如点击浏览器的前进后退按钮或者JS调用 history.back() 或者 history.forward()  从路由到 vue-router 源码,带你吃透前端路由

    vue-router

    那我们来看一下 vue-router 是怎么结合 vue 一起实现前端路由的。

    总的来说就是使用 Vue.util.defineReactive 将实例的 _route 设置为响应式对象。而 push, replace 方法会主动更新属性 _route。而 go,back,或者点击前进后退的按钮则会在 onhashchange 或者 onpopstate 的回调中更新 _route。_route 的更新会触发 RoterView 的重新渲染。

    然后我们就在具体的看下是怎么实现的

    如何在 vue 中注入 vueRouter(插件的安装)

    Vue提供了插件注册机制是,每个插件都需要实现一个静态的 install方法,当执行 Vue.use 注册插件的时候,就会执行 install 方法,该方法执行的时候第一个参数强制是 Vue对象。

    在 vue-router 中,install 方法如下。

    import View from './components/view'
    import Link from './components/link'
    
    // 导出 vue 实例
    export let _Vue
    
    // install 方法 当 Vue.use(vueRouter)时 相当于 Vue.use(vueRouter.install())
    export function install (Vue) {
      // 如果已经注册过了并且已经有了 vue 实例,那么直接返回
      if (install.installed && _Vue === Vue) return
      install.installed = true
    
      // 保存Vue实例,方便其它插件文件使用
      _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)
        }
      }
    
      /**
       * 递归的将所有的 vue 组件混入两个生命周期 beforeCreate 和 destroyed
       * 在 beforeCreated 中初始化 vue-router,并将_route响应式
       */
      Vue.mixin({
        beforeCreate () {
          // 初始化 vue-router
          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)
          } else {
            // 将 _route 变成响应式对象
            this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
          }
          registerInstance(this, this)
        },
        destroyed () {
          registerInstance(this)
        }
      })
      
      /**
       * 给Vue添加实例对象 $router 和 $route
       * $router为router实例
       * $route为当前的route
       */
      Object.defineProperty(Vue.prototype, '$router', {
        get () { return this._routerRoot._router }
      })
    
      Object.defineProperty(Vue.prototype, '$route', {
        get () { return this._routerRoot._route }
      })
    
      /**
       * 注入两个全局组件
       * <router-view>
       * <router-link>
       */
      Vue.component('RouterView', View)
      Vue.component('RouterLink', Link)
    
      /**
       * Vue.config 是一个对象,包含了Vue的全局配置
       * 将vue-router的hook进行Vue的合并策略
       */
      const strats = Vue.config.optionMergeStrategies
      // use the same hook merging strategy for route hooks
      strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
    }
    

    为了保证 VueRouter 只执行一次,当执行 install 逻辑的时候添加一个标识 installed。用一个全局变量保存 Vue,方便插件对 Vue 的使用。

    VueRouter 安装的核心是通过 mixin,向 Vue app 的所有组件混入 beforeCreatedestroyed钩子函数。

    并且还在 Vue 添加实例对象

    • _routerRoot: 指向 vue 实例
    • _router:指向 vueRouter 实例

    在 Vue 的 prototype 上初始化了一些 getter

    • $router, 当前Router的实例
    • $route, 当前Router的信息

    Vue.util.defineReactive, 这是Vue里面观察者劫持数据的方法,劫持 _route,当 _route 触发 setter 方法的时候,则会通知到依赖的组件。

    后面通过 Vue.component 方法定义了全局的 <router-link><router-view> 两个组件。<router-link>类似于a标签,<router-view> 是路由出口,在 <router-view> 切换路由渲染不同Vue组件。 最后定义了路由守卫的合并策略,采用了Vue的合并策略。

    init VueRouter

    刚刚我们提到了在 install 的时候会执行 VueRouter 的 init 方法( this._router.init(this) ),那么接下来我们就来看一下 init 方法做了什么。简单来说就是将 Vue 实例挂载到当前 router 的实例上。

    然后 install 的时候会执行执行 VueRouter 的 init 方法( this._router.init(this) )。init 执行的时候通过 history.transitionTo 做路由过渡。matcher 路由匹配器是后面路由切换,路由和组件匹配的核心函数。

    
      init (app: any /* Vue component instance */) {
        this.apps.push(app)
    
        // main app previously initialized
        // return as we don't need to set up new history listener 
        if (this.app) {
          return
        }
    
        // 在 VueRouter 上挂载 Vue 实例
        this.app = app
    
        const history = this.history
    
        // setupListeners 里会对 hashchange 事件进行监听
        // transitionTo 是进行路由导航的函数
        if (history instanceof HTML5History || history instanceof HashHistory) {
          const setupListeners = routeOrError => {
            history.setupListeners()
          }
          history.transitionTo(
            history.getCurrentLocation(),
            setupListeners,
            setupListeners
          )
        }
    
        // 路由全局监听,维护当前的route
        // 因为 _route 在 install 执行时定义为响应式属性,
        // 当 route 变更时 _route 更新,后面的视图更新渲染就是依赖于 _route
        history.listen(route => {
          this.apps.forEach(app => {
            app._route = route
          })
        })
      }
    

    VueRouter 的 constructor

    VueRouter 的 constructor 相对而言比较简单

    • 定义了一些属性和方法。
    • 创建 matcher 匹配函数,这个函数函数很重要,可以查找 route
    • 设置默认值和做不支持 H5 history 的降级处理
    • 根据不同的 mode 实例化不同的 History 对象
      constructor (options: RouterOptions = {}) {
        this.app = null
        this.apps = []
        this.options = options
        this.beforeHooks = []
        this.resolveHooks = []
        this.afterHooks = []
        // 创建 matcher 匹配函数
        this.matcher = createMatcher(options.routes || [], this)
    
        // 默认使用 哈希路由
        let mode = options.mode || 'hash'
        
        // h5的history有兼容性 对history做降级处理
        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}`)
            }
        }
      }
    

    在实例化 vueRouter 的时候,vueRouter 仿照 history 定义了一些api:pushreplacebackgoforward,还定义了路由匹配器、添加router动态更新方法等。

    如何更改 url

    那么 VueRouter 是如何做路由的跳转的呢?也就是说我们在使用 _this_.$router.push('/foo', increment) 的时候,怎么让渲染的视图展示 Foo 组件。

    const router = new VueRouter({
      mode: 'history',
      base: __dirname,
      routes: [
        { path: '/', component: Home },
        { path: '/foo', component: Foo },
        { path: '/bar', component: Bar },
        { path: encodeURI('/é'), component: Unicode },
        { path: '/query/:q', component: Query }
      ]
    })
    

    还记得我们刚刚在 vue-router 的 constructor 中做了什么吗?我来帮大家回忆一下。在 constructor 中,我们根据不同的 mode 选择不同类型的 history 进行实例化(h5 history 还是 hash history 还是 abstract ),然后在 init 的时候调用 history.transitionTo 进行路由初始化匹配,也就是完成第一次路由导航。

    我们在 history/base.js 文件中可以找到 transitionTo 方法。transitionTo 可以接收三个参数 locationonCompleteonAbort,分别是目标路径、路经切换成功的回调、路径切换失败的回调。

    首先在 router 中找到传入的 location ,然后更新当前的 route,接着就执行路经切换成功的回调函数(在这个函数中,不同模式的 history 的实现是不一样的)。

    回调中会调用 replaceHash 或者 pushHash 方法。它们会更新 location 的 hash 值。如果兼容 historyAPI,会使用 history.replaceState 或者 history.pushState。如果不兼容 historyAPI 会使用 window.location.replace 或者window.location.hash。

    而handleScroll方法则是会更新我们的滚动条的位置。

    transitionTo (
        location: RawLocation,
        onComplete?: Function,
        onAbort?: Function
    ) {
        // 调用 match方法得到匹配的 route对象
        const route = this.router.match(location, this.current)
        
        // 过渡处理
        this.confirmTransition(
            route,
            () => {
                // 更新当前的 route 对象
                this.updateRoute(route)
              
                // 更新url地址 hash模式更新hash值 history模式通过pushState/replaceState来更新
                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)
                    })
                }
            }
        )
    }
    

    url 更改后怎么进行组件的渲染

    到此为止,已经可以让不同模式下的 history 对象拥有了表现相同的 push  replace 功能(详细可以看下面的实现部分)

    那么路由更换之后怎么进行正确的渲染呢。

    记得我们前面说过的 vue 的响应式原理了吗?我们在 install 的时候已经将 _router 设置为响应式的了。只要 _router 进行了改变,那么就会触发 RouterView 的渲染。(我们在 transitionTo 的回调中更新了 _route)

    go, forward, back

    在 VueRouter 上定义的 go,forward,back方法都是调用 history 的属性的 go 方法。

    而hash上go方法调用的是history.go,它是如何更新RouteView的呢?答案是hash对象在setupListeners方法中添加了对popstate或者hashchange事件的监听。在事件的回调中会触发RoterView的更新

    setupListeners

    我们在通过点击后退, 前进按钮或者调用 back, forward, go 方法的时候。我们没有主动更新 _app.route 和current。我们该如何触发 RouterView 的更新呢?通过在 window 上监听 popstate,或者 hashchange 事件。在事件的回调中,调用 transitionTo 方法完成对 _route 和 current 的更新。

    或者可以这样说,在使用 push,replace 方法的时候,hash的更新在 _route 更新的后面。而使用 go, back 时,hash 的更新在 _route 更新的前面。

    setupListeners () {
      const router = this.router
      const expectScroll = router.options.scrollBehavior
      const supportsScroll = supportsPushState && expectScroll
      if (supportsScroll) {
        setupScroll()
      }
      window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', () => {
        const current = this.current
        if (!ensureSlash()) {
          return
        }
        this.transitionTo(getHash(), route => {
          if (supportsScroll) {
            handleScroll(this.router, route, current, true)
          }
          if (!supportsPushState) {
            replaceHash(route.fullPath)
          }
        })
      })
    }
    

    hash 路由

    export class HashHistory extends History {
      constructor (router: Router, base: ?string, fallback: boolean) {
        super(router, base)
        // check history fallback deeplinking
        if (fallback && checkFallback(this.base)) {
          return
        }
        ensureSlash()
      }
    
      // this is delayed until the app mounts
      // to avoid the hashchange listener being fired too early
      setupListeners () {
        if (this.listeners.length > 0) {
          return
        }
    
        const router = this.router
        const expectScroll = router.options.scrollBehavior
        const supportsScroll = supportsPushState && expectScroll
    
        if (supportsScroll) {
          this.listeners.push(setupScroll())
        }
    
        // 添加 hashchange 事件监听
        window.addEventListener(
          hashchange,
          () => {
            const current = this.current
            // 获取 hash 的内容并通过路由配置,把新的页面 render 到 ui-view 的节点
            this.transitionTo(getHash(), route => {
              if (supportsScroll) {
                handleScroll(this.router, route, current, true)
              }
              if (!supportsPushState) {
                replaceHash(route.fullPath)
              }
            })
          }
        )
        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)
            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 => {
            replaceHash(route.fullPath)
            handleScroll(this.router, route, fromRoute, false)
            onComplete && onComplete(route)
          },
          onAbort
        )
      }
    
      go (n: number) {
        window.history.go(n)
      }
    }
    
    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))
      }
    }
    

    H5 history 路由

    其实和 hash 的实现方式是基本类似的,区别点主要在于

    • 监听的事件不一样
    • push 和 replace 方法的实现不一样
    export class HTML5History extends History {
      _startLocation: string
    
      constructor (router: Router, base: ?string) {
        super(router, base)
    
        this._startLocation = getLocation(this.base)
      }
    
      setupListeners () {
        if (this.listeners.length > 0) {
          return
        }
    
        const router = this.router
        const expectScroll = router.options.scrollBehavior
        const supportsScroll = supportsPushState && expectScroll
    
        if (supportsScroll) {
          this.listeners.push(setupScroll())
        }
    
        // 通过监听 popstate 事件
        window.addEventListener('popstate', () => {
          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.
          const location = getLocation(this.base)
          if (this.current === START && location === this._startLocation) {
            return
          }
    
          this.transitionTo(location, route => {
            if (supportsScroll) {
              handleScroll(router, route, current, true)
            }
          })
        })
        this.listeners.push(() => {
          window.removeEventListener('popstate', handleRoutingEvent)
        })
      }
    
      go (n: number) {
        window.history.go(n)
      }
    
      push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
        const { current: fromRoute } = this
        this.transitionTo(location, route => {
          // 使用 pushState 更新 url,不会导致浏览器发送请求,从而不会刷新页面
          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 跟 pushState 的区别在于,不会记录到历史栈
          replaceState(cleanPath(this.base + route.fullPath))
          handleScroll(this.router, route, fromRoute, false)
          onComplete && onComplete(route)
        }, onAbort)
      }
    
      ensureURL (push?: boolean) {
        if (getLocation(this.base) !== this.current.fullPath) {
          const current = cleanPath(this.base + this.current.fullPath)
          push ? pushState(current) : replaceState(current)
        }
      }
    
      getCurrentLocation (): string {
        return getLocation(this.base)
      }
    }
    
    

    能读到这里的同学真的很感谢大家~~ 这是我第一次写源码相关的内容,还没有研究的很透彻,其中不免会有一些错误的地方,希望大家多多指正~~~


    起源地下载网 » 从路由到 vue-router 源码,带你吃透前端路由

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元