后台项目学习笔记(二)

100 阅读11分钟

巩固学习:用自己的语言描述功能如何做的,总结所做过的功能。

功能点

  1. 后台管理系统目录设计
  2. 环境变量如何配置
  3. 通用请求如何封装
  4. 封装通用Svg组件
  5. 动态列表格实现
  6. 可拖拽可改变排序的表格实现
  7. 如何根据路由生成菜单
  8. headerSearch组件页面检索菜单处理方案
  9. tagsView 处理方案

后台管理系统目录设计

需求
后台管理系统,由于功能都类似,所以总结要有哪些文件夹和文件,都是做什么用的。以后新建一个后台系统项目,可以照搬。

实现
以下为单页应用的示范

--- src文件夹
------ api文件夹 // 存放所有的接口,每个业务模块为一个js文件
-------------- 模块名.js
------ assets 文件夹 // 存放静态图片
------ components文件夹 // 存放自定义的公共组件的文件夹
------ constant文件夹// 存放全局常量,里面的js文件根据业务功能模块划分
------ i18n文件夹 // 国际化配置相关
-------------- lang文件夹 // 存放具体的国际化语言包,里面通常还要进一步根据业务模块划分文件夹
---------------------- XX模块文件夹
-------------- index.js // 国际化全局配置,初始化国际化
------ icons文件夹 // 存放自定义图标的文件夹
------ plugins文件夹 // 在此安装第三方插件
------ router文件夹 // 路由相关配置,里面通常会再细分具体模块
-------------- XX.js // 具体某个模块的路由
-------------- index.js // 总的路由配置,在此导入所有模块的路由
------ store文件夹 // vuex全局响应式数据,具体要细分业务模块
-------------- modules // 存放每个业务模块的全局变量
-------------------- XX模块.js
-------------- index.js // 初始化vuex配置,导入所有模块的数据
------ styles文件夹 // 全局公共样式,具体要根据功能点拆分文件,不能全部写入到一个文件内
-------------- XX功能.scss
------ utils文件夹 // 存放全局公用的工具函数
-------------- XX功能.js // 对于后台管理系统,常用的工具函数也几乎都是一样的,通常不同项目之间,这些工具函数可以被复用。
------ views // 存放具体页面,通常按业务模块划分文件夹,必须有的模块有:layout,login,error-page
-------------- layout文件夹 // 页面总体布局,所有页面的统一入口
-------------- login文件夹 // 登录页面
-------------- error-page文件夹 // 公用报错页面, 如404
-------------- XX模块文件夹 // 其他具体的业务模块页面
------ App.vue // 项目根组件,入口,通常在此不做具体的业务
------ mian.js // 项目根js文件,入口,通常在此引入,注册所有全局的组件,插件,css等

环境变量如何配置

需求
多数项目都分有开发环境,测试环境,生产环境。这些不同的环境,配置项各不相同,比如接口地址不同,打包规则不同等。在开发过程中,如何使业务代码识别到当前属于什么开发环境,并且使用对应的配置项。

实现
针对vue-cli,我们可以建立三个文件:

  1. .env.development
  2. .env.production
  3. .env.test

这三个文件对应开发,生成和测试环境,在可以在里面进行配置,注意:里面写的配置除了ENV这个变量以外,其余所有的变量,必须以VUE_APP_前缀命名,否则vue-cli不识别

例子: 开发环境.env.development

# 标志
ENV = 'development'
# base api
VUE_APP_BASE_API = '/api'
# 其他...
VUE_APP_XX = 'XXXX'

生成环境.env.production

# 标志
ENV = 'production'
# base api
VUE_APP_BASE_API = '/proc-api'
# 其他...
VUE_APP_XX = 'XXXX'

如果npm想对某一个环境打包,则应该使用命令添加环境参数,如:

"script": {
    "build:test": "vue-cli-service serve --mode=test"
}

通用请求如何封装

需求
为什么必须要封装通用请求,因为一个请求有很多通用逻辑要处理,如:对各种不同状态码的响应,添加通用请求头,报错处理等。

实现
通常封装请求这个文件写在utils文件夹里面,通常命名为request或者ajax

