preface
Hello, I'm Lin Sanxin. I'm talking about the most difficult knowledge points in the most easy to understand words. I believe everyone must have used Vue router, that is, routing, in Vue project. Therefore, in this article, I will not explain the basic explanation of Vue router. I will not explain the source code of Vue router to you. I will take you to implement a Vue router from scratch!!!
Basic usage of routing
We usually use Vue router a lot, and basically every project will use it, because Vue is a single page application, which can switch components through the path to achieve the effect of switching pages. We usually use it like this. In fact, it is divided into three steps
- 1. Introduce Vue router and use Vue use(VueRouter)
- 2. Define the routing array, pass the array into the vueroouter instance, and expose the instance
3. Introduce the vueroter instance into main JS and register with 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) // First step 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 // Step 2 }) // src/main.js import router from './router' new Vue({ router, // Step 3 render: h => h(App) }).$mount('#app')
Distribution of router view and router link
// src/App.vue <template> <div id="app"> <router-link to="/home">home of link</router-link> <span style="margin: 0 10px">|</span> <router-link to="/hello">hello of link</router-link> <router-view></router-view> </div> </template> // src/components/home.vue <template> <div style="background: green"> <div>home Oh, hey, hey</div> <router-link to="/home/child1">home Son 1</router-link> <span style="margin: 0 10px">|</span> <router-link to="/home/child2">home Son 2</router-link> <router-view></router-view> </div> </template> // src/components/hello.vue <template> <div style="background: orange"> <div>hello Oh, hey, hey</div> <router-link to="/hello/child1">hello Son 1</router-link> <span style="margin: 0 10px">|</span> <router-link to="/hello/child2">hello Son 2</router-link> <router-view></router-view> </div> </template> // src/components/home-child1. The other three sub components of Vue are similar. The difference is that the text and background colors are different, so they are not written out <template> <div style="background: yellow">I am home My 1 son home-child1</div> </template>
After the above three steps, what effect can we achieve?
- 1. Enter the corresponding path in the web address, and the corresponding components will be displayed
- 2. You can access $router and $router in any used component and use their methods or properties
- 3. You can use the route link component for path jumping
- 4. You can use the router view component to display the corresponding content of the route
The following is the dynamic diagram of the effect achieved
Do it!!!
Vueroter class
In the src folder, create a my router js
The options parameter of VueRouter class is actually the parameter object passed in when new VueRouter(options), and install is a method, and the VueRouter class must have this method. Why? We'll talk about it later.
// src/my-router.js class VueRouter { constructor(options) {} init(app) {} } VueRouter.install = (Vue) => {} export default VueRouter
install method
Why do you have to define an install method and assign it to vueroter? Actually, it's similar to Vue Use method. Do you remember how Vue uses VueRouter?
import VueRouter from 'vue-router' Vue.use(VueRouter) // First step export default new VueRouter({ // Incoming options routes // Step 2 }) import router from './router' new Vue({ router, // Step 3 render: h => h(App) }).$mount('#app')
In fact, the second and third steps are very clear, that is, to instance a vuerouter object and hang the vuerouter object to the root component App. That's the problem. Vue in the first step What is use (Vue router) for? Actually, Vue Use (xxx) is to execute the install method on XXX, that is, Vue use(VueRouter) === VueRouter. Install (), but when we get here, we know that install will execute, but we still don't know what it is for. What's the use?
We know that the VueRouter object is attached to the root component App, so the App can directly use the methods on the VueRouter object. However, we know that we definitely want every component used to use the VueRouter methods, such as 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 idea first, and then write the code.
Knowledge points: Vue When use (xxx), the install method of XXX will be executed and Vue will be passed into the install method as a parameter
// src/my-router.js let _Vue VueRouter.install = (Vue) => { _Vue = Vue // Using Vue Mix in every component Vue.mixin({ // Execute in the beforeCreate life cycle of each component beforeCreate() { if (this.$options.router) { // If root component // this is the root component itself this._routerRoot = this // this.$options.router is the vueroter instance hung on the root component this.$router = this.$options.router // Execute the init method on the vueroter instance to initialize this.$router.init(this) } else { // If it is not a root component, the parent component should also be_ routerRoot is saved to itself this._routerRoot = this.$parent && this.$parent._routerRoot // The sub components should also be hung with $router this.$router = this._routerRoot.$router } } }) }
createRouteMap method
What is this method for? As the name suggests, it is to convert the transmitted routes array into a data structure of Map structure. key is path and value is the corresponding component information. Why do you want to convert it? We'll talk about this later. Let's implement the conversion first.
// src/my-router.js function createRouteMap(routes) { const pathList = [] const pathMap = {} // Traverse the passed in routes array 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 } // } // Return pathList and 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) { // If there are children, execute addRouteRecord recursively 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, which requires back-end cooperation
- 3. abstract mode, mode in non browser environment
And how to set the mode? It's set like this. It's passed in through the mode field of options
export default new VueRouter({ mode: 'hash' // Setting mode routes })
If it is not transmitted, the default mode is hash mode, which is also the most commonly used mode in our development, so this chapter only implements hash mode
// src/my-router.js import HashHistory from "./hashHistory" class VueRouter { constructor(options) { this.options = options // If the mode is not transmitted, the default is hash this.mode = options.mode || 'hash' // What is the judgment mode 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
Create hashhistory. In the src folder js
In fact, the principle of hash mode is to monitor the change of hash value in browser url and switch the corresponding components
class HashHistory { constructor(router) { // Save the vueroouter instance passed in this.router = router // If the url does not #, it is automatically populated/#/ ensureSlash() // Listen for hash changes this.setupHashLister() } // Monitor hash changes setupHashLister() { window.addEventListener('hashchange', () => { // Pass in the hash of the current url and trigger a jump this.transitionTo(window.location.hash.slice(1)) }) } // Function triggered when jumping route transitionTo(location) { console.log(location) // Each hash change will be triggered. You can modify it in the browser // For example http://localhost:8080/#/home/child1 The latest hash is / home / Child1 } } // If there is no #, it will be supplemented automatically/#/ function ensureSlash() { if (window.location.hash) { return } window.location.hash = '/' } // I won't talk about this first. I'll use it later 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, each hash modification can obtain the latest hash value, but this is not our ultimate goal. Our ultimate goal is to render different component pages according to hash changes. What should we do?
Remember the createRouteMap method before? We convert the routes array into a Map data structure. With that Map, we can obtain the corresponding components and render them according to the hash value
But is this really OK? In fact, it can't. If you follow the above method, when the hash is / home/child1, only home-child1 will be rendered Vue is a component, but this is definitely not possible. When the hash is / home/child1, it must render home Vue and home Child1 Vue these two components
So we have to write a method to find which components correspond to the hash. This method is createMmatcher
// src/my-router.js class VueRouter { // .... Original code // Get all corresponding components according to hash changes createMathcer(location) { // Get 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) } } // ... Original code 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 { // ... Original code // Function triggered when jumping route transitionTo(location) { console.log(location) // Find out all corresponding components. router is an instance of VueRouter and createMathcer is on it let route = this.router.createMathcer(location) console.log(route) } }
This only ensures that all corresponding components can be found when the hash changes, but one thing we ignore is that if we manually refresh the page, the hashchange event will not be triggered, that is, we can't find components. What should we do? Refreshing the page will certainly reinitialize the route. We only need to perform a jump in place at the beginning of the initialization function init.
// src/my-router.js class VueRouter { // ... Original code init(app) { // Execute once during initialization to ensure that the refresh can render this.history.transitionTo(window.location.hash.slice(1)) } // ... Original code }
Responsive hash change
Above, we have found all the components that need to be rendered according to the hash value, but the final rendering link has not been implemented yet. However, it is not urgent. Before rendering, we have completed one thing, that is, to make the change of hash value a responsive thing. Why? We just got the latest component collection for each hash change, but it's useless. Vue's component re rendering can only be triggered by a responsive change in some data. So we have to make a variable to save this component set, and this variable needs to be responsive. This variable is $route. Pay attention to distinguish it from $route!!! However, this $route needs to be obtained with two mediation variables, current and_ route
There may be a little detour here. Please be patient. I've shown the simplest of complex code.
// src/hashHistory.js class HashHistory { constructor(router) { // ... Original code // Assign an initial value to current at the beginning this.current = createRoute(null, { path: '/' }) } // ... Original code // Function triggered when jumping route transitionTo(location) { // ... Original code // Assign a real value to current during hash update this.current = route } // Listening callback listen(cb) { this.cb = cb } }
// src/my-router.js class VueRouter { // ... Original code init(app) { // Pass in the callback to ensure that each current change can be changed incidentally_ route trigger response this.history.listen((route) => app._route = route) // Execute once during initialization to ensure that the refresh can render this.history.transitionTo(window.location.hash.slice(1)) } // ... Original code } VueRouter.install = (Vue) => { _Vue = Vue // Using Vue Mix in every component Vue.mixin({ // Execute in the beforeCreate life cycle of each component beforeCreate() { if (this.$options.router) { // If root component // ... Original code // Equivalent to existence_ And call the defineReactive method of Vue for responsive processing Vue.util.defineReactive(this, '_route', this.$router.history.current) } else { // ... Original code } } }) // Accessing $route is equivalent to accessing_ route Object.defineProperty(Vue.prototype, '$route', { get() { return this._routerRoot._route } }) }
Router view component rendering
In fact, the key to component rendering is the < router View > component. We can implement a < My View > by ourselves
Create a view under src JS, the old rule, first talk about the idea, and then realize the code
// src/view.js const myView = { functional: true, render(h, { parent, data }) { const { matched } = parent.$route data.routerView = true // Identify this component as router view let depth = 0 // Depth index while(parent) { // If there is a parent component and the parent component is router view, the index needs to be increased by 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 components using render's h function return h(component, data) } } export default myView
Router link jump
In fact, his essence is just an a label
Create link. Under src js
const myLink = { props: { to: { type: String, required: true, }, }, // Render render(h) { // Render using render's h function return h( // Tag name 'a', // Label properties { domProps: { href: '#' + this.to, }, }, // Slot content [this.$slots.default] ) }, } export default myLink
Final effect
Finally, route / index JS in the introduction of change
import VueRouter from '../Router-source/index2'
Then replace all router views and router links with my view and my link
effect
epilogue
If you think this article is of little help to you, give a praise and encourage Lin Sanxin, ha ha. Or you can join my fishing group
If you want to join the learning group and fish, please click here [fish](
https://juejin.cn/pin/6969565... ), I will broadcast the mock interview regularly to answer questions and dispel doubts
Complete code
/src/my-router.js
import HashHistory from "./hashHistory" class VueRouter { constructor(options) { this.options = options // If the mode is not transmitted, the default is hash this.mode = options.mode || 'hash' // What is the judgment mode 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) // Execute once during initialization to ensure that the refresh can render this.history.transitionTo(window.location.hash.slice(1)) } // Get all corresponding components according to hash changes 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 // Using Vue Mix in every component Vue.mixin({ // Execute in the beforeCreate life cycle of each component beforeCreate() { if (this.$options.router) { // If root component // this is the root component itself this._routerRoot = this // this.$options.router is the vueroter instance hung on the root component this.$router = this.$options.router // Execute the init method on the vueroter instance to initialize this.$router.init(this) // Equivalent to existence_ And call the defineReactive method of Vue for responsive processing Vue.util.defineReactive(this, '_route', this.$router.history.current) } else { // If it is not a root component, the parent component should also be_ routerRoot is saved to itself this._routerRoot = this.$parent && this.$parent._routerRoot // The sub components should also be hung with $router this.$router = this._routerRoot.$router } } }) Object.defineProperty(Vue.prototype, '$route', { get() { return this._routerRoot._route } }) } function createRouteMap(routes) { const pathList = [] const pathMap = {} // Traverse the passed in routes array 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 } // } // Return pathList and pathMap return { pathList, pathMap } } function addRouteRecord(route, pathList, pathMap, parent) { // Splice 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) { // If there are children, execute addRouteRecord recursively 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) { // Save the vueroouter instance passed in this.router = router // Assign an initial value to current at the beginning this.current = createRoute(null, { path: '/' }) // If the url does not #, it is automatically populated/#/ ensureSlash() // Listen for hash changes this.setupHashLister() } // Monitor hash changes setupHashLister() { window.addEventListener('hashchange', () => { // Pass in the hash of the current url this.transitionTo(window.location.hash.slice(1)) }) } // Function triggered when jumping route transitionTo(location) { console.log(location) // Find all corresponding components let route = this.router.createMathcer(location) console.log(route) // Assign a real value to current during hash update this.current = route // Simultaneous update_ route this.cb && this.cb(route) } // Listening callback listen(cb) { this.cb = cb } } // If there is no #, it will be supplemented automatically/#/ 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 // Identify this component as router view let depth = 0 // Depth index while(parent) { // If there is a parent component and the parent component is router view, the index needs to be increased by 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 components using render's h function return h(component, data) } } export default myView
src/link.js
const myLink = { props: { to: { type: String, required: true, }, }, // Render render(h) { // Render using render's h function return h( // Tag name 'a', // Label properties { domProps: { href: '#' + this.to, }, }, // Slot content [this.$slots.default] ) }, } export default myLink
epilogue
Some people may think it's unnecessary, but it's actually necessary to be strict with yourself. Only by being strict with yourself at ordinary times can we achieve better downward compatibility in every company.
If you think this article is of little help to you, give a praise and encourage Lin Sanxin, ha ha.
If you want to learn front-end or fishing together, you can add me and join my fishing learning group