preface
In the most easy to understand words, talk about the most difficult knowledge points. I believe everyone must have used Vue router in Vue project, that is, routing. 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 it 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.vue the other three sub components are similar. The difference is that the text and background colors are different, so they are not written <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
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? In fact, this is related to the 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 on the root component App. The problem arises. What is the purpose of Vue.use(VueRouter) in the first step? In fact, Vue.use(XXX) is to execute the install method on XXX, that is, Vue.use(VueRouter) === VueRouter.install(), but here, we know that install will execute, but we still don't know what it is for and what's the use of install?
We know that the VueRouter object is attached to the root component App, so App can directly use the methods on the VueRouter object. However, we know that we certainly want every component used to use VueRouter methods, such as this.$router.push, but now only App can use these methods. What should we 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 point: when Vue.use(XXX), it will execute the install method of XXX and pass Vue as a parameter into the install method
// src/my-router.js let _Vue VueRouter.install = (Vue) => { _Vue = Vue // Mix in each component with Vue.mixin 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 that is 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
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.js in the src folder
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 doesn't work. According to the above method, when the hash is / home/child1, only the home-child1.vue component will be rendered, but this is certainly not possible. When the hash is / home/child1, the home.vue and home-child1.vue components must be rendered
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 // Mix in each component with Vue.mixin 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 view.js under src. The old rule is to talk about the idea first, and then implement 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.js under src
Final effect
Finally, change the introduction in router/index.js
import VueRouter from '../Router-source/index2' Copy code
Then replace all router views and router links with my view and my link
epilogue
If you think this article is a little helpful to you, give it a compliment and encourage it, ha ha.
/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 // Mix in each component with Vue.mixin 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 that is 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