需要封装的东西根据项目实际需要来封装,绝大多数项目都有以下东西要封装:

  1. baseURL,根据不同环境来制定
  2. 超时时间,要和后台商议
  3. 通用请求头,如token,当前language语言
  4. 请求拦截器:根据业务需求加入请求头,根据业务需求判断是否需要取消请求。
  5. 响应拦截器:a)判断接口是否成功,成功则返回过滤后的数据。b)根据不同的状态码对不同的异常场景做处理,如未登录状态,并报出错误提示

具体怎么处理应该是和后台提前约定好的,且约定后所有的接口都必须遵守这个规范。

代码示范:

import axios from 'axios'

const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API,
  timeout: 5000
})

// 请求拦截器
service.interceptors.request.use(
    config => {
      // 在此写入各种通用业务
      // TODO...
      
      return config // 必须返回配置
    },
    error => {
      return Promise.reject(error)
    }
  )

// 响应拦截器
service.interceptors.response.use(
    response => {
        const { success, message, data } = response.data
        //   要根据success的成功与否决定下面的操作
        if (success) {
            return data
        } else {
            // 业务错误
            ElMessage.error(message) // 提示错误消息
            return Promise.reject(new Error(message))
        }
    },
    error => {
        // 在此处理各种请求失败的逻辑,如未登录
    
        // 处理 token 超时问题
        if (
          error.response &&
          error.response.data &&
          error.response.data.code === 401
        ) {
          // token超时
          store.dispatch('user/logout')
        }
        ElMessage.error(error.message) // 提示错误信息
        return Promise.reject(error)
    }
)

export default service

封装通用Svg组件

需求
项目当中通常有显示自定义svg图标的需求,我们希望使用的时候,像使用element-ui的图标一样方便,当成一个组件使用。所以封装一个通用svg图标组件。

前置知识点

  1. use标签:use标签可以重复引用svg图片,只要该svg图片设置了唯一id即可(该svg图片必须已经写入到html中)
  2. svg图片默认不是组件,不可被vue-cli识别,在webpack环境下,需要用svg-sprite-loader插件对其进行打包,以及写入唯一id(供use标签识别)
  3. 有了loader打包svg图片,我们还需要把本地的svg图片导入到项目中,只有导入了的svg图片,才会被svg-sprite-loader打包

导入代码示范:注意require.context这个函数的使用,这个是webpack提供的导入方法。

import SvgIcon from '@/components/SvgIcon'
const svgRequire = require.context('./svg', false, /\.svg$/)
svgRequire.keys().forEach(svgIcon => svgRequire(svgIcon))

export default app => {
  app.component('svg-icon', SvgIcon)
}

实现
我们希望该组件:

  1. 通过传入一个icon参数就可以识别到显示哪一个svg,该参数就是svg图片的定义的id
  2. 可以传入一个class,通过这个class自定义样式,如颜色,大小

代码示范

<template>
  <div
    v-if="isExternal"
    :style="styleExternalIcon"
    class="svg-external-icon svg-icon"
    :class="className"
  />
  <svg v-else class="svg-icon" :class="className" aria-hidden="true">
    <use :xlink:href="iconName" />
  </svg>
</template>

<script setup>
import { isExternal as external } from '@/utils/validate'
import { defineProps, computed } from 'vue'
const props = defineProps({
  // icon 图标
  icon: {
    type: String,
    required: true
  },
  // 图标类名
  className: {
    type: String,
    default: ''
  }
})

/**
 * 判断是否为外部图标
 */
const isExternal = computed(() => external(props.icon))
/**
 * 外部图标样式
 */
const styleExternalIcon = computed(() => ({
  mask: `url(${props.icon}) no-repeat 50% 50%`,
  '-webkit-mask': `url(${props.icon}) no-repeat 50% 50%`
}))
/**
 * 项目内图标
 */
const iconName = computed(() => `#icon-${props.icon}`)
</script>

<style scoped>
.svg-icon {
  width: 1em;
  height: 1em;
  vertical-align: -0.15em;
  fill: currentColor;
  overflow: hidden;
}

.svg-external-icon {
  background-color: currentColor;
  mask-size: cover !important;
  display: inline-block;
}
</style>
/**
 * 判断是否为外部资源
 */
export function isExternal(path) {
  return /^(https?:|mailto:|tel:)/.test(path)
}

动态列表格实现

需求
一个table表格,用户根据自己的查看需要动态添加某一列或者删除某一列,这在后台管理系统中是常用的功能。

image.png 实现
核心实现逻辑:表格的列数据是要响应式的,监听选中列的数据,当有变化后,重新表格的列。

