By Dolymood, Didi public Front End team

In the era of single-page applications, routing has become an essential tool for developing applications; Throughout the framework, there will be a corresponding strong routing support. Vue.js has become the new favorite of the majority of front-end users because of its performance, general, easy to use, volume, low learning cost, and its corresponding routing vue-Router is also designed simple and easy to use, powerful function. This article from the source code to analyze the vue. js official route vue-Router overall process.

This paper mainly analyzes vue-Router version 2.0.3.

Let’s start with the big picture:

Vue – router. Js flow chart

To get a general impression of the whole process, let’s take examples/basic examples from the official warehouse to analyze the whole process.

The directory structure

Let’s look at the overall directory structure:

Vue-router Indicates the directory structure



components
history
create-matcher.js
create-route-map.js
index.js
install.js

The entrance

Let’s start with the code section of the application entry:

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

/ / 1. Plug-in
// Install 
      
        and 
       
         components
       
      
// Inject $router and $route objects into all components of the current application
Vue.use(VueRouter)

// 2. Define the components used in each route
const Home = { template: '<div>home</div>' }
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }

Create VueRouter instance router
const router = new VueRouter({
  mode: 'history'.base: __dirname,
  routes: [{path: '/'.component: Home },
    { path: '/foo'.component: Foo },
    { path: '/bar'.component: Bar }
  ]
})

// 4. Create a startup application
// Be sure to inject the router
// The routing component will be rendered in 
      
new Vue({
  router,
  template: ` 
      

Basic

  • /
  • /foo
  • /bar
  • /bar
`
}).$mount('#app')Copy the code

As a plug-in

The crucial first step in the code above is to install VueRouter using the plugins mechanism.use(plugin) provided by vuue.js. The plugin mechanism calls the install method of the plugin object (of course, the plugin itself is called as a function if the plugin doesn’t have the method). Let’s take a look at the implementation of vue-Router.

The VueRouter object, which is exposed in SRC /index.js, has a static install method:

/* @flow */
// Import the install module
import { install } from './install'
// ...
import { inBrowser, supportsHistory } from './util/dom'
// ...

export default class VueRouter {
// ...
}

Install / / assignment
VueRouter.install = install

// Automatically use plug-ins
if (inBrowser && window.Vue) {
  window.Vue.use(VueRouter)
}Copy the code

As you can see, this is a classic way to write a vue. js plug-in. Add install to the plugin object to install the plugin logic, and at the end of the day, the plugin will be automatically used if it is in a browser environment and window.Vue exists.

Install is a separate module here, continuing with the main logic of SRC /install.js at the same level:

// router-view Router-link component
import View from './components/view'
import Link from './components/link'

// export a Vue reference
export let _Vue

// Install the function
export function install (Vue) {
  if (install.installed) return
  install.installed = true

  // Assign a private Vue reference
  _Vue = Vue

  $router $route
  Object.defineProperty(Vue.prototype, '$router', {
    get () { return this.$root._router }
  })

  Object.defineProperty(Vue.prototype, '$route', {
    get () { return this.$root._route }
  })
  // beforeCreate mixin
  Vue.mixin({
    beforeCreate () {
      // Check whether there is a router
      if (this.$options.router) {
          / / assignment _router
        this._router = this.$options.router
        // Initialize init
        this._router.init(this)
        // Define a responsive _route object
        Vue.util.defineReactive(this.'_route'.this._router.history.current)
      }
    }
  })

  // Register the component
  Vue.component('router-view', View)
  Vue.component('router-link', Link)
// ...
}Copy the code

There’s a little bit of a question here, right?

  • Why export a Vue reference?

The plugin does not want to package the vue as a dependency package, but if you want to use some of the methods of the vue object itself, you can use a similar approach. This allows you to use some of Vue’s methods elsewhere without importing Vue dependencies (as long as you’re sure to use them after install).

  • By givingVue.prototypedefine$router,$routeProperties to inject them into all components?

All components in vue.js are extended Vue instances, which means that all components have access to properties defined on the instance prototype.

BeforeCreate mixin will be discussed later when creating Vue instances.

instantiationVueRouter

In the entry file, you instantiate a VueRouter and pass it into the Options of the Vue instance. Now look at the VueRouter class exposed in SRC /index.js:

