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