实现步骤

  1. 给出所有可以显示的列
  2. 初始化checkbox数据,定义一开始应该显示哪些列
  3. 利用watch方法,监听CheckBox选中数据的变化,当数据变化后,根据checkbox选中的值,重新计算应该显示哪些列

代码示范

// 所有的列数据
const dynamicData = ref([
  {
    label: '名称',
    prop: 'name'
  },
  {
    label: '日期',
    prop: 'date'
  },
  {
    label: '地址',
    prop: 'address'
  },
])

const selectDynamicLabel = ref([])

// 初始化,全部选中
const initSelectDynamicLabel = () => {
  selectDynamicLabel.value = dynamicData.value.map((item) => {
    return item.prop
  })
}
initSelectDynamicLabel()

// 定义表格列,一定要是响应式的
const tableColumns = ref([])

// 监听选中项的变化,根据选中项动态改变 table 列数据的值
watch(
  selectDynamicLabel,
  val => {
    tableColumns.value = []
    const selectData = dynamicData.value.filter(item => {
      return val.includes(item.prop)
    })
    tableColumns.value.push(...selectData)
  },
  {
    immediate: true
  }
)
    <div class="dynamic-box">
      <span class="title">显示列</span>
      <el-checkbox-group v-model="selectDynamicLabel">
        <el-checkbox v-for="(item, index) in dynamicData" :label="item.prop" :key="index">{{ item.label }}</el-checkbox>
      </el-checkbox-group>
    </div>
    <br>
    <el-card>
      <el-table :data="tableData" style="width: 100%">
        <el-table-column v-for="(item) in tableColumns" :prop="item.prop" :label="item.label" :key="item.prop" />
      </el-table>
    </el-card>

可拖拽可改变排序的表格实现

需求
一个表格,做成可以行拖动的,通过拖动和交换行,达到重新给表格排序的目的。 image.png 前置知识点
html5的拖拽相关api

  1. draggable="true"属性,把某一个元素变成可以拖动的元素
  2. html5的拖拽相关事件
dragstart // 当鼠标开始拖放时被触发
drag // 当鼠标拖放过程中,类似于mousemove事件
dragend // 当鼠标结束拖放时被触发

拖拽表格技术原理

  1. 给表格的每一行tr,都加上draggable="true"属性,把每一行表格都变成可以拖动的元素。
  2. 表格每一行,都监听dragstart事件,在拖动时候触发记录当前拖动的是哪一行,以及这行有什么数据
  3. 表格每一行,都监听dragend事件,当触发此事件的时候,记录当前行数据,然后把拖动行数据和当前行数据进行交换,然后重新渲染表格

利用插件实现
实际工作中,拖拽表格该功能已有很多类似的轮子,自己造一个太花时间,使用轮子是好的选择,能兼容vue的轮子有sortablejs,我们通过使用这个插件能快速实现拖拽表格

代码演示

      <el-table ref="tableRef" :data="tableData" style="width: 100%">
        <el-table-column v-for="(item) in tableColumns" :prop="item.prop" :label="item.label" :key="item.prop" />
      </el-table>
const tableRef = ref(null)

/**
 * 初始化排序
 */
const initSortable = (tableData, cb) => {
  // 设置拖拽效果
  // 1. 要拖拽的元素
  const el = tableRef.value.$el.querySelectorAll(
    '.el-table__body-wrapper > table > tbody'
  )[0]
  // 2. 配置对象
  Sortable.create(el, {
    // 拖拽时类名
    ghostClass: 'sortable-ghost',
    // 拖拽结束的回调方法
    async onEnd(event) {
      const { newIndex, oldIndex } = event
      // 修改数据
      await articleSort({
        initRanking: tableData.value[oldIndex].ranking,
        finalRanking: tableData.value[newIndex].ranking
      })
      ElMessage.success({
        message: '排序成功',
        type: 'success'
      })
      // 直接重新获取数据无法刷新 table!!
      tableData.value = []
      // 重新获取数据
      cb && cb()
    }
  })
}

onMounted(() => {
  initSortable(tableData, getListData)
})

如何根据路由生成菜单

需求
为什么不能直接写死菜单,而是要根据路由生成动态菜单?因为路由是有权限的,每一个用户看到的路由可能都不一样,所以只能根据路由生成菜单,且要能做到当路由表变化时候,菜单也实时变化。

