Vue.js framework source code and advancement (1) --- Vue-Router principle implementation

1. Basic review of Vue-Router

(1) Basic use

  1. Register routing plug-in
  2. Create routing instance
    router/index.ts
// 1. 注册路由插件
Vue.use(VueRouter)

const routes: Array<RouteConfig> = [
  {
    
    
    path: '/',
    name: 'home',
    component: HomeView
  },
  {
    
    
    path: '/about',
    name: 'about',
    // 使用import动态懒加载组件,提升首页渲染效率
    component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
  }
]

// 2. 创建路由实例
const router = new VueRouter({
    
    
  routes
})
  1. When creating Vuean instance, hang it in the routing instance
    main.ts.
// 3. 创建 Vue 实例并挂载路由实例
new Vue({
    
    
  router,
  render: h => h(App)
}).$mount('#app')

routerThe act of passing it to Vuethe constructor of an instance will cause the Vueinstance to hang on to the two objects
Insert image description here

  • $route: Route matching rules
  • $router: Routing instance object, you can call push, goand other methods to change the current route. $router.currentRouteThe attribute points to the current route matching rule:
console.log(myVue.$route===myVue.$router.currentRoute) // true
  1. Create a route placeholder
  2. Create routing jump link
    APP.vue
<nav>
  <!--5.创建路由跳转链接-->
  <router-link to="/">Home</router-link> |
  <router-link to="/about">About</router-link>
</nav>
<!--4.创建路由占位符-->
<router-view/>

(2) Dynamic routing

VueDynamic routing in refers to the fact that dynamic parameter transfer can be used to achieve jumps in the routing path url. At this time, the routing urlpath is not hard-coded but variable. Definition and value transfer of dynamic routing:
router/index.ts

{
    
    
	 path: '/about/:id', // :id起占位符的作用
	 name: 'about',
	 component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
}

APP.view

<router-link to="/about/89">About</router-link>

There are two ways to receive dynamic parameters inside a route:

  1. Use $router.params
    view/About.vue
<h2>id: {
   
   { $route.params.id }}</h2>

Insert image description here
One disadvantage of this approach is that it can lead to over-reliance $route.
2. Use propsreceiving
router/index.ts

{
    
    
  path: '/about/:id',
  name: 'about',
  props: true,
  component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
}

views/About.vue

<h2>id: {
   
   { id }}</h2>
<script>
export default {
      
      
  name: 'AboutView',
  props: {
      
      
    id: String,
  },
};
</script>

Insert image description here
Not passing dynamic parameters at this time will cause the page to be blank. Adding it later
indicates that this attribute is optional. If this parameter is not passed, other parts of the routing component can also render router/index.ts normally.:id?

path: '/about/:id?',

(3) Nested routing

Nested routing is used in scenarios where components are nested at multiple levels. As shown in the figure below, /user/johnnycorresponding to a component, it also contains two components that can be switched and displayed. The routes corresponding to these two components can be defined as /user/johnnysub-routes .
Insert image description here
The sub-routes are defined using childrenthe attributes of the route. childrenThe attributes point to an array. The array The objects in have attributes such as and , pathwhich represent the path of the sub-routing. There are two ways of writing:componentpath

  • One is the absolute path, which is defined starting from the root directory and needs to be added at the beginning /;
  • One is the relative path, which defines the path relative to the parent route. The path of the parent route and the relative path will eventually be spliced ​​together as the path of the current subcomponent. It cannot be added at the beginning /.
    router/index.ts
{
    
    
    path: '/about',
    name: 'about',
    component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue'),
    children: [
        {
    
    
            path: '/about/company', // 绝对路径
            // path: 'company' // 相对路径
            name: 'company',
            component: () => import(/* webpackChunkName: "about" */ '../views/CompanyView.vue')
        }
    ]
}

Define routing jump links and placeholders in the component template of the parent component
views/AboutView.vue

<router-link to="/about/company">company</router-link>
<router-view></router-view>

Insert image description here

(4) Programmatic navigation

In addition to using alinks to implement route jumps, you can also use $routermethods on objects to implement route jumps. There are three main methods used:

  • push(). It accepts two parameters. Parameter one can be written in two ways. The first is a path, and the second is an object. The object contains the pathattributes of the route or namethe attributes of the named route. Parameter two is used to pass parameters for the route.
  • replace(). The usage is similar to push()the method, but this history will not be recorded, and the routing history after the jump will replace the current routing history. That is to say, after the jump, you cannot go back to the current route through the browser's back button.
  • go(). This method takes an integer as a parameter, indicating how many steps forward or backward in the history stack, similar to window.history.go(n).
    App.vue