// ...
import { createMatcher } from './create-matcher'
// ...
export default class VueRouter {
// ...
  constructor (options: RouterOptions = {}) {
    this.app = null
    this.options = options
    this.beforeHooks = []
    this.afterHooks = []
    // Create a match function
    this.match = createMatcher(options.routes || [])
    // Instantiate the specific History according to mode
    let mode = options.mode || 'hash'
    this.fallback = mode === 'history' && !supportsHistory
    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)
        break
      default:
        assert(false.`invalid mode: ${mode}`)}}// ...
}Copy the code

This includes an important step: creating the match function.

matchMatching function

The matching function is created by createMatcher in SRC /create-matcher.js:

/* @flow */

import Regexp from 'path-to-regexp'
// ...
import { createRouteMap } from './create-route-map'
// ...

export function createMatcher (routes: Array<RouteConfig>) :Matcher {
  // Create a route map
  const { pathMap, nameMap } = createRouteMap(routes)
  // Match function
  function match (raw: RawLocation, currentRoute? : Route, redirectedFrom? : Location) :Route {
// ...
  }

  function redirect (record: RouteRecord, location: Location) :Route {
// ...
  }

  function alias (record: RouteRecord, location: Location, matchAs: string) :Route {
// ...
  }

  function _createRoute (record: ? RouteRecord, location: Location, redirectedFrom? : Location) :Route {
    if (record && record.redirect) {
      return redirect(record, redirectedFrom || location)
    }
    if (record && record.matchAs) {
      return alias(record, location, record.matchAs)
    }
    return createRoute(record, location, redirectedFrom)
  }
  / / return
  return match
}
// ...Copy the code

The specific logic will be analyzed later. Now we only need to understand that the corresponding route map is generated based on the incoming routes configuration, and then the match matching function is directly returned.

SRC /create-route-map.js createRouteMap:

/* @flow */

import { assert, warn } from './util/warn'
import { cleanPath } from './util/path'

// Create a route map
export function createRouteMap (routes: Array<RouteConfig>) :{
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>
} {
  // path Route map
  const pathMap: Dictionary<RouteRecord> = Object.create(null)
  // name Route map
  const nameMap: Dictionary<RouteRecord> = Object.create(null)
  // Traverse the route configuration object to add a route record
  routes.forEach(route= > {
    addRouteRecord(pathMap, nameMap, route)
  })

  return {
    pathMap,
    nameMap
  }
}

