Vue – Router source code and dynamic routing permission allocation. Vue – Router source code and dynamic routing permission allocation

background

Last month, I set a flag, read the source code of VUe-Router, can gradually find that the source code of VUe-Router is not as easy to understand as many summary articles, read you will find that there will be many layers of function call relationship in many places, and a large number of this point to the problem, And there will be a lot of helper functions to understand. But still insist on gnawing down (of course not finished reading, content is really much), the following is my work in the political cloud (internship) leisure time to read some of the source code and summary, and with the analysis of vue-element-admin used in the third year of the vuer know-it-all background framework dynamic routing permission control principle. Incidentally, this article practice demo address: based on background framework development of student management system.

Vue-router source code analysis

First read the source code before it is best to clone Vue and VUe-Router source code, and then the first time to read the recommendation to follow the official documents to go through the basic usage, and then the second time to read the source code, first clear the role of all levels of directory and extract some core files out, While going through the code, write a small demo and interrupt the debugging while reading. It doesn’t matter if you don’t understand, you can refer to some good summarized articles while reading, and finally sort out the important principle process according to your own understanding, and draw the relevant knowledge brain map to deepen the impression.

Pre-knowledge: Flow syntax

JS may not show some hidden errors during compilation, but various bugs will be reported during execution. The purpose of flow is to do static type checking at compile time to catch errors early and throw exceptions.

Large projects such as Vue and VUe-Router often require such tools to do static type checking to ensure code maintainability and reliability. Vue-router source code analyzed in this paper uses flow to write a large number of functions, so it is necessary to learn the syntax of flow.

First install the Flow environment and initialize the environment

npm install flow-bin -g
flow init
Copy the code

Enter the error code in index.js

/*@flow*/
function add(x: string, y: number): number {
  return x + y
}
add(2, 11)
Copy the code

If you type flow in the console, it will raise an exception, which is a simple way to use flow.

See the Flow website for details, and the syntax is typescript-like.

registered

When we use vue-router, we usually need to pass the vue-Router instance object as a parameter when initializing the vue instance in main.js

Such as:

import Router from 'vue-router'
Vue.use(Router)
const routes = [
   {
    path: '/student'.name: 'student'.component: Layout,
    meta: { title: 'Student Information Enquiry'.icon: 'documentation'.roles: ['student']},children: [{path: 'info'.component: () = > import('@/views/student/info'),
        name: 'studentInfo'.meta: { title: 'Information Query'.icon: 'form'}}, {path: 'score'.component: () = > import('@/views/student/score'),
        name: 'studentScore'.meta: { title: 'Result Enquiry'.icon: 'score'}}]}... ] ;const router = new Router({
  mode: "history".linkActiveClass: "active".base: process.env.BASE_URL,
  routes
});
new Vue({
    router,
    store,
    render: h= > h(App)
}).$mount("#app");
Copy the code

Vue.use

So what does vue. use(Router) do

The SRC /core/global-api/use.js source address is located in the Vue source code

export function initUse (Vue: GlobalAPI) {
  Vue.use = function (plugin: Function | Object) {
    / / get the installPlugins
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    // No double registration is guaranteed
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }
    // Get parameters other than plugins for the first parameter
    const args = toArray(arguments.1)
    // Add the Vue instance to the parameter
    args.unshift(this)
    // The first parameter of each insatll method becomes Vue, no additional import is required
    if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {
      plugin.apply(null, args)
    }
    // Finally save with installPlugins
    installedPlugins.push(plugin)
    return this}}Copy the code

As you can see, Vue’s use method takes a plugin parameter and uses the installPlugins array to save the registered plugin. First, ensure that plugin is not registered repeatedly, and then remove the Vue from the function parameters, the whole Vue as the first parameter of plugin install method, the advantage of this is that there is no need to bother to introduce another Vue, easy to operate. Then determine whether the install method exists on the plugin. If the install method exists, the assigned parameters are passed into execution, and finally all plugins that exist with the install method are handed over to installPlugins for maintenance.

