手把手教你寫VueRouter

 

 

 Vue-Router提供了倆個組件 `router-link` `router-view`, 提供了倆個原型上的屬性`$route` `$router` ,我如今跟着源碼來把它實現一下

 開始

先看平時使用的 `Vue-Router` ,引入`Router` , `Vue.use` 註冊插件。直接從這裏開始入手vue

使用場景

import Vue from 'vue'
import Router from "../vue-router"
import routes from './routes'
Vue.use(Router)
let router =  new Router({
  routes
})

 index

先看`vue-router.js`文件,先生成一個`VueRouter`類,而後導入`install`方法,由於`Vue-Router`的`install`方法比`Vuex`複雜一些,因此將`install`單獨做爲一個文件。node

import install from './install';

class VueRouter {
    constructor(options) {
      
    }

}
VueRouter.install = install;

export default VueRouter

 

 Vue.use()

來先看 `Vue.use()`的源碼中的一部分,這裏面判斷註冊的插件裏的`install`是否是一個函數,有就執行插件裏的`install`函數。或者判斷插件自己是否是一個函數,有就執行插件自己。這裏本質上是沒有區別,有沒有`install`均可以。而`VueRouter`使用了`install`,目的是爲了將`install`做爲入口函數,方便封裝,同時也將`install`和其餘代碼分開。vue-router

if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args)
 } else if (typeof plugin === 'function') {
      plugin.apply(null, args)
 }

install

上述已經將`vue-router`的類構建好,如今`VueRouter`實例已經有了,而後執行`vue.use()`,而後會執行`VueRouter`類裏的`install`函數,那來看`install.js`。
install裏使用了`Vue.mixin`,混入代碼,下面代碼是在當前組件生命週期`beforeCreate`裏混入代碼,代碼邏輯是判斷當前組件是否爲根組件,若是是則將`_routerRoot`做爲鍵放入當前組件中,值爲`Vue`實例。再將`_router`做爲鍵放入當前組件中,值爲`VueRouter`實例。而後執行初始化操做。數組

若是不爲當前組件不是根組件,則該組件爲根組件的子組件。將`_routerRoot`做爲鍵放入當前組件中,值爲父組件的`_routerRoot`,從父親身上獲取.app

`$route`和`$router`是利用`Object.definePropert`代理`_routerRoot`裏的`_router`和`_route`,訪問到的。
接着註冊全局組件`router-view`和`router-link`
函數

import RouterView from '../components/router-view'
import RouterLink from '../components/router-link'

const install = (_Vue, s) => {
    _Vue.mixin({
        beforeCreate() {
            if (this.$options.router) {      // 判斷是否是根組件            
                this._routerRoot = this;              // 把vue實例放在當前組件的——routerRoot屬性上
                this._router = this.$options.router   // 這裏直接獲取根實例
                this._router.init(this);      // 初始化     
                _Vue.util.defineReactive(this, '_route', this._router.history.current)  // 將屬性_route成爲響應式屬性
            } else {
                this._routerRoot = this.$parent && this.$parent._routerRoot   // 這裏的是從父親最頂層獲取Router實例
            }
        }
    })
    Object.defineProperty(_Vue.prototype, '$route', {    // 代理$route
        get() {
            return this._routerRoot._route
        }
    })
    Object.defineProperty(_Vue.prototype, '$router', {  // 代理$router
        get() {
            return this._routerRoot._router
        }
    })
    _Vue.component('router-view', RouterView)   // 註冊組件router-view
    _Vue.component('router-link', RouterLink)   // 註冊組件router-view
}
export default install

init

 vue-router 默認 hash 模式 —— 使用 URL 的 hash 來模擬一個完整的 URL,因而當 URL 改變時,頁面不會從新加載。

而後回到`VueRouter`類中,此時多了個`init`函數。目前作的路由方式是`hash`方式,還有另外倆種方式,`history`,`abstract`。由於有三種方式,因此`Vue-Router`作了一個父類`base`執行一樣的邏輯,子類三種方式繼承父類`base`,再獨自執行本身方式的代碼。
經過`new HashHistory` 獲取 `history`實例,初始化`init`執行`history`實例對應函數。將目光放到`history`實例上,這些函數來自於`base.js`和`hash.js`。post

import install from './install';

class VueRouter {
    constructor(options) {
        this.history = new HashHistory(this);
    }

    

    init(app) {
        const history = this.history;

        const setupHashLister = () => {
            history.setupLister(); // hash的跳轉
        }

        history.transitionTo(
            history.getCurrentLocation(),
            setupHashLister
        )

        history.listen((route) => {
            app._route = route
        })

    }

}
VueRouter.install = install;

export default VueRouter

 base.js

先看構造函數`construcror`,將`router`做爲鍵放在自身實例上,值爲`VueRouter`實例,`curent`爲當前導航正要離開的路由,也就是路由守衛的參數裏的`from`<br/>
1.`transitionTo()`爲跳轉後當即執行的函數,傳入當前路徑和回調函數,`r`爲`$route`,是扁平化後的配置,也就是即將要進入的目標 路由對象<br/>
2.`cb`是`History`的`listen()`函數,將`$route`放入當前組件上供用戶使用。<br/>
3.`callback`是執行`HashHistory`的`setupHashLister()`函數,是給當前`window`添加監聽事件`onhashChange`,`onhashChange`後續經過`hash`變化執行`transitionTo`進行更新。<br/>
4.最後將`r`賦值給`current`,更新路由信息。ui

class History {
    constructor(router) {
        this.router = router;
        this.current = createRoute(null, {
            path: '/'
        })
    }

    transitionTo(location, callback) {

        let r = this.router.match(location)
        if (location == this.current.path && r.matched.length == this.current.matched.length) { // 防止重複跳轉
            return
        }


     
        this.cb && this.cb(r);
        callback && callback();
        this.current = r;
    }



