Preface
Hello, everyone, I’m Lin Sanxin, uses the most easy-to-understand words to talk about the most difficult knowledge point . I believe everyone must have used Vue-router
in the Vue project, which is routing. So in this article I won't explain too much about vue-router
, and I will not explain vue-router
to you, I will take you from scratch to implement a vue-router ! ! !
Basic usage of routing
We usually use vue-router a lot, basically every project will use it, because Vue is a single page application, you can switch components through routing to achieve the effect of switching pages. We usually use it this way, but it's actually divided into 3 steps
- 1. Introduce
vue-router
and useVue.use(VueRouter)
- 2. Define the routing array, and pass the array to the
VueRouter instance, and expose the instance
3.
VueRouter
instance into main.js and register it on the root Vue instance// 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 and router-link distribution
// 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>
After the above three steps, what effect can we achieve?
- 1. Enter the corresponding path at the URL, and the corresponding components will be displayed
- 2. You can access
$router and $router in any component used, and use the methods or attributes on them
- 3. You can use the
route-link
component for path jump - 4. You can use the
router-view
component to display the corresponding content of the routing
The following is an animated picture of the achieved effect
Let's do it! ! !
VueRouter class
In the src folder, create a my-router.js
Options parameter VueRouter class, in fact, new VueRouter(options)
when passed in this parameter object, and install
is a method, and must VueRouter class has this method, why? We will talk about it below.
// src/my-router.js
class VueRouter {
constructor(options) {}
init(app) {}
}
VueRouter.install = (Vue) => {}
export default VueRouter
install method
Why must we define a install
and assign it to VueRouter
? In fact, this is related to the Vue.use
method. Do you still remember how Vue uses 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')
In fact, the second and third steps are very clear. It is to instantiate a VueRouter object and attach this VueRouter object to the root component App. Then the question is, what is the use of Vue.use(VueRouter) in the first step? In fact Vue.use(XXX)
, is executed XXX
on install
method, which is Vue.use (VueRouter) === VueRouter.install () , but to this, we know the install
will perform, but still do not know install
executed is What's the use?
We know that the VueRouter object is linked to the root component App, so the App can directly use the methods on the VueRouter object. However, as we know, we must think every component used in 161b8560e55ef1 can use the methods of VueRouter. For example,
this.$router.push
, but now only App can use these methods, what should I do? How can every component be used? At this time, the install
method comes in handy, let's talk about the implementation ideas first, and then write the code.
Knowledge points: Vue.use(XXX)
, the install method of XXX will be executed, and the Vue
will be used as the parameter to the
install
method
// 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 method
What is this method for? As the name implies, it is to convert the incoming routes array into a
Map structure. The key is the path and the value is the corresponding component information. As for why the conversion is necessary? We will talk about this below. Let's implement the conversion first.
// 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
Routing mode
There are three modes of routing
- 1.
hash mode, the most commonly used mode
- 2.
history mode, a mode that requires back-end cooperation
- 3.
abstract mode, non-browser environment mode
And how to set the mode? It is set like this, and passed in through the mode
field of options
export default new VueRouter({
mode: 'hash' // 设置模式
routes
})
And if you don’t pass it, the default is hash mode, which is also the most used mode in our usual development, so this chapter only implements
hash mode
// 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
hashHistory.js
under the src folder
In fact, the principle of the hash mode is to monitor the change of the hash value in the browser url and switch the corresponding component
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 method
As mentioned above, every time the hash is modified, the latest hash value can be obtained, but this is not our ultimate goal. Our ultimate goal is to render different component pages according to the hash changes. What should we do?
Remember the previous createRouteMap
method? We converted the routes array into the
Map
data structure. With that Map, we can get the corresponding components based on the hash value and perform rendering
But is this really okay? In fact, it doesn’t work. If you follow the above method, when the hash is /home/child1
, only the home-child1.vue
will be rendered, but this is definitely not working. When the hash is /home/child1
, the two components home.vue
and home-child1.vue
must be rendered.
So we have to write a method to find which components the hash corresponds to. This method is 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)
}
}
This just ensures that all the corresponding components can be found when the hash changes, but one thing we overlooked is that if we refresh the page manually, the
hashchange
event will not be triggered, that is, the component cannot be found. What should I do? Refreshing the page will definitely re-initialize the routing. We only need to perform an in-place jump initialization function init.
// src/my-router.js
class VueRouter {
// ...原先代码
init(app) {
// 初始化时执行一次,保证刷新能渲染
this.history.transitionTo(window.location.hash.slice(1))
}
// ...原先代码
}
Responsive hash change
Above, we have realized that all the components that need to be rendered are found based on the hash value of , but the final rendering link has not been implemented yet, but no hurry. Before rendering, let’s complete one thing first, that is, let
hash The value change thing becomes a
responsive thing, why? We just got the latest
component collection every time the hash changes, but it's useless. The re-rendering of Vue components can only be triggered by a responsive change of certain data. So we have to make a variable to save this
component collection, and this variable needs to be responsive, this variable is
$route
, pay attention to distinguish it from $router
! ! But this $route
needs to be obtained with two intermediate variables, namely current and _route
It may be a little bit convoluted here, but I hope everyone will be patient. I have shown the most simple complex code.
// 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 component rendering
In fact, the key to component rendering lies in the <router-view>
component. We can implement a <my-view>
Create src
view.js
, the old rules, first talk about ideas, and then implement the code
// 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 jump
In fact, his essence is just an a tag
In src
created under 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
final effect
Finally, change the introduction in router/index.js
import VueRouter from '../Router-source/index2'
Then replace all router-view and router-link with
my-view and my-link
Effect
Concluding remarks
If you think this article is of little help to you, please give me a thumbs up and encourage Lin Sanxin haha. Or you can join my fish school
If you want to join the learning group and catch the fish, please click here [moyu](
https://juejin.cn/pin/6969565162885873701), I will broadcast the mock interview regularly to answer questions
Complete code
/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
Concluding remarks
Some people may think it is unnecessary, but it is actually necessary to strictly demand oneself. Only by strict requirements of oneself at ordinary times can each company be better able to achieve backward compatibility.
If you think this article is of little help to you, please give me a thumbs up and encourage Lin Sanxin haha.
If you want to learn front-end or fish fishing together, then you can add me and join my fish fishing group
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。