vue=>【輪子系列】手寫一個mini vue-router

mini vue router 要幹啥

  1. 實現一個插件:包含有VueRouter class & 必須的 install 方法;
  2. 實現2個全局組件:router-link & router-view;
  3. 監聽 url 變化,實現 hash 模式進行跳轉;
  4. 實現嵌套子路由跳轉顯示;

開始實現

插件實現

使用 vue-router 時,咱們使用 Vue.use(VueRouter)來註冊路由,而且經過 new VueRouter(routes)接收路由配置,構造路由器實例 router 並掛載到根實例選項中,使得全部組件內均可以經過 :this.$router訪問路由器javascript

let Vue
// 保存路由配置選項
class VueRouter { // new VueRouter(routes)
    constructor(options) {
        this.$options = options
        // todo 
    }
}

VueRouter.install = function (_Vue) { //
    Vue = _Vue // 接受宿主環境的 Vue
    Vue.mixin({
        beforeCreate() {
            // 將根組件上的 router 實例掛載到 Vue 原型,那麼全部的組件實例上都會有 $router
            if (this.$options.router) { 
                Vue.prototype.$router = this.$options.router
            }
        },
    })
    // todo
}

export default VueRouter

實現router-link

本輪子使用hash模式,所以url中會帶有#號。
router-link 組件目的是點擊進行跳轉,能夠將思路簡化爲:
<router-link to="/about">About</router-link> => <a href={'#'+this.to}>{this.$slots.default}</a>
上代碼:vue

const Link = Vue.extend({
        props: {
            to: {
                type: String,
                required: true
            }
        },
        render(h) {
            return h('a', {
                attrs: {
                    href: '#' + this.to
                }
            }, [this.$slots.default]
            )
        }
    })
    // 註冊 router-link
    Vue.component('router-link', Link)

監聽url變化並實現router-view響應視圖

  1. 經過監聽hashchange事件,獲取到更改後的URL標識符,並在路由器中建立響應式的matched數組記錄URL變化匹配到的路由;
  2. 深度標記每個router-view組件,而後從路由器的matched數組中根據當前訪問的深度(depth)取出要顯示的component,在當前router-view中渲染。思路以下圖:

嵌套路由 (1).png
代碼實現以下:java

constructor(options) {
        // 響應式的matched 按深度存放路由配置
        Vue.util.defineReactive(this, 'matched', [])

        // this.current 記錄的當前的URL標識符
        const initPath = window.location.hash.slice(1) || '/'
        Vue.util.defineReactive(this, 'current', initPath)

        // 監聽URL變化
        window.addEventListener('hashchange', this.onHashChange.bind(this))
        // page加載時也要匹配當前URL須要render的組件
        window.addEventListener('load', this.onHashChange.bind(this))
        this.match()
    }
    onHashChange() {
        this.current = window.location.hash.slice(1) || '/'
        this.matched = []
        this.match()
    }
    match(routes) {
        routes = routes || this.$options.routes
        //遞歸遍歷記錄當前URL下全部命中的route
        for (const route of routes) {
            if (route.path === '/' && this.current === '/') { // 首頁
                this.matched.push(route)
                return
            }
            // about/info
            if (route.path !== '/' && ~this.current.indexOf(route.path)) {
                this.matched.push(route)
                if (route.children) {
                    this.match(route.children)
                }
                return
            }
        }
    }
// 1. 標記深度
    const View = Vue.extend({
        render(h) {
            this.$vnode.data.routerView = true // 標記當前組件是 router-view
                let depth = 0
                //遞歸確認 當前 router-view 在組件樹中的深度
                let parent = this.$parent
                while (parent) {
                    const vnodeData = parent.$vnode && parent.$vnode.data
                    if (vnodeData) {
                        if (vnodeData.routerView) { // 說明是一個 router-view
                            ++depth
                        }
                    }
                    parent = parent.$parent
                }
                let component = null
                const { matched } = this.$router
                if(matched[depth]){
                    component = matched[depth].component
                }
                console.log('當前深度:',depth);
                console.log('當前matched:',this.$router.matched);
                return h(component)
           
        }
    })

    Vue.component('router-view', View)

