前言
在应用开发中,为了提高开发效率、代码复用率,通常会抽取大量公共组件,这些组件往往包含大量数据,在设计组件时不得不将这些数据考虑在其中,数据的来源、获取时机、如何流转等问题,可能会成为我们设计组件的阻碍。
痛点
考虑以下场景:列表 + 弹窗,列表中有年级筛选项,弹窗中也有年级筛选项,年级的下拉列表数据是通过接口获取的。


通常代码会这样写:
<template>
<div>
<el-form>
...
<el-form-item label="年级">
<el-select>
<el-option
v-for="item in gradeList"
:key="item.id"
:label="item.name"
:value="item.id" />
</el-select>
</el-form-item>
</el-form>
...
<!-- 弹窗组件 -->
<el-dialog>
<el-form>
...
<el-form-item label="年级">
<el-select>
<el-option
v-for="item in gradeList"
:key="item.id"
:label="item.name"
:value="item.id" />
</el-select>
</el-form-item>
</el-form>
...
</el-dialog>
</div>
</template>
<script setup>
import { ref } from 'vue'
// 接口文件
import { getGradeList } from '@/api'
const gradeList = ref([])
// 获取年级
const fetchGradeList = async () => {
gradeList.value = await getGradeList()
}
fetchGradeList()
</script>
可以看到,弹窗直接写在列表页内,可以很好的复用 gradeList 数据,但有两个弊端:
- 弹窗组件无法复用。
- 如果将来有更多类似的弹窗加入,列表页将会变得臃肿。
为解决上述问题,我们需要将弹窗抽取出来,但组件抽取后,又会出现一个新的问题:gradeList 数据该如何处理?
方式一:在列表中获取 gradeList 数据,通过 props 传给弹窗。
这样做可以复用 gradeList,但每次使用弹窗组件时需要请求一次接口,不仅代码冗余,且增加使用组件的成本,试想一下,别人用这个弹窗还需要考虑传什么样的数据进去,假设不止一个 gradeList,还有 n 多个数据呢?
方式二:将获取 gradeList 的工作封装到弹窗组件中。
使用者不需要再考虑弹窗中的数据,但是数据不能被列表复用,比如 gradeList,列表也需要,所以只能列表也请求一次,造成同一接口请求两次的情况。
这就是我想说的数据会直接阻碍组件的设计,实际开发中情况要比上面的 demo 要复杂得多,尤其是多人协作开发,这个问题会被放大,随着项目的日渐庞大,公共组件越写越多,组件关系错中复杂,如果没有做好数据处理,不仅会造成代码冗余,甚至有可能出现同一个接口同一时间被调用几次的情况。
解决方案
先看一下我的解决方案。
api.js:
import axios from 'axios' const request = axios.create({ baseURL: 'https://www.fastmock.site/mock/61b51f966a9c4d7affc1b62642306e2e/api' }) export function getGradeList (params) { return request({ url: '/getGradeList', method: 'GET', params }).then(res => { return res.data }) } export function getChannelList (params) { return request({ url: '/getChannelList', method: 'GET', params }).then(res => { return res.data }) }
dataset.js,用于集中管理数据:
// dataset 下面会说实现思路 import Dataset from './dataset' import { getGradeList, getChannelList } from '@/api' export default new Dataset({ config: { gradeList: { data: () => getGradeList() }, channelList: { data: () => getChannelList() }, sex: { data: () => { return [ { name: '男', value: 1 }, { name: '女', value: 2 } ] } } } })
取数据:
<template>
<div>
<!-- 连续调用两次 -->
<div>dataset.get({ name: 'gradeList' })"></div>
<div>dataset.get({ name: 'gradeList' })"></div>
<!-- 连续调用两次 -->
<div>dataset.get({ name: 'channelList' })"></div>
<div>dataset.get({ name: 'channelList' })"></div>
<div>{{ dataset.get({ name: 'sex' }) }}</div>
</div>
</template>
<script setup>
import dataset from '@/dataset'
</script>
这样做的好处:
- 使用简单,只需要调用 dataset.get 便可取得数据并渲染到模板上,如果是接口数据,会自动调用接口,且多次调用只会执行一次。
- 组件上无需关注任何数据的细节。
- 所有数据在配置文件中管理,减小代码冗余。
实现思路
dataset 的实现思路也很简单,核心代码就几十行,上代码:
dataset.js:
import Store from './store' import memoize from './memoize' class Dataset { constructor (options) { options.max = options.max || 100 this.options = options this.store = new Store() this.memoFetch = memoize(this.fetch, this.options.max) } get (options) { const { name } = options // 第一次调用执行,之后走缓存 this.memoFetch(name) // 若 store 数据改变,触发渲染,返回数据 return this.store.get(name) } fetch (name) { const data = this.options.config?.[name]?.data if (typeof data === 'function') { const value = await data() // 获取数据后添加到 store this.store.set(name, value) return value } } } export default Dataset
store.js:
import { reactive } from 'vue' export default class Store { private store constructor () { this.store = reactive({}) } set (key, value) { this.store[key] = value } get (key) { return this.store[key] } }
memoize.js:
import Cache from './cache'
const { stringify } = JSON
// 使用函数参数作为 key,参数相同的同一函数调用多次,只会执行一次,其余返回缓存值
export default function (fn, max): Memoize<T> {
const cache = new Cache(max)
const memoize = function (...args) {
if (args.length === 0) return fn.call(this)
const key = stringify(args)
let value = cache.get(key)
if (!value) {
value = fn.call(this, ...args)
cache.set(key, value)
}
return value
}
return memoize
}
cache.js:
// LRU 缓存策略 export default class Cache { constructor (max = 0) { this.max = max this.cache = new Map() } get (key) { // 获取时,将被获取元素推到栈顶 const value = this.cache.get(key) if (value) { this.delete(key) this.set(key, value) } return value } set (key, value) { // 缓存满后,删除第一个 if (this.max !== 0 && this.cache.size === this.max) { this.delete(this.keys()[0]) } this.cache.set(key, value) } }
大概流程如下:
- 调用 dataset.get 时,会调用配置文件中对应 key 的 data 函数。
- 之后将执行结果存入缓存,再次调用不会再执行 data 函数,而是使用缓存。
- 数据返回成功后将数据设置到 store 中,触发渲染。
结语
这个方法很适合处理一些不常变的数据,比如数据字典,因为加了缓存,数据变动需要刷新浏览器,当然也可以添加一些清除缓存的方法,在适当的时候清除缓存。
对此我封装一个插件:vue-reactive-dataset,添加了清除缓存、filter 等方法,有兴趣的小伙伴可以看看,如有帮助,求个 star,谢谢。
评论(0)