Vueroter simulation implementation and source code interpretation

Keywords: Vue Vue.js

1. Vue.use() source code

Source location: Vue / SRC / core / global API / use.js

export function initUse (Vue: GlobalAPI) {
    //The parameter of the use method receives a plug-in. The type of the plug-in can be a function or an object
  Vue.use = function (plugin: Function | Object) {
      //_ The installedPlugins array stores the installed plug-ins.
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    //Judge whether the passed plug-in exists in installedPlugins. If so, it will be returned directly
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }

    // additional parameters
      //Convert arguments to an array and remove the first item in the array.
    const args = toArray(arguments, 1)
    //Insert this (that is, Vue, which is called through Vue.use) into the position of the first element in the array.
    args.unshift(this)
      //At this time, the plugin is an object. Check whether there is the install function.
    if (typeof plugin.install === 'function') {
        //If there is an install function, call it directly
        //Here, each item in the args array is expanded by apply and passed to the install function.
        // plugin.install(args[0],args[1])
        //args[0] is the Vue we inserted above. This is the same as when we simulated the install method earlier.
      plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {
        //If the plugin is a function, it is called directly through apply
      plugin.apply(null, args)
    }
      //Store the plug-in in the installedPlugins array.
    installedPlugins.push(plugin)
    return this
  }
}

2. install method analysis

Let's first look at the directory structure of Vue router

Let's start with the core document.

Under the components directory, there are two files. link.js and view.js files respectively.

Create a RouterLink component from the link.js file

Create a RouterView component from the view.js file.

The file in the history directory records the history of routing (hash.js file is about hash mode, html5.js about HTML5 mode, base.js public content, and abstract.js is the history of routing implemented in server-side rendering).

The index.js file is used to create VueRouter

The install.js file is about the install method

The VueRouter we simulated also implements the above directory structure.

Let's first implement the basic code in the index.js file.

export default class VueRouter {
  //When you create a vueroter object, options are passed
  constructor(options) {
    //Get the routes option, which defines the routing rules
    this._options = options.routes || [];
  }
  // Register events for routing changes. The parameter of this method is a Vue instance, which will be improved later
  init(Vue) {}
}

The following is the basic code of install.js (implemented by viewing the source code)

export let _Vue = null; //It can be exported and Vue instances can be used in other files instead of a separate js file that introduces Vue
export default function install(Vue) {
  //Get Vue constructor
  _Vue = Vue;
  _Vue.mixin({
    //After mixing, there will be a beforeCreate method in all Vue instances
    beforeCreate() {
      //Judge whether it is an instance of Vue. If the condition is true, it is an instance of Vue. Otherwise, it is other corresponding components (because options will be passed when creating Vue instances)
      if (this.$options.router) {
        //By viewing the source code, it is found that the Vue instance will be attached to the current private property_ On the routerRoot property
        this._routerRoot = this;

    
        this._router = this.$options.router;
        //Call the init method defined in the index.js file
        this._router.init(this);
      } else {
      
        this._routerRoot = this.$parent && this.$parent._routerRoot;
      }
    },
  });
}

3. Component creation test

Next, you need to mount the install method on vueroter.

import install from "./install";
export default class VueRouter {
  //When you create a vueroter object, options are passed
  constructor(options) {
    //Get the routes option, which defines the routing rules
    this._routes = options.routes || [];
  }
  // Register events for routing changes.
  init(Vue) {}
}
//Mount the install method on vueroter
VueRouter.install = install;

Next, we can simply implement the router link component and the router view component to do a simple test. (next, we will explain the following contents)

The view.js file in the components directory.

export default {
  render(h) {
    return h("div", "router-view");
  },
};

The above are the basic functions of the router view component, which will be improved later.

The implementation of link.js file is as follows:

export default {
  props: {
    to: {
      type: String,
      required: true,
    },
  },
  render(h) {
      //Get the text in the 'a' label through the slot.
    return h("a", { domProps: { href: "#" + this.to } }, [this.$slots.default]);
  },
};