install

The first parameter is Vue, so we will locate the code in the vue-router source code SRC /install.js source address

// Save Vue local variables
export let _Vue
export function install (Vue) {
  // If yes
  if (install.installed && _Vue === Vue) return
  install.installed = true
 // Local variables retain the Vue passed in
  _Vue = Vue
  const isDef = v= >v ! = =undefined
  const registerInstance = (vm, callVal) = > {
    let i = vm.$options._parentVnode
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
      i(vm, callVal)
    }
  }
  Each component has these hook functions, and the execution will follow the logic here
  Vue.mixin({
    beforeCreate () {
      if (isDef(this.$options.router)) {
        // This.$options. Router is the root component passed in when new Vue
        / / the router
        this._routerRoot = this
        this._router = this.$options.router
        this._router.init(this)
        // become responsive
        Vue.util.defineReactive(this.'_route'.this._router.history.current)
      } else {
        // Non-root components access root components through $parent
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      registerInstance(this.this)
    },
    destroyed () {
      registerInstance(this)}})// The prototype adds $router and $route
  Object.defineProperty(Vue.prototype, '$router', {
    get () { return this._routerRoot._router }
  })
  Object.defineProperty(Vue.prototype, '$route', {
    get () { return this._routerRoot._route }
  })
// Global registration
  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)
// Get the merge policy
  const strats = Vue.config.optionMergeStrategies
  // use the same hook merging strategy for route hooks
  strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}
Copy the code

As you can see, the core part of this code is to mix each component with beforeCreate and destroyed lifecycle hooks using mixin while executing the install method. The beforeCreate function determines whether the current router instance is the root component. If so, it assigns _routerRoot to the current component instance and _router to the incoming VueRouter instance object. The init method is then used to initialize the router, and this_route is then responsified. For non-root components, _routerRoot points to the parent instance of $parent. Install then executes the registerInstance(this,this) method, which adds $Router and $Route to the prototype, and registers the RouterView and RouterLink.

summary

Vue.use(plugin) actually executes the install method on plugin, insatll method has an important step:

  • usemixinInterfused in componentsbeforeCreate , destoryThese two life cycle hooks
  • inbeforeCreateThis hook is initialized.
  • Global registrationrouter-view.router-linkcomponent

VueRouter

Then there is the most important class: VueRouter. This part of the code is more, so not one list, pick the key analysis. VueRouter source address.

The constructor

  constructor (options: RouterOptions = {}) {
    this.app  = null
    this.apps = []
    // Incoming configuration items
    this.options = options
    this.beforeHooks = []
    this.resolveHooks = []
    this.afterHooks = []
    this.matcher = createMatcher(options.routes || [], this)
    // Generally, there are two modes: hash and history. The third mode is abstract
    let mode = options.mode || 'hash'
    // Determine whether the current incoming configuration can use history mode
    this.fallback = mode === 'history'&&! supportsPushState && options.fallback ! = =false
    // Demote processing
    if (this.fallback) {
      mode = 'hash'
    }
    if(! inBrowser) { mode ='abstract'
    }
    this.mode = mode
    The history object is inherited from the History class to manage the route, depending on how the pattern is instantiated
    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, options.base)
        break
      default:
        if(process.env.NODE_ENV ! = ='production') {
          assert(false.`invalid mode: ${mode}`)}}}Copy the code

A number of variables are defined when initializing the entire vueRouter object, app representing the Vue instance, options representing the configuration parameters passed in, and then the route intercepts useful hooks and the important matcher (more on that later). The constructor actually does two things: 1. Determine the mode2 used by the current route. Instantiate the corresponding history object.

init

Then after the vueRouter instance is instantiated, if the vueRouter instance is passed in as Vue initialization, it will execute the init method on the beforeCreate

