preface

As an important member of the VUE ecosystem: VuE-Router has become one of the basic skills for front-end engineers to master. This article put aside the vue-Router daily use, start from the source code, learn the source code together and try to achieve a vue-Router of their own. In the process of reading, if there is any inadequacy, please correct.

It takes about 15 minutes to read this article, which has over 2000 words. If there are any deficiencies, please correct them

Write a vuex by hand from 0 to 1

Source analyses

Let’s take a look at the source directory structure:

/ / path: node_modules/vue - the router ├ ─ ─ dist -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- build output directory file after ├ ─ ─ package. The json -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - don't explain ├ ─ ─ the SRC -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - contains the main source │ ├ ─ ─ the components -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- routing component │ │ ├ ─ ─ the link. The js -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- the router link component │ │ ├ ─ ─ the js, the router - the view component │ ├ ─ ─history-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - routing patterns │ │ ├ ─ ─ the abstract. The js -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- the abstract routing patterns │ │ ├ ─ ─ base. Js -----------------------historyRouting patterns │ │ ├ ─ ─ errors. Js -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- wrong class to handle │ │ ├ ─ ─ hash. Js -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --hashRouting patterns │ │ ├ ─ ─ HTML 5. Js -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- HTML5History patterns encapsulate │ ├ ─ ─ util -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- tools function encapsulation │ ├ ─ ─ The create - the matcher. Js -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- the routing map │ ├ ─ ─ the create - the route - map. Js -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - to create routing tree │ mapping state ├ ─ ─ index. Js -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- main entry file | ├ ─ ─ the js -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- the routing load fileCopy the code

Entry start analysis

Vue instance – the router

From the entry file index.js, we can see that a VueRouter class is exposed. This is the new Router() we used when we introduced vue-Router in the vue project.

exportdefault class VueRouter { constructor (options: RouterOptions = {}) { this.app = null this.apps = [] this.options = options this.beforeHooks = [] this.resolveHooks = []  this.afterHooks = [] this.matcher = createMatcher(options.routes || [], this)let mode = options.mode || 'hash'
    this.fallback = mode === 'history'&&! supportsPushState && options.fallback ! = =false
    if (this.fallback) {
      mode = 'hash'
    }
    if (!inBrowser) {
      mode = 'abstract'
    }
    this.mode = mode

    switch (mode) {
      case 'history':
        this.history = new HTML5History(this, options.base)
        break
      case 'hash':
        this.history = new HashHistory(this, options.base, this.fallback)
        break
      case 'abstract':
        this.history = new AbstractHistory(this, options.base)
        break
      default:
        if(process.env.NODE_ENV ! = ='production') {
          assert(false, `invalid mode: ${mode}`)}}}init () {}
  function registerHook() {}go() {}push(){}
  VueRouter.install = install
  VueRouter.version = '__VERSION__'
    
  if (inBrowser && window.Vue) {
      window.Vue.use(VueRouter)
  }

Copy the code

From the entry file, we can see that it contains the following main steps:

  1. Example Initialize the routing mode
  2. According to incomingroutesParameter generates a route status table
  3. Gets the current route object
  4. Initialize the routing function
  5. registeredHooksEvents such as
  6. addinstallLoad function

Install registration function

The Router class exposed above has an install method mounted on it, which is briefly analyzed here (this is also a mental guide to implementing a route of your own). When we introduced the vue-Router and instantiated it, the vue-Router internals helped us load the Router instance into the vue instance so that we could use router-link, router-View, and other components directly in the component. This.$router, this.$route and other global variables.

  1. First introducedvue-routerPost need utilizationbeforeCreateThe life cycle loads it for initialization_routerRoot, _router, _routeSuch as data,
  2. Also set global access variables$routerand$router
  3. completerouter-linkandrouter-viewRegistration of two components

This can be seen in the source install.js

import View from './components/view'
import Link from './components/link'

export let _Vue

export function install (Vue) {
  if (install.installed && _Vue === Vue) return
  install.installed = true_Vue = Vue // mixin Vue. Mixin ({beforeCreate () {
      if (isDef(this.$options.router)) {
        this._routerRoot = this
        this._router = this.$options.router
        this._router.init(this)
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      registerInstance(this, this)
    },
    destroyed() {registerInstance(this)}}) // Set global access variables$router
  Object.defineProperty(Vue.prototype, '$router', {
    get () { return this._routerRoot._router }
  })

  Object.defineProperty(Vue.prototype, '$route', {
    get () { returnThis._routerroot. _route}}) Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)
}
Copy the code

