vue-router的原理?咱們來手擼一個vue-router!

前言

在使用vue全家桶進行開發時候,其中一個重要的插件就是vue-router,vue-router功能強大,而且使用方便,是咱們構建項目時候不可或缺的一部分,那麼vue-router究竟是怎麼實現的呢?那咱們從源碼出發,手擼一個基礎的vue-routervue

基礎用法

首先咱們先看一下在項目中咱們是如何使用router的git

  • router文件
    在項目中咱們一般都會有一個router文件夾在存放咱們的router文件github

    // src/router/index.js
        import Vue from'vue';
        import VueRouter from 'vue-router';
        import Page1 from'@/pages/detail1';
        import Page2 from'@/pages/detail2';
        
        // 建立VurRouter的插件
        Vue.use(VueRouter);
        
        // 配置router
        let router = [
            {
                path: '/',
                components: Page1,
            },
            {
                path: '/page2',
                components: Page2,
            }
        ]
        
        // 實例化VurRouter,並將配置注入,導出實例
        const Router = new VueRouter(router);
        
        export default Router;
  • vue根實例文件
    在實例化和掛載vue根實例的時候,咱們會將vue-router引入到配置項vue-router

    // src/main.js
        
        import Vue from 'vue';
        import App from './App.vue';
        import router from './router';
        
        new Vue({
            router,
            render: h => h(App),
        }).$mount('#app')
  • 使用路由
    利用路由提供的組件,咱們能夠按照咱們的需求展現和跳轉路由vuex

    <template>
    
        <div id="app">
    
            <div>
    
                <router-link to="/">page1</ router-link>
    
                <span>|</ span>
    
                <router-link to="/page2">page2</ router-link>
    
            </div>
    
            <router-view></ router-view>
    
        </ div>
    
    </ template>

手寫vue-router

