More documentation

preface

It is better to travel thousands of miles than to read thousands of books. Practice is very important, because I will learn a lot from this process, so as to sum up experience and formulate a better execution process based on experience. Today, how to share the actual development of better handling permissions, dynamic routing

Router Indicates a route with parameters

Both Vue and React provide dynamic matching in /: ID mode. This mode is usually used to match certain types of pages with the same structure. This is not the focus of our discussion

Permissions routing

Routes can carry a lot of information, such as mandatory path, component, etc., as well as icon, keepAlive, etc., which will also be covered in the next part. Here is a brief talk. Back to our topic, generally there are two ways to route permission:

The front-end retains full routes

There are also two ways to do this:

Methods a

The front-end retains a full route mapping relationship, generates a new route data based on the permission identifier returned by the back-end interface, and then adds this route data to the router

  • Advantages: There are no routes that do not have permission. Therefore, the routes cannot be accessed by entering urls

  • Disadvantages: This scheme is not very suitable for businesses that want users to know which ones have permissions and which ones do not, so as to promote users to consume and open more advanced accounts. However, corresponding prompts can be made in the route guard to solve such problems

Way 2

The front end saves the full route, and decides the user’s whereabouts according to the permission data of the back-end students in the route guard

  • Advantages: All processing is handled in the route guard, no additional data needs to be maintained

  • Disadvantages: There are routes without permission, which has low fault tolerance

Ok, that’s all, but that’s not the point we want to share with you today, because there are still a lot of questions:

  1. The front-end needs to maintain a piece of data
  2. Data maintained in the front-end still needs to be filtered according to the real permissions
  3. In the actual development, the new front-end module needs to manually configure routing
  4. The front-end directory specification is not practical (more on that later)
  5. Multiple copies of permission – and routing-related data may be generated and maintained

Above all, let’s introduce a more automated, safer, and more disciplined approach to developer behavior

Permission ID Dynamically generates routes

Before that, I will introduce two methods, which are the core methods of dynamic routing implementation

require.context

Require.context is used to help us match the.vue file so that we can dynamically fetch the.vue component, try doing require.context(‘.. /pages’, true, /\.vue$/).keys() not only does it help us to match components, it also helps us to dynamically register components, like this:

// Register all components under require.context
import Vue from 'vue'

const componentsContext = require.context('.. /components'.true./\index.vue$/)

componentsContext.keys().forEach(component= > {
  const componentConfig = componentsContext(component)
  /** * compatible with import export and require module.export */
  const ctrl = componentConfig.default || componentConfig
  Vue.component(ctrl.name, ctrl)
})

Copy the code

addRoutes

Router. AddRoutes is used to dynamically add data to the route, but it also causes route duplication because the route information is reset only when the page is refreshed. Therefore, when you log out and call router. There are two ways to solve this:

Methods a

// The subsequent problems must be solved manually
// If the project is deployed in a non-root directory, assume that it is deployed in the test folder
Href = '/test/login'
window.location.href = '/login'
Copy the code

Way 2

// 2. Clear the routing information before adding routes
// router.js
import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

const createRouter = () = > new Router({
  mode: 'history'.routes: []})const router = createRouter()
export function resetRouter () {
  const newRouter = createRouter()
  router.matcher = newRouter.matcher 
}
export default router
// Call resetRouter before calling addRoutes
/ / login. Login js
import {resetRouter} from '@/router'
resetRouter()
router.addRoutes(routes)
Copy the code

implementation

Static parts

The so-called static part is the page that will not participate in the permission, such as the home page, login and registration page, assuming that we now only have a home page, today’s route is as follows:

const router = new Router({
  mode: 'hash'.routes: [{path: '/'.name: 'Home'.component: Home
    },
    {
      path: '/ 404'.component: Notfound
    },
    {
      path: The '*'.redirect: '/ 404'.component: Notfound
    }
  ]
})
Copy the code

Access data