Above we on vue-Router source code to do a rough context comb, we will implement a simplified version of vue-Router. Before that, we need to briefly understand some knowledge points

Front knowledge

Vue-router provides a mode parameter, you can set the history parameter or hash parameter, and the implementation principle is different

Implementation principle of Hash

http://localhost:8080/#login

The # symbol itself and the character following it are called hashes and can be read using the window.location.hash property. H5 adds a hashchange to help us listen for hash changes in browser links.

<! DOCTYPE html> <html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge"</title> </head> <body> <h1 id="app"></h1>
  <a href="#/jin"< p style = "max-width: 100%; clear: both; min-height: 1em"#/sn"</a> <script> window.addeventListener ('hashchange',()=>{
          app.innerHTML = location.hash.slice(2)
      })
  </script>
</body>
</html>
Copy the code

How history works

http://localhost:8080/login also H5 added pushState and popstate no awareness to help us refresh the browser url

<body>
  <h1 id="app"></h1>
  <a onclick="to('jin')"< span style = "max-width: 100%; clear: both; min-height: 1em"to('sn')"</a> <script >function to(pathname) {
        history.pushState({},null,pathname)
        app.innerHTML = pathname
    }
    window.addEventListener('popstate',()=>{
        to(location.pathname)
    })
</script>
</body>
Copy the code

After we understand the principles of non-perceptive refresh URLS, we will encapsulate a VUe-Router based on these principles

Start implementing your own VUe-Router


Implement the install load method

First we need to initialize our project structure and create a new simple-vue-router.js. Based on the above analysis, we need to expose a Router class that contains an install method

letVue // Saves the Vue instance classVueRouter(){// Router class}functionInstall (_vue){// Install function}export default {
    VueRouter,
    install
}
Copy the code

Install needs to do the following

  1. Initialize the_routerRoot._router._routeSuch as data,
  2. Set global access variables$routerand$router
  3. completerouter-linkandrouter-viewRegistration of two components

The code is as follows:

letVue // Used to save Vue instance class VueRouter {// Router class} VueRouter. Install =function(_vue) {// Load functions // Each component has this.$router / this.$routeSo mixin Vue = _vue // This is available in every component.$routerWith this.$routeVue. Mixin ({beforeCreate() {// If it is the root componentif (this.$options && this.$options.router) {this._root = this // Save the current vue instance to _root.$options.router // Mount router instance on _router}else{// If it is a child it inherits the parent (so that all components share a router instance) this._root = this.$parent._root} // Define the router instance when accessing this.$routerRouter instance Object.defineProperty(this,'$router', {
				get() {
					returnThis._root. _router}}) // Define route when accessing this.$routeObject.defineproperty (this,'$route', {
				get() {
					returnVue.component(vue.component (vue.component (vue.component))'router-link', {
		render(h) {}
	})
	Vue.component('router-view', {
		render(h) {}
	})
}
export default VueRouter

Copy the code

implementationrouterclass

Now that the install method has been implemented to help us mount the Router in the Vue instance, we need to improve the functionality in the Router class. According to the above source code analysis, we need to achieve the following functions:

  1. Generate a routing status table based on the passed Rotues parameters. That is, if the passed argument is
  routes:[    
    {              
      path: '/',  
      name: 'index', 
      component: index   
    },
    {              
      path: '/login',  
      name: 'login', 
      component: login 
    },
    {              
      path: '/learn',  
      name: 'learn', 
      component: learn   
    },
  ]
Copy the code

Format it with path as key and Component as value

{
 '/':index,
 '/login':login,
 '/learn':learn 
}
Copy the code
  1. Define the current route variable and render the corresponding component in real time by hijacking
  2. Define a function that implements the processing that should be used for different schema responses

The specific code is as follows