init (app: any) {
  ...
  this.apps.push(app)
  // Make sure that the following logic only goes once
  if (this.app) {
    return
  }
  // Save the Vue instance
  this.app = app
  const history = this.history
  // After getting the history instance, call transitionTo to transition routes
  if (history instanceof HTML5History) {
    history.transitionTo(history.getCurrentLocation())
  } else if (history instanceof HashHistory) {
    const setupHashListener = () = > {
      history.setupListeners()
    }
    history.transitionTo(
      history.getCurrentLocation(),
      setupHashListener,
      setupHashListener
    )
  }
}
Copy the code

The init method passes in the Vue instance and saves it in this.apps. The Vue instance will fetch the current this.history. If it is a hash route, the setupHashListener function is invoked and the key transitionTo function is called to match this.matcher.

summary

After the vueRouter constructor is executed, the route mode is selected and the matcher is generated. Then the vueRouter instance is passed to initialize the route and the beforeCreate hook is executed. Then get this.history and call transitionTo to transition the route.

Matcher

After initializing macther in vueRouter’s constructor, this section takes a closer look at what this code is doing and what the match method is doing with the source address.

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

Start by locating the code to create-matcher.js

export function createMatcher (
  routes: Array<RouteConfig>,
  router: VueRouter
) :Matcher {
  // Create a mapping table
  const { pathList, pathMap, nameMap } = createRouteMap(routes)
  // Add a dynamic route
  function addRoutes(routes){... }// Compute the new path
  function match (raw: RawLocation, currentRoute? : Route, redirectedFrom? : Location) :Route {... }/ /... Some of the following methods will not be expanded for now
  
   return {
    match,
    addRoutes
  }
}
Copy the code

CreateMatcher takes two parameters: routes, which is the routing table configuration defined in router.js, and router, which is the instance returned by new vueRouter.

createRouteMap

Create a path-record,name-record map. Create a path-record,name-record map

export function createRouteMap (
  routes: Array<RouteConfig>, oldPathList? :Array<string>, oldPathMap? : Dictionary<RouteRecord>, oldNameMap? : Dictionary<RouteRecord>) :{
  pathList: Array<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>
} {
  // Record all paths
  const pathList: Array<string> = oldPathList || []
  // Record the Map of path-Routerecord
  const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
   // Record the Map of name-Routerecord
  const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)
  // Create a mapping table by traversing all routes
  routes.forEach(route= > {
    addRouteRecord(pathList, pathMap, nameMap, route)
  })
  // Adjust the priority
  for (let i = 0, l = pathList.length; i < l; i++) {
    if (pathList[i] === The '*') {
      pathList.push(pathList.splice(i, 1) [0])
      l--
      i--
    }
  }
  return {
    pathList,
    pathMap,
    nameMap
  }
}
Copy the code

CreateRouteMap requires an incoming routing configuration that supports passing in old path arrays and old maps in preparation for subsequent recursion and addRoutes. First we record path,pathMap,nameMap with three variables, and then we look at the core method addRouteRecord. There is too much code in this block. Here are a few important steps

// Parse the path
const pathToRegexpOptions: PathToRegexpOptions =
    route.pathToRegexpOptions || {}
// Splice paths
const normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict)
// Record the key object of routing information, and then build a mapping table based on it
const record: RouteRecord = {
  path: normalizedPath,
  regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
  // Route corresponding component
  components: route.components || { default: route.component },
  // Component instance
  instances: {},
  name,
  parent,
  matchAs,
  redirect: route.redirect,
  beforeEnter: route.beforeEnter,
  meta: route.meta || {},
  props: route.props == null
    ? {}
    : route.components
      ? route.props
      : { default: route.props }
}
Copy the code

Using a Recod object to record the route configuration facilitates the calculation of a new path during subsequent path switches, where the path is actually concatenated from the path passed to the parent Record object and the current path. The Regex then uses a library to parse path into a regular expression. AddRouteRecord is recursively called if the route has child nodes

 // Call addRouteRecord recursively if there are children
    route.children.forEach(child= > {
      const childMatchAs = matchAs
        ? cleanPath(`${matchAs}/${child.path}`)
        : undefined
      addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
    })