In order to realize such automatic routing, it also needs the cooperation of the back-end students. In fact, it only returns the information needed by the front-end according to the fixed data format. Suppose that the routing format returned after we agree with the back-end students is as follows:

[{
	"id": "1"."icon": "el-icon-user-solid"."title": "User"."mark": "user"."children": [{
		"id": "1-1"."icon": "el-icon-s-flag"."title": "White list"."mark": "white"."children": [{
			"id": "1-1-1"."icon": "el-icon-s-order"."title": "Goods"."mark": "commodity"
		}, {
			"id": "1-1-2"."icon": ""."title": "Details of Commodity"."mark": "commodityDetail"."isMenu": false}}]]}, {"id": "2"."icon": "el-icon-takeaway-box"."title": "Production line"."mark": "proline"
}, {
	"id": "3"."icon": "el-icon-takeaway-box"."title": "Access jump to no permission"."mark": "test"."power": false
}]
Copy the code

A quick breakdown of the data:

  1. similaruserUser-like menus have sub-levels that are not added when routing data is generatedpath
  2. similarcommodityDetailMerchandise details such a menuisMenuforfalseWill be added to routing data as routing components, but will not generate menus (routing parameters are not considered here).
  3. similarpowerAccess jump to No permissions such a menu is presented as a menu, but redirects to the no permissions page to incentivize the user to upgrade

Look like complex, actually such configuration belongs to one-time, logic is also one-time

Generate routing

This is the key to generating routing data. Business developers must follow certain rules to create directories. In frameworks like NUXT, routes are generated by file name, and the folder name represents the structure of path

Suppose we match the rules of the components are as follows: to match the same folder. Vue components for routing components, with the above routing for example production line access path for/main/proline, goods access path for/main/user/white/commodity, according to the rules let us write the logic:

// Get the component
const componentsContext = require.context('.. /pages'.true./\.vue$/).keys()

class AddMenuRouter {
  constructor() {
		this.mainRoutes = [{path: '/main'.name: 'main'.component: main}]
		this.componentsContext = componentsContext
	}
  // Match the component
  getCom(v) {
		return resolve= > require([`@/pages${v}`], resolve)
	}
  // Retrieve path, 404 if not present
	checkPath(v) {
		let a, s, l, n, m
		this.componentsContext.map(item= >{
			a = item.split('/')
			s = a[a.length-1]
			l = s.substring(0, s.length-4)
			if(v === l) n = item
		})
		m = n || `./404/Notfound.vue`
		return m.substring(1, m.length)
	}
  // Handle interface permission data
  getChildRoutes(routes, path = "/main") {
		if(! routes)return
		if(Object.prototype.toString.call(routes) ! = ="[object Array]") return console.warn("Routes data format error")
		routes.length && routes.map((item) = > {
			item.meta = { keepAlive: true } // Cache information for later use
			if(item.children) {
				this.getChildRoutes(item.children, `${path}/${item.mark}`)}else {
				item.name = item.mark
				item.path = `${path}/${item.mark}`
				item.component = item.mark ? this.getCom(this.checkPath(item.mark)) : this.getCom(this.checkPath('Notfound'))}})return routes
	}
  // Get the redirect path from /main, assuming you go to the first page with a path from the first menu
  getRedirect(routes) {
		if(! routes)return
		let lastChild
		if (routes.children && routes.children.length > 0) {
			return this.getRedirect(routes.children[0])}else {
			lastChild = routes
			return lastChild
		}
	}
  // Get menu permission
  _getMenuData() {
    // The configuration can be done locally and jointly to cope with the impact of back-end abnormalities on development. DevMenuData is a custom menu data
		return config.devmodule ? devMenuData : getMenuData().then((data) = > {
			if (data.statusCode == '200') return data.result.children ? data.result.children : []
		})
	}
  // Get the data to be added to the route eventually
  async getMainRoutes(menudata) {
    try {
      const menudata = await this._getMenuData();
      const routes = this.getRoutes(menudata)
      const redirect = this.getRedirect(routes[0])
      this.mainRoutes[0].redirect = redirect && redirect.path ? redirect.path : ""
			this.mainRoutes[0].children = routes
      return this.mainRoutes
    } catch(e) {
      console.error(`error:${error}`)}}}export default new AddMenuRouter()

