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