preface

In this paper, according to vue-Router source ideas handwritten a “copycat version” vue-Router, using hash mode to write code, mainly to achieve some core features, there is time to continue to iterate code.

Analyze it according to the use mode of vue-router

We’ll import the vue-router package and use vue.use (VueRouter) to indicate that vue-Router is a plug-in, so it should naturally have an install() method. The user can then configure a routing table object and pass in the routing table object as a parameter to instantiate the VueRouter, passing the resulting instantiation object to the options of the Vue root instance.

import VueRouter from 'vue-router';
import Vue from 'vue';

import Home from '.. /views/Home';
import Setting from '.. /views/Setting';

const routes = [
    {
        path: '/'.component: Home,
    },{
        path: '/index'.component: Home,
        children: [{
            path: 'a'.component: {
                render: h= > h('p'.'aa')]}}}, {path: '/setting'.component: Setting,
    }
];

let router = new VueRouter({
    routes,
    mode: 'hash',
})

Vue.use(VueRouter);

export default router;
Copy the code
import Vue from 'vue';
import App from './App.vue';
import router from './router/index'

window.vm = new Vue({
    el: '#app'.render: h= > h(App),
    router,
})
Copy the code

We can use some of the routing components on the template: router-link, router-view, indicating that vue-router also registers global components.

<template>
    <div>
        <router-link to="/index">Home page</router-link>
        |
        <router-link to="/setting">Set up the</router-link>

        <router-view></router-view>
    </div>
</template>
Copy the code

In addition, we can also access $router and $route from any component, so these two attributes should be put on the prototype of Vue and read only, so should be proxyed to the prototype of Vue using Object.defineProperty ().

console.log(this.$router);
console.log(this.$route);
Copy the code

Implementation principles of front-end routing

Front-end routing mainly refers to the change of URL address, but does not need to refresh the whole page to achieve insensitive refresh of local pages, users feel that they are in two different pages, but in fact they are on the same page. We need to consider two questions:

  1. Ensure that the URL is changed but the page cannot be refreshed;
  2. How to listen for URL changes.

There are generally two modes to implement, hash and history. Hash mode is used to switch routes by changing the hash value after the # sign in the URL, because changing the hash value in the URL does not cause a page refresh. Control page component rendering by listening for hash changes through the Hashchange event. History provides pushState and replaceState methods, which can change the path of a URL without causing a page refresh, and a popState event to monitor route changes. However, popState events are not triggered when changed like hashchange.

  • Changing the URL while moving forward or backward through the browser triggers a PopState event
  • Js invokes historyAPI methods such as back, Go, and Forward to trigger the event

Vue – the router plug-in

This is the basic structure of the VUe-Router plug-in.

import install from "./install";
class VueRouter{
    constructor(options){
        let routes = options.routes || [];
    }
}

VueRouter.install = install;

export default VueRouter
Copy the code

install()

Vue.use(VueRouter) will first call VueRouter’s install method and pass in Vue as an argument. The global components router-link and router-view are registered here. Using vue. mixin ensures that each component has access to the _routerRoot attribute, which is the root node. Since the initial router is placed on the root instance option, we need the root instance to make each component have access to the Router. Using VUe’s parent before child rendering principle, each component can access _routerRoot by having the child get the parent’s _routerRoot property.

import RouterLink from './components/router-link'
import RouterView from './components/RouterView'

export let Vue = null;