Copy the code

Ok to get the dynamic partial routing information, test to see the data


const addMenuRouter =  AddMenuRouter()
addMenuRouter.getMainRoutes()
console.log(addMenuRouter.mainRoutes)

/ / /
/ / {
// "path": "/main",
// "name": "main",
// "redirect": "/main/user/white/commodity",
/ / "component" : ƒ main (resolve),
// "children": [
/ / {
// "id": "1",
// "icon": "el-icon-user-solid",
// "title": "user"
// "mark": "user",
// "children": [
/ / {
// "id": "1-1",
// "icon": "el-icon-s-flag",
// "title": "whitelist ",
// "mark": "white",
// "children": [
/ / {
// "id": "1-1-1",
// "icon": "el-icon-s-order",
// "title": "product ",
// "mark": "commodity",
// "meta": {
// "keepAlive": true
/ /},
// "name": "commodity",
// "path": "/main/user/white/commodity",
/ / "component" : ƒ main (resolve),
/ /},
/ / {
// "id": "1-1-2",
// "icon": "",
// "title": "product details ",
// "mark": "commodityDetail",
// "isMenu": false,
// "meta": {
// "keepAlive": true
/ /},
// "name": "commodityDetail",
/ / "path" : "/ main/user/white/commodityDetail",
/ / "component" : ƒ main (resolve),
/ /}
/ /,
// "meta": {
// "keepAlive": true
/ /}
/ /}
/ /,
// "meta": {
// "keepAlive": true
/ /}
/ /},
/ / {
// "id": "2",
// "icon": "el-icon-takeaway-box",
// "title": "line ",
// "mark": "proline",
// "meta": {
// "keepAlive": true
/ /},
// "name": "proline",
// "path": "/main/proline",
/ / "component" : ƒ main (resolve),
/ /},
/ / {
// "id": "3",
// "icon": "el-icon-takeaway-box",
// "title": "Access jump to no permission ",
// "mark": "test",
// "power": false,
// "meta": {
// "keepAlive": true
/ /},
// "name": "test",
// "path": "/main/test",
/ / "component" : ƒ main (resolve),
/ /}
/ /]
/ /}
// ]

Copy the code

Add the login

Add the data to the route after successful login

{
  methods: {
    async _login() {
      const menuRouter = await this._addMenuRouter()
      this.$router.addRoutes(menuRouter)
      this.$router.push('/main')}_addMenuRouter() {
      return addMenuRouter.getMainRoutes().then(data= > data)
    }
  }
}
Copy the code

Menu, route guard

The menu is also generated based on the data obtained from getMainRoutes. More processing can be done in router.beforeEach, because all permissions are already in the route and can be easily retrieved and processed, there is no extension here

Cache pages

The keep-alive configuration is used to determine whether a page is cached or not. Now AddMenuRouter adds a cache to the keep-alive configuration:

// Get the component
const componentsContext = require.context('.. /pages'.true./\.vue$/).keys()

