前言
大家好,我是林三心,用最通俗易懂的話,講最難的知識點,相信大家在Vue專案中肯定都用過Vue-router
,也就是路由。所以本文章我就不過多講解vue-router
的基本講解了,我也不給你們講解vue-router
的原始碼,我就帶大家從零開始,實現一個vue-router吧!!!
路由基本使用方法
平時我們們vue-router其實都用很多了,基本每個專案都會用它,因為Vue是單頁面應用,可以通過路由來實現切換元件,達到切換頁面的效果。我們們平時都是這麼用的,其實分為3步
- 1、引入
vue-router
,並使用Vue.use(VueRouter)
- 2、定義路由陣列,並將陣列傳入
VueRouter例項
,並將例項暴露出去 3、將
VueRouter
例項引入到main.js,並註冊到根Vue例項上// src/router/index.js import Vue from 'vue' import VueRouter from 'vue-router' import home from '../components/home.vue' import hello from '../components/hello.vue' import homeChild1 from '../components/home-child1.vue' import homeChild2 from '../components/home-child2.vue' Vue.use(VueRouter) // 第一步 const routes = [ { path: '/home', component: home, children: [ { path: 'child1', component: homeChild1 }, { path: 'child2', component: homeChild2 } ] }, { path: '/hello', component: hello, children: [ { path: 'child1', component: helloChild1 }, { path: 'child2', component: helloChild2 } ] }, ] export default new VueRouter({ routes // 第二步 }) // src/main.js import router from './router' new Vue({ router, // 第三步 render: h => h(App) }).$mount('#app')
router-view和router-link
的分佈
// src/App.vue
<template>
<div id="app">
<router-link to="/home">home的link</router-link>
<span style="margin: 0 10px">|</span>
<router-link to="/hello">hello的link</router-link>
<router-view></router-view>
</div>
</template>
// src/components/home.vue
<template>
<div style="background: green">
<div>home的內容哦嘿嘿</div>
<router-link to="/home/child1">home兒子1</router-link>
<span style="margin: 0 10px">|</span>
<router-link to="/home/child2">home兒子2</router-link>
<router-view></router-view>
</div>
</template>
// src/components/hello.vue
<template>
<div style="background: orange">
<div>hello的內容哦嘿嘿</div>
<router-link to="/hello/child1">hello兒子1</router-link>
<span style="margin: 0 10px">|</span>
<router-link to="/hello/child2">hello兒子2</router-link>
<router-view></router-view>
</div>
</template>
// src/components/home-child1.vue 另外三個子元件大同小異,區別在於文字以及背景顏色不一樣,就不寫出來了
<template>
<div style="background: yellow">我是home的1兒子home-child1</div>
</template>
經過上面這3步,我們們能實現什麼效果呢?
- 1、在網址處輸入對應path,就會展示對應元件
- 2、可以在任何用到的元件裡訪問到
$router和$router
,並使用其身上的方法或屬性 - 3、可以使用
route-link
元件進行路徑跳轉 - 4、可以使用
router-view
元件進行路由對應內容展示
以下是達到的效果動圖
開搞!!!
VueRouter類
在src資料夾中,建立一個my-router.js
VueRouter類的options引數,其實就是new VueRouter(options)
時傳入的這個引數物件,而install
是一個方法,並且必須使VueRouter類
擁有這個方法,為什麼呢?我們們下面會講的。
// src/my-router.js
class VueRouter {
constructor(options) {}
init(app) {}
}
VueRouter.install = (Vue) => {}
export default VueRouter
install方法
為什麼必須定義一個install
方法,並且把他賦予VueRouter
呢?其實這跟Vue.use
方法有關,大家還記得Vue是怎麼使用VueRouter的嗎?
import VueRouter from 'vue-router'
Vue.use(VueRouter) // 第一步
export default new VueRouter({ // 傳入的options
routes // 第二步
})
import router from './router'
new Vue({
router, // 第三步
render: h => h(App)
}).$mount('#app')
其實第二步和第三步很清楚,就是例項一個VueRouter物件,並且將這個VueRouter物件掛到根元件App上,那問題來了,第一步的Vue.use(VueRouter)是幹什麼用的呢?其實Vue.use(XXX)
,就是執行XXX
上的install
方法,也就是Vue.use(VueRouter) === VueRouter.install(),但是到了這,我們們是知道了install
會執行,但是還是不知道install
執行了是幹嘛的,有什麼用?
我們們知道VueRouter物件是被掛到根元件App上了,所以App是能直接使用VueRouter物件上的方法的,但是,我們們知道,我們們肯定是想每一個用到的元件
都能使用VueRouter的方法,比如this.$router.push
,但是現在只有App能用這些方法,咋辦呢?咋才能每個元件都能使用呢?這時install
方法派上用場了,我們們先說說實現思路,再寫程式碼哈。
知識點:Vue.use(XXX)
時,會執行XXX的install方法,並將Vue
當做引數
傳入install
方法
// src/my-router.js
let _Vue
VueRouter.install = (Vue) => {
_Vue = Vue
// 使用Vue.mixin混入每一個元件
Vue.mixin({
// 在每一個元件的beforeCreate生命週期去執行
beforeCreate() {
if (this.$options.router) { // 如果是根元件
// this 是 根元件本身
this._routerRoot = this
// this.$options.router就是掛在根元件上的VueRouter例項
this.$router = this.$options.router
// 執行VueRouter例項上的init方法,初始化
this.$router.init(this)
} else {
// 非根元件,也要把父元件的_routerRoot儲存到自身身上
this._routerRoot = this.$parent && this.$parent._routerRoot
// 子元件也要掛上$router
this.$router = this._routerRoot.$router
}
}
})
}
createRouteMap方法
這個方法是幹嘛的呢?顧名思義,就是將傳進來的routes陣列
轉成一個Map結構
的資料結構,key是path,value是對應的元件資訊,至於為什麼要轉換呢?這個我們們下面會講。我們們先實現轉換。
// src/my-router.js
function createRouteMap(routes) {
const pathList = []
const pathMap = {}
// 對傳進來的routes陣列進行遍歷處理
routes.forEach(route => {
addRouteRecord(route, pathList, pathMap)
})
console.log(pathList)
// ["/home", "/home/child1", "/home/child2", "/hello", "/hello/child1"]
console.log(pathMap)
// {
// /hello: {path: xxx, component: xxx, parent: xxx },
// /hello/child1: {path: xxx, component: xxx, parent: xxx },
// /hello/child2: {path: xxx, component: xxx, parent: xxx },
// /home: {path: xxx, component: xxx, parent: xxx },
// /home/child1: {path: xxx, component: xxx, parent: xxx }
// }
// 將pathList與pathMap返回
return {
pathList,
pathMap
}
}
function addRouteRecord(route, pathList, pathMap, parent) {
const path = parent ? `${parent.path}/${route.path}` : route.path
const { component, children = null } = route
const record = {
path,
component,
parent
}
if (!pathMap[path]) {
pathList.push(path)
pathMap[path] = record
}
if (children) {
// 如果有children,則遞迴執行addRouteRecord
children.forEach(child => addRouteRecord(child, pathList, pathMap, record))
}
}
export default createRouteMap
路由模式
路由有三種模式
- 1、
hash模式
,最常用的模式 - 2、
history模式
,需要後端配合的模式 - 3、
abstract模式
,非瀏覽器環境的模式
而且模式怎麼設定呢?是這麼設定的,通過options的mode
欄位傳進去
export default new VueRouter({
mode: 'hash' // 設定模式
routes
})
而如果不傳的話,預設是hash模式
,也是我們平時開發中用的最多的模式,所以本章節就只實現hash模式
// src/my-router.js
import HashHistory from "./hashHistory"
class VueRouter {
constructor(options) {
this.options = options
// 如果不傳mode,預設為hash
this.mode = options.mode || 'hash'
// 判斷模式是哪種
switch (this.mode) {
case 'hash':
this.history = new HashHistory(this)
break
case 'history':
// this.history = new HTML5History(this, options.base)
break
case 'abstract':
}
}
init(app) { }
}
HashHistory
在src資料夾下建立hashHistory.js
其實hash模式的原理就是,監聽瀏覽器url中hash值的變化,並切換對應的元件
class HashHistory {
constructor(router) {
// 將傳進來的VueRouter例項儲存
this.router = router
// 如果url沒有 # ,自動填充 /#/
ensureSlash()
// 監聽hash變化
this.setupHashLister()
}
// 監聽hash的變化
setupHashLister() {
window.addEventListener('hashchange', () => {
// 傳入當前url的hash,並觸發跳轉
this.transitionTo(window.location.hash.slice(1))
})
}
// 跳轉路由時觸發的函式
transitionTo(location) {
console.log(location) // 每次hash變化都會觸發,可以自己在瀏覽器修改試試
// 比如 http://localhost:8080/#/home/child1 最新hash就是 /home/child1
}
}
// 如果瀏覽器url上沒有#,則自動補充/#/
function ensureSlash() {
if (window.location.hash) {
return
}
window.location.hash = '/'
}
// 這個先不講,後面會用到
function createRoute(record, location) {
const res = []
if (record) {
while (record) {
res.unshift(record)
record = record.parent
}
}
return {
...location,
matched: res
}
}
export default HashHistory
createMmatcher方法
上面講了,每次hash修改,都能獲取到最新的hash值,但是這不是我們們的最終目的,我們們最終目的是根據hash變化渲染不同的元件頁面,那怎麼辦呢?
還記得之前createRouteMap
方法嗎?我們們將routes陣列
轉成了Map
資料結構,有了那個Map,我們們就可以根據hash值去獲取對應的元件並進行渲染
但是這樣真的可以嗎?其實是不行的,如果按照上面的方法,當hash為/home/child1
時,只會渲染home-child1.vue
這一個元件,但這樣肯定是不行的,當hash為/home/child1
時,肯定是渲染home.vue
和home-child1.vue
這兩個元件
所以我們們得寫一個方法,來查詢hash對應哪些元件,這個方法就是createMmatcher
// src/my-router.js
class VueRouter {
// ....原先程式碼
// 根據hash變化獲取對應的所有元件
createMathcer(location) {
// 獲取 pathMap
const { pathMap } = createRouteMap(this.options.routes)
const record = pathMap[location]
const local = {
path: location
}
if (record) {
return createRoute(record, local)
}
return createRoute(null, local)
}
}
// ...原先程式碼
function createRoute(record, location) {
const res = []
if (record) {
while (record) {
res.unshift(record)
record = record.parent
}
}
return {
...location,
matched: res
}
}
// src/hashHistory.js
class HashHistory {
// ...原先程式碼
// 跳轉路由時觸發的函式
transitionTo(location) {
console.log(location)
// 找出所有對應元件,router是VueRouter例項,createMathcer在其身上
let route = this.router.createMathcer(location)
console.log(route)
}
}
這只是保證了hash變化
的時候能找出對應的所有元件來,但是有一點我們忽略了,那就是我們如果手動重新整理頁面的話,是不會觸發hashchange
事件的,也就是找不出元件來,那咋辦呢?重新整理頁面肯定會使路由重新初始化,我們們只需要在初始化函式init
上一開始執行一次原地跳轉就行。
// src/my-router.js
class VueRouter {
// ...原先程式碼
init(app) {
// 初始化時執行一次,保證重新整理能渲染
this.history.transitionTo(window.location.hash.slice(1))
}
// ...原先程式碼
}
響應式的hash改變
上面我們們實現了根據hash值
找出所有需要渲染的元件,但最後的渲染環節卻還沒實現,不過不急,實現渲染之前,我們們先把一件事給完成了,那就是要讓hash值改變
這件事變成一件響應式的事
,為什麼呢?我們們剛剛每次hash變化是能拿到最新的元件合集
,但是沒用啊,Vue的元件重新渲染只能通過某個資料的響應式變化來觸發。所以我們們得搞個變數來儲存這個元件合集
,並且這個變數需要是響應式的才行,這個變數就是$route
,注意要跟$router
區別開來哦!!!但是這個$route
需要用兩個中介變數來獲取,分別是current和_route
這裡可能會有點繞,還望大家有點耐心。我已經把複雜的程式碼最簡單化展示了。
// src/hashHistory.js
class HashHistory {
constructor(router) {
// ...原先程式碼
// 一開始給current賦值初始值
this.current = createRoute(null, {
path: '/'
})
}
// ...原先程式碼
// 跳轉路由時觸發的函式
transitionTo(location) {
// ...原先程式碼
// hash更新時給current賦真實值
this.current = route
}
// 監聽回撥
listen(cb) {
this.cb = cb
}
}
// src/my-router.js
class VueRouter {
// ...原先程式碼
init(app) {
// 把回撥傳進去,確保每次current更改都能順便更改_route觸發響應式
this.history.listen((route) => app._route = route)
// 初始化時執行一次,保證重新整理能渲染
this.history.transitionTo(window.location.hash.slice(1))
}
// ...原先程式碼
}
VueRouter.install = (Vue) => {
_Vue = Vue
// 使用Vue.mixin混入每一個元件
Vue.mixin({
// 在每一個元件的beforeCreate生命週期去執行
beforeCreate() {
if (this.$options.router) { // 如果是根元件
// ...原先程式碼
// 相當於存在_routerRoot上,並且呼叫Vue的defineReactive方法進行響應式處理
Vue.util.defineReactive(this, '_route', this.$router.history.current)
} else {
// ...原先程式碼
}
}
})
// 訪問$route相當於訪問_route
Object.defineProperty(Vue.prototype, '$route', {
get() {
return this._routerRoot._route
}
})
}
router-view元件渲染
其實元件渲染關鍵在於<router-view>
元件,我們們可以自己實現一個<my-view>
在src
下建立view.js
,老規矩,先說說思路,再實現程式碼
// src/view.js
const myView = {
functional: true,
render(h, { parent, data }) {
const { matched } = parent.$route
data.routerView = true // 標識此元件為router-view
let depth = 0 // 深度索引
while(parent) {
// 如果有父元件且父元件為router-view 說明索引需要加1
if (parent.$vnode && parent.$vnode.data.routerView) {
depth++
}
parent = parent.$parent
}
const record = matched[depth]
if (!record) {
return h()
}
const component = record.component
// 使用render的h函式進行渲染元件
return h(component, data)
}
}
export default myView
router-link跳轉
其實他的本質就是個a標籤而已
在src
下建立link.js
const myLink = {
props: {
to: {
type: String,
required: true,
},
},
// 渲染
render(h) {
// 使用render的h函式渲染
return h(
// 標籤名
'a',
// 標籤屬性
{
domProps: {
href: '#' + this.to,
},
},
// 插槽內容
[this.$slots.default]
)
},
}
export default myLink
最終效果
最後把router/index.js裡的引入改一下
import VueRouter from '../Router-source/index2'
然後把所有router-view和router-link
全都替換成my-view和my-link
效果
結語
如果你覺得此文對你有一丁點幫助,點個贊,鼓勵一下林三心哈哈。或者可以加入我的摸魚群
想進學習群,摸魚群,請點選這裡[摸魚](
https://juejin.cn/pin/6969565...),我會定時直播模擬面試,答疑解惑
完整程式碼
/src/my-router.js
import HashHistory from "./hashHistory"
class VueRouter {
constructor(options) {
this.options = options
// 如果不傳mode,預設為hash
this.mode = options.mode || 'hash'
// 判斷模式是哪種
switch (this.mode) {
case 'hash':
this.history = new HashHistory(this)
break
case 'history':
// this.history = new HTML5History(this, options.base)
break
case 'abstract':
}
}
init(app) {
this.history.listen((route) => app._route = route)
// 初始化時執行一次,保證重新整理能渲染
this.history.transitionTo(window.location.hash.slice(1))
}
// 根據hash變化獲取對應的所有元件
createMathcer(location) {
const { pathMap } = createRouteMap(this.options.routes)
const record = pathMap[location]
const local = {
path: location
}
if (record) {
return createRoute(record, local)
}
return createRoute(null, local)
}
}
let _Vue
VueRouter.install = (Vue) => {
_Vue = Vue
// 使用Vue.mixin混入每一個元件
Vue.mixin({
// 在每一個元件的beforeCreate生命週期去執行
beforeCreate() {
if (this.$options.router) { // 如果是根元件
// this 是 根元件本身
this._routerRoot = this
// this.$options.router就是掛在根元件上的VueRouter例項
this.$router = this.$options.router
// 執行VueRouter例項上的init方法,初始化
this.$router.init(this)
// 相當於存在_routerRoot上,並且呼叫Vue的defineReactive方法進行響應式處理
Vue.util.defineReactive(this, '_route', this.$router.history.current)
} else {
// 非根元件,也要把父元件的_routerRoot儲存到自身身上
this._routerRoot = this.$parent && this.$parent._routerRoot
// 子元件也要掛上$router
this.$router = this._routerRoot.$router
}
}
})
Object.defineProperty(Vue.prototype, '$route', {
get() {
return this._routerRoot._route
}
})
}
function createRouteMap(routes) {
const pathList = []
const pathMap = {}
// 對傳進來的routes陣列進行遍歷處理
routes.forEach(route => {
addRouteRecord(route, pathList, pathMap)
})
console.log(pathList)
// ["/home", "/home/child1", "/home/child2", "/hello", "/hello/child1"]
console.log(pathMap)
// {
// /hello: {path: xxx, component: xxx, parent: xxx },
// /hello/child1: {path: xxx, component: xxx, parent: xxx },
// /hello/child2: {path: xxx, component: xxx, parent: xxx },
// /home: {path: xxx, component: xxx, parent: xxx },
// /home/child1: {path: xxx, component: xxx, parent: xxx }
// }
// 將pathList與pathMap返回
return {
pathList,
pathMap
}
}
function addRouteRecord(route, pathList, pathMap, parent) {
// 拼接path
const path = parent ? `${parent.path}/${route.path}` : route.path
const { component, children = null } = route
const record = {
path,
component,
parent
}
if (!pathMap[path]) {
pathList.push(path)
pathMap[path] = record
}
if (children) {
// 如果有children,則遞迴執行addRouteRecord
children.forEach(child => addRouteRecord(child, pathList, pathMap, record))
}
}
function createRoute(record, location) {
const res = []
if (record) {
while (record) {
res.unshift(record)
record = record.parent
}
}
return {
...location,
matched: res
}
}
export default VueRouter
src/hashHistory.js
class HashHistory {
constructor(router) {
// 將傳進來的VueRouter例項儲存
this.router = router
// 一開始給current賦值初始值
this.current = createRoute(null, {
path: '/'
})
// 如果url沒有 # ,自動填充 /#/
ensureSlash()
// 監聽hash變化
this.setupHashLister()
}
// 監聽hash的變化
setupHashLister() {
window.addEventListener('hashchange', () => {
// 傳入當前url的hash
this.transitionTo(window.location.hash.slice(1))
})
}
// 跳轉路由時觸發的函式
transitionTo(location) {
console.log(location)
// 找出所有對應元件
let route = this.router.createMathcer(location)
console.log(route)
// hash更新時給current賦真實值
this.current = route
// 同時更新_route
this.cb && this.cb(route)
}
// 監聽回撥
listen(cb) {
this.cb = cb
}
}
// 如果瀏覽器url上沒有#,則自動補充/#/
function ensureSlash() {
if (window.location.hash) {
return
}
window.location.hash = '/'
}
export function createRoute(record, location) {
const res = []
if (record) {
while (record) {
res.unshift(record)
record = record.parent
}
}
return {
...location,
matched: res
}
}
export default HashHistory
src/view.js
const myView = {
functional: true,
render(h, { parent, data }) {
const { matched } = parent.$route
data.routerView = true // 標識此元件為router-view
let depth = 0 // 深度索引
while(parent) {
// 如果有父元件且父元件為router-view 說明索引需要加1
if (parent.$vnode && parent.$vnode.data.routerView) {
depth++
}
parent = parent.$parent
}
const record = matched[depth]
if (!record) {
return h()
}
const component = record.component
// 使用render的h函式進行渲染元件
return h(component, data)
}
}
export default myView
src/link.js
const myLink = {
props: {
to: {
type: String,
required: true,
},
},
// 渲染
render(h) {
// 使用render的h函式渲染
return h(
// 標籤名
'a',
// 標籤屬性
{
domProps: {
href: '#' + this.to,
},
},
// 插槽內容
[this.$slots.default]
)
},
}
export default myLink
結語
有人可能覺得沒必要,但是嚴格要求自己其實是很有必要的,平時嚴格要求自己,才能做到每到一個公司都能更好的做到向下相容難度。
如果你覺得此文對你有一丁點幫助,點個贊,鼓勵一下林三心哈哈。
如果你想一起學習前端或者摸魚,那你可以加我,加入我的摸魚學習群