<template>
  <div id="app">
    <router-view/>
    <button @click="goHome()">go HomeView</button>
    <button @click="goAbout()">go AboutView</button>
    <button @click="back()">回退</button>
  </div>
</template>
<script>
export default {
      
      
  name: 'App',
  methods: {
      
      
    goHome() {
      
      
      this.$router.push('/');
    },
    goAbout() {
      
      
      this.$router.replace({
      
       name: 'about' });
    },
    back() {
      
      
      this.$router.go(-1);
    },
  },
};
</script>

2. History mode

(1) The difference between Hash mode and History mode

Both are front-end routes, changing the page rendering content through path changes.

  • HashThe pattern is based on anchors and onhashchangeevents, and front-end anchors can be implemented through HTMLelement and link hrefattributes. When the path changes, the page rendering content is determined based on the path. The following data Hashin the pattern represents the routing configuration#
  • HistoryThe pattern is based on HTML5medium History APIand mainly uses pushState()methods and replaceState()methods. pushState()The method does not send a request to the server, it only changes urlthe address and records the address in the history.
<template>
  <div id="app">
    <router-view/>
    <button @click="goHome()">go HomeView</button>
    <button @click="goAbout()">go AboutView</button>
    <button @click="back()">回退</button>
  </div>
</template>
<script>
export default {
      
      
  name: 'App',
  methods: {
      
      
    goHome() {
      
      
      this.$router.push('/');
    },
    goAbout() {
      
      
      this.$router.replace({
      
       name: 'about' });
    },
    back() {
      
      
      this.$router.go(-1);
    },
  },
};
</script>

(2) History working mode

HistoryIn mode, urlthe path is an ordinary address: https://www.teambition.com/project. For single-page applications, the server only saves htmlthe resources of the homepage. If the server does not have the corresponding configuration, accessing the server with such an address will not find the resource; therefore, the server needs to be configured accordingly to support the Historymode. When the server finds When the requested resource is not available, the static file of the home page is returned to the client, and the client parses the page based on the current routing address to implement page jump.

  • For nodethe server, historymiddleware needs to be configured
  • For nginxthe server, you need to modify nginx.confthe configuration file location /and add a line of code in: try_files $uri $uri/ /index.html; It means trying to load the resource corresponding to the requested path on the server. If it cannot be found, use the address as the folder and search down index.html; if it still If it is not found, it returns to the root file in the website index.html.

3. Vue Router implementation

(1) Working principle of Vue Router

  1. HashHow the pattern works
  • #The last string in the address bar represents the routing address. When only #the last address changes, no request will be sent to the server, and the new address will be saved in the history.
  • onhashchangeThe method will monitor changes in routing addresses.
  • Inside onhashchangethe method, the corresponding component will be found based on the routing address and the page will be rendered.
  1. Historyworking principle
  • When clicking a routing jump link, History APIthe pushState()method adds a new record to the browser history stack and changes the current one without changing the page URL.
  • When the user clicks the browser's back button or calls a go()method, popStatethe method will be triggered, the current address will be modified, and the execution of the method Vue-Routerwill be monitored popStateto obtain the current routing information.
  • Find the corresponding component according to the current routing information for page rendering.

(2) Vue Router analysis

  1. Let’s first take a look Vue Routerat the usage and analyze Vue Routerthe types of
// 1. 注册路由插件
Vue.use(VueRouter)
// 2. 创建路由实例
const router = new VueRouter({
    
    
    routes
})
  • Register Vue Router usage Vue.use()method. This method is used to register plug-ins. There are two parameter types that can be received. One is a function, which will be executed directly; the other is a class, which will execute the static install()method of the class.
  • Vue RouterYou can use newthe keyword to create an instance object, accepting an object as a parameter.
  • So Vue Routerit's a class, the constructor receives an object as a parameter, the object has a property routes; and Vue Routerthe class has a static method install.
  1. Vue RouterThe object needs to have three properties:
  • optionsSave incoming routing rules
  • routeMapSave the corresponding relationship between routes and components as a mapping relationship
  • dataTo Vue.observableimplement two-way data update, it needs to be a responsive object defined
  1. According to Vue Routerthe function of the object, it can be divided into four methods:
  • initEvent()Register popStatean event listener for subsequent processing when routing changes
  • createRouteMap()initializationrouteMap
  • initComponents()Initialization router-linkand router-viewcomponents
  • init()Execute the above three methods
    Insert image description here