// Add a route record function
function addRouteRecord (pathMap: Dictionary
       
        , nameMap: Dictionary
        
         , route: RouteConfig, parent? : RouteRecord, matchAs? : string
        
       ) {
  // Get path and name
  const{ path, name } = route assert(path ! =null.`"path" is required in a route configuration.`)
  // Route record object
  const record: RouteRecord = {
    path: normalizePath(path, parent),
    components: route.components || { default: route.component },
    instances: {},
    name,
    parent,
    matchAs,
    redirect: route.redirect,
    beforeEnter: route.beforeEnter,
    meta: route.meta || {}
  }
  // Nested subroutes add records recursively
  if (route.children) {
// ...
    route.children.forEach(child= > {
      addRouteRecord(pathMap, nameMap, child, record)
    })
  }
  // Process alias logic to add the corresponding record
  if(route.alias ! = =undefined) {
    if (Array.isArray(route.alias)) {
      route.alias.forEach(alias= > {
        addRouteRecord(pathMap, nameMap, { path: alias }, parent, record.path)
      })
    } else {
      addRouteRecord(pathMap, nameMap, { path: route.alias }, parent, record.path)
    }
  }
  // Update the path map
  pathMap[record.path] = record
  // Update the name map
  if (name) {
    if(! nameMap[name]) { nameMap[name] = record }else {
      warn(false.`Duplicate named routes definition: { name: "${name}", path: "${record.path}" }`)}}}function normalizePath (path: string, parent? : RouteRecord) :string {
  path = path.replace(/ / / $/.' ')
  if (path[0= = ='/') return path
  if (parent == null) return path
  return cleanPath(`${parent.path}/${path}`)}Copy the code

It can be seen that the main thing to be done is to generate a map of common routing records corresponding to path and routing records corresponding to name according to the user route configuration object, so as to facilitate subsequent matching and correspondence.

Instantiation of the History

This is also an important step. All History classes are in the SRC/History/directory. We don’t need to worry about the implementation differences for each History class right now. Just know that they all inherit from the history class in SRC /history/base.js:

/* @flow */

// ...
import { inBrowser } from '.. /util/dom'
import { runQueue } from '.. /util/async'
import { START, isSameRoute } from '.. /util/route'
// Export _Vue from install.js, which was analyzed earlier
import { _Vue } from '.. /install'

export class History {
// ...
  constructor(router: VueRouter, base: ? string) {this.router = router
    this.base = normalizeBase(base)
    // start with a route object that stands for "nowhere"
    this.current = START
    this.pending = null
  }
// ...
}

// Get the base value
function normalizeBase (base: ? string) :string {
  if(! base) {if (inBrowser) {
      // respect <base> tag
      const baseEl = document.querySelector('base')
      base = baseEl ? baseEl.getAttribute('href') : '/'
    } else {
      base = '/'}}// make sure there's the starting slash
  if (base.charAt(0)! = ='/') {
    base = '/' + base
  }
  // remove trailing slash
  return base.replace(/ / / $/.' ')}// ...Copy the code

Having instantiated the VueRouter, it’s time to look at the Vue instance.

instantiationVue

Instantiation is simple:

new Vue({
  router,
  template: ` 
      

Basic

  • /
  • /foo
  • /bar
  • /bar
`
}).$mount('#app')Copy the code

The router and template are passed in options. Create a new Vue instance and the beforeCreate hook will be called for each new Vue instance.

// ...
  Vue.mixin({
    beforeCreate () {
      // Check whether there is a router
      if (this.$options.router) {
          / / assignment _router
        this._router = this.$options.router
        // Initialize init
        this._router.init(this)
        // Define a responsive _route object
        Vue.util.defineReactive(this.'_route'.this._router.history.current)
      }
    }
  })Copy the code

To be specific, check whether the router is included in the options during the instantiation. If the router is included, it means that an instance with routing configuration is created. In this case, it is necessary to continue initializing the route-related logic. We then assign _router to the current instance so that when we access $router on the prototype, we get the router.

Let’s look at the two keys: router.init and defining a responsive _route object.

router.init

Now let’s see what the router init method does, again in SRC /index.js:

/* @flow */

import { install } from './install'
import { createMatcher } from './create-matcher'
import { HashHistory, getHash } from './history/hash'
import { HTML5History, getLocation } from './history/html5'
import { AbstractHistory } from './history/abstract'
import { inBrowser, supportsHistory } from './util/dom'
import { assert } from './util/warn'

export default class VueRouter {
// ...
  init (app: any /* Vue component instance */) {
// ...
    this.app = app

    const history = this.history

    if (history instanceof HTML5History) {
      history.transitionTo(getLocation(history.base))
    } else if (history instanceof HashHistory) {
      history.transitionTo(getHash(), () => {
        window.addEventListener('hashchange', () => {
          history.onHashChange()
        })
      })
    }

    history.listen(route= > {
      this.app._route = route
    })
  }
// ...
}
// ...Copy the code

It can be seen that initialization is mainly to assign values to app, which is special for HTML5History and HashHistory, because only in these two modes can there be a default page when entering, and the corresponding route needs to be activated according to the path or hash in the address bar of the current browser. This is done by calling transitionTo; There is also a special handling for HashHistory. Why not just listen for the HashChange event when initializing HashHistory? This is to fix github.com/vuejs/vue-r… BeforeEnter hook is fired twice if it is asynchronous in a hook function like beforeEnter. The reason for this is that the beforeEnter hook is initialized with #/ if the hash value does not start with a /. This process triggers the Hashchange event, so the lifecycle hook is walked again, which means the beforeEnter hook function is called again.

To take a look at the general logic of the specific transitionTo method, SRC /history/base.js:

/* @flow */

import type VueRouter from '.. /index'
import { warn } from '.. /util/warn'
import { inBrowser } from '.. /util/dom'
import { runQueue } from '.. /util/async'
import { START, isSameRoute } from '.. /util/route'
import { _Vue } from '.. /install'

export class History {
// ...transitionTo (location: RawLocation, cb? :Function) {
      // Call match to get the matching route object
    const route = this.router.match(location, this.current)
    // Confirm the transition
    this.confirmTransition(route, () => {
      // Update the current route object
      this.updateRoute(route)
      cb && cb(route)
      // Update the url of the subclass implementation
      // Update the hash value for hash mode
      // PushState/replacEstate is used for history mode
      // Browser address
      this.ensureURL()
    })
  }
  // Confirm the transition
  confirmTransition (route: Route, cb: Function) {
    const current = this.current
    // If it is the same, return directly
    if (isSameRoute(route, current)) {
      this.ensureURL()
      return
    }
    // Cross-compares the routing record of the current route with that of the current route
    // In order to be able to accurately get parent-child routing updates can be exactly known
    // Which components need to be updated and which do not
    const {
      deactivated,
      activated
    } = resolveQueue(this.current.matched, route.matched)

    // Queue for the entire switchover cycle
    const queue: Array<? NavigationGuard> = [].concat(// Leave's hook
      extractLeaveGuards(deactivated),
      // Global router before hooks
      this.router.beforeHooks,
      // The beforeEnter hook for the route to be updated
      activated.map(m= > m.beforeEnter),
      // Asynchronous components
      resolveAsyncComponents(activated)
    )

    this.pending = route Specifies the iterator function executed by each queueconst iterator = (hook: NavigationGuard, next) = > {
      // Ensure that the period is still the current route
      if (this.pending ! == route)return
      hook(route, current, (to: any) => {
        if (to === false) {
          // next(false) -> abort navigation, ensure current URL
          this.ensureURL(true)}else if (typeof to === 'string' || typeof to === 'object') {
          // next('/') or next({ path: '/' }) -> redirect
          this.push(to)
        } else {
          // confirm transition and pass on the value
          next(to)
        }
      })
    }
    // Execute queue
    runQueue(queue, iterator, () => {
      const postEnterCbs = []
      // The hook inside the component
      const enterGuards = extractEnterGuards(activated, postEnterCbs, () => {
        return this.current === route
      })
      // Execute the hook in the component after the last queue execution
      // Because you need to wait for asynchronous components and OK to execute
      runQueue(enterGuards, iterator, () => {
          // Ensure that the period is still the current route
        if (this.pending === route) {
          this.pending = null
          cb(route)
          this.router.app.$nextTick((a)= > {
            postEnterCbs.forEach(cb= > cb())
          })
        }
      })
    })
  }
  // Update the current route object
  updateRoute (route: Route) {
    const prev = this.current
    this.current = route
    // Note the value of cb
    // Every update will be called with the need below!
    this.cb && this.cb(route)
    // Perform the After hooks callback
    this.router.afterHooks.forEach(hook= > {
      hook && hook(route, prev)
    })
  }
}
// ...Copy the code

