前言
大家好,我是林三心,用最通俗易懂的话,讲最难的知识点,置信大家在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
结语
有人可能感觉没必要,然而严格要求本人其实是很有必要的,平时严格要求本人,能力做到每到一个公司都能更好的做到向下兼容难度。
如果你感觉此文对你有一丁点帮忙,点个赞,激励一下林三心哈哈。
如果你想一起学习前端或者摸鱼,那你能够加我,退出我的摸鱼学习群
发表回复