從零開始實現一個vue-router插件

1、回顧一下官方vue-router插件的使用

要想本身實現一個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()方法進行 全局註冊這兩個組件。

2、理解路由的兩種模式

路由有兩種模式,一種是 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);
});

3、開始實現本身的vue-router路由插件

① 在路由插件中聲明一個VueRouter類,由於咱們使用vue-router的時候,是先引入路由插件,而後經過路由插件去new出一個router對象,因此引入的路由插件是一個類,同時在建立router對象的時候須要傳遞一個options參數配置對象,有moderoutes等屬性配置,如:
// 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()函數就會再次執行從而動態渲染出當前路由

4、總結

至此,已經實現了一個簡單的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;
相關文章
相關標籤/搜索