Vue Router 基礎
讓我們先來了解下Vue Router的簡單使用吧,先了解怎么使用,之后再去想辦法怎么去實現
1.簡介
路由:本質上是一種對應關系
分類分為前端路由和后端路由
后端路由
比如node.js 的路由是 URL的請求地址和服務器上面的資源對應,根據不同的請求地址返回不同的資源
前端路由
在SPA(單頁應用)中根據用戶所觸發的事件改變了URL 在無需刷新的前提下 顯示不同的頁面內容,比如等下就要講的Vue Router
2.Vue-Router最基礎的使用步驟
2.1.引入Vue-Router文件
<!-- 使用vue router前提 vue 必不可少 --><script src=nnzzn/skin/m04blueskin/image/nopic.gif 引入vue-router文件 --><script src=nnzzn/skin/m04blueskin/image/nopic.gif>
2.2.在頁面上添加 router-link 和 router-view
<!-- 添加路由 --><!-- 會被渲染為 <a href="#/home"></a> --><router-link to="/home">Home</router-link><router-link to="/login">Login</router-link><!-- 展示路由的內容 --><router-view></router-view>
2.3.創建路由組件
//創建路由組件const home = { template: `<div>歡迎來到{{name}}</div>`, data() { return { name: '首頁', } },}const login = { template: ` <div>歡迎來到登錄頁</div>`,}
2.4.配置路由規則
// 配置路由規則const router = new VueRouter({ routes: [ //每一個路由規則都是一個對象 //path 路由的 hash地址 //component 路由的所展示的組件 { path: '/', // 當訪問 '/'的時候 路由重定向 到新的地址 '/home' redirect: '/home', }, { path: '/home', component: home, }, { path: '/login', component: login, }, ],})
2.5.掛載路由
let vm = new Vue({ el: '#app', data: {}, methods: {}, // 掛載到vue 上面 router, })
3.嵌套路由
這里的嵌套路由是基于上面的例子繼續寫的
3.1.在路由里面添加 子路由鏈接和 占位符
//創建路由組件const home = { template: ` <div> 歡迎來到首頁 <br> <!-- 子路由鏈接 --> <router-link to="/tab1">Tab1</router-link> <router-link to="/tab2">Tab2</router-link> <!-- 子路由展示 --> <router-view></router-view> </div>}復制代碼
3.2.添加路由組件
// 創建兩個子路由組件const tab1 = { template: ` <div> 子路由1 </div> `,}const tab2 = { template: ` <div> 子路由2 </div> `,}復制代碼
3.3.配置路由規則
// 配置路由規則const router = new VueRouter({ routes: [ { path: '/home', component: home, //children 表示子路由規則 children: [ { path: '/tab1', component: tab1 }, { path: '/tab2', component: tab2 }, ], }, ],})復制代碼
4.動態路由
path屬性加上/:id 使用route對象的params.id獲取動態參數
比如現在有這么多個路由,如果自己也配置多個路由,豈不是有點。。。多余
<div id="app"> <!-- 添加路由 --> <!-- 會被渲染為 <a href="#/home"></a> --> <router-link to="/goods/1">goods1</router-link> <router-link to="/goods/2">goods2</router-link> <router-link to="/goods/3">goods3</router-link> <router-link to="/goods/4">goods4</router-link> <!-- 展示路由的內容 --> <router-view></router-view></div>
然后這里就可以使用 動態路由來解決
<script> //創建路由組件 const goods = { // this.$route.parms.id 可以省略 this template: ` <div>歡迎來到商品 {{$route.params.id}}頁</div> `, } // 配置路由規則 const router = new VueRouter({ routes: [ { // 加上`/:id` path: '/goods/:id', component: goods, }, ], }) let vm = new Vue({ el: '#app', data: {}, methods: {}, // 掛載到vue 上面 router, })</script>
最后提一下還可以用query進行傳參.
// 比如<router-link to="/goods?id=1">goods</router-link>復制代碼
然后使用this.$route.query.id就可以在路由組件中獲取到id
添加動態路由
使用 this.$router.addRoutes([]) 可以添加動態路由,里面傳遞是一個數組 和 routes里面一樣
5.路由傳參
我們可以使用 props 進行傳值
為啥要用 props 進行傳值,route不香了嗎,確實route 不夠靈活
props 值有三種情況
5.1.布爾值類型
//創建路由組件const goods = { // 使用props接收 props: ['id'], template: ` <div>歡迎來到商品 {{id}}頁</div> `,}// 配置路由規則const router = new VueRouter({ routes: [ { path: '/goods/:id', component: goods, //props為true, route.params將會被設置為組件屬性 props: true, }, ],})復制代碼
5.2.對象類型
但是這里就獲取不到 id 了,會報錯
這里的id 需要 $route.params.id 獲取
const goods = { // 使用props接收 props: ['name', 'info', 'id'], // 這里的 id 是獲取不到的 template: ` <div>{{info}}來到{{name}} {{id}}頁</div> `,}// 配置路由規則const router = new VueRouter({ routes: [ { path: '/goods/:id', component: goods, //props為對象 就會把這個對象傳遞的路由組件 //路由組件使用props接收 props: { name: '商品', info: '歡迎', }, }, ],})復制代碼
5.3.函數
const goods = { // 使用props接收 props: ['name', 'info', 'id'], template: ` <div>{{info}}來到{{name}} {{id}}頁</div> `,}// 配置路由規則const router = new VueRouter({ routes: [ { path: '/goods/:id', component: goods, //prop是一個函數的話 就可以組合傳值 props: (route) => { return { name: '商品', info: '歡迎', id: route.params.id, } }, }, ],})復制代碼
6.route 和 router
在上面提到了route 那么和 router有什么區別呢
7.命名路由
路由組件
//創建路由組件const goods = { // 使用props接收 props: ['id'], template: ` <div>商品{{id}}頁</div> `,}復制代碼
路由配置
//配置路由const router = new VueRouter({ routes: [ { path: '/goods/:id', // 命名路由 name: 'goods', component: goods, }, ],})復制代碼
綁定 :to 通過name找到定義的路由 還可以使用 params 傳遞參數
<router-link :to="{name: 'goods', params: { id: 1 } }">goods1</router-link><!-- 展示路由的內容 --><router-view></router-view>復制代碼
8.編程式導航
8.1.聲明式導航
既然提到了編程式導航,那么先簡單說下聲明式導航
上面所展示的都是聲明是導航 比如router-link
<router-link to="/goods/1">goods1</router-link>
還有a標簽
<a href="#/goods/1">goods1</a>
8.2.編程式導航
使用javascript來控制路由跳轉
在普通的網頁中使用 loaction.href window.open 等等進行跳轉
現在我要介紹的是Vue Router中的編程式導航
我們平時都是用router.push() **router.go(n)**方法進行跳轉
//字符串this.$router.push('/home')//對象this.$ruter.push({path:'/home'})//比如這個 /goods?id=1this.$router.push({path:'/goods',query:{id:'1'}})//命名路由 /goods/1this.$router.push({name:'goods',params:{id:1}})//后退this.$router.go(-1)復制代碼
9.路由守衛
9.1.全局守衛
router.beforeEach 全局守衛 對所有的路由都起作用
router.beforeEach((to, from, next) => { next();//使用時,千萬不能漏寫next!!! }).catch(()=>{ //跳轉失敗頁面 next({ path: '/error', replace: true, query: { back: false }} )})復制代碼
全局的守衛的三個參數
to: 即將要進入的目標 路由對象
from: 當前導航正要離開 路由對象
next: 參數不同做的事也不同
next() 直接進入下一個鉤子
next(false) 停止當前導航
next('/路徑') 跳轉到path路由地址 當然這里面也可以寫成對象形式 next({path : '/路徑'}) next(error): 如果傳入參數是一個 Error 實例,則導航會被終止且該錯誤會被傳遞給 router.onError()
9.2.路由獨享的守衛
beforeEnter 路由對象獨享的守衛寫在routes里面
const router = new VueRouter({ routes: [ { path: '/goods', component: goods, beforeEnter: (to, from, next) => { // 一樣的用法 } } ]})復制代碼
9.3.組件內的守衛(了解)
組件內的守衛 寫在組件內部 下面是官方介紹
const goods = { template: `<div>goods</div>`, beforeRouteEnter (to, from, next) { // 具體邏輯 }, beforeRouteUpdate (to, from, next) { // 具體邏輯 }, beforeRouteLeave (to, from, next) { // 具體邏輯 }}復制代碼
10.組件緩存keep-alive
頁面重新加載會重新渲染頁面比如回退的時候等等,我們有的組件它不是一個活動的(數據不變)不希望它被重新渲染,所以這里就可以使用 <keep-alive> </keep-alive> 包裹起來,這樣就不會觸發created鉤子
應用場景:獲取一個商品的詳情然后回退在前進的時候就使用緩存,提高性能
10.1.不使用 keep-alive例子
這里home 組件在created進行打印當前的時間
<div id="app"> <router-link to="/home">home</router-link><router-link to="/login">login</router-link><router-view></router-view></div>復制代碼
<script> const login = { template: ` <div>Login</div> `, } const home = { template: ` <div>Home</div> `, created() { console.log(new Date()) }, } const router = new VueRouter({ routes: [ { path: '/', redirect: '/home', }, { path: '/home', component: home, }, { path: '/login', component: login, }, ], }) let vm = new Vue({ el: '#app', data: {}, methods: {}, router, }) </script>復制代碼
如上,每切換home 的路由 組件就會重新渲染,打印當前的時間
如果使用 keep-alive 會有什么效果呢
10.2.使用keep-alive
這里只需簡單的包裹起來就行了
<div id="app"> <router-link to="/home">home</router-link> <router-link to="/login">login</router-link> <keep-alive> <router-view></router-view> </keep-alive></div>復制代碼
可以看到的是只打印一次,說明切換了路由它并沒有重新渲染組件
當然可以在 組件內取個name名字 keep-alive 標簽里面添加 include 屬性就可以對相應的組件進行緩存
const login = { name: login, template: ` <div>Login</div> `,}const home = { name: home, template: ` <div>Home</div> `, created() { console.log(new Date()) },}復制代碼
<div id="app"> <router-link to="/home">home</router-link> <router-link to="/login">login</router-link> <keep-alive include="login,home"> <router-view></router-view> </keep-alive></div>復制代碼
10.3.activated 和 deactivated
keep-alive 生命周期執行順序
第一次訪問路由時:
以后進入只會觸發 activated
11.hash 和 history 模式
11.1.hash模式
在vue-router中默認使用的是 hash 模式
hash是url中的錨點就是**#,通過錨點作為路由地址,我們通常改變的是改變#**后面部分,實現瀏覽器渲染指定的組件.,錨點發生改變會觸發 onhashchange 事件
11.2.history模式
history 模式就是平時正常的地址,使用方面需要服務器支持
如果訪問的路徑資源沒有 直接就是 404
在HTML5后新增了兩個API
pushState(): IE10后支持
replaceState()
在vue-router中如果要使用 history 模式需要指定
const router = new VueRouter({ mode: 'history'})復制代碼
實現一個基礎 Vue Router
復習上面的路由的基礎那么我們不如來寫個Vue Router吧
實現的這個 Vue Router是基于 history模式
所有的步驟都放到代碼的注釋中,每一行都寫個注釋
這個簡單的沒有按照Vue Router源碼來寫主要是一些基礎功能的實現
為后面的按照源碼寫打基礎
1.注冊全局Vue Router
首先就是先注冊自己的 Vue Router
判斷是否注冊了組件
在Vue實例創建完成進行注冊
// 保存一個全局變量 Vuelet _Vue = null// 默認導出自己寫的 VueRouterexport default class MyVueRouter { // 實現install 注冊 MyVueRouter vue提供install可供我們開發新的插件及全局注冊組件等 // 把Vue傳進去 static install(Vue) { // 定義一個標識判斷是否注冊了 MyVueRouter ,注冊了就不用下一步了 if (MyVueRouter.install.installed) return // 沒有就進行下面的,把標識改變true MyVueRouter.install.installed = true // 把全局變量 _Vue 保存 _Vue = Vue // 為了獲取Vue中的this執行這里使用 混入 _Vue.mixin({ // 在Vue實例創建好的時候進行操做 beforeCreate() { // 判斷是否是實例創建還是組件創建 ,可以判斷是否掛載 了router if (this.$options.router) { // 把router注冊到 _Vue上 _Vue.prototype.$router = this.$options.router } }, }) }}復制代碼
2.實現 構造方法
optoins 保存傳入的規則
routerMap 確定地址和組件的關系
current 表示當前的地址是響應式的之后渲染組件和它相關
export default class MyVueRouter { ... //實現構造 constructor(optoins) { // 這個保存的是 routes this.optoins = optoins // routerMap 保存路由和 組件之間的關系 this.routerMap = {} // 用來記錄數據 這里面的數據都是 響應式 this.data = _Vue.observable({ // 當前路由的地址 current: '/', }) }}復制代碼
3.解析路由規則
傳入的路由規則拿到一個對象里 地址 和 組件一一匹配
export default class MyVueRouter { ... // 解析路由規則 createRouterMap() { // 把之前構造函數的中的傳入的 routes 規則進行遍歷 this.optoins.routes.forEach((item) => { // 把路由 和 組件的對應關系添加到 routerMap中 this.routerMap[item.path] = itemponent }) }}復制代碼
4.實現 router-link 組件
router-link就是頁面上所展示的路由鏈接
因為一般使用的基本都是運行版的Vue 所以自己把組件轉為 虛擬DOM
還有就是鏈接會刷新的問題
自己寫個函數進行跳轉阻止默認事件
還得注意對應的路由所要渲染的組件
export default class MyVueRouter { ... // 實現組件 initComponents(Vue) { // 實現 router-link組件 Vueponent('router-link', { props: { // router-link上面的to屬性將訪問的地址 to: String, }, // 由于運行版的Vue不能渲染template所以這里重新寫個render 這里h 也是個函數 // template: `<a :href="to"><slot></slot></a>`, render(h) { // 第一個參數是標簽 return h( 'a', // 第二個參數是對象是 tag 里面的屬性 { // 設置屬性 attrs: { href: this.to, }, // 綁定事件 on: { // 重新復寫點擊事件,不寫的話會點擊會向服務器發送請求刷新頁面 click: this.myClick, }, }, // 這個是標簽里面的內容 這里渲染是 默認插槽 [this.$slots.default] ) }, methods: { //router-link的點擊事件 myClick(e) { // 因為我這里是模擬是 history的路由所以用pushState ,hash路由可以這里用 push // 使用history修改瀏覽器上面的地址 // pushState 第一個參數是傳遞的參數,第二個是標題,第三個是鏈接 history.pushState({}, '', this.to) // 渲染相應的組件 // 渲染的頁面也需要改變 data中的current是響應式的 router-view是根據current來渲染的 this.$router.data.current = this.to // 阻止默認跳轉事件 e.preventDefault() }, }, })復制代碼
5.實現 router-view 組件
這里從之前解析的規則里面拿到當前的對應的組件進行轉為虛擬DOM
最后router-view占位渲染到頁面上
export default class MyVueRouter { ... // 實現組件 initComponents(Vue) { // 實現 router-view組件 Vueponent('router-view', { render(h) { // 獲取的當前路徑所對應的組件 // 因為當前this是Vue,this.$router才是MyVueRouter const component = this.$router.routerMap[this.$router.data.current] // 轉化為虛擬Dom return h(component) }, }) }}復制代碼
6.前進和后退
在完成之前的編寫還是不夠的,因為在瀏覽器點后退和前進雖然改變了瀏覽器的地址,但是組件卻沒有刷新,下面就來解決這個問題
export default class MyVueRouter { ... // 初始化事件 initEvent() { // 監聽瀏覽器地址的改變 window.addEventListener('popstate', () => { // 改變VueRouter的當前的地址 重新渲染組件 this.data.current = window.location.pathname }) }}復制代碼
7.在router掛載后進行初始化
最后寫個函數進行初始化
在router注冊到Vue之后進行 初始化
export default class MyVueRouter { // 初始化 init() { // 解析路由規則 this.createRouterMap() // 初始化組件 this.initComponents(_Vue) // 初始化事件 this.initEvent() } static install(Vue) { if (MyVueRouter.install.installed) return MyVueRouter.install.installed = true _Vue = Vue _Vue.mixin({ beforeCreate() { if (this.$options.router) { _Vue.prototype.$router = this.$options.router // 注冊完router后進行初始化 this.$options.router.init() } }, }) } ...}復制代碼
8.放上完整的 index.js
// 保存一個全局變量 Vuelet _Vue = nullexport default class MyVueRouter { // 實現install 注冊 MyVueRouter vue提供install可供我們開發新的插件及全局注冊組件等 // 把Vue傳進去 static install(Vue) { // 定義一個標識判斷是否注冊了 MyVueRouter ,注冊了就不用下一步了 if (MyVueRouter.install.installed) return // 沒有就進行下面的,把標識改變true MyVueRouter.install.installed = true // 把全局變量 _Vue 保存 _Vue = Vue // 為了獲取Vue中的this執行這里使用 混入 _Vue.mixin({ // 在Vue實例創建好的時候進行操做 beforeCreate() { // 判斷是否是實例創建還是組件創建 ,可以判斷是否掛載 了router if (this.$options.router) { // 把router注冊到 _Vue上 _Vue.prototype.$router = this.$options.router // 注冊完router后進行初始化 this.$options.router.init() } }, }) // 判斷是否掛載 } // 實現構造方法 constructor(optoins) { // 這個保存的是 routes this.optoins = optoins // routerMap 保存路由和 組件之間的關系 this.routerMap = {} // 用來記錄數據 這里面的數據都是 響應式 this.data = _Vue.observable({ // 當前路由的地址 current: '/', }) } // 解析路由規則 createRouterMap() { // 把之前構造函數的中的傳入的 routes 規則進行遍歷 this.optoins.routes.forEach((item) => { // routes中的每一項都是一個對象 { path: '/XXX', component: XXX} // 把路由 和 組件的對應關系添加到 routerMap中 this.routerMap[item.path] = itemponent }) } // 實現組件 initComponents(Vue) { // 實現 router-link組件 Vueponent('router-link', { props: { // router-link上面的to屬性將訪問的地址 to: String, }, // 由于運行版的Vue不能渲染template所以這里重新寫個render 這里h 也是個函數 // template: `<a :href="to"><slot></slot></a>`, render(h) { // 第一個參數是標簽 return h( 'a', // 第二個參數是對象是 tag 里面的屬性 { // 設置屬性 attrs: { href: this.to, }, // 綁定事件 on: { // 重新復寫點擊事件,不寫的話會點擊會向服務器發送請求刷新頁面 click: this.myClick, }, }, // 這個是標簽里面的內容 這里渲染是 默認插槽 // 比如<router-link to="/">首頁</router-link> // 插槽就是給首頁兩個字留位置,當前這只是個例子 [this.$slots.default] ) }, methods: { //router-link的點擊事件 myClick(e) { // 因為我這里是模擬是 history的路由所以用pushState ,hash路由可以這里用 push // 使用history修改瀏覽器上面的地址 // pushState 第一個參數是傳遞的參數,第二個是標題,第三個是鏈接 history.pushState({}, '', this.to) // 渲染相應的組件 // 渲染的頁面也需要改變 data中的current是響應式的 router-view是根據current來渲染的 this.$router.data.current = this.to // 阻止默認跳轉事件 e.preventDefault() }, }, }) // 實現 router-view組件 Vueponent('router-view', { render(h) { // 獲取的當前路徑所對應的組件 // 因為當前this是Vue,this.$router才是MyVueRouter const component = this.$router.routerMap[this.$router.data.current] // 轉化為虛擬Dom return h(component) }, }) } // 初始化事件 initEvent() { // 監聽瀏覽器地址的改變 window.addEventListener('popstate', () => { // 改變VueRouter的當前的地址 重新渲染組件 this.data.current = window.location.pathname }) } // 初始化 init() { // 解析路由規則 this.createRouterMap() // 初始化組件 this.initComponents(_Vue) // 初始化事件 this.initEvent() }}復制代碼
到了這里基礎的實現功能差不多了,上面的例子是為了下面打基礎,所有的功能實現基本都是在一個文件下很不嚴謹,下面就嚴格按照Vue Router 源碼來實現自己 Vue Router
Vue Router實現
經過上面簡單的實現,現在我們按照Vue Router源碼的方式進行編寫
1.首先是Vue Router 構造
// 導出自己寫的 VueRouterexport default class VueRouter { // 實現構造函數功能 constructor(options) { // 獲取options中的routes路由規則 沒有就為空數組 this._options = options.routes || [] } // 初始化 init(Vue) {}}復制代碼
2.注冊組件 install
在 install.js 對自己寫的Vue-Router進行全局的注冊
之后還會在這里創建 router????router** **router????route
還有注冊 router-link router-view
// 定義一個全局 的Vueexport let _Vue = null// 導出 install方法export default function install(Vue) { // 保存到全局的Vue _Vue = Vue // 混入 _Vue.mixin({ // Vue實例創建完畢之后操做 beforeCreate() { // 這里是new Vue if (this.$options.router) { // 保存 Vue this._routerRoot = this // 保存 Vue Router 的實例,以后可以通過Vue Router構造的一些方法 this._router = this.$options.router // 調用Vue Router的init(Vue) 初始化操做 this._router.init(this) } else { // 這里是創建 Vue的組件等等 // 判斷是否有父組件 ,有的話就把父組件的 _roterRoot(也就是Vue)給 子組件 // 沒有父組件就把 this 這是也是(Vue) 給子組件 this._routerRoot = (this.$parent && this.$parent._routerRoot) || this } }, })}復制代碼
然后在 index.js中導入install 進行為構造添加 install
// 導入 installimport install from './install'// 導出自己寫的 VueRouterexport default class VueRouter {...} // 為VueRouter 添加 install方法VueRouter.install = install復制代碼
3.編寫 create-route-map.js
這個主要的作用就是用來解析傳遞過來的路由 需要導出然后在 create-matcher.js進行使用
具體的細節都寫了注釋
// 導出具體的路由解析export default function createRouteMap(routes, oldPathList, oldPathMap) { // 傳入了就是添加動態路由 沒有傳入就默認為空 const pathList = oldPathList || [] const pathMap = oldPathMap || [] // 遍歷規則操作 routes.forEach((route) => { // 記錄路由 也是核心的解析路由 為了分工明確寫的外面 addRouteRecord(route, pathList, pathMap) }) // 返回新的路由列表 和 路由對應關系 return { pathList, pathMap, }}function addRouteRecord(route, pathList, pathMap, parentRecord) { // 路由地址 判斷是否存在父級的路由 有的話拼接父級路由和當前路由的path 沒有就是當前route.path const path = parentRecord ? `${parentRecord.path}/${route.path}` : route.path // record作為一個路由記錄 記錄了路由地址,組件,父級路由 用于路由對應關系去對應相對應的path const record = { path, component: routeponent, parent: parentRecord, } // 判斷是否在路由列表中 存在當前路由,不存在進行添加當前路由,更新路由列表 if (!pathList[path]) { // 向路由列表中添加路由 pathList.push(path) // 向路由對應關系中 添加path 相對應的記錄 pathMap[path] = record } // 判斷當前的 路由是否有子路由,有的話進行遞歸 if (route.children) { route.children.forEach((childRoute) => { // 就簡單說下最后一個參數 就是父級路由記錄 addRouteRecord(childRoute, pathList, pathMap, record) }) }}復制代碼
4.編寫 create-matcher.js
這個模塊的意義也是解析路由不過這個是個指揮家,上面實現的是具體解析操作
在這個模塊里進行調用上面的具體解析路由的方法就行了
有了上面面具體的路由解析,這個create-matcher.js就容易實現了,只需要簡單的調用它即可
這個模塊返回了兩個方法
match : 根據路由路徑創建路由規則對象,之后就可以通過 規則對象獲取到所有的路由信息然后拿到所有的組件進行創建
addRoutes : 添加動態路由
// 導入具體的路由解析規則import createRouteMap from './create-route-map'// 導出解析路由規則 傳入的是規則export default function createMatcher(router) { // pathList 路由的列表 pathMap 路由與組件的對應關系 nameMap這里沒有考慮,先完成個簡單的 // 具體的解析規則是使用 createRouteMap const { pathList, pathMap } = createRouteMap(router) // match是 從pathMap 根據path獲取 相應的路由記錄 function match(path) { //待實現 } // 添加動態路由 function addRoutes(router) { // 添加動態路由肯定也要解析路由規則 createRouteMap(router, pathList, pathMap) } // 返回match 和 addRoutes return { match, addRoutes, }}復制代碼
然后在index.js也就是Vue Router的構造中使用 createMatcher. 使用this.matcher接收
// 導入 installimport install from './install'// 導入解析路由import createMatcher from './create-matcher'// 導出自己寫的 VueRouterexport default class VueRouter { // 實現構造函數功能 constructor(options) { // 獲取options中的routes路由規則 沒有就為空數組 this._routes = options.routes || [] // 解析路由 傳入規則 這里還返回了兩個方法 match,addRoutes 用matcher接收一下之后有用 this.matcher = createMatcher(this._routes) } // 初始化 init(Vue) {}}// 為VueRouter 添加 install方法VueRouter.install = install復制代碼
5.編寫 createMatcher
看見上面在 createMatcher中定義了 一個match了嗎,
match是 從pathMap 根據path獲取 相應的路由記錄
上面還沒有去實現,現在來實現它
需要實現它的話還需要編寫個 createRoute 方法,我這里寫在 uitl/route.js模塊里
// 導出 createRouteexport default function createRoute(record, path) { // 保存路由的記錄 里面可能有多個路由 是這種模式保存 [parentRecord,childRecord] const matched = [] // 判斷是否是子路由 // 下面 record = record.parent 在不斷向上找parent有繼續執行 // 沒有就直接return 下面的對象 while (record) { // 循環得到的 record不斷插入到 數組的最前面 matched.unshift(record) // 把父記錄給當前record 繼續循環 record = record.parent } // 返回path 和 matched 以便之后 router-view渲染 return { path, matched, }}復制代碼
上面編寫了 createRoute方法我們就可以在 create-mathcer.js 調用 來獲取到記錄了
然后再 create-mathcer.js中繼續 完善 match方法
// 導入具體的路由解析規則import createRouteMap from './create-route-map'// 導入 createRouteimport createRoute from './util/route'// 導出解析路由規則 傳入的是規則export default function createMatcher(router) { // pathList 路由的列表 pathMap 路由與組件的對應關系 nameMap這里沒有考慮,先完成個簡單的 // 具體的解析規則是使用 createRouteMap const { pathList, pathMap } = createRouteMap(router) // match是 從pathMap 根據path獲取 相應的路由記錄 function match(path) { // 取出path對應的記錄 const record = pathMap[path] // 判斷記錄是否存在 if (record) { return createRoute(record, path) } return createRoute(null, path) } // 添加動態路由 function addRoutes(router) { // 添加動態路由肯定也要解析路由規則 createRouteMap(router, pathList, pathMap) } // 返回match 和 addRoutes return { match, addRoutes, }}復制代碼
6.歷史記錄的處理 History
在 history目錄下新建一個 base模塊用來編寫 父類
這個父類有 hash 模式 和 history(html5) 模式共同的方法
這里就主要演示下 hash 模式的代碼
// 導入 我們上面寫好的 createRouteimport createRoute from '../util/route'// 導出 Historyexport default class History { // router 是路由對象 也就是 VUe-Router的一個實例 constructor(router) { // 賦值給自己的 router this.router = router // 默認的的當前路徑為 / this.current = createRoute(null, '/') } // 將要跳轉的鏈接 // path 是路由的地址, onComplete是一個回調 transitionTo(path, onComplete) { // 獲取當前的應該跳轉的路由 調用的是 Vue-Router中 this.matcher中收到的match方法 // 在這里 this.router就是 Vue-Router的一個實例 所以寫成 // this.router.matcher.match(path) this.current = this.router.matcher.match(path) // 回調存在觸發回調 onComplete && onComplete() }}復制代碼
編寫 HashHistory 模式 繼承 History
// 導入 base中的 Historyimport History from './base'// 繼承了 Historyexport default class HashHistory extends History { constructor(router) { super(router) // 確保第一次訪問的時候路由加上 #/ ensuerSlash() } // 監聽URL的改變 設置當前的current setUpListener() { // 監聽 hash的變化 window.addEventListener('hashchange', () => { // 改變 this.current this.transitionTo(this.getCurrentLocation()) }) } // 獲取當前的URL的hash 當然這里要去除 # getCurrentLocation() { // 這里不建議寫成這個 return window.location.hash.slice(1) 有兼容問題 let href = window.location.href const index = href.indexOf('#') // 當沒有 #的時候 直接返回 空字符串 if (index < 0) return '' // 獲取 #后面的地址 href = href.slice(index + 1) return href }}// 確保第一次加上 #/function ensuerSlash() { // 如果存在 hash的話就不行加 / if (window.location.hash) { return } // 如果沒有hash值 只要給 hash 加上一個 / 它會自動加上 /#/ window.location.hash = '/'}復制代碼
關于 html5模式 這里 就沒寫了
然后回到 index.js 就是自己寫的 Vue Router中繼續編寫模式判斷
最后就是 初始化 init方法
// 導入 installimport install from './install'// 導入解析路由import createMatcher from './create-matcher'// 導入 HashHistoryimport HashHistory from './history/hash'// 導入 HTML5Historyimport HTML5History from './history/html5'// 導出自己寫的 VueRouterexport default class VueRouter { // 實現構造函數功能 constructor(options) { // 獲取options中的routes路由規則 沒有就為空數組 this._routes = options.routes || [] // 解析路由 傳入規則 這里還返回了兩個方法 match,addRoutes 用matcher接收一下之后有用 this.matcher = createMatcher(this._routes) // 獲取模式 沒有就默認為 hash 模式 this.mode = options.mode || 'hash' // 使用 if 或者 分支都行 根據不同的模式執行不同的路由跳轉功能等等 switch (this.mode) { case 'history': this.history = new HTML5History(this) break case 'hash': // 模式的實例使用 this.history接收等下用的上 // 傳入的this是 VueRouter this.history = new HashHistory(this) break default: throw new Error('該模式不存在') } } // 初始化 init(Vue) { // 拿到模式的實例 const history = this.history // 進行跳轉 第一個參數是path ,第二個是回調函數 history.transitionTo(history.getCurrentLocation, () => // 監聽URL的改變 設置當前的 this.current history.setUpListener() ) }}// 為VueRouter 添加 install方法VueRouter.install = install復制代碼
7.定義一個響應值 _route
渲染不同路由頁面有個前提的就是需要一個表示 當前路由 響應式的屬性
所以我們來到 install.js 添加一個響應式的 屬性**_route**
和這個無關的代碼 ...省略
export let _Vue = nullexport default function install(Vue) { _Vue = Vue Vue.mixin({ beforeCreate() { if (this.$options.router) { ... // 創建一個代表當前路由 響應式的值_route // 其實不建議使用 defineReactive直接創建。。 // 第一個參數是綁定在誰身上,第二是值名稱,第二個是值 Vue.util.defineReactive(this, '_route', this._router.history.current) } else { ... } }, })}復制代碼
然后得回到 history下面的 base 添加一個修改響應式 _route的值的回調 this.cb
import createRoute from '../util/route'export default class History { constructor(router) { ... // cb 一個回調函數,它的作用就是修改 響應式路由的值_route ,對應的視圖然后就刷新 this.cb = null } // 通過 listen來修改 cb的值 listen(cb) { this.cb = cb } transitionTo(path, onComplete) {... // cb 存在就修改響應式路由的值 this.cb && this.cb(this.current)... }}復制代碼
最后在 index.js 的 init 調用 listen 方法 傳入回調修改 響應式值**_route**
...export default class VueRouter { ... init(Vue) { ... // 修改 響應式的 route history.listen((route) => { Vue._route = route }) }}...復制代碼
8.添加 $router 和 $route
我們知道在 Vue Router 提供了 $router (這個是路由對象是**Vue Router**的實例) 還有 $route(路由規則對象)
我們自己可以來到 install.js 中進行 添加這兩個屬性
...export default function install(Vue) { ... // 添加 $router 路由對象 Object.defineProperty 參數分別是 為誰添加,屬性名,屬性值 Object.defineProperty(Vue.prototype, '$router', { get() { // this._routerRoot代表的是 Vue ,他的_router是 Vue Router實例 // 可以回過去看看第二點 return this._routerRoot._router }, }) // 添加 $route Object.defineProperty(Vue.prototype, '$route', { get() { // 他的_route是就是剛才添加 響應式 的當前 路由 return this._routerRoot._route }, })}復制代碼
9.router-link
基本的介紹就不多說了,之前也是有介紹的。然后現在重新來實現下
在 components 文件下新建 link.js
// 導出 linkexport default { props: { to: { type: String, required: true, }, }, // 渲染 render(h) { // 轉化為虛擬DOM return h( // 標簽名 'a', // 標簽屬性 { domProps: { href: '#' + this.to, }, }, // 標簽里面的內容 這里是 默認插槽 [this.$slots.default] ) },}復制代碼
10.router-view
在 components 文件下新建 view.js 具體步驟干了什么都寫在注釋里了
// 導出 viewexport default { render(h) { // 獲取路由規則對象 const route = this.$route // 定義一個變量,用來等下 取 matched 中的值 let depth = 0 // 該組件為 router-view this.routerView = true // 嘗試去獲取父組件 let parent = this.$parent // 判斷是否有父組件 while (parent) { // 判斷該組件是否為 routerView if (parent.routerView) { depth++ } // 繼續向上判斷還有無父組件 parent = parent.$parent } // 這里的route是 this.$route 就是 _route 響應式值,也就是 current // 當初 current 是 調用了 match方法 獲取到的 返回值是 matched 和 path // matched 里面是多個路由對象 是這種模式保存 [parentRecord,childRecord] // 通過 變量depth取出來 舉個栗子 ['/login','/login/tab'] // 因為使用的unshif添加后面的父組件添加到前面 // depth 一直加 ,直接取出后面即可 const record = route.matched[depth] // 沒有記錄直接渲染 if (!record) { return h() } // 有的話就獲取記錄中的組件 const component = recordponent // 最后把組件渲染 return h(component) },}復制代碼
好了到了這里 Vue Router的第二次編寫就完成了,雖然和官方的差距很大。。額,因為這里是簡化寫的
11.文件目錄
忘了最后貼上文件的目錄
這個模擬Vue Router的demo 放在了 github,有需要的可以這里 MyVueRouter
到了這里也只是僅僅實現了 VueRouter的一小部分功能
但是大體上的功能都差不多實現了,嵌套路由 添加動態路由也實現了
其實我覺得到這里了也可以了,不過還是得繼續加油學習
作者:小浪努力學前端
鏈接:juejin/post/6988316779818778631
來源:掘金