Copy the code

Finally, map two tables, and save record·path into pathList,nameMap logic similar not enumerated

  if(! pathMap[record.path]) { pathList.push(record.path) pathMap[record.path] = record }Copy the code

Why all the effort to separate pathList and pathMap and nameMap? First of all, pathList is to record all path of route configuration, and pathMap and nameMap are convenient for us to pass path or name to quickly locate a record, and then assist subsequent path switching to calculate routes.

addRoutes

This is a new API added after vue2.2.0. In many cases, routes are not written dead and need to be added dynamically. Based on the previous createRouteMap, we just need to pass in routes, and it can be modified on the original basis

function addRoutes (routes) {
  createRouteMap(routes, pathList, pathMap, nameMap)
}
Copy the code

And see that this method is returned at the end of createMathcer, so we can use this method

return {
    match,
    addRoutes
  }
Copy the code

match

function match (raw: RawLocation, currentRoute? : Route, redirectedFrom? : Location) :Route {... }Copy the code

Next comes the match method, which takes three arguments. Raw is RawLocation, which can be a URL string or a Location object. CurrentRoute is of Route type and represents the current path. RedirectedFrom is related to redirection. The match method returns a path that computes a new path based on the incoming raw and the current path currentRoute. As for how he calculates this path, take a closer look at how he calculates the normalizeLocation method and _createRoute method for location.

summary

  • createMatcher: Creates a mapping table based on the configuration description of routes, including paths, names, and routesrecordThe most important thing is thatcreateRouteMapThis method is also the principle of dynamic routing matching and nested routing.
  • addRoutes: Dynamically adds the route configuration
  • match: According to incomingrawAnd the current pathcurrentRouteCompute a new path and return.

Routing patterns

Vue-router supports three routing modes: hash, history, and Abstract, where abstract is the source address of the routing mode used in non-browser environments.

This part was mentioned earlier when initializing the vueRouter object. First, you get the mode of the configuration item, and then determine whether the current browser supports this mode based on the current incoming configuration. By default, IE9 will degrade to hash. Then initialize the different history instances according to the different patterns.

    // There are two modes of hash and history routes. The third mode is the abstract mode which is not commonly used
    let mode = options.mode || 'hash'
    // Determine whether the current incoming configuration can use history mode
    this.fallback = mode === 'history'&&! supportsPushState && options.fallback ! = =false
    // Demote processing
    if (this.fallback) {
      mode = 'hash'
    }
    if(! inBrowser) { mode ='abstract'
    }
    this.mode = mode
    // Instantiate different history objects according to the schema to manage the route inherits from the history class
    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, options.base)
        break
      default:
        if(process.env.NODE_ENV ! = ='production') {
          assert(false.`invalid mode: ${mode}`)}}Copy the code

summary

Vue-router supports hash, History, and Abstract routing modes. The default value is hash. If the current browser does not support history, history will be degraded and initialized.

Routing switch

Switching urls is mainly calledpushMethod, the following hash mode as an example, analysispushMethod implementation principle.pushMethods The realization principle of switching routeThe source address

First find the push method defined by vueRouter under SRC /index.js

push (location: RawLocation, onComplete? :Function, onAbort? :Function) {
    // $flow-disable-line
    if(! onComplete && ! onAbort &&typeof Promise! = ='undefined') {
      return new Promise((resolve, reject) = > {
        this.history.push(location, resolve, reject)
      })
    } else {
      this.history.push(location, onComplete, onAbort)
    }
  }
Copy the code

Next we need to navigate to history/hash.js. Get the current path and call transitionTo to switch the path, executing pushHash as the core method in the callback function.