至此,一個簡單的 mini vue-router 就實現了,咱們能夠像使用官方的vue-router同樣,引入咱們本身的 mini-vue-router在項目中使用。
固然,官方庫要遠遠複雜於本輪子,本輪子旨在理解vue-router核心思想,更深層次研究請閱讀官方源碼【》》傳送門】
本輪子實現效果以下:
image.pngnode

擴展與思考

  1. 如何實現路由守衛
  2. 如何實現路由緩存
  3. 如何實現history模式
  4. 如何實現路由懶加載

附註

本輪子的完整代碼:git

// 1. 是一個插件 有 VueRouter class 以及 install 方法
// 2. new VueRouter(options) 一個實例 掛載到 根實例上 而且全部組件能經過 this.$router  訪問router 實例
// 3. router-link router-view 兩個全局組件 router-link 跳轉,router-view 顯示內容
// 4. 監聽 url 變化 監聽 haschange || popstate 事件
// 5. 響應最新的 url : 建立一個響應式的屬性 current 當它改變的時候獲取對應的組件並顯示
// 6. 子組件 深度標記 與 macth()
let Vue
// 保存路由配置選項
class VueRouter { // new VueRouter(routes)
    constructor(options) {
        this.$options = options
        // todo 緩存一個路由映射表

        // 響應式的matched 按深度存放路由配置
        Vue.util.defineReactive(this, 'matched', [])

        // this.current 記錄的當前的URL標識符
        const initPath = window.location.hash.slice(1) || '/'
        Vue.util.defineReactive(this, 'current', initPath)

        // 監聽URL變化
        window.addEventListener('hashchange', this.onHashChange.bind(this))
        // page加載時也要匹配當前URL須要render的組件
        window.addEventListener('load', this.onHashChange.bind(this))
        this.match()
    }
    onHashChange() {
        this.current = window.location.hash.slice(1) || '/'
        this.matched = []
        this.match()
    }
    match(routes) {
        routes = routes || this.$options.routes
        //遞歸遍歷記錄當前URL下全部命中的route
        for (const route of routes) {
            if (route.path === '/' && this.current === '/') { // 首頁
                this.matched.push(route)
                return
            }
            // about/info
            if (route.path !== '/' && ~this.current.indexOf(route.path)) {
                this.matched.push(route)
                if (route.children) {
                    this.match(route.children)
                }
                return
            }
        }
    }
}

VueRouter.install = function (_Vue) { //
    Vue = _Vue // 接受宿主環境的 Vue
    Vue.mixin({
        beforeCreate() {
            // 將根組件上的 router 實例掛載到 Vue 實例原型,那麼全部的 組件實例上都會有 $router
            if (this.$options.router) {
                Vue.prototype.$router = this.$options.router
            }
        },
    })

    const Link = Vue.extend({
        props: {
            to: {
                type: String,
                required: true
            }
        },
        render(h) {
            return h('a', {
                attrs: {
                    href: '#' + this.to
                }
            }, [this.$slots.default]
            )
        }
    })

    // 1. 標記深度
    const View = Vue.extend({
        render(h) {
            this.$vnode.data.routerView = true // 標記當前組件是 router-view
            let depth = 0
            //遞歸確認 當前 router-view 在組件樹中的深度
            let parent = this.$parent
            while (parent) {
                const vnodeData = parent.$vnode && parent.$vnode.data
                if (vnodeData) {
                    if (vnodeData.routerView) { // 說明是一個 router-view
                        ++depth
                    }
                }
                parent = parent.$parent
            }
            let component = null
            const { matched } = this.$router
            if (matched[depth]) {
                component = matched[depth].component
            }
            console.log('當前深度:', depth);
            console.log('當前matched:', this.$router.matched);
            return h(component)

        }
    })

    Vue.component('router-link', Link)
    Vue.component('router-view', View)
}

export default VueRouter
相關文章
相關標籤/搜索