詳解vue-router

120902.jpg

我有一碗酒,可以慰風(fēng)塵

前端路由定義

在SPA中,路由指的是URL與UI之間的映射,這種映射是單向的,即URL變化引起UI更新(無需刷新界面)

實現(xiàn)前端路由

實現(xiàn)前端路由,需要解決兩個核心問題

  • 如何改變URL卻不引起頁面刷新
  • 如何檢測URL變化了

vue-router里面有hash和history兩種方式,下面介紹一下這兩種方式

hash實現(xiàn)

hash指的是URL中hash(#)及后面的那一part,改變URL中的hash部分不會引起頁面刷新,并且可以通過hashchange事件監(jiān)聽URL的變化。改變URL的方式有如下幾種:

  • 通過瀏覽器的前進后退改變URL
  • 通過<a>標(biāo)簽改變URL
  • 通過window.location改變URL

history實現(xiàn)

HTML5中的history提供了pushState和replaceState兩個方法,這兩個方法改變URL的path部分不會引起頁面刷新
history提供類似hashchange事件的popstate事件,但又有不同之處

  • 通過瀏覽器前進后退改變URL是會觸發(fā)popstate事件
  • 通過pushState/replaceState或者<a>標(biāo)簽改變URL不會觸發(fā)popstate事件
  • 我們可以攔截pushState/replaceState的調(diào)用和<a>標(biāo)簽的點擊事件來檢測URL的變化
  • 通過js調(diào)用history的back,go,forward方法來觸發(fā)該事件

實操js實現(xiàn)前端路由

基于hash

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title></title>
    </head>
    <body>
        <ul>
            <li><a href="#/home">home</a></li>
            <li><a href="#/about">about</a></li>
        </ul>   
        <!-- 路由渲染口子 -->
        <div id="routerView"></div>
    </body>
    <script>
        let routerView = document.getElementById('routerView')
        window.addEventListener('hashchange', () => {
           const hash = location.hash
           routerView.innerHTML = hash
        })
        
        window.addEventListener('DOMContentLoaded', () => {
            if (!location.hash) {
                location.hash = "/"
            } else {
                const hash = location.hash
                routerView.innerHTML = hash
            }
        })
        
    </script>
</html>

效果圖


image.png

上面的代碼干了哪些活?

  • 通過<a>標(biāo)簽的href屬性來改變URL中的hash值
  • 監(jiān)聽了hashchange事件,當(dāng)事件觸發(fā)的時候,改變routerView中的內(nèi)容
  • 監(jiān)聽了DOMContentLoaded事件,初次的時候需要渲染成對應(yīng)的內(nèi)容

基于HTML5 history實現(xiàn)

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title></title>
    </head>
    <body>
        <ul>
            <li><a href="#/home">home</a></li>
            <li><a href="#/about">about</a></li>
        </ul>   
        <!-- 路由渲染口子 -->
        <div id="routerView"></div> 
        <script>
            const router = document.getElementById('routerView')
            window.addEventListener('DOMContentLoaded', () => {
                const linkList = document.querySelectorAll('a[href]')
                linkList.forEach(el => el.addEventListener('click', function(e) {
                    e.preventDefault()
                    history.pushState(null, '', el.getAttribute('href'))
                    router.innerHTML = location.pathname
                }))
            })
            
            window.addEventListener('popstate', () => {
                router.innerHTML = location.pathname
            })
            
        </script>
    </body>
</html>
  • 我們通過a標(biāo)簽的href屬性來改變URL的path值(當(dāng)然,你觸發(fā)瀏覽器的前進后退按鈕也可以,或者在控制臺輸入history.go,back,forward賦值來觸發(fā)popState事件)。這里需要注意的就是,當(dāng)改變path值時,默認(rèn)會觸發(fā)頁面的跳轉(zhuǎn),所以需要攔截 <a> 標(biāo)簽點擊事件默認(rèn)行為, 點擊時使用 pushState 修改 URL并更新手動 UI,從而實現(xiàn)點擊鏈接更新 URL 和 UI 的效果。
  • 我們監(jiān)聽popState事件。一旦事件觸發(fā),就改變routerView的內(nèi)容

vue 中vue-router

import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
import About from "../views/About.vue"
Vue.use(VueRouter)
  const routes = [
  {
    path: '/home',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About
  }
]
const router = new VueRouter({
  mode:"history",
  routes
})
export default router


// App.vue
<template>
  <div id="app">
    <div id="nav">
      <router-link to="/home">Home</router-link> |
      <router-link to="/about">About</router-link>
    </div>
    <router-view/>
  </div>
</template>

截圖如下

image.png

改造vue-router文件

import Vue from 'vue'
// import VueRouter from 'vue-router'
import VueRouter from './myVueRouter'
import Home from '../views/Home.vue'
import About from "../views/About.vue"


Vue.use(VueRouter)
  const routes = [
  {
    path: '/home',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About
  }
]
const router = new VueRouter({
  mode:"history",
  routes
})
export default router

分析vue-router文件干了啥

1, 通過import VueRouter from 'vue-router' 引入了VueRouter
2,const router = new VueRouter({})
3,Vue.use(VueRouter)使得每個組件都可以擁有router實例

  • 通過new VueRouter({})獲得實例,也就是說VueRouter其實是一個類
class VueRouter {
   
}
  • 使用Vue.use(),而Vue.use其實就是執(zhí)行對象的install這個方法
class VueRouter {

}

VueRouter.install = function () {

}

export default VueRouter

分析Vue.use

Vue.use(plugin)
1,參數(shù)

{ object | Function } plugin

2,用法
安裝Vue.js插件。如果插件是一個對象,必須提供install方法。如果插件是一個函數(shù),它會被作為install方法。調(diào)用install方法時,會將Vue作為參數(shù)傳入。install方法被同一個插件多次調(diào)用時,插件也只會被安裝一次。
3, 作用
注冊插件,此時只需要調(diào)用install方法并將Vue作為參數(shù)傳入即可。但在細節(jié)上有兩部分邏輯要處理:

1、插件的類型,可以是install方法,也可以是一個包含install方法的對象。

2、插件只能被安裝一次,保證插件列表中不能有重復(fù)的插件。

4,實現(xiàn)

Vue.use = function(plugin) {
    const installPlugins = (this._installedPlugins || (this._installedPlugins = []))
    if (installPlugins.indexOf(plugin) > -1) {
        return
    }

    const args = toArray(arguments, 1)
    args.unshift(this)
    if (typeof plugin.install === 'function') {
        plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {
        plugin.apply(null, plugin, args)
    }

    installPlugins.push(plugin)
    return this    
}

1、在Vue.js上新增了use方法,并接收一個參數(shù)plugin。
2、首先判斷插件是不是已經(jīng)別注冊過,如果被注冊過,則直接終止方法執(zhí)行,此時只需要使用indexOf方法即可。
3、toArray方法我們在就是將類數(shù)組轉(zhuǎn)成真正的數(shù)組。使用toArray方法得到arguments。除了第一個參數(shù)之外,剩余的所有參數(shù)將得到的列表賦值給args,然后將Vue添加到args列表的最前面。這樣做的目的是保證install方法被執(zhí)行時第一個參數(shù)是Vue,其余參數(shù)是注冊插件時傳入的參數(shù)。
4、由于plugin參數(shù)支持對象和函數(shù)類型,所以通過判斷plugin.install和plugin哪個是函數(shù),即可知用戶使用哪種方式祖冊的插件,然后執(zhí)行用戶編寫的插件并將args作為參數(shù)傳入。
5、最后,將插件添加到installedPlugins中,保證相同的插件不會反復(fù)被注冊。

了解以上開始寫myVueRouter.js

let Vue = null

class VueRouter {

}

VueRouter.install = function (v) {
    Vue = v
    Vue.component('router-link',{
        render(h) {
            return h('a', {}, 'Home')
        }
    })
    Vue.component('router-view', {
        render(h) {
            return h('h1', {}, '我是首頁')
        }
    })
}

export default VueRouter

簡易版果然跑起來了,截圖如下


image.png

完善install方法

install 是給每個vue實例添加?xùn)|西的,在router中給每個組件添加了$route和$router

這倆的區(qū)別是:

$router是VueRouter的實例對象,$route是當(dāng)前路由對象,也就是說$route是$router的一個屬性

// main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'

Vue.config.productionTip = false

new Vue({
  router,
  render: h => h(App),
}).$mount('#app')

我們可以發(fā)現(xiàn)這里只是將router ,也就是./router導(dǎo)出的router實例,作為Vue 參數(shù)的一部分。
但是這里就是有一個問題咯,這里的Vue 是根組件啊。也就是說目前只有根組件有這個router值,而其他組件是還沒有的,所以我們需要讓其他組件也擁有這個router。
因此,install方法我們可以這樣完善

let Vue = null

class VueRouter {

}

VueRouter.install = function (v) {
    Vue = v

    Vue.mixin({
        beforeCreate() {
            if (this.$options && this.$options.router) { // 如果是根組件
                this._root = this // 把當(dāng)前實例掛載到_root上
                this._router = this.$options.router
            } else {
                this._root = this.$parent && this.$parent._root
            }

            Object.defineProperty(this, '$router', {
                get () {
                    return this._root._router
                }
            })
        }
    })
    Vue.component('router-link',{
        render(h) {
            return h('a', {}, 'Home')
        }
    })
    Vue.component('router-view', {
        render(h) {
            return h('h1', {}, '我是首頁')
        }
    })
}

export default VueRouter

一通操作之下的解釋

  • 參數(shù)Vue,我們在分析Vue.use的時候,再執(zhí)行install的時候,將Vue作為參數(shù)傳進去。,
  • mixin的作用是將mixin的內(nèi)容混合到Vue的初始參數(shù)options中。相信使用vue的同學(xué)應(yīng)該使用過mixin了。
  • 為什么是beforeCreate而不是created呢?因為如果是在created操作的話,$options已經(jīng)初始化好了。
  • 如果判斷當(dāng)前組件是根組件的話,就將我們傳入的router和_root掛在到根組件實例上。
  • 如果判斷當(dāng)前組件是子組件的話,就將我們_root根組件掛載到子組件。注意是引用的復(fù)制,因此每個組件都擁有了同一個_root根組件掛載在它身上。

? 為啥判斷是子組件就直接取父組件的_root根組件呢
來,看一下父子組件執(zhí)行順序先
父beforeCreate-> 父created -> 父beforeMounte -> 子beforeCreate ->子create ->子beforeMount ->子 mounted -> 父mounted

在執(zhí)行子組件的beforeCreate的時候,父組件已經(jīng)執(zhí)行完beforeCreate了,拿到_root那就沒問題了


在vueRouter文件中

const router = new VueRouter({
  mode:"history",
  routes
})

我們傳了兩個參數(shù),一個模式mode,一個是路由數(shù)組routes

let Vue = null

class VueRouter {
    constructor (options) {
        this.mode = options.mode || 'hash'
        this.routes = options.routes || []
        this.routesMap = this.createMap(this.routes)
        
    }
    createMap (routes) {
        return routes.reduce((pre, current) => {
            pre[current.path] = current.component
            return pre
        }, {})
    }
}

VueRouter.install = function (v) {
    Vue = v

    Vue.mixin({
        beforeCreate() {
            if (this.$options && this.$options.router) { // 如果是根組件
                this._root = this // 把當(dāng)前實例掛載到_root上
                this._router = this.$options.router
            } else {
                this._root = this.$parent && this.$parent._root
            }

            Object.defineProperty(this, '$router', {
                get () {
                    return this._root._router
                }
            })
        }
    })
    Vue.component('router-link',{
        render(h) {
            return h('a', {}, 'Home')
        }
    })
    Vue.component('router-view', {
        render(h) {
            return h('h1', {}, '我是首頁')
        }
    })
}

export default VueRouter

路由中需要存放當(dāng)前的路徑,來表示當(dāng)前的路徑狀態(tài),為了方便管理,用一個對象來表示。初始化的時候判斷是那種模式,并將當(dāng)前的路徑保存到current中

let Vue = null

class HistoryRoute {
    constructor () {
        this.current = null
    }
}

class VueRouter {
    constructor (options) {
        this.mode = options.mode || 'hash'
        this.routes = options.routes || []
        this.routesMap = this.createMap(this.routes)
        this.history = new HistoryRoute()
        this.init()
    }

    init () {
        if (this.mode === 'hash') {
            // 先判斷用戶打開是是否有hash值,沒有跳轉(zhuǎn)到#/
            location.hash ? '' : location.hash = '/'
            window.addEventListener('load', () => {
                this.history.current = location.hash.slice(1)
            })
            window.addEventListener('hashchange', () => {
                this.history.current = location.hash.slice(1)
            })
        } else {
            location.pathname ? '' : location.pathname = '/'
            window.addEventListener('load', () => {
                this.history.current = location.pathname
            })
            window.addEventListener('popstate', () => {
                this.history.current = location.pathname
            })
        }
    }

    createMap (routes) {
        return routes.reduce((pre, current) => {
            pre[current.path] = current.component
            return pre
        }, {})
    }
}

VueRouter.install = function (v) {
    Vue = v

    Vue.mixin({
        beforeCreate() {
            if (this.$options && this.$options.router) { // 如果是根組件
                this._root = this // 把當(dāng)前實例掛載到_root上
                this._router = this.$options.router
            } else {
                this._root = this.$parent && this.$parent._root
            }

            Object.defineProperty(this, '$router', {
                get () {
                    return this._root._router
                }
            })
        }
    })
    Vue.component('router-link',{
        render(h) {
            return h('a', {}, 'Home')
        }
    })
    Vue.component('router-view', {
        render(h) {
            return h('h1', {}, '我是首頁')
        }
    })
}

export default VueRouter

完善$route

其實/$route就是獲取當(dāng)前的路徑

VueRouter.install = function (v) {
    Vue = v

    Vue.mixin({
        beforeCreate() {
            if (this.$options && this.$options.router) { // 如果是根組件
                this._root = this // 把當(dāng)前實例掛載到_root上
                this._router = this.$options.router
            } else {
                this._root = this.$parent && this.$parent._root
            }

            Object.defineProperty(this, '$router', {
                get () {
                    return this._root._router
                }
            })

            Object.defineProperty(this, '$route', {
                get () {
                    return this._root._router.history.current
                }
            })
        }
    })
    Vue.component('router-link',{
        render(h) {
            return h('a', {}, 'Home')
        }
    })
    Vue.component('router-view', {
        render(h) {
            return h('h1', {}, '我是首頁')
        }
    })
}

完善router-view

Vue.component('router-view', {
        render(h) {
            let current = this._self._root._router.history.current
            let routeMap = this._self._root._router.routesMap
            return h(routeMap[current])
        }
    })

render函數(shù)里的this指向的是一個Proxy代理對象,代理Vue組件,而我們前面講到每個組件都有一個_root屬性指向根組件,根組件上有_router這個路由實例。
所以我們可以從router實例上獲得路由表,也可以獲得當(dāng)前路徑。
然后再把獲得的組件放到h()里進行渲染。
現(xiàn)在已經(jīng)實現(xiàn)了router-view組件的渲染,但是有一個問題,就是你改變路徑,視圖是沒有重新渲染的,所以需要將_router.history進行響應(yīng)式化。

VueRouter.install = function (v) {
    Vue = v

    Vue.mixin({
        beforeCreate() {
            if (this.$options && this.$options.router) { // 如果是根組件
                this._root = this // 把當(dāng)前實例掛載到_root上
                this._router = this.$options.router
                // 新增
                Vue.util.defineReactive(this, 'xxx', this._router.history)
            } else {
                this._root = this.$parent && this.$parent._root
            }

            Object.defineProperty(this, '$router', {
                get () {
                    return this._root._router
                }
            })

            Object.defineProperty(this, '$route', {
                get () {
                    return this._root._router.history.current
                }
            })
        }
    })
    Vue.component('router-link',{
        render(h) {
            return h('a', {}, 'Home')
        }
    })
    Vue.component('router-view', {
        render(h) {
            let current = this._self._root._router.history.current
            let routeMap = this._self._root._router.routesMap
            return h(routeMap[current])
        }
    })
}

效果如下


image.png

image.png

完善router-link

Vue.component('router-link',{
        props: {
            to: String
        },
        render(h) {
            let mode = this._self._root._router.mode
            let to = mode === 'hash' ? '#'+this.to : this.to
            return h('a', {attrs: {href: to}}, this.$slots.default)
        }
    })

截圖如下


image.png

image.png

myVueRouter.js完整代碼

let Vue = null

class HistoryRoute {
    constructor () {
        this.current = null
    }
}

class VueRouter {
    constructor (options) {
        this.mode = options.mode || 'hash'
        this.routes = options.routes || []
        this.routesMap = this.createMap(this.routes)
        this.history = new HistoryRoute()
        this.init()
    }

    init () {
        if (this.mode === 'hash') {
            // 先判斷用戶打開是是否有hash值,沒有跳轉(zhuǎn)到#/
            location.hash ? '' : location.hash = '/'
            window.addEventListener('load', () => {
                this.history.current = location.hash.slice(1)
            })
            window.addEventListener('hashchange', () => {
                this.history.current = location.hash.slice(1)
            })
        } else {
            location.pathname ? '' : location.pathname = '/'
            window.addEventListener('load', () => {
                this.history.current = location.pathname
            })
            window.addEventListener('popstate', () => {
                this.history.current = location.pathname
            })
        }
    }

    createMap (routes) {
        return routes.reduce((pre, current) => {
            pre[current.path] = current.component
            return pre
        }, {})
    }
}

VueRouter.install = function (v) {
    Vue = v

    Vue.mixin({
        beforeCreate() {
            if (this.$options && this.$options.router) { // 如果是根組件
                this._root = this // 把當(dāng)前實例掛載到_root上
                this._router = this.$options.router
                // 新增
                Vue.util.defineReactive(this, 'xxx', this._router.history)
            } else {
                this._root = this.$parent && this.$parent._root
            }

            Object.defineProperty(this, '$router', {
                get () {
                    return this._root._router
                }
            })

            Object.defineProperty(this, '$route', {
                get () {
                    return this._root._router.history.current
                }
            })
        }
    })
    Vue.component('router-link',{
        props: {
            to: String
        },
        render(h) {
            let mode = this._self._root._router.mode
            let to = mode === 'hash' ? '#'+this.to : this.to
            return h('a', {attrs: {href: to}}, this.$slots.default)
        }
    })
    Vue.component('router-view', {
        render(h) {
            let current = this._self._root._router.history.current
            let routeMap = this._self._root._router.routesMap
            return h(routeMap[current])
        }
    })
}

export default VueRouter
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容