(3) Install() method implementation

Create a new src/vuerouter/index.jsfile and export Vue Routera class
Insert image description here

export default class VueRouter {
    
    
	static install(){
    
    }
}

install()Functions that the method needs to implement:

  1. To determine whether the current plug-in has been installed, you need to give install()the method an attribute to identify it. When install()the method is executed, this attribute will change true.
if(VueRouter.install.installed) return;
VueRouter.install.installed = true;
  1. Receive Vuethe constructor as a parameter and Vuerecord the constructor as a global variable because you need to use Vuethe methods on the class later, such asVue.component()
let _Vue = null;
export default class VueRouter {
    
    
    //接受Vue的构造函数作为参数
    static install(Vue) {
    
    
        // 1. 判断当前插件是否已经被安装,如果已经安装则不需要重复安装
        if(VueRouter.install.installed) return;
        VueRouter.install.installed = true;
        // 2. 把Vue构造函数记录到全局变量
        _Vue = Vue;
    }
}

Why accept Vuea constructor as a parameter? You can take a look at install()the source code definition of the method:

// router.d.ts
static install: PluginFunction<never>
// plugin.d.ts
export type PluginFunction<T> = (Vue: typeof _Vue, options?: T) => void
// vue.d.ts
export const Vue: VueConstructor

Being able to know what a function looks like so easily is TypeScripta very powerful place.

  1. Let’s review Vuethe code that creates the instance:
const myVue = new Vue({
    
    
  router,
  render: h => h(App)
}).$mount('#app')

At this time, Vuean object is passed to the constructor router, and install()the method needs to routerinject the object into Vuethe instance. What is currently passed in is Vuethe constructor. To Vueadd attributes to the instance, you need to mount the attributes on the prototype chain, and Vuethe instance object can inherit Vuethe attributes on the constructor in a chain. VueParameters passed during instance construction are saved in Vuethe instance's $optionsproperties. But at install()the time of execution, we don't know Vuewho the instance is yet, so we need to delay this operation and perform it during the life cycle Vueof the instance beforeCreate(). So you need to use mixin here to beforeCreate()mix the method into each Vueinstance object. VueMoreover, this method can be accessed on all instance objects as long as it is executed once, so it only needs to be executed once in the myAppapplication and does not need to be executed repeatedly in the beforeCreate()subsequent component life cycle. VueThe mixed hook function will be executed before the hook function of the instance itself.

_Vue.mixin({
    
    
    beforeCreate() {
    
    
    	// 实例创建过程即new Vue()的时候有传递router参数,证明当前Vue实例是应用
        if(this.$options.router) {
    
    
        	// this指向当前Vue实例对象
            _Vue.prototype.$router = this.$options.router;
        }
    }
})

(4) constructor constructor

constructorThe constructor needs to initialize three properties options: , routeMap, and . Code reviewed :data
new VueRouter()

const router = new VueRouter({
    
    
    routes
})

constructorThe constructor will accept an object as a parameter, which needs to be saved in the attribute options; routeMapit is a key-value pair object used to save the correspondence between the path and the component. In the initialization phase, you only need to initialize it to an empty object. The value is assigned later; datathe attribute is a responsive object, with an currentattribute pointing to the current routing address. The initialization phase is set to '/'. You need to use the method Vueprovided by the constructor observableto create the responsive object. When the responsive object changes, it will be automatically triggered. Rely on the functions in the component of this responsive object renderto automatically update the view.

constructor(options) {
    
    
    this.options = options;
    this.routeMap = {
    
    };
    this.data = _Vue.observable({
    
    
        current: '/'
    });
}

(5) createRouteMap() method

createRouteMap()The method is used to create routeMapthis variable, which stores the relationship between the routing path and the component in the form of a key-value pair, with the path as the key and the component as the value.

createRouteMap() {
    
    
    // 遍历所有路由规则,把路由规则解析成键值对的形式,存储到routeMap中
    this.options.routes.forEach(route => {
    
    
        this.routeMap[route.path] = route.component;
    })
}

(6) initComponent() method

initComponent()Methods are used to create router-linkcomponents and router-viewcomponents. How to use component registration Vue.component().