In the install.js file, import the above components for testing.

import View from "./components/view";
import Link from "./components/link";
export let _Vue = null; //It can be exported and Vue instances can be used in other files instead of a separate js file that introduces Vue
export default function install(Vue) {
  //Get Vue constructor
  _Vue = Vue;
  _Vue.mixin({
    //After mixing, there will be a beforeCreate method in all Vue instances
    beforeCreate() {
      //Judge whether it is an instance of Vue. If the condition is true, it is an instance of Vue. Otherwise, it is other corresponding components (because options will be passed when creating Vue instances)
      if (this.$options.router) {
        //By viewing the source code, it is found that the Vue instance will be attached to the current private property_ On the routerRoot property
        this._routerRoot = this;

     
        this._router = this.$options.router;
        //Call the init method defined in the index.js file
        this._router.init(this);
      } else {
    
        this._routerRoot = this.$parent && this.$parent._routerRoot;
      }
    },
  });
    //Complete component registration
  Vue.component("RouterView", View);
  Vue.component("RouterLink", Link);
}

In the above code, import the component and complete the component registration.

Next, let's test it.

In the src directory, import the vue router.js file defined by yourself

import Router from "./my-vue-router";

4. Resolve routing rules

Next, what we need to do is to parse all routing rules and parse them into an array. It is convenient to find the corresponding components according to the address.

In the index.js file of the source code, the vueroter class is created. The corresponding construction method has the following code:

 this.matcher = createMatcher(options.routes || [], this)

The createMatcher method is created in the create-matcher.js file.

The matcher returned by this method is a matcher with two members, match and addRoutes

Match: match the corresponding routing rule object according to the routing address.

addRoutes dynamically add routes

First, add the following code to our own index.js file:

import install from "./install";
import createMatcher from "./create-matcher";
export default class VueRouter {
  //When you create a vueroter object, options are passed
  constructor(options) {
    //Get the routes option, which defines the routing rules
    this._routes = options.routes || [];
    this.matcher = createMatcher(this._routes);
  }
  // Register events for routing changes.
  init() {}
  //init(Vue){}
}
//Mount the install method on vueroter
VueRouter.install = install;

In the above code, the createMatcher method is imported.

And the routing rules are passed when the method is called.

The code of create-matcher.js file is as follows:

import createRouteMap from "./create-route-map";
export default function createMatcher(routes) {
  
  const { pathList, pathMap } = createRouteMap(routes);
  function match() {}
  function addRoutes(routes) {
 
    createRouteMap(routes, pathList, pathMap);
  }
  return {
    match,
    addRoutes,
  };
}

Next, we need to implement the createRouteMap method in the create-route-map.js file.

export default function createRouteMap(routes, oldPathList, oldPathMap) {
  const pathList = oldPathList || [];
  const pathMap = oldPathMap || {};
  //Traverse all routing rules for parsing. At the same time, we should also consider the form of children,
  //So you need to use recursion here.
  routes.forEach((route) => {
    addRouteRecord(route, pathList, pathMap);
  });

  return {
    pathList,
    pathMap,
  };
}

function addRouteRecord(route, pathList, pathMap, parentRecord) {
  //Get the path from the routing rule.
  const path = parentRecord ? `${parentRecord.path}/${route.path}` : route.path;
  //Build record
  const record = {
    path,
    component: route.component,
    parent: parentRecord, //If it is a child route, record the parent record object corresponding to the child route (the object has path and component), which is equivalent to recording the parent-child relationship
  };
  //If you already have a path, skip the same path directly
  if (!pathMap[path]) {
    pathList.push(path);
    pathMap[path] = record;
  }
  //Determine whether there is a sub route in the route
  if (route.children) {
    //Traverse the sub route and add the sub route to pathList and pathMap.
    route.children.forEach((childRoute) => {
      addRouteRecord(childRoute, pathList, pathMap, record);
    });
  }
}

