微前端模式下子应用最常访问页面最佳实现

907 阅读5分钟

一、前言

最近一直在做tob项目,一条业务线多个tob工程

之前有文章写过实现微前端 👉 tob系统微前端实践总结

也写过多工程之间都存在的公共模块如何处理 👉 vue多工程间公共模块处理最佳实践

最近遇到一个比较有意思、且通用的需求🧐 本着总结为最佳实践的初心,写下本篇文章,欢迎大家讨论🤩

二、项目需求

在微前端的基座系统“工作台“页面实现一个”最常访问页面“功能,根据用户使用各子应用页面的频率,记录出该用户最常访问的子应用top4页面🤖

(注意:top4页面属于各个不同的子应用页面,基座系统的页面不在记录范围内,因为基座系统的入口相较子应用页面入口浅,可能记录在最常访问页面模块,同时工作台上有其他快捷入口)

问题关键点:

  1. 需要至少记录页面的url地址,及页面对应的菜单名称
    • url地址:好说,有页面就有路由,只是需要注意,有些页面是动态路由、带有参数,所以需要记录的是完成的url地址,比如/lego/#/cms-page-manage/template-instance/view?pageId=555&authCode=lego_page_555
    • 菜单名称:一般在一级、二级菜单上的页面都会有对应的菜单名称,而非菜单上的页面,比如详情页等,除非在router-mate路由元信息里面有对应菜单名称
      • 我们的系统因为之前做过统一面包屑处理,所以每个页面router-mate路由元信息里面都有对应菜单名称
      • 如果没有的话可以加上,顺便把统一面包屑给实现了,或者加判断过滤掉没有菜单名称的页面(兜底),即没有菜单名称的页面不再记录范围内
  2. 服务端记录还是前端记录,考虑到SPA时代、服务端记录实在太重,前端用localstorage记录实现
    • 需要记录页面的访问次数,根据次数取出最常访问top4
    • 记录页面最后一次访问的时间戳,比如访问次数最多的6个页面访问的次数依次是23(页面1),17(页面2),14(页面3),9(页面4),9(页面5),9(页面6);那么top4的第四个页是页面几,应该根据时间戳来,取时间戳大的那个页面(也就是最近访问的那个页面)
  3. 在哪个时刻将页面访问记录存到localstorage也很关键,是否可以不侵入子应用,在基座系统就实现?下面技术方案实现具体说明

三、技术实现

我们的技术栈都是vue,所以下面的实现方案都是基于vue,但是思路是通用的~
在全局前置守卫,打印子应用路由信息

import VueRouter from "vue-router";
const router = new VueRouter({ ... })

router.beforeEach((to, from, next) => {
    console.log(to)
}

子应用打印出如下: image.png 基座系统打印出如下: image.png 可以看到没有name,没有meta,针对问题关键点1, 在基座系统通过路由可以拿到url,但是拿不到菜单名称

为什么没有name,没有mate?
基座系统的路由跟子应用的路由不是同一个实例。因为在基座系统中,子应用的路由是/*,不清楚这块的指路tob系统微前端实践总结qiankun

方案一:微前端提供事件,在子应用的全局前置守卫触发事件存localstorage

基座系统:注册全局事件中心,监听用户打开子应用页面行为

import Event from 'eventemitter3';
import { setMenuUrl } from "@/utils/menu-url";
const eventCenter = new Event();

// 监听用户打开子应用页面行为
window.eventCenter.on('GET_SUPAPP_MENU', ({ path, name }) => {
     // 存localstorage操作
     setMenuUrl(name, path)
 })

子应用系统:在全局前置守卫触发用户打开页面事件

import VueRouter from "vue-router";
const router = new VueRouter({ ... })

router.beforeEach((to, from, next) => {
     if (window.__POWERED_BY_MICRO__) { // 微前端模式下
         // 触发用户打开页面事件
         window.eventCenter.emit("GET_SUPAPP_MENU", {path: `/${process.env.VUE_APP_NAME}#${to.fullPath}`, name: to.meta.menu.title});
     }
}

缺点: 侵入子应用,需要对n个子应用都做处理

那有没有可以不侵入子应用去实现呢,见方案二

方案二:在基座系统的子应用容器组件中通过获取document.title存localstorage

其实就是想,还有没有办法可以拿到页面的菜单名称,document.title是可以的,也就是浏览器的标题,如下图 image.png
上面说了,我们的系统每个页面都有名称,document.title也都根据页面名称设置了,没有设置的话,可以设置上,这样更规范~

基座系统是可以拿到子系统的url的,所以可以在基座系统的子应用容器组件(可参考:tob系统微前端实践总结)中去处理,如下代码

// 子应用容器.vue
import { setMenuUrl } from "@/utils/menu-url";
<script>
export default {
  data() {
    return {
      documentTitle: document.title
    };
  },
  watch: {
    $route(val) {
      this.handleRouteChange(val);
    },
  },
  beforeRouteEnter(to, from, next) {
    next((vm) => {
      const targetNode = document.getElementsByTagName("title")[0];
      const config = { attributes: true, childList: true, subtree: true };
      const callback = function () {
        vm.documentTitle = document.title
      };
      // 监听dom节点 titie变化
      const observer = new MutationObserver(callback);
      observer.observe(targetNode, config);
      vm.handleRouteChange.apply(vm, [to]);
    });
  },
   methods: {
    // 监听路由变化
    handleRouteChange(val) {
      if(this.documentTitle != process.env.VUE_APP_INDEX_TITLE && val.fullPath.split("/").length>3){
          // 存localstorage操作
          setMenuUrl(this.documentTitle, val.fullPath)
      }
    }
   }
}
</scripe>

针对问题关键点2,如何存localstorage

\\ src/utils/menu-url.js
import { storage } from './storage'

export const setMenuUrl = (name, url) => {
  if(!storage.getItem('menuUrl')){
    // 默认常用访问模块
    const initMenuUrl = {
      url: {
        name: "xxx",
        count: 1,
        time: new Date().getTime()
      }
    }
    storage.setItem('menuUrl', JSON.stringify(initMenuUrl))
  }
  const menuUrl = JSON.parse(storage.getItem('menuUrl'))
  if(menuUrl[url]){
    menuUrl[url].count++,
    menuUrl[url].time = new Date().getTime()
  } else {
    menuUrl[url] = {
      name,
      count: 1,
      time: new Date().getTime()
    }
  }
  storage.setItem('menuUrl', JSON.stringify(menuUrl))
}

浏览器localstorage截图 image.png