1.router-link

Let's first review router-linkthe use of: <router-link to="/">Home</router-link>. routerLinkThe component receives a parameter toand will eventually be rendered in athe form of a label, so the template is a alabel; athe content in the label is the content passed by the parent component when the component is used, so when defining it, you need to use the slot to first modify the label a. The content is placed in place. From the above analysis it can be defined routerLink:

Vue.component('router-link', {
    
    
    props: {
    
    
        to: String
    },
    template: '<a :href="to"><slot></slot></a>'
})

But there are still some problems with this way of definition. The problem lies in Vuethe build version. VueThe build versions are divided into runtime version and full version.

  • The full version includes a compiler that compiles templates at runtime. Therefore, it supports Vuethe use of attributes in components templateand is suitable for development environments.
  • The runtime version does not contain a compiler and cannot use templateattributes. However, it is more lightweight and can mostly HTMLwork by putting the template into and used in a production environment.

Vue-cliThe default is runtime Vue, so templateattributes are not supported. There are two solutions

  1. Use the full version Vue. Add to vue.config.jsthe configuration options of the fileruntimeCompiler: true,
  2. templateInstead of using a renderfunction. renderThe function receives a hfunction as a parameter and returns hthe result of function processing. hThe function is to create a virtual DOMand accept three parameters
    • Parameter 1: Virtual DOMselector (label selector a)
    • Parameter two: virtual DOMattributes ( hrefattributes, pointers this.to)
    • Parameter three: DOMan array composed of virtual sub-elements (there is only one sub-element: an anonymous slot, using the acquisition vueof the instance . The function is a method mounted on the instance, and internally points to the instance; if an arrow function is used, the point of point to the same, i.e. instance). Arrow functions cannot be used here to point to the created component.$slot.defaultrenderVuethisVuethisinitComponent()thisVueRouterthisrouterLink
Vue.component('router-link', {
    
    
    props: {
    
    
        to: String
    },
    render(h) {
    
    
        return h('a', {
    
    
            attrs: {
    
    
                href: this.to
            }
        }, [this.$slots.default])
    }
})
2.router-view

routerViewThe component needs to find the component corresponding to the current route and then render this component. routerMapThe corresponding relationship between routing and components is stored in the attribute. The dataattribute stores the current route, so you need to this.routeMap[this.data.current]obtain the component that currently needs to be rendered and pass the component to the h function.

Vue.component('router-view', {
    
    
	// 注意使用箭头函数
    render:(h) =>{
    
    
        const component = this.routeMap[this.data.current];
        // h函数会帮我们把组件转化为虚拟DOM
        return h(component);
    }
})

In router-link, we also need to improve the click event processing function.

  • When a link is clicked a, a request will be sent to the server by default to request athe resource pointed to by the link. Therefore, in the handler function of the click event, the default behavior should be prevented;
  • Let's review this.datathe attribute. It is a responsive object, in which currentthe attribute points to the routing address. When the data.currentattribute is modified, it will cause router-viewthe renderfunction to be re-executed. Because routerViewthe component depends on it , it triggers the view update, so the attributes this.data.currentof the current route need to be modified as , Let the view update automatically.data.currentthis.to
  • Not only do you need to modify the view, but you also need to modify the address bar address, so you need to call window.historya pushState()method to modify the address bar address.
Vue.component('router-link', {
    
    
    props: {
    
    
        to: String
    },
    render(h) {
    
    
        return h('a', {
    
    
            attrs: {
    
    
                href: this.to
            },
            on: {
    
    
                click: this.handleClick
            }
        }, [this.$slots.default])
    },
    methods: {
    
    
        handleClick(e) {
    
    
            // 改变current的值,触发组件的重新渲染
            // current是一个响应式的数据,所以会触发render函数的重新执行
            // render函数将会重新生成href指向this.to的a标签
            this.$router.data.current = this.to;
            // 通过pushState改变地址栏的地址,但是不会向后端发送请求
            // 参数一:state对象,可以在触发popstate事件时获取到
            // 参数二:title
            // 参数三:url
            history.pushState({
    
    }, '', this.to);
            // 点击a链接默认会向服务端发送请求去请求a链接对应的数据
            // 具体表现为页面会刷新,
            // 这里要阻止向服务端发送请求
            e.preventDefault();
        }
    }
})

