一、起步需求分析
從添加router插件開始,在執行了vue add router
命令後,項目目錄中會增加一個router目錄並在main.js中導入router選項。
先從router目錄下的index.js開始:
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
// 註冊VueRouter插件
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
}
]
const router = new VueRouter({
routes
})
// 導出router實例
export default router
從router的index.js文件可以看出,這裏做了三件事:
- 註冊了VueRouter插件
- 實例化了一個VueRouter類並將route配置傳入
- 導出VueRouter實例
在main.js文件中,vue-rotuer的修改:
- 導入router目錄下的index.js
- 在Vue實例化時將VueRouter實例當成選項傳入
表面上Vue-Router就做了這些事情,但在我們平常開發時,我們可以在每個組件中訪問$router
,而在表面上我們並沒有發現Vue-Router在main.js中將$router
掛載到Vue.prototype
上,那說明**$router
是在Vue-Router內部掛載**的。
還有一個就是我們平時在任意組件中都可以使用<router-link>
和<router-view>
組件,而這並不是Vue自身的組件,說明Vue-Router還聲明瞭兩個全局組件。
所以這裏可以轉換成Vue-Router實現的起步需求就是實現一個Vue插件,該插件起步的功能有:
- 掛載
$rotuer
- 聲明兩個全局組件
router-link
和router-view
- 實現一個VueRouter類,實現hash模式下路由跳轉
二、需求實現
先創建一個vue-router.js文件,在文件中創建一個VueRouter類
2.1 實現掛載$router
掛載$router
我們很容易想到在install方法去掛載,但這裏有個問題,就是在執行install方法時,Vue還未實例化,這裏並不能拿到router實例,Vue-Router的解析方法是在執行install方法時全局混入beforeCreate
生命週期方法,等這個生命週期方法執行時,Vue是已經實例化了,就可以拿到router實例了。
新建vue-router.js
class VueRouter {
}
// Vue插件需要聲明一個install方法
VueRouter.install = function (Vue) {
// 全局混入beforeCreate生命週期方法
Vue.mixin({
beforeCreate() {
// 只要在Vue根實例中掛載,利用只有根實例選項上有router屬性找到根實例
if (this.$options.router) {
Vue.prototype.$router = this.$options.router
}
}
})
}
2.2 聲明兩個全局組件
首先,我們要清楚在hash模式下,<router-link>
和<router-view>
組件到底有什麼用。
<router-link to="/about">About</router-link>
<router-view></router-view>
<router-link>
組件接收一個to
屬性,並將標籤中內容渲染,從瀏覽器中可以看出其實就是一個a標籤,to
屬性改變的是a標籤的href
屬性。
<router-view>
組件的功能是在路由發生改變時,將對應路由的組件渲染到其中。
2.2.1 <router-link>
組件
新建link.js
// 導出router-link組件配置對象
export default {
name: 'router-link',
props: {
to: {
type: String,
default: ''
}
},
render (h) {
// 利用渲染函數,生成一個a標籤,href屬性值如 /#/about,將默認插槽作爲子元素
return h('a', {attrs: {href: '#' + this.to}}, this.$slots.default)
}
}
2.2.2 <router-view>
組件
新建view.js
// 導出router-view組件配置對象
export default {
name: 'router-view',
render (h) {
// component存放當前路由對應的組件
let component = null
return h(component)
}
}
2.3 完善VueRouter類,實現hash模式下路由跳轉
因爲這是是用hash值的改變來實現地址修改,但其實頁面並不會刷新,這裏就需要手動去觸發渲染
vue-router.js完善
import Link from './link'
import View from './view'
let Vue = null
class VueRouter {
constructor (options) {
// 保存路由選項
this.$options = options
// 聲明一個響應式屬性,存放當前路由路徑
Vue.util.defineReactive(this, 'currentPath', '/')
// 監聽初始化和hash值變化時,改變當前路由路徑
window.addEventListener('hashchange', this.onHashChange.bind(this))
window.addEventListener('load', this.onHashChange.bind(this))
// 創建路由map,保存路由表
this.routeMap = {}
this.$options.routes.forEach(route => {
this.routeMap[route.path] = route
})
}
onHashChange () {
// 這裏只是先簡單將 # 後的路徑賦值給currentPath
this.currentPath = window.location.hash.slice(1)
}
}
// Vue插件需要聲明一個install方法
VueRouter.install = function (_Vue) {
// 保存Vue引用
Vue = _Vue
// 全局混入beforeCreate生命週期方法
Vue.mixin({
beforeCreate() {
// 只要在Vue根實例中掛載,利用只有根實例選項上有router屬性找到根實例
if (this.$options.router) {
Vue.prototype.$router = this.$options.router
}
}
})
// 註冊全局組件
Vue.component('router-link', Link)
Vue.component('router-view', View)
}
完善view.js
// 導出router-view組件配置對象
export default {
name: 'router-view',
render (h) {
// component存放當前路由對應的組件
let component = null
// 因爲currentPath是響應式數據,在當前組件的render方法中使用該屬性後,當currentPath改變,render會重新執行
// 根據$router上的currentPath的變化,動態匹配組件
const {routeMap, currentPath} = this.$router
component = routeMap[currentPath] ? routeMap[currentPath].component : null
return h(component)
}
}
三、實現嵌套路由
嵌套路由在路由配置時多了children
數組,在父路由組件中需要添加<router-view>
組件。
實現嵌套路由的原理就是:
- 把匹配hash路徑的路由及子路由push到matched數組中
- 利用當前匹配的
<router-view>
組件的深度得到depth,即可在matched中匹配到對應的路由
vue-router.js修改
import Link from './link'
import View from './view'
let Vue = null
class VueRouter {
constructor (options) {
// 保存路由選項
this.$options = options
// 聲明一個響應式數據,存放當前路由路徑
// Vue.util.defineReactive(this, 'currentPath', '/')
// 將matched聲明爲一個響應式數據,存放當前匹配的路由數組
Vue.util.defineReactive(this, 'matched', [])
// 監聽初始化和hash值變化時,改變當前路由路徑
window.addEventListener('hashchange', this.onHashChange.bind(this))
window.addEventListener('load', this.onHashChange.bind(this))
// 創建路由map,保存路由表
// this.routeMap = {}
// this.$options.routes.forEach(route => {
// this.routeMap[route.path] = route
// })
}
onHashChange () {
// 這裏只是先簡單將 # 後的路徑賦值給currentPath
// this.currentPath = window.location.hash.slice(1)
// 獲取當前hash路徑
this.current = window.location.hash.slice(1)
this.matched = []
// 匹配hash路徑對應的路由
this.match()
}
match (routes) {
routes = routes || this.$options.routes
// 遍歷路由選項
for (let route of routes) {
if (route.path === '/' && this.current === '/') {
this.matched.push(route)
return
}
// 匹配嵌套路由的情況
if (route.path !== '/' && this.current.indexOf(route.path) !== -1) {
this.matched.push(route)
if (route.children) {
this.match(route.children)
}
return
}
}
}
}
// Vue插件需要聲明一個install方法
VueRouter.install = function (_Vue) {
// 保存Vue引用
Vue = _Vue
// 全局混入beforeCreate生命週期方法
Vue.mixin({
beforeCreate() {
// 只要在Vue根實例中掛載,利用只有根實例選項上有router屬性找到根實例
if (this.$options.router) {
Vue.prototype.$router = this.$options.router
}
}
})
// 註冊全局組件
Vue.component('router-link', Link)
Vue.component('router-view', View)
}
view.js修改
// 導出router-view組件配置對象
export default {
name: 'router-view',
render (h) {
// 標記當前組件爲routerView
this.$vnode.data.routerView = true
// component存放當前路由對應的組件
let component = null
// 當前router-view組件的深度
let depth = 0
let parent = this.$parent
// 向上遍歷是否還存在router-view組件,得到當前router-view的深度
while(parent) {
if (parent.$vnode.data && parent.$vnode.data.routerView) {
depth++
}
parent = parent.$parent
}
// 取出匹配路由的組件
const matched = this.$router.matched[depth]
component = matched && matched.component
// 因爲currentPath是響應式數據,在當前組件的render方法中使用該屬性後,當currentPath改變,render會重新執行
// 根據$router上的currentPath的變化,動態匹配組件
// const {routeMap, currentPath} = this.$router
// component = routeMap[currentPath] ? routeMap[currentPath].component : null
return h(component)
}
}
這裏的實現並沒有像官方一樣使用函數式組件