VueRouter4 – Dynamic route refresh goes blank or 404

In the past, the company had been using the Ruoyi framework, so I didn’t have a good grasp of some content, so I took advantage of this time at home to write a management system, which involved dynamic routing and handling of users with different permissions. , display different menu bars and append routes matching the user’s permissions to the router.

1. Why use dynamic routing

If the routing table is written directly, the user can directly reach the target page by manually entering the URL without logging in.

When the user logs in, we take the route list returned by the backend and match the routes in the local dynamic routing list -> Route comparison.

  • Assuming that the backend returns that the user has route A, and route A also exists in the local dynamic route list, then the local route A is directly added to the router.
  • Of course, you can not choose route comparison, but directly use the route list returned by the backend.
  • However, if there is no comparison, the routing table returned by the backend may contain routing files that are not available locally, and the project will report an error.
  • In the following examples, the route comparison step will be added.

2. Create routing table

We should first write a public route. This public route can be accessed without logging in, such as Layout, login page, and 404 page.

  • The layout cannot be accessed without logging in, so the global front routing guard will be used for judgment later.
  • The reason why it is written in the public route is because: Subsequently appended dynamic routes need to be added to the sub-node routes of the Layout route (nested routes).
  • And this public routing table should be added to the routes attribute when the route is initialized.

Then write the routes that require permission to access into an array. This array is the local dynamic routing table, which needs to be compared with the routes returned by the backend.

import {<!-- --> createRouter, Router, createWebHashHistory, RouteRecordRaw } from 'vue-router';
import Layout from '@/layout/index.vue'; //Layout layout

//Public routing table
const constantRoutes: RouteRecordRaw[] = [
    {<!-- -->
        path: '/',
        name: 'Layout',
        component: Layout,
    },
    {<!-- -->
        path: '/login',
        name: 'Login',
        component: () => import('@/view/login.vue'),
        meta: {<!-- --> title: 'Login page' }
    },
    {<!-- -->
        path: '/:pathMatch(.*)*',
        name: 'NotFound',
        component: () => import('@/view/404.vue'),
        meta: {<!-- --> title: 'The page is lost~' }
    }
];

// dynamic routing table
const asyncRoutes: RouteRecordRaw[] = [
    {<!-- -->
        path: '/',
        name: '/',
        component: () => import('@/view/index.vue'),
        meta: {<!-- --> title: 'Console' }
    },
    {<!-- -->
        path: '/goods/list',
        name: '/goods/list',
        component: () => import('@/view/commodity/commodity.vue'),
        meta: {<!-- --> title: 'Product Management' }
    }
];

//Create a route and export the instance
export const router: Router = createRouter({<!-- -->
    history: createWebHashHistory(),
    routes: constantRoutes
});

There is still a function of route comparison. We can write a function to add routes in the above file, implement route comparison in it, and expose it.

//Parameter: Receive the user-accessible route list given by the backend.
export function addRouters(menus: any[]){<!-- -->
    // Function: Route comparison, add matching ones to routes
    const routeComparison = (menus: any[])=>{<!-- -->
        // Traverse the route list returned by the backend
        menus.forEach((menuItem: any)=>{<!-- -->
            // Determine whether the currently traversed route exists in the local dynamic routing list. Frontpath is equivalent to the local path.
            const isMatching = asyncRoutes.find((asyncItem: any)=>menuItem.frontpath === asyncItem.path);
            // If it matches, do the logical AND: whether it has been registered, if not we will add it.
            if(isMatching & amp; & amp; !router.hasRoute(isMatching.path)){<!-- -->
                // Add the matched route to the children of the Layout layout route.
                router.addRoute('Layout', isMatching);
            }

            // Determine whether the currently traversed route has child nodes. If there are child nodes and the length of the child node is greater than 0, recurse and use the child nodes for comparison.
            if(mentItem.child & amp; & amp; mentItem.child.length > 0){<!-- -->
                routeComparison(mentItem.child);
            }
        });
    }

    // Call routing comparison
    routeComparison(menus);
}

At this time, we can improve the code of the global front routing guard, create a new premission.ts file under src, and introduce it in main.ts.

import store from '@/store';
import {<!-- --> router, addRouters } from '@/router';
// (Get | delete) Token from Cookies.
import {<!-- --> getToken, removeToken } from '@/composables/auth';

//Whether user information has been obtained
const hasGetUserInfo = false;