Let's test the above code.

import createRouteMap from "./create-route-map";
export default function createMatcher(routes) {

  const { pathList, pathMap } = createRouteMap(routes);
  console.log("pathList==", pathList);
  console.log("pathMap==", pathMap);
  function match() {}
  function addRoutes(routes) {
   
    createRouteMap(routes, pathList, pathMap);
  }
  return {
    match,
    addRoutes,
  };
}

In the above code, we printed pathList and pathMap

Of course, children have not been added to our defined routing rules to build corresponding sub routes. Let's revise it again.

In the router.js file under the project root directory, add the corresponding sub routing rule.

import Vue from "vue";
// import Router from "vue-router";
// import Router from "./vuerouter";
import Router from "./my-vue-router";
import Login from "./components/Login.vue";
import Home from "./components/Home.vue";
import About from "./components/About.vue";
import Users from "./components/Users";
Vue.use(Router);
export default new Router({
  // model: "history",
  routes: [
    { path: "/", component: Home },
    { path: "/login", component: Login },
    {
      path: "/about",
      component: About,
      children: [{ path: "users", component: Users }],
    },
  ],
});

At this time, you can view the corresponding output results.

5. Implementation of match function

In the create-matcher.js file, we have implemented the createRouteMap method and also need to implement the match method.

The match method is used to match a routing object according to the routing address. In fact, it is to find the corresponding routing record from the pathMap according to the routing address. The component information is recorded in the routing record. After it is found, the component can be created and rendered.

 function match(path) {
    const record = pathMap[path];
    if (record) {
      //Create a route routing rule object according to the routing address
      return createRoute(record, path);
    }
    return createRoute(null, path);
  }

In the above code, when we call the match method, a path will be passed. According to this path, we can find the corresponding route record information from the pathMap (this has been created in the previous section). If it is found, we need to do further processing. Why? Because the path we pass on may be a sub path. At this time, we not only need to obtain the corresponding sub route information, but also need to find the corresponding parent route information. Therefore, further processing is required here. The processing of this piece is encapsulated in the createRoute method, which is also required in other places, so we define the import createRoute from "./util/route" in the util directory;.

The complete code of create-matcher.js file is as follows:

import createRouteMap from "./create-route-map";
import createRoute from "./util/route";
export default function createMatcher(routes) {
 
  const { pathList, pathMap } = createRouteMap(routes);
  console.log("pathList==", pathList);
  console.log("pathMap==", pathMap);
    //Implement match method
  function match(path) {
    const record = pathMap[path];
    if (record) {
      //Create a route routing rule object according to the routing address
      return createRoute(record, path);
    }
    return createRoute(null, path);
  }
  function addRoutes(routes) {
   
    createRouteMap(routes, pathList, pathMap);
  }
  return {
    match,
    addRoutes,
  };
}

Next, we need to create a util directory under my Vue router directory, and create a route.js file under this directory. The code of this file is as follows:

export default function createRoute(record, path) {

  const matched = [];
 
  while (record) {
    matched.unshift(record);
    record = record.parent;
  }

  return {
    path,
    matched,
  };
}

Summary: the function of match is to create a routing rule object according to the path. The so-called routing rule object actually contains the information of the path and the corresponding routing record (it may contain the parent route and child route records, which are stored in an array).

Later, we can directly obtain the array containing the whole routing record according to the path, so that we can create all the corresponding components.

6. History processing

There are three modes for routing: hash mode, html5 mode and abstract mode (this mode is related to server-side rendering)

Here, we implement the history management of hash mode. No matter which mode, it has the same content. Here, we define the same content to

In the parent class.

The main contents of this parent class are as follows:

router property: routing object (ViewRouter)

The current attribute records the routing rule object {path:'/',matched: []} corresponding to the current path. We have handled this object earlier. That is, the content returned in the createRoute method.

