前言
以前我在做单页面应用时,由于静态页面资源都是打包扔在 nginx 上的,而 nginx 只作为流量接入层,也没法写业务逻辑,使之产品提出特定页面需要指定用户才能访问的功能时,基于“经验”限制,我只能举白旗。
后果就是,用户可以“自由”通过链接看到非预期的页面内容,虽然我们可以通过后端接口的数据限制,或者页面加载后的二次强跳转使“流程回归正常”,不过页面加载后的一次重新加载体验总不是很好。虽然试图通过 node 做个服务端渲染,但相比静态资源直接托管成本过大,还是放弃了。
有幸最近接触 jeecg,看到了 动态路由 那么种形式,结合现代前端框架,让单页面应用也能达到服务端过滤渲染的效果,还是涨了不少见识,下面逐步展开说明。
普通路由和路由懒加载
首先还是通过 vue 先讲下普通路由和路由懒加载两种方式。
普通路由
这是“最最最”基本的路由使用方式:
import Common from '../pages/router/Common.vue';
const routes = [
{
path: '/router/common',
component: Common,
},
];
这种方式很简单,但当一个项目有路由变多后,文件会变得很大,影响页面加载。后面就有了路由的“懒加载”方式。
路由懒加载
通过 webpack 的 Code Splitting 机制实现:
import Common from '../pages/router/Common.vue';
const routes = [
{
path: '/router/common',
component: Common,
},
{
path: '/router/lazy',
component: () => import(/*webpackChunkName: "router-lazy"*/ '../pages/router/Lazy.vue'),
},
];
webpack 提供了很强大的模块打包机制,我们能借助 import 方式,把指定的路由模块切割出来,当访问到此页面路由时再请求这个 js 文件。
下面提供了一个简单示例,注意看 network 请求:
动态路由
上述两种方式,也是我之前常用的。不过,对于复杂的项目,或者偏后台管理的应用,这些路由机制却显得有些薄弱。因为我们无法做到特定页面访问的限制,只要你知道地址都能跳转,甚至如果代码写的不够健壮,会被有些人“搞事”,毕竟都是静态资源,F12 都能看到。
这里将介绍我在 jeecg 中学到的 动态路由 加载方式,它解决了上述问题,即使在单页面 SPA 应用中。那我们该如何做呢?
beforeEach
我们知道 vue-router 有 beforeEach 做路由守卫 api,在它里面可以做路由的拦截,通常我们会在里面判断 token 或者一些业务标识,来做跳转限制。
不过在这,我们将访问特定的接口,来做动态路由效果:
router.beforeEach((to, from, next) => {
const _vue = router.app;
fetchDynamicRoutes()
.then((dynamicRoutes) => {
// 组装新路由
const newRoutes = normalizeRoutes(dynamicRoutes);
_vue.$router.addRoutes(newRoutes);
next();
})
.catch((err) => {});
});
fetchDynamicRoutes 是一个封装后端请求的 Promise 方法,它将获取前端路由结构的数据,就像这样:

标准化路由
由于后端返回的数据和前端路由结构有些差异,所以会有个 normalizeRoutes 方法做整合,并且通过 懒加载 机制做进一步优化。
/**
* 标准化路由
*
* @param {*} routes
*/
export function normalizeRoutes(routes) {
return routes.map((router) => {
return {
path: router.path,
component: _parseComponent(router.componentName),
meta: router.meta,
children: Array.isArray(router.children) ? normalizeRoutes(router.children) : [],
};
});
}
/**
* 解析路由
*
* 如果 routesMap[componentName] 取不到值,将动态加载打包后对应的组件文件
* @param {*} componentName
*/
function _parseComponent(componentName) {
return routesMap[componentName] || (() => import(/*webpackChunkName: "router-dynamic"*/ `../pages${componentName}.vue`));
}
这需要前后端约定好,因为后端返回的路由数据,在前端文件必须存在。
加入状态管理(vuex)
我们不能每次都访问后端接口拿路由数据,更需要状态管理机制让整个交互性更流畅:
router.beforeEach((to, from, next) => {
const _vue = router.app;
// 获取权限路由
if (store.getters.permissionList.length === 0) {
fetchDynamicRoutes()
.then((dynamicRoutes) => {
const newRoutes = normalizeRoutes(dynamicRoutes);
_vue.$router.addRoutes(newRoutes);
store.commit('SET_PERMISSIONLIST', newRoutes);
store.commit('SET_SIDE_MENUS', newRoutes[0].children);
next(to.path);
})
.catch((err) => {
console.log(err);
});
} else {
next();
}
});
我们可以通过用户 Logout 或者后端统一接口来清空目前授权路由数据,如果没有尽可能的让前端缓存下来,这样我们可能拿这些数据做更多的事情,比如:页面导航栏、菜单栏:
data() {
return {
list: this.$store.getters.sideMenus
};
},
比如,目前我示例的页面左侧菜单都是通过这样的机制渲染出来的:
最后
如此,简单的动态路由机制就完成了,虽然不及服务端 SSR 能做更严格的控制,不过就普通级别的项目基本够用了。
这样就能把 vue 中路由中几个核心的知识点给贯穿起来,jeecg 里这样的操作还是挺厉害的。有时候限制我们的不是代码怎么实现,而是脱离业务代码往程序设计上走,才能在某天也有这样的奇思妙想,不然还是需要读万卷书。
本文使用 mdnice 排版