//Global routing front guard
router.beforeEach(async (to, from, next)=>{<!-- -->
    // Get Token
    const token = getToken();

    // If there is no Token and the access is not to the login page, redirect directly to the login page. This will prevent the user from directly entering the Layout interface without logging in.
    if(!token & amp; & amp; to.path !== '/login'){<!-- -->
        return next('/login');
    }

    // If there is a Token, but the login page is accessed
    if(token & amp; & amp; to.path === '/login'){<!-- -->
        // Where did it come from, where is it going?
        return next(from.path);
    }

    // What if there is a Token and no user information is obtained?
    if(token & amp; & amp; !hasGetUserInfo){<!-- -->
        // Pull user information. If the token expires or is illegally tampered with, it will be processed in the axios interceptor.
        const getInfoRes = await store.dispatch("getInfo");
        //Add routing
        addRouters(getInfoRes.menus);
        // Set hasGetUserInfo to true
        hasGetUserInfo = true;
    }

    // Must be released at the end
    next();
});

After logging in, the server will return the Token, then store the Token in cookies, and then call router.push('/') to jump to the homepage, but the global routing pre-guard will be triggered, and then Synchronously obtain user information and then append routes.

3. After refreshing, the page becomes 404 or blank page

In this way, the dynamic route has been added, but there is a problem. The problem is reproduced below:

  • When the current browser’s routing address is one of the routes on the asyncRoutes list, if you press F5 to refresh the page, it will become 404 or blank.
  • If there is a routing rule matching 404 in constRoutes, then 404 will be displayed, otherwise it will be a blank page, and there will be a warning in the console.
  • But, after you add the routes, call the router.getRoutes() method, and you can see that the routes have been added.

According to normal code logic, the following three steps will be performed: obtain user information, add routes, and release routes.

  • These three steps are all executed synchronously, and my dynamic routing has been added. There should be no problems after release.

4. Problem Analysis: Prelude:

First, we comment out the 404 routing item, put a breakpoint on the first line in the beforeEach() callback function, and enter debugger;.

router.beforeEach((to, from, next)=>{<!-- -->
    debugger;
    //.......
});

Observing the console when refreshing, we can see that a warning was thrown to us, meaning: did not find the location corresponding to the path.

[Vue Router warn]: No match found for location with path "The path of the refreshed dynamic route"

From this we know thatthe problem has already occurred before entering the callback function in beforeEach().

We can try to delete debugger; and print the to attribute in the callback function at the same location.

console.log(to);

// result
{<!-- -->
    "fullPath": "/goods/list",
    "path": "/goods/list",
    "query": {<!-- -->},
    "hash": "",
    "params": {<!-- -->},
    "matched": [],
    "meta": {<!-- -->},
    "href": "#/goods/list"
}

You can see that the matched array is empty, which means that no relevant routes are matched. If you delete the 404 rule comment in constRoutes, then there will be only one element in matched, which is the 404 route.

will render the content on matched. There are two router-view in our project, in App.vue and Layout There is one layout each.

  • Suppose there are two elements in the matched array, the first element is the Layout route, and the second is the /goods/list route.
  • Then the router-view in App.vue will render the Layout component, and then the router-view in the Layout will render the /goods/list routing component.
  • The closer the matched element is, the closer the router-view used is to the outer layer.

5. Problem analysis: Reason:

In fact, this is all related to to. After refreshing, we trigger the global front routing guard, and then call the callback function in it.

Then when the beforeEach callback function is triggered, vueRouter needs to set the matching route for matched. If the 404 page is not set, then the array is empty and a warning will be issued. Since it is empty, there is no content to give to the router- view renders, and a blank page will appear.

If there is a 404 page, before adding the dynamic route, enter a non-existent address. The element of the matched array must be a 404, so router-view will render the 404 page~

In short, this to is the to when dynamic routing is not appended, so that’s why it is like this.

6. Solve the problem:

First of all, we need to know: in the global front guard, the difference between next() and next('/').

In the global front guard, calling next() means releasing, while calling next(/’) means redirecting.

  • The difference between the two is: next('/') will interrupt this navigation and re-trigger the routing guard, but next() will not, it will simply let it go.

so! ! ! ! We can solve the problem by re-triggering the route guard after adding the route instead of letting it go directly.

if(token & amp; & amp; !hasGetUserInfo){<!-- -->
    // Pull user information. If the token expires or is illegally tampered with, it will be processed in the axios interceptor.
    const getInfoRes = await store.dispatch("getInfo");
    //Add routing
    addRouters(getInfoRes.menus);
    // Set hasGetUserInfo to true
    hasGetUserInfo = true;
    //When the addition is completed, perform a redirection directly
    return next(to.path);
}

// Must be released
next();

Ok!! Problem solved~