您好,登錄后才能下訂單哦!
本文旨在介紹 vue-router 的實現思路,并動手實現一個簡化版的 vue-router 。我們先來看一下一般項目中對 vue-router 最基本的一個使用,可以看到,這里定義了四個路由組件,我們只要在根 vue 實例中注入該 router 對象就可以使用了.
import VueRouter from 'vue-router'; import Home from '@/components/Home'; import A from '@/components/A'; import B from '@/components/B' import C from '@/components/C' Vue.use(VueRouter) export default new VueRouter.Router({ // mode: 'history', routes: [ { path: '/', component: Home }, { path: '/a', component: A }, { path: '/b', component: B }, { path: '/c', component: C } ] })
vue-router 提供兩個全局組件, router-view
和 router-link
,前者是用于路由組件的占位,后者用于點擊時跳轉到指定路由。此外組件內部可以通過 this.$router.push , this.$rouer.replace
等api實現路由跳轉。本文將實現上述兩個全局組件以及 push 和 replace 兩個api,調用的時候支持 params 傳參,并且支持 hash 和 history 兩種模式,忽略其余api、嵌套路由、異步路由、 abstract 路由以及導航守衛等高級功能的實現,這樣有助于理解 vue-router 的核心原理。本文的最終代碼不建議在生產環境使用,只做一個學習用途,下面我們就來一步步實現它。
install實現
任何一個 vue 插件都要實現一個 install 方法,通過 Vue.use 調用插件的時候就是在調用插件的 install 方法,那么路由的 install 要做哪些事情呢?首先我們知道 我們會用 new 關鍵字生成一個 router 實例,就像前面的代碼實例一樣,然后將其掛載到根 vue 實例上,那么作為一個全局路由,我們當然需要在各個組件中都可以拿到這個 router 實例。另外我們使用了全局組件 router-view 和 router-link ,由于 install 會接收到 Vue 構造函數作為實參,方便我們調用 Vue.component 來注冊全局組件。因此,在 install 中主要就做兩件事,給各個組件都掛載 router 實例,以及實現 router-view
和 router-link
兩個全局組件。下面是代碼:
const install = (Vue) => { if (this._Vue) { return; }; Vue.mixin({ beforeCreate() { if (this.$options && this.$options.router) { this._routerRoot = this; this._router = this.$options.router; Vue.util.defineReactive(this, '_routeHistory', this._router.history) } else { this._routerRoot = (this.$parent && this.$parent._routerRoot) || this } Object.defineProperty(this, '$router', { get() { return this._routerRoot._router; } }) Object.defineProperty(this, '$route', { get() { return { current: this._routerRoot._routeHistory.current, ...this._routerRoot._router.route }; } }) } }); Vue.component('router-view', { render(h) { ... } }) Vue.component('router-link', { props: { to: String, tag: String, }, render(h) { ... } }) this._Vue = Vue; }
這里的 this 代表的就是 vue-router 對象,它有兩個屬性暴露出來供外界調用,一個是 install ,一個是 Router 構造函數,這樣可以保證插件的正確安裝以及路由實例化。我們先忽略 Router 構造函數,來看 install ,上面代碼中的 this._Vue 是個開始沒有定義的屬性,他的目的是防止多次安裝。我們使用 Vue.mixin 對每個組件的 beforeCreate 鉤子做全局混入,目的是讓每個組件實例共享 router 實例,即通過 this.$router 拿到路由實例,通過 this.$route 拿到路由狀態。需要重點關注的是這行代碼:
Vue.util.defineReactive(this, '_routeHistory', this._router.history)
這行代碼利用 vue 的響應式原理,對根 vue 實例注冊了一個 _routeHistory 屬性,指向路由實例的 history 對象,這樣 history 也變成了響應式的。因此一旦路由的 history 發生變化,用到這個值的組件就會觸發 render 函數重新渲染,這里的組件就是 router-view 。從這里可以窺察到 vue-router 實現的一個基本思路。上述的代碼中對于兩個全局組件的 render 函數的實現,因為會依賴于 router 對象,我們先放一放,稍后再來實現它們,下面我們分析一下 Router 構造函數。
Router構造函數
經過剛才的分析,我們知道 router 實例需要有一個 history 對象,需要一個保存當前路由狀態的對象 route ,另外很顯然還需要接受路由配置表 routes ,根據 routes 需要一個路由映射表 routerMap 來實現組件搜索,還需要一個變量 mode 判斷是什么模式下的路由,需要實現 push 和 replace 兩個api,代碼如下:
const Router = function (options) { this.routes = options.routes; // 存放路由配置 this.mode = options.mode || 'hash'; this.route = Object.create(null), // 生成路由狀態 this.routerMap = createMap(this.routes) // 生成路由表 this.history = new RouterHistory(); // 實例化路由歷史對象 this.init(); // 初始化 } Router.prototype.push = (options) => { ... } Router.prototype.replace = (options) => { ... } Router.prototype.init = () => { ... }
我們看一下路由表 routerMap 的實現,由于不考慮嵌套等其他情況,實現很簡單,如下:
const createMap = (routes) => { let resMap = Object.create(null); routes.forEach(route => { resMap[route['path']] = route['component']; }) return resMap; }
RouterHistory 的實現也很簡單,根據前面分析,我們只需要一個 current 屬性就可以,如下:
const RouterHistory = function (mode) { this.current = null; }
有了路由表和 history , router-view 的實現就很容易了,如下:
Vue.component('router-view', { render(h) { let routerMap = this._self.$router.routerMap; return h(routerMap[this._self.$route.current]) } })
這里的 this 是一個 renderProxy 實例,他有一個屬性 _self 可以拿到當前的組件實例,進而訪問到 routerMap ,可以看到路由實例 history 的 current 本質上就是我們配置的路由表中的 path 。
接下來我們看一下 Router 要做哪些初始化工作。對于 hash 路由而言,url上 hash 值的改變不會引起頁面刷新,但是可以觸發一個 hashchange 事件。由于路由 history.current 初始為 null ,因此匹配不到任何一個路由,所以會導致頁面刷新加載不出任何路由組件。基于這兩點,在 init 方法中,我們需要實現對頁面加載完成的監聽,以及 hash 變化的監聽。對于 history 路由,為了實現瀏覽器前進后退時準確渲染對應組件,還要監聽一個 popstate 事件。代碼如下:
Router.prototype.init = function () { if (this.mode === 'hash') { fixHash() window.addEventListener('hashchange', () => { this.history.current = getHash(); }) window.addEventListener('load', () => { this.history.current = getHash(); }) } if (this.mode === 'history') { removeHash(this); window.addEventListener('load', () => { this.history.current = location.pathname; }) window.addEventListener('popstate', (e) => { if (e.state) { this.history.current = e.state.path; } }) } }
當啟用 hash 模式的時候,我們要檢測url上是否存在 hash 值,沒有的話強制賦值一個默認 path , hash 路由時會根據 hash 值作為 key 來查找路由表。 fixHash 和 getHash 實現如下:
const fixHash = () => { if (!location.hash) { location.hash = '/'; } } const getHash = () => { return location.hash.slice(1) || '/'; }
這樣在刷新頁面和 hash 改變的時候, current 可以得到賦值和更新,頁面能根據 hash 值準確渲染路由。 history 模式也是一樣的道理,只是它通過 location.pathname
作為 key 搜索路由組件,另外 history 模式需要去除url上可能存在的 hash , removeHash 實現如下:
const removeHash = (route) => { let url = location.href.split('#')[1] if (url) { route.current = url; history.replaceState({}, null, url) } }
我們可以看到當瀏覽器后退的時候, history 模式會觸發 popstate 事件,這個時候是通過 state 狀態去獲取 path 的,那么 state 狀態從哪里來呢,答案是從 window.history 對象的 pushState 和 replaceState 而來,這兩個方法正好可以用來實現 router 的 push 方法和 replace 方法,我們看一下這里它們的實現:
Router.prototype.push = (options) => { this.history.current = options.path; if (this.mode === 'history') { history.pushState({ path: options.path }, null, options.path); } else if (this.mode === 'hash') { location.hash = options.path; } this.route.params = { ...options.params } } Router.prototype.replace = (options) => { this.history.current = options.path; if (this.mode === 'history') { history.replaceState({ path: options.path }, null, options.path); } else if (this.mode === 'hash') { location.replace(`#${options.path}`) } this.route.params = { ...options.params } }
pushState 和 replaceState 能夠實現改變url的值但不引起頁面刷新,從而不會導致新請求發生, pushState 會生成一條歷史記錄而 replaceState 不會,后者只是替換當前url。在這兩個方法執行的時候將 path 存入 state ,這就使得 popstate 觸發的時候可以拿到路徑從而觸發組件渲染了。我們在組件內按照如下方式調用,會將 params 寫入 router 實例的 route 屬性中,從而在跳轉后的組件 B 內通過 this.$route.params
可以訪問到傳參。
this.$router.push({ path: '/b', params: { id: 55 } });
router-link實現
router-view 的實現很簡單,前面已經說過。最后,我們來看一下 router-link 的實現,先放上代碼:
Vue.component('router-link', { props: { to: String, tag: String, }, render(h) { let mode = this._self.$router.mode; let tag = this.tag || 'a'; let routerHistory = this._self.$router.history; return h(tag, { attrs: tag === 'a' ? { href: mode === 'hash' ? '#' + this.to : this.to, } : {}, on: { click: (e) => { if (this.to === routerHistory.current) { e.preventDefault(); return; } routerHistory.current = this.to; switch (mode) { case 'hash': if (tag === 'a') return; location.hash = this.to; break; case 'history': history.pushState({ path: this.to }, null, this.to); break; default: } e.preventDefault(); } }, style: { cursor: 'pointer' } }, this.$slots.default) } })
router-link 可以接受兩個屬性, to 表示要跳轉的路由路徑, tag 表示 router-link 要渲染的標簽名,默認為標簽。如果是 a 標簽,我們為其添加一個 href 屬性。我們給標簽綁定 click 事件,如果檢測到本次跳轉為當前路由的話什么都不做直接返回,并且阻止默認行為,否者根據 to 更換路由。 hash 模式下并且是 a 標簽時候可以直接利用瀏覽器的默認行為完成url上 hash 的替換,否者重新為 location.hash 賦值。 history 模式下則利用 pushState 去更新url。
以上實現就是一個簡單的vue-router,完整代碼參見vue-router-simple 。
總結
以上所述是小編給大家介紹的簡化版的vue-router實現思路詳解,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復大家的。在此也非常感謝大家對億速云網站的支持!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。