This is the fourth day of my participation in the August More text Challenge. For details, see: August More Text Challenge

The following content involves functional components and rendering functions, it is recommended that students who are not familiar with the official document functional components, rendering functions

Create a components directory in the vRouter directory and create link.js and view.js.

RouterLink

// src/vRouter/components/link.js

export default {
  name: "RouterLink".props: {
    // Indicates the link of the destination route. When clicked, the interior immediately sends the value of to to router.push(), so the value can be either a string or an object describing the target location.
    to: {
      type: [String.Object].required: true,},tag: {
      type: String.default: "a",},event: {
      type: String.default: "click",},// After the append attribute is set, the base path is added before the current (relative) path. For example, we navigate from /a to a relative path b, which is /b if append is not configured, or /a/b if it is
    append: {
      type: Boolean.default: false,}},render(h) {
    const router = this.$router
    const current = this.$route
    // Use the resolve method of vue-router to get the destination location and href
    const { location, href } = router.resolve(
      this.to,
      current,
      this.append
    );
    
    // Override the router-link component's click event
    const handler = (e) = > {
      if (guardEvent(e)) {
        if (this.replace) {
          router.replace(location)
        } else {
          router.push(location)
        }
      }
    };

    const on = { 
      click: guardEvent,
      [this.event] : handler,
    }

    const data = {}

    if (this.tag === "a") {
      data.on = on;
      data.attrs = { href };
    } else {
      //todo else
    }
    return h(this.tag, data, this.$slots.default); }};// Set a click event guard to cancel the execution of the click event
function guardEvent(e) {
  if (e.preventDefault) {
    e.preventDefault();
  }
  return true;
}

Copy the code

Explain the code in the render method:

This.$router and this.$route can be used to get the route instance and the current route object from within the component.

Router.resolve () is used to resolve the location object and the href properties. Finally, a <a> tag is generated through the render function. And set the href attribute on the <a> tag.

The important thing to note here is that we must prevent the click event behavior of the <a> tag, so I define a guardEvent() method. The navigation is also used using the push, replace, and other methods provided by the Vue-Router object. Internally, the window.history. PushState method is used (window.location.hash is set if the browser does not support it).

Of course, the source code also covers the default style handling of the component. Those who are interested can do their own research.

The context argument to the Render method

Functional components, also known as stateless components, also have no lifecycle methods. In fact, it’s just a function that takes some prop. Everything the component needs is passed through the context parameter, which is an object containing the following key fields (not all listed) :

  • Props: provides all prop objects
  • Children: An array of VNode children
  • Slots: a function that returns an object containing all slots
  • Data: The entire data object passed to the component as the second argument to createElement
  • Parent: A reference to the parent component

So let’s go ahead and see how do we implement RouterView

RouterView

// src/vRouter/components/view.js

export default {
  name: "RouterView".functional: true.props: {
    name: {
      type: String.default: "default",}},// Use render to generate the virtual DOM. The vnode = render. Call (vm) _renderProxy, vm. $createElement method), defined in the vue source directory: SRC/core/instance/render. Js
  // So the first argument to render is the createElement function (h)
  // vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
  Render (h, {props, children, parent, data}) === render(h, context)
  // render(h, { props, children, parent, data }) {
  render(h, { props, parent, data }) {
    // Everything the component needs is passed through the context argument
    const name = props.name;
    const route = parent.$route;
    const depth = 0; // Do not consider the nested set
    const matched = route.matched[depth];
    const component = matched && matched.components[name];

    returnh(component, data); }};Copy the code

Because RouterView needs to know the view contents corresponding to the current location and have the ability to update the view. We use the second parameter, context, in the Render method.

With the context object we have the ability to access the state information of the parent component. Let’s examine the execution of the render function:

  1. You can use props. Name to obtain the name of the component passed by the parent component (default: default).
  2. Parent.$route is used to obtain the route object of the parent component. (figure 1)
  3. The matched array on the route object stores the information about the matched routing component, and matches the content of the component defined through it. (figure 2)
  4. Take the component content and create a virtual DOM with createElment method, and finally render the real DOM with Vue.

At this point our two global routing components are written.

Graph one:

Figure 2:

Add push, replace, and resolve methods to the Vue-Router object

Continue to improve the Vue-Router:

// src/vRouter/index.js

import { cleanPath } from "./util/path";
import { createMatcher } from "./create-matcher";
import { install } from "./install";
import { HashHistory } from "./history/hash";
import { normalizeLocation } from "./util/location";

export default class VueRouter {
  constructor(options = {}) {
    // Get the user's incoming configuration
    this.options = options;
    // this.app represents the root Vue instance
    this.app = null;
    //this.apps holds Vue instances for all child components
    this.apps = [];
    //createMatcher returns an object {match, addRoutes, getRoutes, addRoutes}
    this.matcher = createMatcher(options.routes || [], this);
    this.mode = options.mode || "hash";
    // Implement front-end routing in different modes
    switch (this.mode) {
      case "hash":
        this.history = new HashHistory(this, options.base);
        break;
      default:
        return new Error(`invalid mode: The ${this.mode}`); }}match(raw, current) {
    return this.matcher.match(raw, current);
  }