前置知识点
需要知道通过router.getRoutes()能获取所有路由

router.options.routes // 获取初始路由列表,新增的路由无法获取到
router.getRoutes() // 获取所有路由记录的完整列表

实现思路

image.png

根据上图的最终效果图,可知最终菜单需要显示有图标和名称,还可能存在层级关系。可以有如下实现思路:

  1. 在定义路由的时候,给将来需要在菜单上显示的路由配置mate对象,mate对象里有两个字段icon和title,分别对应菜单的icon和名称,没有这两个字段则表示这个路由不需要显示在菜单上。
  2. 在菜单组件初始化的时候,获取当前路由表路由路由。
  3. 过滤掉没有icon和title的路由,即过滤掉不需要显示在菜单上的路由。
  4. 得到最终可以用的菜单数据,渲染到html上。

关键js代码实现
给路由对象加上icon和title字段

    ...
    {
        "path":"/user/role",
        "meta":{
            "title":"roleList",
            "icon":"role"
        }
    },
    {
        "path":"/user/permission",
        "meta":{
            "title":"permissionList",
            "icon":"permission"
        }
    },
    ...

在菜单组件中,获取完整路由表,并且过滤出需要在菜单栏显示的路由,生成菜单。

const router = useRouter()
const routes = computed(() => {
  // 获取所有的路由
  const allRouter = router.getRoutes()
  // 根据icon和title,过滤出需要显示在菜单上的路由
  return generateMenus(allRouter)
})

function generateMenus(routes, basePath = '') {
  const result = []
  // 遍历路由表
  routes.forEach(item => {
    // 不存在 children && 不存在 meta 直接 return
    if (isNull(item.meta) && isNull(item.children)) return
    // 存在 children 不存在 meta,进入迭代
    if (isNull(item.meta) && !isNull(item.children)) {
      result.push(...generateMenus(item.children))
      return
    }
    // 合并 path 作为跳转路径+
    const routePath = path.resolve(basePath, item.path)
    // 存在同名父路由的情况,需要单独处理,如果不存在,则可以不要这一步处理
    let route = result.find(item => item.path === routePath)
    if (!route) {
      route = {
        ...item,
        path: routePath,
        children: []
      }
      // icon 与 title 必须全部存在
      if (route.meta.icon && route.meta.title) {
        // meta 存在生成 route 对象,放入 arr
        result.push(route)
      }
    }
    // 存在 children 进入迭代到children
    if (item.children) {
      route.children.push(...generateMenus(item.children, route.path))
    }
  })
  return result
}

headerSearch组件页面检索菜单处理方案

需求
headerSearch组件,是一个能检索当前应用所有菜单的组件,当检索到对应的菜单后,以选项形式进行展示,

前置知识点
理解 模糊搜索算法(近似字符串匹配算法)
定义:系统允许被搜索信息和搜索提问之间存在一定的差异,这种差异就是“模糊”在搜索中的含义。例如,查找名字Smith时,就会找出与之相似的Smithe, Smythe, Smyth, Smitt等。

模糊搜索功能的实现过于复杂,不在当前项目的范围中,我们只需要知道模糊搜索的定义即可。这里我们实现模糊搜索时,使用第三方插件fuse.js

fuse.js demo使用示范

import Fuse from 'fuse.js'

// 被检索的数据源
const list = [
    {
        "path":"/user",
        "title":[
            "用户"
        ]
    },
    {
        "path":"/user/manage",
        "title":[
            "用户",
            "用户管理"
        ]
    }
]

const fuse = new Fuse(list, {
    // 是否按优先级进行排序
    shouldSort: true,
    // 匹配长度超过这个值的才会被认为是匹配的
    minMatchCharLength: 1,
    // 将被搜索的键列表。 这支持嵌套路径、加权搜索、在字符串和对象数组中搜索。
    // name:搜索的键
    // weight:对应的权重
    keys: [
      {
        name: 'title',
        weight: 0.7
      },
      {
        name: 'path',
        weight: 0.3
      }
    ]
  })

实现思路

  1. 获取所有的菜单数据,并将这些数据转化成可以被检索的数据格式,作为可以被检索的数据源
  2. 制作可以模糊搜索的select组件,把模糊搜索到的内容当成options选项
  3. 监听select的change事件,当触发后拿到对应的路由数据,并跳转

核心js实现代码
获取所有的菜单数据,作为可以被检索的数据。