As you can see, the whole process is about implementing the various hooks of the convention and dealing with asynchronous components. Some of the function-specific details have been left out (more on that later), but it doesn’t affect understanding the process. However, we need to pay attention to a concept: route record. Every route object should have a matched attribute, which corresponds to the route record. Its specific meaning will be dealt with in the call to match(). SRC /create-matcher.js: SRC /create-matcher.js

// ...
import { createRoute } from './util/route'
import { createRouteMap } from './create-route-map'
// ...
export function createMatcher (routes: Array<RouteConfig>) :Matcher {
  const { pathMap, nameMap } = createRouteMap(routes)
  // 关键的 match
  function match (raw: RawLocation, currentRoute? : Route, redirectedFrom? : Location) :Route {
    const location = normalizeLocation(raw, currentRoute)
    const { name } = location

    // Named route processing
    if (name) {
      // nameMap[name] = Route record
      const record = nameMap[name]
      const paramNames = getParams(record.path)
// ...
      if (record) {
        location.path = fillParams(record.path, location.params, `named route "${name}"`)
        / / create the route
        return _createRoute(record, location, redirectedFrom)
      }
    } else if (location.path) {
      // Common routing processing
      location.params = {}
      for (const path in pathMap) {
        if (matchRoute(path, location.params, location.path)) {
          // The route was successfully created
          // pathMap[path] = Route record
          return _createRoute(pathMap[path], location, redirectedFrom)
        }
      }
    }
    // no match
    return _createRoute(null, location)
  }
// ...
  // Create a route
  function _createRoute (record: ? RouteRecord, location: Location, redirectedFrom? : Location) :Route {
    // Redirection and alias logic
    if (record && record.redirect) {
      return redirect(record, redirectedFrom || location)
    }
    if (record && record.matchAs) {
      return alias(record, location, record.matchAs)
    }
    // Create a routing object
    return createRoute(record, location, redirectedFrom)
  }

  return match
}
// ...Copy the code

