要想本身實現一個vue-router插件,就必須 先了解一下vue-router插件的基本使用,咱們在使用vue-router的時候,一般會定義一個router.js文件,裏面主要就是幹了如下幾件事:
① 引入vue-router模塊;html
import Router from 'vue-router'
② 引入Vue並使用vue-router,即所謂的安裝vue-router插件vue
import Vue from 'vue' Vue.use(Router)
③ 建立路由對象並對外暴露
④ 配置路由對象,即在建立路由對象的時候傳遞一個options參數,裏面主要包括mode(路由模式)、routes(路由表)vue-router
export default new Router({ mode: 'history', routes: [ { path: '/', name: 'home', component: Home }, { path: '/about', name: 'about', component: About } ] });
固然,還有最後一步就是在main.js中 引入router.js對外暴露的路由對象,而且將該路由對象 配置到Vue項目的根實例上,至此,就能夠在項目中使用vue-router了,所謂使用路由,就是 在頁面中使用<router-link>和<router-view>組件來實現路由的跳轉和跳轉內容的顯示。而<router-link>和<router-view>組件這兩個組件是 vue-router提供的, 只有通過安裝路由以後,才能識別這兩個組件,不然會報錯,提示 Unknown custom element: <router-link> - did you register the component correctly?,由於vue-router內部會經過Vue.component()方法進行 全局註冊這兩個組件。
路由有兩種模式,一種是 hash路由,即以 #號加路徑名的方式,將路由添加到 瀏覽器url地址末尾;另外一種就是 history路由,就是經過 瀏覽器提供的history api實現路由的跳轉,能夠實現直接將路由路徑追加到 瀏覽器域名末尾。而且這兩種方式,都是 路徑變化,即 瀏覽器url地址變化,可是 頁面不會跟着刷新,因此vue-router中要作的事,就是 監聽瀏覽器路徑即路由的變化,而後動態渲染對應的路由組件,能夠經過 雙向綁定機制實現,後面會具體講。咱們先來簡單瞭解一下這兩種路由的使用方式
① hash路由
// index.htmlvuex
<a href="#/home">Home</a> <a href="#/about">About</a> <div id="content"></div> <!--顯示路由內容區域-->
當用戶點擊上面的這兩個連接,就會在瀏覽器 url地址末尾追加上"#/home"或者"#/about",但此時頁面並無刷新,因此咱們要監聽hash路由的變化,瀏覽器提供了一個hashchange事件,咱們能夠經過該事件實現路由顯示區內容的變化
window.addEventListener("hashchange", () => { content.innerHTML = location.hash.slice(1); // 監聽到hash值變化後更新路由顯示區內容 });
② history路由api
history路由,要實現路由地址的變化,那麼須要用到history提供的 pushState(state, title, path)方法,從該方法名能夠知道,第一個參數爲 state對象, 能夠爲null;第二個參數爲 title字符串,即路由title,暫時無具體做用,後續可能會有用, 能夠爲null;第三個參數纔是有用的,即 路由要跳轉的路徑字符串,雖然 前兩個參數均可覺得null,可是咱們爲了給State進行標識,能夠傳遞一個state對象, 對象中包含title和path屬性分別表示路由的名稱和要跳轉的路徑,這樣就能夠經過 history.state知道當前路由的狀態信息,是哪一個路由了。
// index.html數組
<a onclick="go('/home')">Home</a> <a onclick="go('/about')">About</a> <div id="content"></div>
這裏沒有直接使用href="/home",由於路由的變化要經過pushState()實現,路由變化的時候才能監聽到popstate事件,因此這裏監聽了click事件經過pushState()的方式去更改路由地址
function go(path) { history.pushState({title:path.slice(1), path}, path.slice(1), path); content.innerHTML = path; } window.addEventListener("popstate", () => { // 監聽到路由變化後再次調go()方法更新路由顯示區內容 go(location.pathname); });
① 在路由插件中聲明一個VueRouter類,由於咱們使用vue-router的時候,是先引入路由插件,而後經過路由插件去new出一個router對象,因此引入的路由插件是一個類,同時在建立router對象的時候須要傳遞一個options參數配置對象,有mode和routes等屬性配置,如:
// vue-router.js瀏覽器
class VueRouter { // VueRouter其實是一個function,也能夠看作是一個對象 constructor(options) { this.mode = options.mode || ''; this.routes = options.routes || []; } static install(Vue) { // 添加一個靜態的install方法,該方法執行的時候會傳入Vue構造函數 // 這裏主要是給全部Vue實例添加一些屬性等操做 } } export default VueRouter;
上面在VueRouter類中添加了一個 靜態的install方法,之因此是靜態的,是由於咱們使用路由插件的時候傳遞給Vue.use()方法的參數是導出的這個VueRouter類,而 use方法執行的時候會調用傳遞給其參數的install方法,即調用 VueRouter.install(),因此install是靜態方法,因此若是路由插件導出的是一個對象,那麼 這個對象上也必需要有一個install()方法
// 如下導出插件對象的方式也能夠,好比,vuex就是導出的插件對象
class VueRouter {} const install = () => {} export default { // 導出插件對象 VueRouter, install // 導出的插件對象中必需要有一個install方法 }
// 使用的時候也要進行相應的變化,如:函數
import rt from "./vue-router" Vue.use(rt); const router = new rt.VueRouter({});
② 建立路由表對象,咱們要根據路徑的變化動態渲染出相應的組件,因此爲了方便,咱們須要構造一個路由表對象,其屬性爲路徑,屬性值爲組件,這樣當路徑發生變化的時候,咱們能夠直接經過路由表對象獲取到對應的組件並渲染出來this
this.routesMap = this.createMap(this.routes); // 給VueRouter類添加一個routesMap屬性,用於保存構建的路由表對象 createMap(routes) { // 經過傳遞進來的routes構建路由表對象 return routes.reduce((result, current) => { result[current.path] = current.component; return result; }, {}); }
這裏構造路由表對象使用到了reduce方法,reduce方法是一個用於 實現累加操做的方法,array.reduce(function(total, currentValue, currentIndex, arr), initialValue),當傳入了initialValue,那麼total就會等於initialValue的值,currentValue就是數組的第一個元素,接着下一輪循環,會把函數的返回值當作total傳入,數組的第二個元素當作currentValue傳入,一直循環直到數組元素遍歷完畢。
③ 保存當前路由,咱們能夠建立一個對象用於專門保存當前路由信息,咱們只須要去更改當前路由信息,就能夠動態渲染出相應的路由視圖了,如:url
class CurrentRoute { // 建立一個類,專門用於保存當前路由信息,同時方便在當前路由對象上添加屬性和方法 constructor() { this.path = null; // 添加一個path屬性保存當前路由的路徑 } } this.currentRoute = new CurrentRoute();// 給VueRouter添加一個currentRoute屬性,保存當前路由信息對象
④ 執行init()方法進行路由初始化,就是根據當前url地址進行判斷,若是頁面一加載什麼路徑都沒有,那麼就跳轉到"/"首頁,若是路徑變化則保存當前路由信息,如:
init() { // 初始化路由信息 if (this.mode === "hash") { // 若是是hash路由 if (location.hash) { // 若是頁面一加載的時候就有hash值 this.currentRoute.path = location.hash.slice(1); // 保存當前路由的路徑 } else { // 若是頁面一加載的時候沒有hash值 location.hash = "/"; // 跳轉到首頁"/",即在url地址末尾添加"#/" } window.addEventListener("hashchange", () => { // 監聽瀏覽器地址欄hash值變化 this.currentRoute.path = location.hash.slice(1); // hash改變,一樣更新當前路由信息 }); } else { // 若是是history路由 if (location.pathname) { // 若是頁面一加載的時候就有pathname值 this.currentRoute.path = location.pathname; // 保存當前路由的路徑 } else { // 若是頁面一加載的時候沒有pathname值 location.pathname = "/"; // 跳轉到首頁"/",即在域名地址末尾添加"/" } window.addEventListener("popstate", () => { // 監聽點擊瀏覽器前進或後退按鈕事件 this.currentRoute.path = location.pathname; }); } }
須要注意的是, history.pushState()方法不會觸發popstate事件, 只有點擊瀏覽器前進或後退按鈕纔會觸發popstate事件,可是隻要瀏覽器地址欄hash值變化就會觸發hashchange事件
⑤ 在每一個Vue實例上都添加上$router和$route屬性
咱們在使用vue-router的時候, 每一個實例上均可以經過this.$router和this.$route獲取到對應的路由對象和當前路由對象,因此咱們須要使用Vue的 mixin()方法在每一個實例上混入$router和$route,咱們在第①步的時候還遺留了install()方法的具體實現,咱們能夠在執行install()方法的時候 混入一個beforeCreate鉤子函數,mixin混入的方法若是和vue實例上的方法 同名, 並不會覆蓋,而是將同名的方法 放到一個數組中,而且mixin中混入的方法在數組的最前面,即 mixin中混入的方法先執行,這樣 每一個實例建立的時候都會執行該beforeCreate(),那麼咱們能夠在這裏將$router和$route混入到每一個實例上,如:
static install(Vue) { // install方法執行的時候會傳入Vue構造函數 Vue.mixin({ // 調用Vue的mixin方法在每一個Vue實例上添加一個beforeCreated鉤子 beforeCreate () { if (this.$options && this.$options.router) { // 若是是根組件,那麼其options上就會有router屬性 this._router = this.$options.router; } else { // 非根組件 this._router = this.$parent && this.$parent._router; // 從其父組件上獲取router } Object.defineProperty(this, "$router", { // 給每一個Vue實例添加$router屬性 get() { // 返回VueRouter實例對象 return this._router; } }); Object.defineProperty(this, "$route", { // 給每一個Vue實例添加$route屬性 get() { return { // 返回一個包含當前路由信息的對象 path: this._router.currentRoute.path } } }); } }); }
在使用路由插件的時候, 會在根實例上注入一個router屬性,因此若是this.$options有router的就是根實例,即 main.js中建立的那個Vue實例,因爲Vue組件的建立順序是 由外到內的,也就是說 根組件-->子組件 --> 孫組件 -->...,因此渲染的時候能夠 依次從其父組件上獲取到父組件上保存的_router實例,從而保存到當前組件的_router上。
⑥ 註冊router-link和router-view組件
咱們須要在install()方法執行的同時,經過Vue在全局上註冊router-link和router-view這兩個組件,可使用 Vue.component()方法全局註冊。
static install(Vue) { Vue.component("router-link", { // 全局註冊router-link組件 props: { to: { type: String, default: "/" } }, methods: { handleClick(e) { if (this._router.mode === "history") { // 若是是history路由 history.pushState({}, null, this.to); //經過pushState()方法更新瀏覽器地址欄路徑,不會刷新頁面 this._router.currentRoute.path = this.to; // 點擊連接後更新當前路由路徑 e.preventDefault(); // 阻止<a>標籤的默認行爲,防y頁面默認跳刷新s頁面 } } }, render() { const mode = this._router.mode; // 當前<router-link>組件上也會注入_router屬性從而能夠獲取到路由的mode return <a on-click={this.handleClick} href={mode === "hash" ? `#${this.to}` : this.to}>{this.$slots.default}</a>; } }); Vue.component("router-view", { // 全局註冊router-view組件 render(h) { const currentPath = this._router.currentRoute.path; const routesMap = this._router.routesMap; return h(routesMap[currentPath]); // 根據路由表傳入當前路由路徑,獲取對應的組件,並渲染 } }); }
上面給<a>標籤添加了一個click事件,由於當使用history路由的時候,<a>標籤上的href是一個路徑,點擊後會進行默認跳轉到該路徑,從而 會刷新頁面,因此須要 阻止其默認跳轉行爲,並 經過history的api進行跳轉。
⑦ 添加響應式路由
這裏目前有一個問題,就是如今點擊<router-link>的連接,僅僅是瀏覽器地址欄url地址發生了變化,咱們也能夠看到點擊連接後, 咱們在handleClick事件函數中確實更新了當前路由的path,可是<router-view>中的內容並無跟着變化,由於這個當前路由currentRoute對象裏面的數據並非響應式的,因此當前路由變化,視圖並不會跟着變化,要想讓currentRoute對象中的數據變成響應式的,那麼咱們能夠經過 Vue.util提供的一個defineReactive()方法,其能夠給某個對象添加某個屬性,而且 其屬性值是響應式的,如:
static install(Vue) { // 在install方法中添加響應式路由,由於install方法會傳入Vue // 將當前路由對象定義爲響應式數據 Vue.util.defineReactive(this, "current", this._router.currentRoute); }
這樣,this._router.currentRoute這個當前路由對象中的數據就 變成響應式的了, <route-view>組件的render()函數中使用到了this._router.currentRoute,因此其 render()函數就會再次執行從而動態渲染出當前路由
至此,已經實現了一個簡單的vue-router插件,其核心就是, 監聽路由路徑變化,而後動態渲染出對應的組件,其完整代碼以下:
class CurrentRoute { constructor() { this.path = null; } } class VueRouter { // VueRouter其實是一個function,也能夠看作是一個對象 constructor(options) { this.mode = options.mode || ''; this.routes = options.routes || []; this.routesMap = this.createMap(this.routes); this.currentRoute = new CurrentRoute(); this.init(); } init() { if (this.mode === "hash") { // 若是是hash路由 if (location.hash) { // 若是頁面一加載的時候就有hash值 this.currentRoute.path = location.hash.slice(1); // 保存當前路由的路徑 } else { // 若是頁面一加載的時候沒有hash值 location.hash = "/"; // 跳轉到首頁"/",即在url地址末尾添加"#/" } window.addEventListener("hashchange", () => { //監聽瀏覽器地址欄hash值變化 this.currentRoute.path = location.hash.slice(1); // hash改變,一樣更新當前路由信息 }); } else { // 若是是history路由 if (location.pathname) { // 若是頁面一加載的時候就有pathname值 this.currentRoute.path = location.pathname; // 保存當前路由的路徑 } else { // 若是頁面一加載的時候沒有pathname值 location.pathname = "/"; // 跳轉到首頁"/",即在域名地址末尾添加"/" } window.addEventListener("popstate", () => { // 監聽點擊瀏覽器前進或後退按鈕事件 this.currentRoute.path = location.pathname; // 若是頁面一加載就帶有pathname,那麼就將路徑保存到當前路由中 }); } } createMap(routes) { return routes.reduce((result, current) => { result[current.path] = current.component; return result; }, {}); } // Vue的use方法會調用插件的install方法,也就是說,若是導出的插件是一個類,那麼install就是類的靜態方法 // 若是導出的是一個對象,那麼install就是該對象的實例方法 static install(Vue) { // install方法執行的時候會傳入Vue構造函數 Vue.mixin({ // 調用Vue的mixin方法在每一個Vue實例上添加一個beforeCreated鉤子 beforeCreate () { if (this.$options && this.$options.router) { // 若是是根組件,那麼其options上就會有router屬性 this._router = this.$options.router; } else { // 非根組件 this._router = this.$parent && this.$parent._router; // 從其父組件上獲取router } // 將當前路由對象定義爲響應式數據 Vue.util.defineReactive(this, "current", this._router.currentRoute); Object.defineProperty(this, "$router", { get() { // 返回VueRouter實例對象 return this._router; } }); Object.defineProperty(this, "$route", { get() { return { // 返回一個包含當前路由信息的對象 path: this._router.currentRoute.path } } }); } }); Vue.component("router-link", { props: { to: { type: String, default: "/" } }, methods: { handleClick(e) { if (this._router.mode === "history") { // 若是是history路由 history.pushState({}, null, this.to); //經過pushState()方法更路徑,不會刷新頁面 this._router.currentRoute.path = this.to; // 更新路徑 e.preventDefault(); // 阻止<a>標籤的默認行爲,防y頁面默認跳刷新s頁面 } } }, render() { const mode = this._router.mode; // 當前<router-link>組件上也會注入_router屬性從而能夠獲取到路由的mode return <a on-click={this.handleClick} href={mode === "hash" ? `#${this.to}` : this.to}>{this.$slots.default}</a>; } }); Vue.component("router-view", { render(h) { const currentPath = this._router.currentRoute.path; // const currentPath = this.current.path; const routesMap = this._router.routesMap; return h(routesMap[currentPath]); } }); } } export default VueRouter;