  push(location, onComplete) {
    this.history.push(location, onComplete);
  }

  replace(location, onComplete) {
    this.history.replace(location, onComplete);
  }

  init(app) {
    this.apps.push(app);
    // Only the root Vue instance will be saved to this.app
    if (this.app) {
      return;
    }
    // Save the Vue instance
    this.app = app;
    const history = this.history;
    if (history instanceof HashHistory) {
      // Add a route event listener function
      const setupListeners = () = > {
        history.setupListeners();
      };
      // Perform route transition
      history.transitionTo(history.getCurrentLocation(), setupListeners);
    }

    /** * register a function that takes a currentRoute as an argument. * Execute vue._route = currentRoute every time the route is switched. * This allows each vue instance to get a currentRoute and update the view */
    history.listen((route) = > {
      this.apps.forEach((app) = > {
        // Update _route on the app
        app._route = route;
      });
    });
  }

 // Parse the route
  resolve(to, current, append) {
    current = current || this.history.current;
    const location = normalizeLocation(to, current, append, this);
    const route = this.match(location, current);
    const fullPath = route.redirectedFrom || route.fullPath;
    const base = this.history.base;
    const href = createHref(base, fullPath, this.mode);
    return {
      location,
      route,
      href,
      normalizedTo: location,
      resolved: route, }; }}function createHref(base, fullPath, mode) {
  var path = mode === "hash" ? "#" + fullPath : fullPath;
  return base ? cleanPath(base + "/" + path) : path;
}

VueRouter.install = install;

Copy the code

The resolve() method is used to resolve routes. It takes three arguments:

  • To: indicates the link of the destination route. When clicked, the interior immediately sends the value of to to router.push(), so the value can be either a string or an object describing the target location.
  • Current: indicates the current route object
  • Append: After the append property is set, the base path is added before the current (relative) path. For example, we navigate from /a to a relative path b, which is /b if append is not configured, or /a/b if it is

Perfect hash. Js

The hash mode of vue-router inherits from the Hash class.

// src/vRouter/history/hash.js

import { History } from "./base";
import { pushState, replaceState, supportsPushState } from ".. /util/push-state";

export class HashHistory extends History {
  constructor(router, base) {
    super(router, base);
    ensureSlash();
  }

  setupListeners() {
    // Avoid repeated listening
    if (this.listeners.length > 0) {
      return;
    }
    Listen for browser forward, back, or HashChange events and maintain the History object */
    const handleRoutingEvent = () = > {
      this.transitionTo(getHash(), (route) = > {
        if (!supportsPushState) {
          replaceHash(route.fullPath);
        }
      });
    };
    const eventType = supportsPushState ? "popstate" : "hashchange";
    window.addEventListener(eventType, handleRoutingEvent);
    // Event listening must be unlistened
    this.listeners.push(() = > {
      window.removeEventListener(eventType, handleRoutingEvent);
    });
  }

  push(location, onComplete) {
    this.transitionTo(
      location,
      (route) = >{ pushHash(route.fullPath); onComplete && onComplete(route); }); }replace(location, onComplete) {
    this.transitionTo(
      location,
      (route) = >{ replaceHash(route.fullPath); onComplete && onComplete(route); }); }go(n) {
    window.history.go(n);
  }

  getCurrentLocation() {
    returngetHash(); }}function pushHash(path) {
  if (supportsPushState) {
    pushState(getUrl(path));
  } else {
    window.location.hash = path; }}function ensureSlash() {
  const path = getHash();
  if (path.charAt(0) = = ="/") {
    return true;
  }
  replaceHash("/" + path);
  return false;
}

export function getHash() {
  let href = window.location.href;
  const index = href.indexOf("#");
  if (index < 0) return "";
  href = href.slice(index + 1);
  return href;
}

// path = "/"
function replaceHash(path) {
  if (supportsPushState) {
    replaceState(getUrl(path));
  } else {
    window.location.replace(getUrl(path)); }}function getUrl(path) {
  const href = window.location.href;
  const i = href.indexOf("#");
  const base = i >= 0 ? href.slice(0, i) : href;
  return `${base}#${path}`;
}

Copy the code

Hash.js is the source code for hand-written Vue-router. Hash.js is the source code for hand-written Vue-router. Hash.js is the source code for hand-written Vue-router.

Navigation is implemented through <router-link>

By running the project check element, you can see that the router-link component is rendered as an <a> tag, and the to attribute is set to the href value of the <a> tag. The component content is displayed correctly. This means that we have successfully implemented the component function.

Let’s take a look at the GIF to show the routing switchover:

Next up

The next series will explore how navigational guards are implemented! Stay tuned… I wish all programmers have a romantic Tanabata! Ha ha

Vue-router source code

  • Handwritten VueRouter source series 1: implement VueRouter
  • Handwritten Vue Router source series two: matcher implementation
  • Hand-written VUe-Router source series three: implement changes to hash after updating the view
  • Handwritten Vue-Router source series four: implement global components router-link, router-view