前言

在后台管理系统中基本上都会有字典之类的操作。这类操作有一个共性:相同接口、不同参数返回不同的目标数据、数据结构一致。使用上无非是一些下拉选项、多选、状态翻译等。

有一个很实际的问题,A页面中存在某个下拉B页面也存在,一般的做法是两次请求,分别获取数据。但是当项目中字典数据应用越来越多的时候,大量重复的请求无法避免,造成资源浪费。

一般来说有两个解决办法:

1.随着项目启动将所有需要获取的数据获取一遍,缓存起来,用的时候直接取。优点是直观,方便管理。缺点是启动时的网络压力很大,用户可能不会点开很多页面,一定程度上资源浪费。

2.当需要的时候进行缓存,再次使用的时候直接取数据,不用再次请求。优点是灵活,按需就取。缺点是,依赖发布订阅模式(Vue中双向绑定),每次都需确认调用。

今天要说的是方法2的一种实现。

实现

直接上干货

import Vue from 'vue'
import Api from '@/api/index.js'
const cache = {
  namespaced: true,
  state: {
    dict: {},
    queue: []
  },
  mutations: {
    SET_DICT: (state, { type, list }) => {
      // 设置值:注意得用Vue.set方法
      Vue.set(state.dict, type, list)
    },
    SET_QUEUE: (state, type) => {
      state.queue.push(type)
    },
    DEL_QUEUE: (state, type) => {
      // 找到并删除
      for (let i = 0; i < state.queue.length; i++) {
        const item = state.queue[i]
        if (item === type) {
          state.queue.splice(i, 1)
          break
        }
      }
    }
  },
  actions: {
    // 缓存字典内容
    dict({ commit, state }, type) {
      // 判断是否已经缓存过,缓存过后将不再获取数据
      const isExistence = state.queue.some((item) => {
        return item === 'dict/' + type
      })
      // 没有缓存则请求数据
      if (!isExistence) {
        // 在异步请求之前将该任务标记为已缓存(存入队列)
        commit('SET_QUEUE', 'dict/' + type)
        return new Promise((resolve, reject) => {
          return Api.getDict(type)
            .then((res) => {
              const { code, data } = res
              if (code && code !== 200) {
                // 获取失败移除缓存队列
                commit('DEL_QUEUE', 'dict/' + type)
                reject(res)
              } else {
                // 获取到的值存入字典
                commit('SET_DICT', { type: type, list: data })
                resolve(res)
              }
            })
            .catch((error) => {
              // 获取失败移除缓存队列
              commit('DEL_QUEUE', 'dict/' + type)
              reject(error)
            })
        })
      } else {
        return state.dict[type]
      }
    }
  },
  getters: {
    getDict: (state) => {
      return state.dict
    },
    getDictByType: (state) => (type) => {
      /**
       * 闭包特性。返回一个函数,该函数接收一个type参数。
       * 同时由于闭包还持有state对象,故可以返回需要的值。
       */
      return state.dict[type]
    }
  }
}

export default cache

其中Api对象具体实现由各位自己定义,以及getDict函数是假定的,回调内容也可以根据具体的架构进行调整。

解析

上述代码到底实现了什么呢?缓存与队列

在异步编程中要考虑的是并发,如何才能避免同一数据多次请求的发生,是这个程序的一个重点。我采用了一个简单队列来避免,只要发起了就不再发起相同的请求,在成功时存入缓存,失败时清除队列里的数据。

这里还可以拓展一下,类似于失败后重试功能,思路大概是定时定次轮询,需要自行实现。

上述只是针对字典的缓存,但,只要符合相关共性的都可以集成到这个程序中,大同小异。

使用

实现一个与之相关的组件,字典下拉组件,采用的是Element UI。

<template>
  <el-select v-model="dataValue" :placeholder="getPlaceholder"
    style="width:100%" :disabled="disabled" :readonly="readonly" :size="size">
    <el-option v-show="!isRequired" :label="getPlaceholder" value="">
    </el-option>
    <el-option v-for="item in cacheDict[code]" :key="item.dm" :label="item.dmmc"
      :value="item.dm"></el-option>
  </el-select>
</template>

<script>
/** *
 * 字典字段过滤组件
 */
import { mapGetters } from 'vuex'
export default {
  name: 'DictSelect',
  components: {},
  props: {
    /**
     * 双向绑定
     */
    value: [String, Number, Boolean, Array, Object],
    /**
     * 字典编码
     */
    code: {
      type: String,
      required: true
    },
    /**
     * 描述
     */
    placeholder: {
      type: String,
      default: undefined
    },
    /**
     * 是否显示请选择
     */
    isRequired: {
      type: Boolean,
      default: false
    },
    /**
     * 是否禁用
     */
    disabled: {
      type: Boolean,
      default: false
    },
    /**
     * 是否只读
     */
    readonly: {
      type: Boolean,
      default: false
    },
    /**
     * 组件大小
     */
    size: {
      type: String,
      default: 'small'
    }
  },
  data() {
    return {
      dataValue: ''
    }
  },
  computed: {
    getPlaceholder() {
      return this.placeholder != undefined &&
        this.placeholder != 'undefined' &&
        this.placeholder != ''
        ? '请选择' + this.placeholder
        : '请选择'
    },
    ...mapGetters({ cacheDict: 'cache/getDict' })
  },
  watch: {
    value(val, oldValue) {
      if (val !== this.dataValue) {
        this.dataValue = val
      }
    },
    dataValue(val, oldValue) {
      if (val !== this.value) {
        this.$emit('input', val)
      }
    }
  },
  created: function () {
    this.dataValue = this.value
  },
  mounted: function () {
    this.getData()
  },
  methods: {
    async getData() {
      await this.$store.dispatch('cache/dict', this.code)
    }
  }
}
</script>

结合上述缓存程序,我们可以发现,在字典下拉组件中的 getData() 虽然每渲染一次都调用了,但由于 缓存程序中 dict 函数实现里会检测是否已经存在,所以不管他有没有返回值,这里都只是作为一个资源确认方式。

使用了vue的双向绑定,所以我们无需关心数据什么时候会有,有了自然会渲染。

而在页面中使用起来就更加方便

<DictSelect v-model="formData.dictType" code="106"></DictSelect>

总结

核心问题是节流,引申出弱业务关联的抽象实现。本质上是一种思路,具体方案还得根据具体框架实现。

将问题分解,然后以叠金字塔的方式逐步实现,最后塔尖为我们所用。

Logo

前往低代码交流专区

更多推荐