In this way, the function of clicking the routing link to realize the routing jump has been generally realized, but there is still a problem. When clicking the forward and back buttons of the browser, only the address bar path changes, and the view is not refreshed.

3.initEvent()

When the browser's forward and back buttons are clicked, popState()the event listening function will be triggered, so page refresh needs to be handled in this method. In popState(), the address bar has been updated, so the routing address can be obtained directly from the address in the address bar. The value assigned to is this.data.current
window.locationan object containing the current URL information, which is Locationan instance of the object. This object has many properties and methods, the following are some commonly used ones:

  • location.href: Returns the complete URL.
  • location.protocol: Returns the protocol used by the current page (such as: http, https).
  • location.host: Returns the host name (domain name) and port number of the current page.
  • location.pathname: Returns the path part of the current page.
  • location.search: Returns the query string part of the current page.
  • location.hash: Returns the anchor part of the current page.
initEvent() {
    
    
    // 监听浏览器地址栏地址的变化
    window.addEventListener('popstate', () => {
    
    
        this.data.current = window.location.pathname;
    })
}

Don’t forget init()to call initEvent()functions within functions

init() {
    
    
    this.createRouteMap();
    this.initComponents(_Vue);
    this.initEvent();
}

VueRouterIntroduce your own router/index.js where introducedVueRouter

import VueRouter from '../vuerouter'

4. Complete code

vuerouter/index.js

let _Vue = null;
export default class VueRouter {
    
    
    //接受Vue的构造函数作为参数
    static install(Vue) {
    
    
        // 1. 判断当前插件是否已经被安装,如果已经安装则不需要重复安装
        if (VueRouter.install.installed) return;
        VueRouter.install.installed = true;
        // 2. 把Vue构造函数记录到全局变量
        _Vue = Vue;
        // 3. 把创建Vue实例时候传入的router对象注入到Vue实例上
        _Vue.mixin({
    
    
            beforeCreate() {
    
    
                // this是Vue实例
                if (this.$options.router) {
    
    
                    _Vue.prototype.$router = this.$options.router;
                    this.$options.router.init();
                }
            }
        })
    }

    // 构造函数
    constructor(options) {
    
    
        this.options = options;
        this.routeMap = {
    
    };
        this.data = _Vue.observable({
    
    
            current: '/'
        });
    }

    init() {
    
    
        this.createRouteMap();
        this.initComponents(_Vue);
        this.initEvent();
    }

    createRouteMap() {
    
    
        // 遍历所有路由规则,把路由规则解析成键值对的形式,存储到routeMap中
        this.options.routes.forEach(route => {
    
    
            this.routeMap[route.path] = route.component;
        })
    }

    initComponents(Vue) {
    
    
        // 将Vue实例传过来是为了减少方法与外部的依赖
        Vue.component('router-link', {
    
    
            props: {
    
    
                to: String
            },
            render(h) {
    
    
                return h('a', {
    
    
                    attrs: {
    
    
                        href: this.to
                    },
                    on: {
    
    
                        click: this.handleClick
                    }
                }, [this.$slots.default])
            },
            methods: {
    
    
                handleClick(e) {
    
    
                    // 改变current的值,触发组件的重新渲染
                    // current是一个响应式的数据,所以会触发render函数的重新执行
                    // render函数将会重新生成href指向this.to的a标签
                    this.$router.data.current = this.to;
                    // 通过pushState改变地址栏的地址,但是不会向后端发送请求
                    // 参数一:state对象,可以在触发popstate事件时获取到
                    // 参数二:title
                    // 参数三:url
                    history.pushState({
    
    }, '', this.to);
                    // 点击a链接默认会向服务端发送请求去请求a链接对应的数据
                    // 具体表现为页面会刷新,
                    // 这里要阻止向服务端发送请求
                    e.preventDefault();
                }
            }
        })
        // router-view起一个占位符的作用
        // 首先要在routeMap找到目标组件
        // 然后渲染组件
        Vue.component('router-view', {
    
    
            render:(h) =>{
    
    
                const component = this.routeMap[this.data.current];
                // h函数会帮我们把组件转化为虚拟DOM
                return h(component);
            }
        })

    }

    initEvent() {
    
    
        // 监听浏览器地址栏地址的变化
        window.addEventListener('popstate', () => {
    
    
            this.data.current = window.location.pathname;
        })
    }
}

Guess you like

Origin blog.csdn.net/weixin_45855469/article/details/131292709