push (location: RawLocation, onComplete? :Function, onAbort? :Function) {
    const { current: fromRoute } = this
    PushHash is called in the path-switching callback function
    this.transitionTo(
      location,
      route= > {
        pushHash(route.fullPath)
        handleScroll(this.router, route, fromRoute, false)
        onComplete && onComplete(route)
      },
      onAbort
    )
  }
Copy the code

The pushHash method, on the other hand, calls the pushState method after checking for browser compatibility, passing in the URL

export function pushState (url? : string, replace? : boolean) {
  const history = window.history
  try {
   // Call the browser's native history pushState or replaceState interface. PushState pushes the URL
    if (replace) {
      history.replaceState({ key: _key }, ' ', url)
    } else {
      _key = genKey()
      history.pushState({ key: _key }, ' ', url)
    }
  } catch (e) {
    window.location[replace ? 'replace' : 'assign'](url)
  }
}
Copy the code

As you can see, the push layer invokes the browser’s native history pushState and replaceState methods, rather than replace mode, which pushes the URL into the history stack.

In addition, the principle of splicing hash

Source location

When HashHistory is initialized, the constructor executes the method ensureSlash

export class HashHistory extends History {
  constructor (router: Router, base: ? string, fallback: boolean) {... ensureSlash() } ... }Copy the code

This method first calls getHash and then replaceHash()

function ensureSlash () :boolean {
  const path = getHash()
  if (path.charAt(0) = = ='/') {
    return true
  }
  replaceHash('/' + path)
  return false
}
Copy the code

Here are some of them

export function getHash () :string {
  const href = window.location.href
  const index = href.indexOf(The '#')
  return index === -1 ? ' ' : href.slice(index + 1)}// Real hash method
function getUrl (path) {
  const href = window.location.href
  const i = href.indexOf(The '#')
  const base = i >= 0 ? href.slice(0, i) : href
  return `${base}#${path}`
}
function replaceHash (path) {
  if (supportsPushState) {
    replaceState(getUrl(path))
  } else {
    window.location.replace(getUrl(path))
  }
}
export function replaceState (url? : string) {
  pushState(url, true)}Copy the code

Here’s an example: Assuming that the current URL is http://localhost:8080, the path is empty, ReplcaeHash (‘/’ + path), getUrl internally calculates the URL to be http://localhost:8080/#/, and pushState(URL,true), and you’re done!

summary

The hash push method calls transitionTo, followed by a callback to pushHash, whose pushState method calls the browser’s native history method. The difference between push and replace is that one pushes the URL into the history stack, while the other does not. The most intuitive manifestation is that in replace mode, when the browser clicks back, it does not return to the previous route, while the other does.

router-view & router-link

Vue-router globally registers two components during install: router-View and router-link. These two components are typical functional components. The source address

router-view

First, execute the beforeCreate hook on the Router component and turn this._route into a responsive object

 Vue.util.defineReactive(this.'_route'.this._router.history.current)
Copy the code

So each route switch triggers a router-view rerender to render a new view.

See the code comments for the core render function

  render (_, { props, children, parent, data }) {
    ...
    // Depth traverses the router-view component up to the root component. If other router-view components are encountered, the route depth is +1
    let depth = 0
    let inactive = false
    while(parent && parent._routerRoot ! == parent) {// parent.$vnode.data. RouterView is true if the component being looked up also has nested router-Views
      if (parent.$vnode && parent.$vnode.data.routerView) {
        depth++
      }
      if (parent._inactive) {
        inactive = true
      }
      parent = parent.$parent
    }
    data.routerViewDepth = depth
    if (inactive) {
      return h(cache[name], data, children)
    }
   // Matched record to find the corresponding RouteRecord
    const matched = route.matched[depth]
    if(! matched) { cache[name] =null
      return h()
    }
 // Use RouteRecord to find the Component
    const component = cache[name] = matched.components[name]
   // Register the registerRouteInstance method with the parent component
    data.registerRouteInstance = (vm, val) = > {     
      const current = matched.instances[name]
      if( (val && current ! == vm) || (! val && current === vm) ) { matched.instances[name] = val } }// Render component
    return h(component, data, children)
  }