SRC /util/route.js: SRC /util/route.js: SRC /util/route.js: SRC /util/route.js: SRC /util/route.js

// ...
export function createRoute (record: ? RouteRecord, location: Location, redirectedFrom? : Location) :Route {
  // You can see that it is a normal object that is frozen
  const route: Route = {
    name: location.name || (record && record.name),
    meta: (record && record.meta) || {},
    path: location.path || '/'.hash: location.hash || ' '.query: location.query || {},
    params: location.params || {},
    fullPath: getFullPath(location),
    // Get all matched routing records according to the record hierarchy
    matched: record ? formatMatch(record) : []
  }
  if (redirectedFrom) {
    route.redirectedFrom = getFullPath(redirectedFrom)
  }
  return Object.freeze(route)
}
// ...
function formatMatch (record: ? RouteRecord) :Array<RouteRecord> {
  const res = []
  while (record) {
    res.unshift(record)
    record = record.parent
  }
  return res
}
// ...Copy the code

Back to init, the history.listen method is called:

history.listen(route= > {
  this.app._route = route
})Copy the code

Listen simply sets the cb value of the current history object. This cb value is called when the history is updated. This function updates the _route value of the current application instance. Look at the analysis in the next paragraph.

DefineReactive definition _route

Return to the beforeCreate hook function, and finally define a response_route attribute for the current application instance using the tool method of Vue. The value is this._router.history.current. This is the currently active routing object for the current History instance. Defining such a responsive property value for an application instance means that if the property value changes, the update mechanism will be triggered and the application instance’s render will be called to re-render. The value of _route of the application instance will be updated every time history is successfully updated, which means that once history is changed, the update mechanism will trigger the application instance’s render method to re-render.

Back to instantiating the application instance:

new Vue({
  router,
  template: ` 
      

Basic

  • /
  • /foo
  • /bar
  • /bar
`
}).$mount('#app')Copy the code

You can see that the template of this instance contains two custom components: router-link and router-View.

The router – the view components

The router-view component is relatively simple, so we will analyze it first. It is defined in the source code SRC /components/view.js:

export default {
  name: 'router-view'.functional: true.// Function components pure render
  props: {
    name: {
      type: String.default: 'default' // Default default Specifies the name of the default named view
    }
  },
  render (h, { props, children, parent, data }) {
    // Solve the nesting depth problem
    data.routerView = true
    / / the route objects
    const route = parent.$route
    / / cache
    const cache = parent._routerViewCache || (parent._routerViewCache = {})
    let depth = 0
    let inactive = false
    // Depth of the current component
    while (parent) {
      if(parent.$vnode && parent.$vnode.data.routerview) {depth++} handles keepalive logicif (parent._inactive) {
        inactive = true
      }
      parent = parent.$parent
    }

    data.routerViewDepth = depth
    // Get the routing record matching the current component level
    const matched = route.matched[depth]
    if(! matched) {return h()
    }
    // Get the component to render
    const name = props.name
    const component = inactive
      ? cache[name]
      : (cache[name] = matched.components[name])

    if(! inactive) {// The hook needs to be set every time in non-keepalive mode
      // Then update (assign & destroy) matched instance elements
      const hooks = data.hook || (data.hook = {})
      hooks.init = vnode= > {
        matched.instances[name] = vnode.child
      }
      hooks.prepatch = (oldVnode, vnode) = > {
        matched.instances[name] = vnode.child
      }
      hooks.destroy = vnode= > {
        if (matched.instances[name] === vnode.child) {
          matched.instances[name] = undefined}}}// Call createElement to render the matching component
    return h(component, data, children)
  }
}Copy the code

As you can see the logic is relatively simple, just get the matching component and render it.