注意点
实际项目中,很多页面虽然允许被访问,但需要经过一定的业务流程,传递一定的参数才允许进入,这一部分页面路由,是不应该被当成可以检索的数据的,应该当在获取菜单数据的时候就把这些排除掉。

import { ref, computed } from 'vue'
import { filterRouters, generateMenus } from '@/utils/route'
import { useRouter } from 'vue-router'
...
// 检索数据源
const router = useRouter()
// 获取菜单的方法generateMenus在上文中有写,这里不在重复
const menusData = generateMenus(router.getRoutes())

将获得的菜单数据源menusData转化为fuse.js需要的格式

const searchPool = computed(() => {
  return generateRoutes(router.getRoutes())
})
console.log(searchPool)

const generateRoutes = (routes, basePath = '/', prefixTitle = []) => {
  // 创建 result 数据
  let res = []
  // 循环 routes 路由
  for (const route of routes) {
    // 创建包含 path 和 title 的 item
    const data = {
      path: path.resolve(basePath, route.path),
      title: [...prefixTitle]
    }
    // 动态路由不允许被搜索
    // 匹配动态路由的正则
    const re = /.*\/:.*/
    if (route.meta && route.meta.title && !re.exec(route.path)) {
      // 当前存在 meta 时,使用 i18n 解析国际化数据,组合成新的 title 内容
      // const i18ntitle = i18n.global.t(`msg.route.${route.meta.title}`)
      // data.title = [...data.title, i18ntitle]
      data.title = [...data.title]
      res.push(data)
    }

    // 存在 children 时,迭代调用
    if (route.children) {
      const tempRoutes = generateRoutes(route.children, data.path, data.title)
      if (tempRoutes.length >= 1) {
        res = [...res, ...tempRoutes]
      }
    }
  }
  return res
}

HeaderSearch的html模板代码,渲染被搜索到的数据,并监听change事件跳转路由

<template>
  <div :class="{ show: isShow }" class="header-search">
    <svg-icon
      class-name="search-icon"
      icon="search"
      @click.stop="onShowClick"
    />
    <el-select
      ref="headerSearchSelectRef"
      class="header-search-select"
      v-model="search"
      filterable
      default-first-option
      remote
      placeholder="Search"
      :remote-method="querySearch"
      @change="onSelectChange"
    >
      <el-option
          v-for="option in searchOptions"
          :key="option.item.path"
          :label="option.item.title.join(' > ')"
          :value="option.item"
      ></el-option>
    </el-select>
  </div>
</template>

<script setup>
import { ref } from 'vue'

// 控制 search 显示
const isShow = ref(false)
// el-select 实例
const headerSearchSelectRef = ref(null)
const onShowClick = () => {
  isShow.value = !isShow.value
  headerSearchSelectRef.value.focus()
}

// search 相关
const search = ref('')
// 搜索结果
const searchOptions = ref([])
// 搜索方法
const querySearch = query => {
  if (query !== '') {
    searchOptions.value = fuse.search(query)
  } else {
    searchOptions.value = []
  }
}
// 选中回调
const onSelectChange = val => {
  router.push(val.path)
}
</script>

tagsView 处理方案

需求

  1. 我们希望被打开过的页面,能够缓存下来,下次再次打开时候不需要完全重新加载,达到更快打开的目的。
  2. 我们希望有一组tag标签,可以查看哪些页面已经被打开过且正在被缓存,并且可以通过点击tag再次快速切换被打开过的页面。整个功能非常类似浏览器的页面标签。

前置知识点
keep-alive组件,路由缓存的原理
keep-alive组件,能将一个不活跃的组件实例保存在内存中(组件实例是指根据数据生成虚拟dom VNode),而不是直接销毁,当需要再次激活的时候,从cache中取出缓存的虚拟dom,而不需要再次重新生成。达到用内存换计算时间的效果。

  1. 需要一个tag标签组件,用于展示当前已经打开了哪些页面,每一个tag标签可以点击跳转路由。
  2. 需要一个渲染页面的router-view组件,这个组件要有缓存功能。

