默寫Vue-Router

默寫Vue-Router

寫在前面

做爲國內使用最多的庫之一的Vue,你們都在學習其源碼,作到知其然而知其因此然;
由於咱們不只要懂得如何使用它,還得了解其原理或者怎麼模擬實現;
可是光看可不行,我們還得動手,一遍不行就兩遍,兩遍不行就三遍...
誰也不是天才,一用就會,一問就懵,那今天從默寫實現Vue-router開始,刪繁就簡,手寫vue-router核心源碼。html

路由的源頭

路由經歷了從多頁面應用到單頁面應用的發展,最開始的網頁是多頁面的,一個完整的網頁應用有多個完整的html構成,經過a標籤對應到不一樣url,服務器端來根據URL的不一樣返回不一樣的頁面,那些頁面在服務端都是實實在在存在的。前端

我畢業參加工做是2014年,這個時候前端能作的事情頗有限,先後端還不能徹底分離,依賴於 Ajax ,使得前端可以勝任更多更復雜的事,先後端的職責愈來愈清晰,在業務不斷髮展的過程當中,因爲前端項目變得愈來愈複雜,因此咱們要考慮拆分出前端應用部分,使之成爲一個能獨立開發、運行的應用,而非依賴於後端渲染出HTML的多頁面應用。單頁面應用就應運而生了。vue

單頁面應用就是一個WEB項目只有一個 HTML 頁面,一旦頁面加載完成,SPA 不會由於用戶的操做而進行頁面的從新加載或跳轉。 取而代之的是利用 JS 動態的變換 HTML 的內容,從而來模擬多個視圖間跳轉。nginx

說到底路由是根據不一樣的URL來展現不一樣的內容或頁面,而路由的本質 就是創建起url和頁面之間的映射關係。vue-router

大部分單頁面應用的架構都是爲響應式Dom鋪路,咱們都知道常規的Dom操做開銷太大,尤爲是在各個板塊有大量數據交互和用戶IO交互場景的時候。單頁面應用就是在一個Public的Html架子上,進行虛擬Dom的展現和響應式數據模型的運用,讓一切看起來都在觀察者的監視下運行着,藉助路由展現着不一樣的模板內容。編程

路由模式Hash和History

Hash 模式使用了瀏覽器 URL 後綴中的#xxx部分來實現前端路由。默認狀況下,URL後綴中的#xxx hash 部分是用來作網頁的錨點功能的,如今前端路由看上了這個點,並對其加以利用。 好比這個 URL:http://www.abc.com/#/hello,hash 的值爲 #/hello。 爲何會看上瀏覽器URL後綴中的 hash 部分呢?緣由也簡單:後端

瀏覽器URL後綴中的 hash 改變了,不會觸發請求,對服務器徹底沒有影響。它的改變不會從新加載瀏覽器頁面。 更關鍵的一點是,由於hash發生變化的url都會被瀏覽器記錄下來,從而你會發現瀏覽器的前進後退均可以用了,頁面的狀態與瀏覽器的URL就發生了掛鉤。api

大白話: hash模式不會真實跳轉,留心觀察瀏覽器加載狀態並沒有變化,監聽背後的原理是onhashchange事件而已。瀏覽器

History 路由模式服務器

隨着 HTML5 中 history api 的到來,前端路由開始進化了。hashchange 只能改變 # 後面的代碼片斷,history api (pushState、replaceState、go、back、forward) 則給了前端徹底的自由。

在 HTML5 以前,瀏覽器就已經有了 history 對象。但在早期的 history 中只能用於多頁面的跳轉,正如早期咱們編寫路由時,總會用到以下api控制頁面跳轉。

history.go(-1);       // 後退一頁history.go(2);        // 前進兩頁history.forward();     // 前進一頁history.back();      // 後退一頁//Html5 新增history.pushState();         // 添加新的狀態到歷史狀態棧history.replaceState();      // 用新的狀態代替當前狀態history.state                // 返回當前狀態對象複製代碼

大白話:一般在單頁面使用中是須要後臺配置nginx跳轉支持的,試想當用戶在瀏覽器直接訪問oursite.com/user/id,默認會由服務器檢索這個文件,檢索不到就會返回 404;還有另外一個使用場景:***的服務端渲染項目中,資料挺多,具體你們能夠查一下,當前最火的一些衍生生態庫next和nuxt都很好的詮釋了這個特色,可是咱們今天不展開history模式,就默寫最簡單的核心代碼來實現路由。

默想是怎麼使用的Vue-Router

VueRouter 是做爲插件的形式引入到 Vue 系統內部的。而將具體的 router 信息嵌入到每一個 Vue 實例中,則是做爲 Vue 的構造函數參數傳入。