letVue // Save Vue instance class VueRouter {// Router class constructor(options) {// DefaulthashMode. This mode = options. Mode | |'hash'This. Routes = options. Routes | | [] / / routing map enclosing routeMaps = this. GenerateMap (enclosing routes) / / the current routing enclosing currentHistory =  newhistoryRoute() // Initialize Route function this.initroute ()} generateMap(routes) {return routes.reduce((prev, current) => {
  		prev[current.path] = current.component
  		return prev
  	}, {})
  }
  initRoute() {// only processing herehashPatterns andhistorymodelif (this.mode === 'hash') {// Check if the user opens it in the URLhashThere is no redirect to# /
  		location.hash ? ' ' : (location.hash = '/'// Monitor browser load events to change the currently stored routing variable window.adDeventListener ('load', () => {
  			this.currentHistory.current = location.hash.slice(1)
  		})
  		window.addEventListener('hashchange', () => {
  			this.currentHistory.current = location.hash.slice(1)
  		})
  	} else {
  		location.pathname ? ' ' : (location.pathname = '/')
  		window.addEventListener('load', () => {
  			this.currentHistory.current = location.pathname
  		})
  		window.addEventListener('popstate', () => {
  			this.currentHistory.current = location.pathname
  		})
  	}
  }
}
class historyRoute {
  constructor() {
  	this.current = null
  }
}
VueRouter.install = function(_vue) {// omit some code}export default VueRouter

Copy the code

Improve the code to achieve real-time refresh page view

After building the Router class, we found that the current route state currentHistory. Current is static, and the page does not display the template when we change the current route. Here we can use vue’s own bidirectional binding mechanism

The specific code is as follows

letVue // Save Vue instance class VueRouter {// Router class constructor(options) {// DefaulthashMode. This mode = options. Mode | |'hash'This. Routes = options. Routes | | [] / / routing map enclosing routeMaps = this. GenerateMap (enclosing routes) / / the current routing enclosing currentHistory =  newhistoryRoute() // Initialize Route function this.initroute ()} generateMap(routes) {return routes.reduce((prev, current) => {
			prev[current.path] = current.component
			return prev
		}, {})
	}
	initRoute() {// only processing herehashPatterns andhistorymodelif (this.mode === 'hash') {// Check if the user opens it in the URLhashThere is no redirect to# /
			location.hash ? ' ' : (location.hash = '/'// Monitor browser load events to change the currently stored routing variable window.adDeventListener ('load', () => {
				this.currentHistory.current = location.hash.slice(1)
			})
			window.addEventListener('hashchange', () => {
				this.currentHistory.current = location.hash.slice(1)
			})
		} else {
			location.pathname ? ' ' : (location.pathname = '/')
			window.addEventListener('load', () => {
				this.currentHistory.current = location.pathname
			})
			window.addEventListener('popstate', () => {
				this.currentHistory.current = location.pathname
			})
		}
	}
}
class historyRoute {
	constructor() {
		this.current = null
	}
}
VueRouter.install = function(_vue) {// Load functions // Each component has this.$router / this.$routeSo mixin Vue = _vue // This is available in every component.$routerWith this.$routeVue. Mixin ({beforeCreate() {// If it is the root componentif (this.$options && this.$options.router) {this._root = this // Save the current vue instance to _root.$optionsRouter // Use vue library to hijack the current route vue.util. DefineReactive (this,'route',this._router.currentHistory)
			} else{// If it is a child it inherits the parent (so that all components share a router instance) this._root = this.$parent._root} // Define the router instance when accessing this.$routerRouter instance Object.defineProperty(this,'$router', {
				get() {
					returnThis._root. _router}}) // Define route when accessing this.$routeObject.defineproperty (this,'$route', {
				get() {
					return{// Current route: this._root._router.history.current}}})}}) Vue.component('router-link', { props: { to: String, tag: String }, methods: {handleClick(event) {// prevent a tag from skipping event && event.preventDefault && event.preventdefault ()let mode = this._self._root._router.mode
				let path = this.to
				this._self._root._router.currentHistory.current = path
				if (mode === 'hash') {
					window.history.pushState({}, ' '.'# /' + path.slice(1))
				} else {
					window.history.pushState({}, ' ', path.slice(1))
				}
			}
		},
		render(h) {
			let mode = this._self._root._router.mode
			let tag = this.tag || 'a'
			return (
				<tag on-click={this.handleClick} href={mode === 'hash' ? `#${this.to}` : this.to}>
					{this.$slots.default}
				</tag>
			)
		}
	})
	Vue.component('router-view', {render(h) {// this current is already dynamic through the above hijackinglet current = this._self._root._router.currentHistory.current
			let routeMap = this._self._root._router.routeMaps
			returnH (routeMap[current]) // Dynamic render corresponding component}})}export default VueRouter

Copy the code

At this point, a simple route manager is complete. In fact, compared to vue-Router, it still lacks many functions such as navigation guard and dynamic routing. A journey of a thousand miles begins with a single step. This article aims to briefly analyze the vue-Router principle and practice a simple router to help you enter the door of vue-Router principle. The following will rely on your own perseverance to continue in-depth learning.

The resources

Vue-router official source handwritten vue-router source code