    listen(cb) {

        this.cb = cb;
    }

}

hash.js

`hash`方式的函數就簡單介紹一下,看構造函數`constructor`,跟父類同樣賦值`router`。執行`ensureSlash`函數,由於`hash`相比其餘函數,一進入頁面就會多個#。因此就初始化的時候處理一下。`getCurrentLocation`函數是獲取當前路徑的,`push`是`hash`方式的跳轉,`setupLister`函數是剛剛所述的監聽函數`hashchange`。this

import Histroy from './base';

function ensureSlash() {
    if (window.location.hash) {
        return
    }
    window.location.hash = '/';
}


class HashHistory extends Histroy {
    constructor(router) {
        super();
        this.router = router;
        ensureSlash();
    }
    getCurrentLocation() {
        return window.location.hash.slice(1);
    }
    push(location){
        this.transitionTo(location,()=>{
            window.location.hash = location
        })
    }
    setupLister() {
        window.addEventListener('hashchange', () => {
            this.transitionTo(window.location.hash.slice(1));
        })

    }

}
export default HashHistory

 

扁平化

剛剛`base.js`裏執行的`this.router.match(location)`以及`createRoute()`,都是須要創建在扁平化配置基礎之上的。
平時配置的路由是這樣的,須要將配置進行扁平化,才能用得上。spa

 [
    {
        path: '/',
        name: 'home',
        component: Home
    },
    {
        path: '/about',
        name: 'about',
        component: About,
        children: [
            {
                path: 'add',
                name: 'add',
                component: Add
            },
            {
                path: 'bull',
                name: 'bull',
                component: Bull
            }
        ]
    }
]

 

扁平化後是這樣的

/: {path: "/", component: {…}, parnent: undefined}
/about: {path: "about", component: {…}, parnent: {…}}
/about/add: {path: "add", component: {…}, parnent: {…}}
/about/bull: {path: "bull", component: {…}, parnent: {…}}

接着看扁平化函數createMatcher以及createRouteMap

 createMatcher

`createMatcher`返回一個`match`函數,`match`方法是匹配路徑,根據路徑拿扁平化對象裏的配置,而後執行`createRoute`方法,將其轉化爲`route`,返回。`pathMap`由`createRouteMap`生成

import createRouteMap from './create-route-map'
import { createRoute } from './history/base';

export default function createMatcher(routes) {
    let { pathList, pathMap } = createRouteMap(routes);


    function match(location) {
        console.log(pathMap)
        let record = pathMap[location];
        
        return createRoute(record,{
            path: location
        })
    }

    return {
        match
    }
}

 createRouteMap

將`routes`配置傳入`createRouteMap`中,遍歷`routes`,進行扁平化操做
`pathMap`以路徑爲鍵名,值爲一個對象包裹着路徑,組件,父組件。
將路徑匹配上父組件的路徑和自身的路徑
若是有子組件就進行遞歸,所有轉爲扁平化返回。

export default function createRouteMap(routes, oldPathList, oldpathMap) {

    let pathList = oldPathList || [];
    let pathMap = oldpathMap || Object.create(null);
    routes.forEach(route => {
        addRouteRecord(route, pathList, pathMap);
    })

    return {
        pathList,
        pathMap
    }
}

function addRouteRecord(route, pathList, pathMap,parnent) {
    let path = parnent ? `${parnent.path}/${route.path}`: route.path;
    let record = {
        path: route.path,
        component: route.component,
        parnent
    }
    if (!pathMap[path]) {
        pathList.push(path);
        pathMap[path] = record;
    }
    if(route.children){
        route.children.forEach(route=>{
            addRouteRecord(route, pathList, pathMap,record)
        })
    }
}

createRoute

`createRoute`是生成`$route`的函數,傳入參數爲扁平化配置,路徑。將`res`做爲空數組,若是傳進來的扁平化配置有值,則進行`while`循環,將本身從數組頭部插入,取出父組件再從頭部插入,如此反覆,獲得一個含着層次關係的數組。將`loaction`和數組包裹爲對象返回。

export function createRoute(record, location) {
    let res = [];

    if (record) {
        while (record) {
            res.unshift(record)
            record = record.parnent
        }
    }

    return {
        ...location,
        matched: res
    }
}

router-view

而後在來看看`routerview`
是一個函數式組件,
返回`render`方法
進行`while`循環,遍歷出嵌套的`routerview`
用`depth`做爲深度,也是`matched`的`index`.
每遍歷一次,就在`$vnode.data.rouView` 改成`true`,將深度加1
返回對應的組件便可

export default {
    name:'routerView',
    functional: true,
    render(h,{parent,data}) {
        let route = parent.$route

        let depth = 0;
        while (parent) {  
            if (parent.$vnode && parent.$vnode.data.routeView) {
                depth++;
            }
            parent = parent.$parent;
        }
        data.routeView = true;
        let record = route.matched[depth];
        if (!record) {
            return h();
        }
        return h(record.component, data);
    }
}

router-link

再來看看`routerlink`
沒什麼東西就返回一個`a`標籤,用插槽把對應的文本顯示出來,在添加的跳轉事件
調用`$router`的`push`方法,也就是`Router`類上的`push`

export default {
    name: 'routerLink',
    props: {
        to: {
            type: String,
            required: true
        },
        tag: {
            type: String,
            default: 'a'
        }
    },
    methods: {
        handler(to) {
            this.$router.push(to)   // 路由跳轉
        }
    },
    render() {
    
        return <a onClick={this.handler.bind(this,this.to)}>{this.$slots.default[0].text}</a>
    }
}

vuerouter草稿:https://juejin.im/post/6862215979745673224

相關文章
相關標籤/搜索