<!DOCTYPE html><html lang="en">
  <head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>今天進步了嗎?</title><script src="https://unpkg.com/vue/dist/vue.js"></script><!-- <script src="https://unpkg.com/vue-router/dist/vue-router.js"></script> --><script src="./vue-router-recode.js"></script>
  </head>
  <body><div id="app">  <h1>手寫<strong>Vue-Router</strong></h1>  <p><!-- 使用 router-link 組件來導航. --><!-- 經過傳入 `to` 屬性指定連接. --><!-- <router-link> 默認會被渲染成一個 `<a>` 標籤 --><router-link to="/foo">Go to Foo</router-link><router-link to="/bar">Go to Bar</router-link>  </p>  <!-- 路由出口 -->  <!-- 路由匹配到的組件將渲染在這裏 -->  <router-view></router-view></div><script>  // 0. 若是使用模塊化機制編程,導入Vue和VueRouter,要調用 Vue.use(VueRouter)  // 1. 定義 (路由) 組件。  // 能夠從其餘文件 import 進來  const Foo = { template: '<div>foo</div>' };      const Bar = { template: '<div>bar</div>' };      // 2. 定義路由  // 每一個路由應該映射一個組件。 其中"component" 能夠是  // 經過 Vue.extend() 建立的組件構造器,  // 或者,只是一個組件配置對象。  // 咱們晚點再討論嵌套路由。  const routes = [
        { path: '/foo', component: Foo },
        { path: '/bar', component: Bar },
      ];      // 3. 建立 router 實例,而後傳 `routes` 配置  // 你還能夠傳別的配置參數, 不過先這麼簡單着吧。  const router = new VueRouter({
        routes, // (縮寫) 至關於 routes: routes  });      // 4. 建立和掛載根實例。  // 記得要經過 router 配置參數注入路由,  // 從而讓整個應用都有路由功能  const vm = new Vue({
        router,
      }).$mount('#app');      // 如今,應用已經啓動了!</script>
  </body></html>複製代碼

直接使用router-link和router-view這兩個組件。它們是隨着 Vue Router 一塊兒引入的,做爲全局組件使用。有了以上的想法,咱們構建了這樣的簡單頁面,接下來咱們就手寫一個vue-router-recode.js用於替換vue-router.js的職能。

常規操做:wiki書寫功能邏輯和實現思路

就從業這些年的經驗來說,通常開發功能或者本身造輪子的時候,合格的開發者都有着本身的一套體系,像什麼Markdown記錄筆記啊,代碼良好的md說明文件啊,還有些畫圖的使用Xmind或流程圖ProcessOn之類的,均可以,不侷限方法,可是得有該有的習慣,不然哪天老闆讓寫個工具,寫完給別人使用還得手把手傳給別人,那可太痛苦了。

基本原理圖:

完整實現思路圖:

簡單版代碼默寫(快寫hash,一個文件幹完)

對,幹就完了!
完整瀏覽了以上思路的,都不是難事兒,手敲速度慢的,建議多敲多寫。Open with Live Server運行完美,直接上代碼:

let _Vue;class VueRouter {  constructor({ routes }) {let routerMap = {};
    routes.forEach((route) => {      let path = route.path;      if (!routerMap[path]) {
        routerMap[path] = route;
      }
    });this.routerMap = routerMap;console.log(this.routerMap);//TODOthis.current = {      path: '',      component: {template: `<div>default</div> `,
      },
    };this.linstener();
  }  linstener() {window.addEventListener('load', () => {      debugger;      console.log('load');      let hash = window.location.hash;      if (!hash) {window.location.hash = '/';
      }      let route = this.search(hash.slice(1));      if (route) {this.current.path = route.path;this.current.component = route.component;
      }
    });window.addEventListener('hashchange', () => {      debugger;      console.log('hashchange');      let hash = window.location.hash;      let route = this.search(hash.slice(1));      if (route) {this.current.path = route.path;this.current.component = route.component;
      }
    });
  }  search(path) {if (this.routerMap[path]) {      return this.routerMap[path];
    }
  }
}

VueRouter.install = function (Vue, options) {
  _Vue = Vue;
  _Vue.mixin({beforeCreate() {      let vm = this;      // console.log(vm);  if (vm.$options.router) {
        vm._routerRoot = this;
        vm._router = vm.$options.router;//定義響應式數據_Vue.util.defineReactive(vm, '_route', vm._router.current);
      } else {
        vm._routerRoot = vm.$parent && vm.$parent._routerRoot;
      }
    },
  });
  _Vue.component('router-link', {props: {      to: String,
    },render(c) {      // h => createElement  return c('a', { attrs: { href: '#' + this.to } }, this.$slots.default);
    },
  });
  _Vue.component('router-view', {render(c) {      debugger;      let component = this._routerRoot._route.component;      return c(component);
    },
  });
};if (typeof Vue !== 'undefined') {
  Vue.use(VueRouter);
}複製代碼

總結

本文主要爲理解vueRouter源碼提供一個最基礎的框架和思路,如須要對vue生態有着較爲深刻的理解,如vue的插件機制,vue的雙向綁定原理,後端路由和前端路由,hash模式相關api以及history模式相關api,更多相關的 Vue-Router 的細節,能夠參考其官網。但願本文對你有用。

幾點關鍵之處:

  • 自定義插件內部必須實現一個 install 方法,傳入參數是Vue的構造函數。
  • 使用了一個新的Vue 實例,將 URL 的 hash 變量進行數據響應化處理。
  • util.defineReactive屬於Vue源碼中提供的自有方法,就是用於建立響應式數據。
  • 關於渲染函數 render 的參數 c,它其實是 createElement 函數。
相關文章
相關標籤/搜索