Copy the code

To trigger an update is a call to the setter, located in SRC /index.js, which triggers an update when _route is modified.

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

router-link

Analyze several important parts:

  • Set up theactiveRouting style

Router-link-active and router-link-exact-active classes can be added to router-link-active classes, because the render function will render the router according to the current route status. Add class to the rendered active element

render (h: Function) {...const globalActiveClass = router.options.linkActiveClass
  const globalExactActiveClass = router.options.linkExactActiveClass
  // Support global empty active class
  const activeClassFallback = globalActiveClass == null
    ? 'router-link-active'
    : globalActiveClass
  const exactActiveClassFallback = globalExactActiveClass == null
    ? 'router-link-exact-active'
    : globalExactActiveClass
    ...
}
Copy the code
  • router-linkThe default rendering isaTag, if not, will go up to find the first oneaThe label
 if (this.tag === 'a') {
      data.on = on
      data.attrs = { href }
    } else {
      // find the first <a> child and apply listener and href
      const a = findAnchor(this.$slots.default)
      if (a) {
        // in case the <a> is a static node
        a.isStatic = false
        const aData = (a.data = extend({}, a.data))
        aData.on = on
        const aAttrs = (a.data.attrs = extend({}, a.data.attrs))
        aAttrs.href = href
      } else {
        // Render the element if it does not exist
        data.on = on
      }
    }
Copy the code
  • Switching routes triggers related events
const handler = e= > {
  if (guardEvent(e)) {
    if (this.replace) {
      / / replace routing
      router.replace(location)
    } else {
      / / push routing
      router.push(location)
    }
  }
}
Copy the code

Principle analysis of dynamic routing for permission control

I believe that students who develop backconsole projects often encounter the following scenario: a system is divided into different roles, and then different roles correspond to different operation menus and operation permissions. For example: the teacher can query the teacher’s own personal information query and then also can query the operation of the student’s information and the student’s result system, the student user only allows to query the individual result and the information, does not allow to change. Before Vue2.2.0, it was very difficult to add addRoutes to the API.

The current mainstream route permission control methods are as follows:

  1. Acquired at LogintokenIt’s saved locally and then carried by the front endtokenThen call the interface that obtains user information to obtain the role information of the current user.
  2. The front end calculates the routing table according to the current role and splices it to the regular routing table.

Log in to the whole process of generating dynamic routes

Now that you know how to control dynamic routing, here is a flowchart of the whole process

The front end judges in beforeEach:

  • The JWT token exists in the cache
    • access/login: Redirects to home page/
    • access/loginExternal routes: First access, obtain user role information, then generate dynamic routes, then access toreplaceMode access/xxxRouting. In this mode, users do not log inhistoryStoring records
  • No JWT token exists
    • Route in the whitelist: Access normal/xxxrouting
    • Not in the whitelist: Redirect to/loginpage

Combined with framework source code analysis

The following uses vue-element-admin’s source code to analyze how the routing logic is handled in the framework.

Route access logic analysis

The first step is to locate permission.js at the same level as the entry file main.js, where the global routing guard is handled. The source address

const whiteList = ['/login'.'/register'] // Route whitelist, no redirection
// Global route guard
router.beforeEach(async(to, from, next) => {
  NProgress.start() // Route loading progress bar
  // Set the meta title
  document.title = getPageTitle(to.meta.title)
  // Check whether the token exists
  const hasToken = getToken()
  if (hasToken) {
    if (to.path === '/login') {
      // There is a token redirect home page
      next({ path: '/' })
      NProgress.done()
    } else {
      const hasRoles = store.getters.roles && store.getters.roles.length > 0
      if (hasRoles) {
        next()
      } else {
        try {
          // Get the dynamic route and add it to the routing table
          const { roles } = await store.dispatch('user/getInfo')
          const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
          router.addRoutes(accessRoutes)
          // Use replace to access the route, leaving no record in history, and back to a blank page when you log in to Dashbordnext({ ... to,replace: true})}catch (error) {
          next('/login')
          NProgress.done()
        }
      }
    }
  } else {
    / / no token
    // Whitelist is accessed directly without redirection
    if(whiteList.indexOf(to.path) ! = = -1) {
      next()
    } else {
      // Take parameters to redirect to the path to go
      next(`/login? redirect=${to.path}`)
      NProgress.done()
    }
  }
})
Copy the code

