基于vue-router动态组装路由,以及tags持久化的实现方案

616 阅读4分钟

最近基于项目需求进行了一些页面访问的功能迭代,需求如下

  1. 支持tags刷新页面后保持打开的记录(暂时不需要保留数据)
  2. 同一个业务页面可以同时打开任意数量并独立keepalive

页面标签的实现

这里取element-admin 的代码 进行伪代码举例

const loadLocal = () => {
  // 加载初期从local取持久化的tag数据,同时路由也要同步加载
  const data = getTags()
  return {
    visitedViews: data,
    cachedViews: data.map(item => item.name)
  }
}
const state = {
  ...loadLocal(),
}
const mutations = {
    //...默认未改动的代码
    // 只要state.visitedViews 数据出现更新同步到local
  ADD_VISITED_VIEW: (state, view) => {
    if (state.visitedViews.some(v => v.path === view.path)) return
    state.visitedViews.push(
      Object.assign({}, view, {
        title: view.meta.title || 'no-name'
      })
    )
    
    // 这个位置调用 localStorage.setItem 进行数据保存,记得处理一下环形数据
    setTags(state.visitedViews)
  },
  
  DEL_OTHERS_VISITED_VIEWS: (state, view) => {
    state.visitedViews = state.visitedViews.filter(v => {
      return v.meta.affix || v.path === view.path
    })
    setTags(state.visitedViews)
  },
  DEL_ALL_VISITED_VIEWS: state => {
    // keep affix tags
    const affixTags = state.visitedViews.filter(tag => tag.meta.affix)
    state.visitedViews = affixTags
    setTags(state.visitedViews)
  },
}

路由相关

简单说说实现的思路

  1. 首先将路由相关的数组进行拆分,需要动态插入的路由数组单独维护
  2. 实现一个动态插入数组的方法以及更新路由的方法
  3. 调用过程为 []需要插入的路由信息 => 存入数组 => 创建新的路由对象 => 使用新的路由对象覆盖原有的matcher属性
    注:如果需要替换可以添加splice方法
    下面是一部分可用的伪代码
import Vue from 'vue'
import Router from 'vue-router'
import Layout from '../layout' // 通用的layout
import NotFind from '../NotFind' // 404页面
import welcome from '../welcome' // 欢迎页面
import Login from '../login' // 登录页面

Vue.use(Router)

// 实际业务页面数组
const layoutChildren = [
// .. 这里放实际的业务组件如果有需要权限控制的通过下方的setAsyncRouter插入
{
  path: '/dashboard',
  name: 'Dashboard',
  component: welcome,
  meta: {
    title: "首页",
    icon: 'dashboard',
    affix: true
  }
}]

// 基础路由数组
const routes = [{
    // 登录页面
    path: '/login',
    component: Login,
    hidden: true
  },
  {
    path: "/",
    redirect: '/dashboard',
  },
  {
    path: "/",
    component: Layout,
    // 实际业务页面数组放置的位置
    // 如果有多个layout那么就用多个数组进行区分
    children: layoutChildren
  },
  // 兜底400页面
  {
    path: '*',
    component: NotFind,
  }
]


// 创建全新的路由生成时读取 routes
const createRouter = () => new Router({
  //  一个构造全新router的方法 网上直接cv应该都差不多
  mode: 'history',
  base: process.env.VUE_APP_ROOT_PATH,
  scrollBehavior: () => ({
    y: 0
  }),
  routes
})

// 先初始化一个路由
const router = createRouter() // 最开始的路由

// 内置默认的路由表调用后修改routes数据
const setAsyncRouter = arr => {
  // 把需要新增的路由数据推入到数组顶部
  // 这里只做更新路由数据源的操作
  v.unshift(...[].concat(arr))
}

// 刷新路由的方法重新构建新的路由然后替换 matcher 达到刷新的效果 如果routes内容出现变动的话
const resetRouter = () => {
  // 这一步通过重新创建新的router会根据最新的路由数据生成新的路由对象
  const newRouter = createRouter()
  router.matcher = newRouter.matcher // 替换matcher完成路由更新
}

// 暴露给外部调用的动态插入的实现
export const append = arr => {
  if (Array.isArray(arr)) {
    setAsyncRouter(arr)
    resetRouter()
  }
}
// 从local取到持久化的tags记录
JSON.parse(localStorage.getItem('tags') || '[]').forEach(item =>{
  const {
    name: newName,
    path,
    meta
  } = item
  const [name, uuid] = newName.split('/')
  
  // 这个地方是个人开发约定切割/获取实际组件的name和uuid
  // 从newName中取到uuid 如果取不到uuid说明不是需要多个打开的组件
  if (uuid) {
    const {
    // 通过resolve取得路由信息中的路径 不要取href,href会携带base路径后续拼接会重复
      resolved: {
        path: p
      }
    } = router.resolve({
      name,
    });
    asyncRoute.push({
      path,
      component: {
      // getMatchedComponents通过传入的路径会查找现有路由中存在的组件
      // 返回的数组是查询路径,路径的最后一个是我们实际需要的路由对象所以使用pop弹出
      // 结构是为了隔离对象防止出现引用的问题
        ...router.getMatchedComponents(p).pop(),
        // 因为keepalive使用name进行组件缓存所以需要做到和基础组件名字有规则的区分
        name: newName
      },
      meta,
      // 这里同理给路由方法使用
      name: newName
    })
  }
})

// 这里是暴露给实际页面使用的push方法
export const push = (name, uuid, query, meta) => {
// name = 需要提取的组件在路由中的name
// uuid = 这个很重要用来区分不同的页面因为你可能需要打开之前已经打开的页面
// query = 因为需要对页面跳转做持久化所以携带的数据只能放到query保存了
// meta = 需要修改的meta信息 比如tag标签的名称
  const {
    resolved: {
      path: p
    },
    route: {
      meta: m
    }
  } = router.resolve({
    name,
  });
  const z = router.getMatchedComponents(p).pop();
  const path = [p, uuid].join('/')
  const newName = [name, uuid].join('/')
  // 👆以上部分代码不做重复说明
  append([{
    path,
    component: {
      ...z
    },
    meta: Object.assign(m || {}, meta || {}),
    name: newName
  }])
  Object.assign(m || {}, meta || {})
  
  // 👆以上代码做简单的数据合并
  const info = {
    name: newName,
  }
  if (query) {
    info.query = query
  }
  // 路由更新完毕后开始跳转
  router.push(info)
}
router.$push = push
export default router