transitionTo()

Jump to the specified path, obtain the matching routing rule object route according to the current path, and then update the view.

In the my Vue router directory and the base.js file in the history directory, write the following code:

import createRoute from "../util/route";
export default class History {
  // router routing object ViewRouter
  constructor(router) {
    this.router = router;
  
    this.current = createRoute(null, "/");
  }
  
  transitionTo(path, onComplete) {
 
    this.current = this.router.matcher.match(path);
    //The callback function will be passed when calling the transitionTo method.
    onComplete && onComplete();
  }
}

The parent class has been implemented, and the corresponding child classes are implemented below. That is, HashHistory

HashHistory inherits History and ensures that the first access address is # /

There are also two methods to be defined in the History. The first method is getCurrentLocation() to get the current routing address (# later part)

The setUpListener() method listens for the event of route address change (hashchange).

The code in the hash.js file in the history directory is implemented as follows:

import History from "./base";
export default class HashHistory extends History {
  constructor(router) {
    //Pass the routing object to the constructor of the parent class
    super(router);
    //Ensure that the first access address is added with # / (/ / because this is not added, it is a normal method)
    ensureSlash();
  }
  // Get the current routing address (# later part), so it needs to be removed here#
  getCurrentLocation() {
    return window.location.hash.slice(1);
  }
  // Listening for hashchange events
  //That is, monitor the change of routing address
  setUpListener() {
    window.addEventListener("hashchange", () => {
      //When the routing address changes, jump to the new routing address.
      this.transitionTo(this.getCurrentLocation());
    });
  }
}

function ensureSlash() {
  //Judge whether there is a hash currently
  // If you click a link, there must be a hash
  if (window.location.hash) {
    return;
  }
  
  window.location.hash = "/";
}

7. Init method implementation

We know that when creating VueRouter, you need to pass mode to specify the route form, such as hash mode or html5 mode.

Therefore, you need to select different js in the history directory to process according to the mode of the specified mode.

Therefore, in the index.js file in my Vue router directory, make the following modifications:

import install from "./install";
import createMatcher from "./create-matcher";
import HashHistory from "./history/hash";
import HTML5History from "./history/html5";
export default class VueRouter {
  //When you create a vueroter object, options are passed
  constructor(options) {
    //Get the routes option, which defines the routing rules
    this._routes = options.routes || [];
    this.matcher = createMatcher(this._routes);
    //Get the mode in the passed option, which determines the form of the route set by the user.
    //Here, the mode attribute is added to vueroter
    const mode = (this.mode = options.mode || "hash");
    switch (mode) {
      case "hash":
        this.history = new HashHistory(this);
        break;
      case "history":
        this.history = new HTML5History(this);
        break;
      default:
        throw new Error("mode error");
    }
  }
  // Register events for routing changes.
  init() {}
  //init(Vue){}
}
//Mount the install method on vueroter
VueRouter.install = install;

First, import HashHistory and HTML5History

import HashHistory from "./history/hash";
import HTML5History from "./history/html5";

Get the mode in the options below. If mode is not specified when creating the VueRouter object, the default value is hash

Next, judge the obtained mode and create different history instances according to different values of the mode.

 //Get the mode in the passed option, which determines the form of the route set by the user.
    //Here, the mode attribute is added to vueroter
    const mode = (this.mode = options.mode || "hash");
    switch (mode) {
      case "hash":
        this.history = new HashHistory(this);
        break;
      case "history":
        this.history = new HTML5History(this);
        break;
      default:
        throw new Error("mode error");
    }

At the same time, the basic code is added to the html5.js file

import History from "./base";
export default class HTML5History extends History {}

The form of Html5 is not implemented here.

Let's improve the init method

 // Register events for routing changes.
  init() {}

The specific implementation code is as follows:

  // Register the event of route change (initialize the event listener and listen for the change of route address).
  init() {
    const history = this.history;
    const setUpListener = () => {
      history.setUpListener();
    };
    history.transitionTo(
      history.getCurrentLocation(),
      //If you directly use history.setUpListener
      // In this case, there will be a problem with this in setUpListener.
      setUpListener
    );
  }

The reason why the transitionTo method is called here is that the address is modified once in the ensueslash method in the hash.js file, so you need to jump here.

At the same time, the binding of hashchange events (routing change events) is completed.

Next, you can test it by printing the value of the current attribute in the transitionTo method in the base.js file.

  transitionTo(path, onComplete) {
    this.current = this.router.matcher.match(path);
    console.log("current===", this.current);
 //The callback function will be passed when calling the transitionTo method.
    onComplete && onComplete();
  }

Next, after entering different URL addresses in the address bar of the browser, different routing rule objects, that is, routing record information, are presented on the console.

http://localhost:8080/#/about/users

Enter the above address, which is the address of the child route, and finally output the record information of the corresponding parent route.

Later, you can obtain specific components for rendering.

8. Set responsive_ route

The next thing we need to do is render components.

Here, we first create a responsive attribute related to routing. When the routing address changes, the corresponding attribute will also change, so as to complete the re rendering of the page.

Add the following code to the install.js file:

        Vue.util.defineReactive(this, "_route", this._router.history.current);

The above completes the creation of responsive attributes, but it should be noted that the defineReactive method is the internal method of Vue. It is not recommended to create responsive objects through this method at ordinary times.

  beforeCreate() {
      //Judge whether it is an instance of Vue. If the condition is true, it is an instance of Vue. Otherwise, it is other corresponding components (because options will be passed when creating Vue instances)
      if (this.$options.router) {
        //By viewing the source code, it is found that the Vue instance will be attached to the current private property_ On the routerRoot property
        this._routerRoot = this;
        this._router = this.$options.router;
        //Call the init method defined in the index.js file
        this._router.init(this);
          
          
        //Create a responsive property on an instance of Vue`_ route`.
        Vue.util.defineReactive(this, "_route", this._router.history.current);
      } 

The following consideration is that when the routing address changes, it needs to be modified_ The value of the route property.

Where_ How to modify the value of route attribute?

In the base.js file, because the transitionTo method is defined in the file, and this method is used to complete the address jump and the rendering of components at the same time.

The modified code of base.js file is as follows:

import createRoute from "../util/route";
export default class History {
  // router routing object ViewRouter
  constructor(router) {
    this.router = router;
    this.current = createRoute(null, "/");
    //This callback function is assigned in hashmemory to change the value on the vue instance_ routeļ¼Œ_ When the value of route changes, the view will be refreshed
    this.cb = null;
  }
  //Assign value to cb
  listen(cb) {
    this.cb = cb;
  }

  transitionTo(path, onComplete) {
   
    this.current = this.router.matcher.match(path);
    // Call cb
    this.cb && this.cb(this.current);
    // console.log("current===", this.current);

    //The callback function will be passed when calling the transitionTo method.
    onComplete && onComplete();
  }
}

Initialize the cb function in the constructor in History.

  this.cb = null;

Define the listen method to assign a value to the cb function.

//Assign value to cb
  listen(cb) {
    this.cb = cb;
  }

In the transitionTo method, calling the cb function and passing the current routing rule object is also the routing record information.

  this.cb && this.cb(this.current);

Where do I call the listen method?

Complete the call of the listen method in the init method in the index.js file.

// Register the event of route change (initialize the event listener and listen for the change of route address).
  init(app) {
    const history = this.history;
    const setUpListener = () => {
      history.setUpListener();
    };
    history.transitionTo(
      history.getCurrentLocation(),
      //If you directly use history.setUpListener
      // In this case, there will be a problem with this in setUpListener.
      setUpListener
    );
    //Call the listen method in the parent class
    history.listen((route) => {
      app._route = route;
    });
  }

In the above code, the listen method in the parent class is invoked, and the arrow function is passed to listen.

At this time, cb is invoked in the transitionTo method, that is, calling the arrow function. The passed parameter route is passed to the app in the current routing rule information. Route attribute.

app is actually an instance of Vue, because the init method is invoked in the install.js file and the instance of Vue is passed.

This completes the responsive properties on the Vue instance_ The modification of the route value will update the component.

9. $route / $route create

The purpose of creating $route and $route is to obtain the data in all Vue instances (components).

$route is a routing rule object, including path,component, etc

$router is a routing object (ViewRouter object).

By looking at the source code (install.js), you can find that $router and $route are actually mounted on the Vue prototype.

So you can copy the source code directly.

 Object.defineProperty(Vue.prototype, "$router", {
    get() {
      return this._routerRoot._router;
    },
  });

  Object.defineProperty(Vue.prototype, "$route", {
    get() {
      return this._routerRoot._route;
    },
  });

Through the above code, you can see that both $route and $route are read-only, because the corresponding values have been set previously, and here they are just obtained.

$router is through_ routerRoot.

$route is through_ routerRoot._route to get.

Vue.util.defineReactive(this, "_route", this._router.history.current);

Created on Vue object_ route attribute. The value of this attribute is the content of the routing rule

10. Route view creation

Router view is a placeholder, which will be replaced by specific components.

The creation process of router view is as follows:

  • Gets the $route routing rule object of the current component
  • Find the matched record (with component) in the routing rule object
  • If it is / about, matched matches a record and directly renders the corresponding component
  • If it is / about/users,matched matches two record s (the first is the parent component and the second is the child component)

The code of view.js in my Vue router / components directory is as follows:

export default {
  render(h) {
    //Gets the currently matching routing rule object
    const route = this.$route;
    //Get the routing record object. There is only one content, so the first item in 'matched' is obtained.
    const record = route.matched[0];
    if (!record) {
      return h();
    }
    //Get the corresponding component in the record
    const component = record.component;
    return h(component);
  },
};

The above code deals with the case where there is no sub route.

Next, let's take a look at the processing of routing.

Of course, before writing the sub route processing code, let's improve the route in the case.

Add an "about" link to App.vue in src directory.

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link>
      <router-link to="/login">Login</router-link>
      <router-link to="/about">About</router-link>
    </div>
    <router-view></router-view>
  </div>
</template>

<script>
export default {};
</script>

Corresponding to the About component, the sub routing application is completed

<template>
  <div>
    About components
    <router-link to="/about/users">user</router-link>
    <router-view></router-view>
  </div>
</template>

<script>
export default {};
</script>

<style>
</style>

Let's improve the processing of sub routes.

export default {
  render(h) {
    //Gets the currently matching routing rule object
    const route = this.$route;
    let depth = 0;
    //Record the current component as RouterView
    this.routerView = true;
    let parent = this.$parent;
    while (parent) {
      if (parent.routerView) {
        depth++;
      }
      parent = parent.$parent;
    }
    //Gets the routing record object
    // If it is a sub route, for example: sub route / about/users
    //The child route has two parts, matched[0]: the parent component content and matched[1] the child component content
    const record = route.matched[depth];
    if (!record) {
      return h();
    }
    //Get the corresponding component in the record
    const component = record.component;
    return h(component);
  },
};

Suppose, now we enter in the address bar of the browser: http://localhost:8080/#/about Address,

If there is no parent component, the value of depth attribute is 0. At this time, the first component obtained is then rendered.

If the address bar reads: http://localhost:8080/#/about/users There are subcomponents at this time. Get the corresponding parent component content and start the cycle.

During the loop, a judgment is made on the condition that the current parent component must be a RouterView component (the RouterView in the sub component and the RouterView in the parent component form a parent-child relationship), then add 1 to depth, and then take out the sub component for rendering.

Posted by mbdonner on Sun, 26 Sep 2021 03:51:18 -0700