In order to access route/XXX, it is necessary to verify whether the token exists. If so, it is necessary to determine whether the user accesses the home page for the first time. Then it is necessary to determine whether the user accesses the home page for the first time. If you use a login route, you can directly locate the route to the home page. If you do not have a token, you can check whether the route is in the whitelist (a route that can be accessed under any circumstances). If yes, you can access the route; otherwise, you are redirected to the login page.

Below is a screenshot of the route change after passing the global guard

Combine Vuex to generate dynamic routes

Here is to analyze the step const accessRoutes = await store. Dispatch (‘ permission/generateRoutes’ roles) how routing generation. The source address

First, vue-element-admin can be divided into two types:

  • ConstantRoutes: routes that do not require permission judgment
  • AsyncRoutes: routes requiring dynamic permission determination
// Identity route verification is not required
export const constantRoutes = [
  {
    path: '/login'.component: () = > import('@/views/login/index'),
    hidden: true}... ] .// Verify the identity route
export const asyncRoutes = [
  // Student role routing
  {
    path: '/student'.name: 'student'.component: Layout,
    meta: { title: 'Student Information Enquiry'.icon: 'documentation'.roles: ['student']},children: [{path: 'info'.component: () = > import('@/views/student/info'),
        name: 'studentInfo'.meta: { title: 'Information Query'.icon: 'form'}}, {path: 'score'.component: () = > import('@/views/student/score'),
        name: 'studentScore'.meta: { title: 'Result Enquiry'.icon: 'score'}}]}...Copy the code

Generate dynamic routing of the source code is located in the SRC/store/modules/permission of js generateRoutes methods, source code is as follows:

 generateRoutes({ commit }, roles) {
    return new Promise(resolve= > {
      let accessedRoutes
      if (roles.includes('admin')) {
        accessedRoutes = asyncRoutes || []
      } else {
      // Admin does not iterate to generate the corresponding permission routing table
        accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
      }
      Vuex stores asynchronous routes and regular routes
      commit('SET_ROUTES', accessedRoutes)
      resolve(accessedRoutes)
    })
  }
Copy the code

AsyncRoutes and constantRoutes from route.js check whether the current role is admin. If yes, the default super administrator can access all routes. Then save to Vuex. Finally, the filtered asyncRoutes and constantRoutes are merged. The source code for filtering permission routes is as follows:

export function filterAsyncRoutes(routes, roles) {
  const res = []
  routes.forEach(route= > {
    / / shallow copy
    consttmp = { ... route }// Filter out permission routes
    if (hasPermission(roles, tmp)) {
      if (tmp.children) {
        tmp.children = filterAsyncRoutes(tmp.children, roles)
      }
      res.push(tmp)
    }
  })
  return res
}
Copy the code

Firstly, define an empty array and traverse the incoming asyncRoutes to determine whether each route has the permission. The method for determining the permission of the unmatched route is as follows:

function hasPermission(roles, route) {
  if (route.meta && route.meta.roles) {
    // Roles returns true if there is a role that corresponds to a routing element
    return roles.some(role= > route.meta.roles.includes(role))
  } else {
    return true}}Copy the code

Then we need to judge the situation of secondary routes, tertiary routes and so on, and then do a layer of iterative processing, and finally push the filtered routes into the array to return. Then append to the constantRoutes

 SET_ROUTES: (state, routes) = > {
    state.addRoutes = routes
    state.routes = constantRoutes.concat(routes)
  }
Copy the code

Dynamic route generation process

conclusion

  • Vue-router source code analysis part

    • Register: ExecuteinstallMethod to inject a lifecycle hook initialization
    • VueRouter: When the component executesbeforeCreateThe incomingrouterInstance, executesinitFunction, and then executehistory.transitionToRouting transition
    • Matcher: Based on incomingroutesConfiguration creates the correspondingpathMapnameMap, you can calculate the new location based on the incoming location and path and match the correspondingrecord
    • Routing mode: The routing mode is being initializedvueRouter, or demoted if the browser does not support it
    • Route switching: Hash mode is used in the underlying browser nativepushStatereplaceStatemethods
    • Router-view: calls stored on the parent component$route.matchControls the rendering of components corresponding to routes and supports nesting.
    • The router – link: passtoTo determine the target routing component for the click event jump, and support rendering to differenttagYou can also modify the style of the active route.
  • Permissions control the dynamic routing section

    • Routing logic: global route interception, obtain token from cache, if exist, if enter route for the first time need to obtain user information, generate dynamic route, need to deal with here/loginIn special cases, if the whitelist does not exist, determine the whitelist and go to the corresponding logic
    • Dynamically generated routes: incoming needsrouter.jsTwo routes defined. Check whether the current identity is an administrator. If yes, you need to directly join the routes. Otherwise, you need to filter out the routes that have permissionaddRoutesAdditional.

My feelings got

Perhaps reading source role not as a development documentation immediately help daily development directly, but it is in the long run, the influence of the process of reading the source code can be learned many knowledge, similar closures, design patterns, cycle time, the callback, and so on JS advanced skills, and solid and promote the foundation of your JS. Of course, this article is flawed, there are several places are not analyzed, such as navigation guard implementation principle and route lazy loading implementation principle, this part, I am still exploring.

If rote learning some of the so-called face, or rote learning related behavior of the framework or API directly, it is hard to under the complex problems to rapid positioning problem, understand how to solve the problem, and I found a lot of people in the use of a new framework after some problems immediately to the corresponding Issues, As a result, there are more than hundreds or thousands of popular framework Issues, but many of the problems are caused by mistakes because we did not follow the direction set by the designer at the beginning of development, and more are caused by carelessness.

Refer to the article

Take you to a comprehensive analysis of vue-Router source code (ten thousand word long article)

Vuejs source code analysis

Recommended reading

XSS attacks in React

Write high quality maintainable code: comments at a glance

, recruiting

ZooTeam, a young passionate and creative front-end team, belongs to the PRODUCT R&D department of ZooTeam, based in picturesque Hangzhou. The team now has more than 40 front-end partners, with an average age of 27, and nearly 30% of them are full-stack engineers, no problem in the youth storm group. The members consist of “old” soldiers from Alibaba and netease, as well as fresh graduates from Zhejiang University, University of Science and Technology of China, Hangzhou Electric And other universities. In addition to daily business docking, the team also carried out technical exploration and practice in material system, engineering platform, building platform, performance experience, cloud application, data analysis and visualization, promoted and implemented a series of internal technical products, and continued to explore the new boundary of front-end technology system.

If you want to change what’s been bothering you, you want to start bothering you. If you want to change, you’ve been told you need more ideas, but you don’t have a solution. If you want change, you have the power to make it happen, but you don’t need it. If you want to change what you want to accomplish, you need a team to support you, but you don’t have the position to lead people. If you want to change the pace, it will be “5 years and 3 years of experience”; If you want to change the original savvy is good, but there is always a layer of fuzzy window… If you believe in the power of believing, believing that ordinary people can achieve extraordinary things, believing that you can meet a better version of yourself. If you want to be a part of the process of growing a front end team with deep business understanding, sound technology systems, technology value creation, and impact spillover as your business takes off, I think we should talk. Any time, waiting for you to write something and send it to [email protected]