閒話說了很多,下面咱們就開始手擼vue-router,let‘go!數組

  • 實現router的思路
    首先要實現router,咱們必須清楚咱們此次到底要作什麼東西,先把他們列出來:緩存

    1. 實現並導出一個Router實例,供外部調用
    2. 實現一個公共組件RouterView,用來加載路由視圖
    3. 實現一個RouterLink,用來跳轉路由
  • 整理一下每一個組件的需求app

    1. Router實例,須要接收router傳入的options,將全部路由以及嵌套路由處理和收集起來,並監聽url的變化。而且他做爲插件存在,應該有一個install方法
    2. RouterView組件,根據本身的路由嵌套層級,找出對應的路由裏面的組件,並展現出來
    3. RouterLink組件,建立一個a標籤,插入寫入的文本,並根據props中to的值進行跳轉
  • 開始寫代碼函數

    1. 分析完需求,咱們首先來寫一下vue-router實例學習

      class VueRouter {
          constructor(options) {
              // 將傳入路由配置保存在屬性$options中,方便以後調用
              this.$options = options;
          }
      }

      建立了實例方法,咱們開始寫install方法

      // 將install中的參數Vue緩存起來,這麼作緣由有兩個:
      // 1.這裏的Vue實例在上面的VueRouter實例中也會用到,緩存起來能夠直接使用
      // 2.由於插件是獨立的,直接引入Vue會致使打包時候把Vue打包到插件中進去,增長代碼打包後體積
      var Vue;
      
      VueRouter.install = function(_Vue) {
          Vue = _Vue;
          
          //使用mixin和生命週期beforeCreate作全局混入, 拿到實例後的相關屬性並掛載到prototype上
          Vue.mixin({
              beforeCreate() {
                  if (this.$options.router) {
                      // 這裏能夠看出咱們爲什麼可使用this.$router拿到router實例
                      // 也能夠看出爲什麼要在main.js中的根實例中將router實例做爲options配置進去
                      Vue.prototype.$router = this.$options.router;
                  }
              }
          })
      }

      接下來咱們接着完善router實例

      class VueRouter {
          constructor(options) {
              // 將傳入路由配置保存在屬性$options中,方便以後調用
              this.$options = options;
              
              // 這裏咱們利用hash的方法來構造路由地址,即咱們常看到的http://localhost/#/xxx
              // 截取url,去掉#號
              this.current = window.location.hash.slice(1) ||'/';
              
              // 利用Vue中的defineReactive方法響應式的建立一個數組matched,存放當前路由以及其嵌套路由 
              Vue.util.defineReactive(this, 'matched', []);
              
              // 處理路由以及嵌套路由
              this.matchs(this.$options);
              
              // 監聽拿到地址並修改地址
              window.addEventListener('hashchange', this.currentHash.bind(this));
              
              // 頁面首次加載的時候也須要監聽並修改地址
              window.addEventListener('load', this.currentHash.bind(this));
          }
          
          currentHash() {
              this.current = window.location.hash.slice(1);
              this.matched = [];
              this.matchs(this.$options);
          }
          
          matchs(routers) {
              // 遞歸處理路由
              // 輸出結果通常是['parent', 'child1', 'child1-child1']
              routers.forEach(router => {
                  // 通常來講/路徑下不會存放嵌套路由
                  if (this.current ==='/' && router.path === '/') {
                      this.matched.push(router);
                  } else if (this.current.includes(router.path) && router.path !== '/') {
                      this.matched.push(router);
                      if (router.children) {
                          this.matchs(router.children);
                      }
                  }
              }
          }
      }

      到這裏,對照需求,router的實例基本完成。

    2. 咱們接下來來寫RouterView組件
      RouterView組件在實現的過程當中,咱們須要思考一個問題,就是怎麼能找到對應的RouterView視圖並匹配到對應的路由組件。這裏咱們根據matched存入的方式,引入一個概念,路由深度,表示路由的層級。

      // 因爲是一個全局組件,咱們選擇寫在install中,在配置插件時候一塊兒註冊
      
      VueRouter.install = function(_Vue) {
          Vue = _Vue;
          
          // 掛載router-view組件
          Vue.component('RouterView', {
              render(h) {
                  // 上面提到的深度標記,父組件深度爲0
                  let deep = 0;
                  
                  // 將組件標記
                  this.routerView = true;
                  
                  // 循環找出全部RouterView的父元素 並標記深度
                  // 利用父子通訊的中的$parent
                  let parent = this.$parent;
                  
                  // 循環查找父組件,以此標記當前組件的深度,當parent不存在時候,說明已經到了頂層組件,退出循環
                  while(parent) {
                      //  父組件存在且是routerView
                      if (parent && parent.routerView) {
                          deep+=1;
                      }
                      parent = parent.$parent;
                  }
                  
                  // 找到匹配深度層級,找出component
                  let matched = this.$router.matched;
                  let _component = null;
                  // 若是能在matched中找出,爲_component賦值
                  if (matched[deep]) {
                      _component =  matched[deep].components;
                  }
                  
                  // 渲染_component
                  return h(_component);
              }
          })
      }

      到這一步RouterView也已經完成了,咱們只剩下一個組件了

    3. 最後咱們來實現一下RouterLink組件
      從需求中看,這個組件比起RouterView很簡單了,他只須要渲染一個a標籤,並展現對應文本並跳轉,和RouterView同樣,咱們在install函數中寫

      VueRouter.install = function(_Vue) {
          Vue = _Vue;
          
          // 掛載router-link組件
          Vue.component('RouterLink', {
              // 聲明props,to並設置爲必填
              props: {
                  to: {
                      type: String,
                      required: true
                  }
              },
              render(h) {
                  'a',
                  {
                      attrs: {
                          href: `#${this.to}`
                      }
                  },
                  // 利用匿名插槽將組件中文本寫入
                  [
                      this.$slots.default
                  ]
              }
          })
      }

      RouterLink組件功能也完成了,是否是很簡單呢?

完整代碼

var Vue;

class VueRouter {
    constructor(options) {
        // 將傳入路由配置保存在屬性$options中,方便以後調用
        this.$options = options;

        // 這裏咱們利用hash的方法來構造路由地址,即咱們常看到的http://localhost/#/xxx
        // 截取url,去掉#號
        this.current = window.location.hash.slice(1) ||'/';

        // 利用Vue中的defineReactive方法響應式的建立一個數組matched,存放當前路由以及其嵌套路由 
        Vue.util.defineReactive(this, 'matched', []);

        // 處理路由以及嵌套路由
        this.matchs(this.$options);

        // 監聽拿到地址並修改地址
        window.addEventListener('hashchange', this.currentHash.bind(this));

        // 頁面首次加載的時候也須要監聽並修改地址
        window.addEventListener('load', this.currentHash.bind(this));
    }

    currentHash() {
        this.current = window.location.hash.slice(1);
        this.matched = [];
        this.matchs(this.$options);
    }

    matchs(routers) {
        // 遞歸處理路由
        // 輸出結果通常是['parent', 'child1', 'child1-child1']
        routers.forEach(router => {
            // 通常來講/路徑下不會存放嵌套路由
            if (this.current ==='/' && router.path === '/') {
                this.matched.push(router);
            } else if (this.current.includes(router.path) && router.path !== '/') {
                this.matched.push(router);
                if (router.children) {
                    this.matchs(router.children);
                }
            }
        }
    }
}


VueRouter.install = function(_Vue) {
    Vue = _Vue;

    //使用mixin和生命週期beforeCreate作全局混入, 拿到實例後的相關屬性並掛載到prototype上
    Vue.mixin({
        beforeCreate() {
            if (this.$options.router) {
                // 這裏能夠看出咱們爲什麼可使用this.$router拿到router實例
                // 也能夠看出爲什麼要在main.js中的根實例中將router實例做爲options配置進去
                Vue.prototype.$router = this.$options.router;
            }
        }
    })
    
    // 掛載router-view組件
    Vue.component('RouterView', {
        render(h) {
            // 上面提到的深度標記,父組件深度爲0
            let deep = 0;

            // 將組件標記
            this.routerView = true;

            // 循環找出全部RouterView的父元素 並標記深度
            // 利用父子通訊的中的$parent
            let parent = this.$parent;

            // 循環查找父組件,以此標記當前組件的深度,當parent不存在時候,說明已經到了頂層組件,退出循環
            while(parent) {
                //  父組件存在且是routerView
                if (parent && parent.routerView) {
                    deep+=1;
                }
                parent = parent.$parent;
            }

            // 找到匹配深度層級,找出component
            let matched = this.$router.matched;
            let _component = null;
            // 若是能在matched中找出,爲_component賦值
            if (matched[deep]) {
                _component =  matched[deep].components;
            }

            // 渲染_component
            return h(_component);
        }
    })
    
    // 掛載router-link組件
    Vue.component('RouterLink', {
        // 聲明props,to並設置爲必填
        props: {
            to: {
                type: String,
                required: true
            }
        },
        render(h) {
            'a',
            {
                attrs: {
                    href: `#${this.to}`
                }
            },
            // 利用匿名插槽將組件中文本寫入
            [
                this.$slots.default
            ]
        }
    })
}

export default VueRouter;

寫在最後

  • 到這裏,此次的vue-router介紹就基本結束了。其實這裏實現的只是一個最簡單,最基礎的vue-router,也是我在學習過程當中的一個總結,旨在爲你們作一個拓展和介紹。其中裏面的重定向,路由懶加載以及路由守衛等都沒有體現,你們能夠按照這裏分析的思路去看和學習源碼。本人不才,文筆也通常,不敢說授人以漁,哪怕是給你們一個提示和啓發也是極好的。若是不明白的地方,能夠你們一塊兒討論,有錯誤的話歡迎你們指正,一塊兒學習,一塊兒進步。
  • 下一篇可能會寫一篇關於vuex的實現,喜歡的同窗們給個贊也是棒棒的,hhhhh。
  • 最後的最後,源碼在個人github上,以爲有幫助的同窗們能夠給個小小的star。
相關文章
相關標籤/搜索