class AddMenuRouter {
  constructor() {
		this.mainRoutes = [{path: '/main'.name: 'main'.component: main}]
		this.componentsContext = componentsContext
    this.keepAliveArr = {}
	}
  // Match the component
  getCom(v) {
		return resolve= > require([`@/pages${v}`], resolve)
	}
  // Retrieve path, 404 if not present
	checkPath(v) {
		let a, s, l, n, m
		this.componentsContext.map(item= >{
			a = item.split('/')
			s = a[a.length-1]
			l = s.substring(0, s.length-4)
			if(v === l) n = item
		})
		m = n || `./404/Notfound.vue`
		return m.substring(1, m.length)
	}
  // Handle interface permission data
  getChildRoutes(routes, path = "/main") {
		if(! routes)return
		if(Object.prototype.toString.call(routes) ! = ="[object Array]") return console.warn("Routes data format error")
		routes.length && routes.map((item) = > {
			item.meta = { keepAlive: true } // Cache information for later use
			if(item.children) {
        // Only the routing component's cache is recorded
        this.keepAliveArr[item.mark] = { keepAlive: item.meta.keepAlive }
				this.getChildRoutes(item.children, `${path}/${item.mark}`)}else {
				item.name = item.mark
				item.path = `${path}/${item.mark}`
				item.component = item.mark ? this.getCom(this.checkPath(item.mark)) : this.getCom(this.checkPath('Notfound'))}})return routes
	}
  // Get the redirect path from /main, assuming you go to the first page with a path from the first menu
  getRedirect(routes) {
		if(! routes)return
		let lastChild
		if (routes.children && routes.children.length > 0) {
			return this.getRedirect(routes.children[0])}else {
			lastChild = routes
			return lastChild
		}
	}
  // Get menu permission
  _getMenuData() {
    // The configuration can be done locally and jointly to cope with the impact of back-end abnormalities on development. DevMenuData is a custom menu data
		return config.devmodule ? devMenuData : getMenuData().then((data) = > {
			if (data.statusCode == '200') return data.result.children ? data.result.children : []
		})
	}
  // Get the data to be added to the route eventually
  async getMainRoutes(menudata) {
    try {
      const menudata = await this._getMenuData();
      const routes = this.getRoutes(menudata)
      const redirect = this.getRedirect(routes[0])
      this.mainRoutes[0].redirect = redirect && redirect.path ? redirect.path : ""
			this.mainRoutes[0].children = routes
      // Store cached information
      store.commit('SET_KEEPALIVEARR'.this.keepAliveArr)
      return this.mainRoutes
    } catch(e) {
      console.error(`error:${error}`)}}}export default new AddMenuRouter()
Copy the code

We use keepAliveArr to store all cached pages. Then how to quickly toggled whether to cache pages or not

// Vue2.0 writes a mixin global mix by calling m_setPagesCache and m_removePagesCache
// vue3.0 write to hooks
methods: {
  /** * Cache the page *@param {e} Parameter e: The name of the page to cache, for example: {to.name} */
  m_setPagesCache(e) {
    let deepKeepAliveArr = this.m_copy(store.state.keepAliveArr)
    deepKeepAliveArr[e].keepAlive = ture
    setTimeout(() = > store.commit('SET_KEEPALIVEARR', deepKeepAliveArr), 0)},/** * Cancel page caching *@param {e} Parameter e: uncache the page name */
  m_removePagesCache(e) {
    let deepKeepAliveArr = this.m_copy(store.state.keepAliveArr)
    deepKeepAliveArr[e].keepAlive = false
    store.commit('SET_KEEPALIVEARR', deepKeepAliveArr)
  }
},
// Route the hook function to switch cache quickly
beforeRouteEnter(to,from,next) {
  if(store.state.keepAliveArr[to.name]){
      to.meta.keepAlive = store.state.keepAliveArr[to.name].keepAlive
    }
    next()
  }
}
Copy the code

Now that the basic functionality is complete, it may seem like a lot, but this is a one-time, permanent package with the following benefits:

  1. Business development requires only relational business code and no logic at the business architecture level
  2. Adding a module is extremely simple. You only need to create a file, and the file path is the access address
  3. Specification front-end directory structure, which is a mandatory specification
  4. Standardize permission configuration and integrate permission data
  5. There is no need to maintain data in the front end. Pages with permissions, pages without menus, pages with permissions and references (not dealt with in demo), pages without permissions and pages without permissions can be processed in the process of obtaining dynamic routes, which is relatively flexible

conclusion

There are a lot of details, like routing data and caching data in conjunction with front-end caching, and that’s pretty much it. Practice is still very important, sometimes want to do a thing to do it, regardless of how much return he has, the process itself is harvest