前端单页面应用怎么做跳转拦截?试试“动态路由”

1,632 阅读3分钟

前言

以前我在做单页面应用时,由于静态页面资源都是打包扔在 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 排版