const install = function(_Vue){
    The install method usually defines some global content inside, such as global components, global directives, global mixins, and vUE prototype methods
    Vue = _Vue;

    Vue.component('router-link', RouterLink);
    Vue.component('router-view', RouterView);

    // Make the router available to each component
    Vue.mixin({
        beforeCreate(){
            // The initial router is placed on the root instance option, so the initial router can be accessed by the root instance
            if(this.$options.router){
                / / root
                this._routerRoot = this;
                this._router = this.$options.router;

                this._router.init(this);    // Pass in the root instance to invoke the plug-in's initial method
            }else{
                / / son
                this._routerRoot = this.$parent && this.$parent._routerRoot;
            }
            // Finally, all components can access the root instance through the attribute _routerRoot}})}export default install;
Copy the code

The router – link component

This component is used to redirect routes. To specifies the destination location, and tag indicates what tag the component needs to render. In effect, you make use of the $router.push() jump method, which can be used in any valence.

export default { name: 'router-link', props: { to: { type: String, required: true, }, tag: { type: String, }, }, render(h){ let tag = this.tag || 'a'; return h(tag, { on: { click: this.clickHandler, }, attrs: { href: this.$router.mode == 'hash' ? "#"+this.to : this.to, } }, this.$slots.default); }, methods: { clickHandler(e){ e.preventDefault(); this.$router.push(this.to); }}}Copy the code

The router – the view components

Router – the view component is just a placeholder, don’t need to use the data, do not need to use the life cycle, do not need to instantiate the component, so consider using functional components to implement the components, functional components don’t need to instantiate the component constructor function, no, no this, no life cycle data to the data, so the better the performance of the functional components.

export default {
    name: 'router-view'.functional: true.render(h, context){
        // context: functional component rendering context
        // This.$route () {// This.$route () {// This.
        
        let {parent, data} = context;
        // parent is the current parent
        // Data are some identifiers on this component
        let route = parent.$route;
        let depth = 0;

        data.routerView = true;     // Identify the route attributes, and then the current component's $vNode.data.RouterView is true

        // Find the level at which the router-View is located
        while(parent){
            if(parent.$vnode && parent.$vnode.data.routerView){
                depth++;
            }
            parent = parent.$parent;
        }
        // Notice the difference between $vnode and _vnode
        
        
        let record = route.matched[depth];
        if(! record){return h();
        }
        return h(record.component, data)
    }
}
Copy the code

$routerand$routeattribute

const install = function(_Vue){...// Use defineProperty to ensure that it is read-only
    Object.defineProperty(Vue.prototype, '$route', {
        get(){
            return this._routerRoot._route;     // Record of the current match}})Object.defineProperty(Vue.prototype, '$router', {
        get(){
            return this._routerRoot._router;    // Route instance}})}Copy the code

init()

The root instance will then be passed as a parameter to _router.init(VM) in the beforeCreate hook of the root instance, because we will use the root instance when we initialize the VueRouter. In this case, the page is jumped to the initial location of the route, and the root instance is added with the _route attribute. When the current route object is changed, the app._route will also be updated, and the route listening is set. After the root instance is destroyed the fallback outlet is monitored by the listener.

class VueRouter{
    constructor(options){
        let routes = options.routes || [];

        // Create history management (two modes: hash, history)
        this.mode = options.mode || 'hash';

        switch(this.mode){
            case 'hash':
                this.history = new HashHistory(this);
                break;
            case 'history':
                this.history = new BrowserHistory(this);
                break;
        }

    init(app){      // This app refers to the root instance
        // We need to implement the initial page jump logic according to the current path

        const history = this.history;
        // The jump path will perform matching operations to obtain corresponding records based on the path

        let setupHashListener = () = > {
            history.setupListener();
        }
        // Redirect the path to monitor
        history.transitionTo(history.getCurrentLocation(), setupHashListener())

        history.listen((route) = > {
            app._route = route;
        })

        // transitionTo jump logic hash \ browser has both
        // getCurrentLocation hash \ browser implementation is different
        // setupListener hash listener

        app.$once('hook:destroy'.this.history.teardown); }}Copy the code

new VueRouter()

When you instantiate the VueRouter, you only do two things:

  • Create matcher;
  • Create a historical managed object.

The routing table object passed by the user will generate a routing mapping table. The routing table is only convenient for users to use, and the routing mapping table is what we really need. If the user uses nested routines, the routing table provided by the user will be a tree structure. For us, we need to further transform the routing table into {key: value} structure mapping table.

class VueRouter{
    constructor(options){
        let routes = options.routes || [];

        Add the addRoutes privilege. Add the addRoutes privilege
        this.matcher = createMatcher(routes);

        // Create history management (two modes: hash, history)
        this.mode = options.mode || 'hash';

        switch(this.mode){
            case 'hash':
                this.history = new HashHistory(this);
                break;
            case 'history':
                this.history = new BrowserHistory(this);
                break;
        }

        this.beforeHooks = []; }}Copy the code

createMatcher()

Basically, you generate a pathList array from the routing table object, as well as a pathMap object.

export default function createMatcher(routes){
    / / an array a pathList will put all of the routing of ['/', '/ idnex', '/ index/a', '/ setting]
    // pathMap {/: {}, /index: {}, /setting: {}}
    // pathList, pathMap constitutes a closure reference
    let {pathList, pathMap} = createRouteMap(routes);

    // Route matching The matching record is obtained through the path entered by the user
    function match(location){
        let record = pathMap[location];     // Get the corresponding record
        return createRoute(record, {
            path: location
        })
    }

    // Add routes dynamically (the parameter must be an array that matches the routes option)
    function addRoutes(routes){
        // Parses the incoming new routing object and adds it to the old pathMap object
        createRouteMap(routes, pathList, pathMap)
    }

    // Dynamically add routes (add a new routing rule)
    function addRoute(){}// Get a list of active routing records
    function getRoutes(){
        return pathMap;
     }

    return {
        match,
        addRoutes,
        addRoute,
        getRoutes,
    }
}
Copy the code

When createRouteMap() is used, it can also accept oldPathList\oldPathMap parameters, which are used to facilitate dynamic route addition. If these parameters are not passed, the route mapping table is initialized by default. In the case of nested routes handled by addRouteRecord(), it traverses the child routes and then recursively finds the parent route using the parent property on record.

const addRouteRecord = function(route, pathList, pathMap, parentRecord){
    let path = parentRecord ? `${parentRecord.path}/${route.path}` : route.path;
    // Generates a record based on the current route
    let record = {
        path,
        component: route.component,
        parent: parentRecord,
    }

    // Prevent users from writing routes that duplicate
    if(! pathMap[path]){ pathMap[path] = record; pathList.push(path); }// Put the child path into the corresponding pathList,pathMap
    if(route.children){
        route.children.forEach(r= > {
            addRouteRecord(r, pathList, pathMap, record)
        })
    }
}

export function createRouteMap(routes, oldPathList, oldPathMap){
    let pathList = oldPathList || [];
    let pathMap = oldPathMap || [];
    routes.forEach(route= > {
        addRouteRecord(route, pathList, pathMap);
    });

    return {
        pathList,
        pathMap,
    }
}
Copy the code

The history object

For user convenience, no matter which pattern is used to implement history management, it should be guaranteed that most of their methods are the same, just the implementation is different. The common logic is written to a parent class, and the subclasses implement their different logic. For example, the transitionTo() method is written to the parent class, while getCurrentLocation() and setupListener() methods are implemented differently in each subclass.

Base the parent class

Current is the core of the entire VueRouter, and it will be set as responsive data to update the page.

export default class History{
    constructor(router){
        this.router = router;

        {path: '/', matched: []} (vue-router core)
        this.current = createRoute(null, {
            path: '/'})}// Basically do three things: 1. 2. Update url. 3. Update component rendering
    transitionTo(location, complete){
        // Get the corresponding record through the path
        let current = this.router.match(location)

        // The number of matches and paths are the same, there is no need to jump again
        if(this.current.path === location && this.current.matched.length === current.matched.length){
            return;
        }

        // Update the view with the latest matching result
        this.current = current;         // This current only changes the current. Its changes do not update _route
        this.cb && this.cb(current);    // In cb, app._route will be changed, which will trigger the response, and the view will be updated

        // The current property is updated when the path changes
        complete && complete();
    }

    listen(cb){
        this.cb = cb; }}Copy the code

Create a full match object based on the path and the current record, mainly to deal with nested routing cases, it needs to find the parent route record based on the current record. For example, path /setting/user, you can’t just render the component corresponding to the child path user, you must also find the component corresponding to the setting route.

export const createRoute = (record, location) = > {
    let matched = [];
    if(record){
        while(record){
            matched.unshift(record);
            record = record.parent; // Find all fathers from the current record}}return {
        ...location,
        matched
    }
}
Copy the code

HashHistory does two things: get the current location; Listen for route changes.

// Make sure the URL address has a hash value of '/'
const ensureSlash = () = > {
    if(window.location.hash){
        return;
    }
    window.location.hash = '/';
}

export default class HashHistory extends History{
    constructor(router){
        super(router);
        this.router = router;

        this.teardownListenerQueue = [];

        ensureSlash();
    }
    getCurrentLocation(){
        return window.location.hash.slice(1);
    }
    setupListener(){
        const hashChagneHandler = () = > {
            console.log('the hash change')
            // Perform the match again
            this.transitionTo(this.getCurrentLocation())
        };
        window.addEventListener('hashchange', hashChagneHandler);
        this.teardownListenerQueue.push(() = > {
            window.removeEventListener('hashchange', hashChagneHandler); })}// Unload (easy garbage collection)
    teardown(){
        this.teardownListenerQueue.forEach(fn= > {
            fn();
        });
        this.teardownListenerQueue = [];
    }

    push(location){
        window.location.hash = location; }}Copy the code

Update the page

Page updates are as simple as setting the _route attribute to responsive data in the root instance and using VUE’s responsive system to implement page updates. The rendering watcher collects deP dependencies for _route. When this dependency changes, notify() the dependency to notify the corresponding Watcher execution, including the rendering Watcher.

const install = function(_Vue){
    Vue.mixin({
        beforeCreate(){
            if(this.$options.router){
                / / root
                this._routerRoot = this;
                this._router = this.$options.router;

                this._router.init(this);    // Pass in the root instance to invoke the plug-in's initial method

                Vue.util.defineReactive(this.'_route'.this._router.history.current)
            }else{
                / / son
                this._routerRoot = this.$parent && this.$parent._routerRoot; }}})... }Copy the code

Routing hooks

There is only one hook implemented here, so this is the only one described. Put all hooks in the beforeHooks array and the hooks will execute in sequence.

class VueRouter{
    constructor(options){...this.beforeHooks = []; }...beforeEach(fn){
        this.beforeHooks.push(fn); }}Copy the code

Overrides the transitionTo method in the route parent class, completing the beforeEach hook function before implementing a route jump. RunQueue () can be understood as a closure and loop that only executes the next hook after the current one. This is why routing hooks beforEach() must call the next() method, it doesn’t go down if you don’t call next().

transitionTo(location, complete){...// Call the hook function
        let queue = this.router.beforeHooks;
        const iterator = (hook, next) = > {
            hook(current, this.current, next);
        }
        const runQueue = (queue, iterator, complete) = > {
            function next(index){
                if(index >= queue.length){
                    return complete();
                }
                let hook = queue[index];
                iterator(hook, () = > {
                    next(index+1);
                })
            }
            next(0);
        }
        runQueue(queue, iterator, () = > {

            // Update the view with the latest matching result
            this.current = current;         // This current only changes the current. Its changes do not update _route
            this.cb && this.cb(current);    // In cb, app._route will be changed, which will trigger the response, and the view will be updated

            // The current property is updated when the path changescomplete && complete(); })}Copy the code

The effect

Project address: Mini-VUe-Router