核心实现思路

  1. 需要一个渲染页面的router-view组件,这个组件要有缓存功能,缓存实例化过的组件,这个vue 的keep-alive组件已经帮我们实现,我们只需要把router-view外层包裹上keep-alive组件即可。
  2. 创建一个tagView组件,用于展示已经被打开过哪些页面,一个页面对应一个tag。
  3. 创建一个全局字段tagViewList,用于记录当前已经被缓存了路由对象,tagViewList的数据来源于监听路由的变化,当路由变化的时候,把最新的路由添加进tagViewList中(需要检测以前是否曾经打开过,如果打开过就不再重复添加)。
  4. 为tagView添加点击事件,当点击某一个tag之后,路由切换到对应的新的页面。
  5. 为每一个tag添加删除按钮,当点击删除后,删除对应的tagViewList里的字段,对应的页面缓存实例也要删除。

核心js代码实现
在appMain组件中监听路由的变化,当路由变化的时候,把新的路由添加进tabView的数组中,tagsViewList数据建议写在store当中,因为全局都要用。

import { watch } from 'vue'
import { isTags } from '@/utils/tags'
import { useRoute } from 'vue-router'
import { useStore } from 'vuex'

const route = useRoute()

// 获得title没有title则用path路径代替
const getTitle = route => {
  let title = ''
  if (!route.meta) {
    // 处理无 meta 的路由
    const pathArr = route.path.split('/')
    title = pathArr[pathArr.length - 1]
  } else {
    title = route.meta.title
  }
  return title
}

// 监听路由变化,当路由变化的时候,把新的路由添加进tabView的数组中
const store = useStore()
watch(
  route,
  (to, from) => {
    if (!isTags(to.path)) return
    const { fullPath, meta, name, params, path, query } = to
    store.commit('app/addTagsViewList', {
      fullPath,
      meta,
      name,
      params,
      path,
      query,
      title: getTitle(to)
    })
  },
  {
    immediate: true
  }
)

在sotre当中添加tagsViewList字段,以及新增添加tag标签的方法addTagsViewList,删除tag标签的方法removeTagsView

import { LANG, TAGS_VIEW } from '@/constant'
import { getItem, setItem } from '@/utils/storage'
export default {
  namespaced: true,
  state: () => ({
    ...
    tagsViewList: getItem(TAGS_VIEW) || []
  }),
  mutations: {
    ...
    // 添加新的tag标签
    addTagsViewList(state, tag) {
      const isFind = state.tagsViewList.find(item => {
        return item.path === tag.path
      })
    // 处理重复
      if (!isFind) {
        state.tagsViewList.push(tag)
        setItem(TAGS_VIEW, state.tagsViewList)
      }
    }
  },
  // 删除某一个tag标签
   removeTagsView(state, payload) {
      if (payload.type === 'index') {
        state.tagsViewList.splice(payload.index, 1)
        return
      } else if (payload.type === 'other') {
        state.tagsViewList.splice(
          payload.index + 1,
          state.tagsViewList.length - payload.index + 1
        )
        state.tagsViewList.splice(0, payload.index)
      } else if (payload.type === 'right') {
        state.tagsViewList.splice(
          payload.index + 1,
          state.tagsViewList.length - payload.index + 1
        )
      }
      setItem(TAGS_VIEW, state.tagsViewList)
    },
  
  actions: {}
}

创建tagView组件,用于展示tagView,数据源即上一步通过监听路由添加到全局的tagsViewList

<template>
  <div class="tags-view-container">
      <router-link
        class="tags-view-item"
        :class="isActive(tag) ? 'active' : ''"
        v-for="(tag, index) in $store.getters.tagsViewList"
        :key="tag.fullPath"
        :to="{ path: tag.fullPath }"
      >
        {{ tag.title }}
        <i
          v-show="!isActive(tag)"
          class="el-icon-close"
          @click.prevent.stop="onCloseClick(index)"
        />
      </router-link>
  </div>
</template>

<script setup>
import { useRoute } from 'vue-router'
const route = useRoute()

/**
 * 是否被选中
 */
const isActive = tag => {
  return tag.path === route.path
}

/**
 * 关闭 tag 的点击事件
 */
const store = useStore()
const onCloseClick = index => {
  store.commit('app/removeTagsView', {
    type: 'index',
    index: index
  })
}
</script>
</style>

对于路由的缓存方案,vue-router已经帮我们做好了,使用keep-alive组件。

<template>
  <div class="app-main">
    <router-view v-slot="{ Component, route }">
      <transition name="fade-transform" mode="out-in">
        <keep-alive>
          <component :is="Component" :key="route.path" />
        </keep-alive>
      </transition>
    </router-view>
  </div>
</template>