Take a look at the navigation link component, which is defined in the source SRC /components/link.js:

// ...
import { createRoute, isSameRoute, isIncludedRoute } from '.. /util/route'
// ...
export default {
  name: 'router-link'.props: {
    // The component properties passed in
    to: { // The link to the destination route
      type: toTypes,
      required: true
    },
    // Create the HTML tag
    tag: {
      type: String.default: 'a'
    },
    // Full mode, if true then means
    // Routes that are absolutely equal will add activeClass
    // Otherwise it is containment
    exact: Boolean.// Append the path to the current (relative) path
    append: Boolean.// If true, call router.replace() to replace history
    replace: Boolean.// The name of the CSS class used when the link is activated
    activeClass: String
  },
  render (h: Function) {
    // Get the router instance and the currently active Route object
    const router = this.$router
    const current = this.$route
    const to = normalizeLocation(this.to, current, this.append)
    // Based on the matching result of the current destination link and the currently active route
    const resolved = router.match(to, current)
    const fullPath = resolved.redirectedFrom || resolved.fullPath
    const base = router.history.base
    // Create the href
    const href = createHref(base, fullPath, router.mode)
    const classes = {}
    // Activating class takes precedence over getting the linkActiveClass on the current component or router configuration
    / / the default router - link - active
    const activeClass = this.activeClass || router.options.linkActiveClass || 'router-link-active'
    // Compare the target
    // There may not be a path because there are named routes
    const compareTarget = to.path ? createRoute(null, to) : resolved
    // Check if the route is the same if the mode is strict (path query params hash)
    // Otherwise include logic (path contains, query contains hash is null or the same)
    classes[activeClass] = this.exact
      ? isSameRoute(current, compareTarget)
      : isIncludedRoute(current, compareTarget)

    // Event binding
    const on = {
      click: (e) = > {
        // Ignore clicks with function keys
        if (e.metaKey || e.ctrlKey || e.shiftKey) return
        // Blocked return
        if (e.defaultPrevented) return
        / / right click
        if(e.button ! = =0) return
        / / ` target = "_blank" ` ignored
        const target = e.target.getAttribute('target')
        if (/\b_blank\b/i.test(target)) return
        // Prevent a jump by blocking the default behavior
        e.preventDefault()
        if (this.replace) {
          / / replace logic
          router.replace(to)
        } else {
          / / push logic
          router.push(to)
        }
      }
    }
    // Additional data is required to create the element
    const data: any = {
      class: classes
    }

    if (this.tag === 'a') {
      data.on = on
      data.attrs = { href }
    } else {
      // Find the first  to give this element the event binding and href attribute
      const a = findAnchor(this.$slots.default)
      if (a) {
        // in case the <a> is a static node
        a.isStatic = false
        const extend = _Vue.util.extend
        const aData = a.data = extend({}, a.data)
        aData.on = on
        const aAttrs = a.data.attrs = extend({}, a.data.attrs)
        aAttrs.href = href
      } else {
        // If there is no , the current element is bound to itself
        data.on = on
      }
    }
    // Create the element
    return h(this.tag, data, this.$slots.default)
  }
}

function findAnchor (children) {
  if (children) {
    let child
    for (let i = 0; i < children.length; i++) {
      child = children[i]
      if (child.tag === 'a') {
        return child
      }
      if (child.children && (child = findAnchor(child.children))) {
        return child
      }
    }
  }
}

function createHref (base, fullPath, mode) {
  var path = mode === 'hash' ? '/ #' + fullPath : fullPath
  return base ? cleanPath(base + path) : path
}Copy the code

The router-link component calls router push or replace to update the route according to the value set to when it is clicked. It checks whether it matches the current route (strict match and contain match) to determine whether its activeClass is added.

summary

The code of the whole process has been analyzed almost here, let’s review:

Vue – router. Js flow chart

I believe that the overall feeling is different from the initial feeling after seeing this picture, and I have a clear understanding of the overall process of VUe-Router. Of course, due to the limited space, there are still many details that have not been carefully analyzed, and specific analysis will be carried out according to modules in the future.


Welcome to DDFE GITHUB: github.com/DDFE wechat official account: wechat search the official account “DDFE” or scan the qr code below