使用 Proxy 创造一个“万能” MockData

225 阅读3分钟

背景

有一个组件需要展示很多图表、表格等,并不需要真实请求,但需要有假数据。 已知每个子组件所需要的值和格式是不一样的,例如,一个 echarts 图表,需要将接口中的 data.aList和 data.bList 转换成图表所需的数据,并且这个 echarts 图表的写法有可能是写在series.data 中,也有可能是 dataset 的写法,图表也有可能不只是折线图,可能还会是饼图,散点图,所以想给图表一个公共假数据是很难满足的。 除了 echarts 图表之外,还有表格的数据,自定义组件的数据,将来还有可能会有其他的,而一个个组件针对性的写假数据又很麻烦。

思路

好在设计架构时,每一个子组件都会经由一个公共request去请求该组件的数据

image.png 因此我决定在请求返回值上做文章,因为每个子组件将请求来的数据转换成自身所需的格式是千变万化的,但请求回来的数据都是类似的。 统计了下对 response 数据的处理方法,基本上是这几种

  1. 直接将 res 用作值,或稍作处理,例如 res => res.map(item => ({ ...item, name: item.xxxname }))
  2. res 是一个对象,对其取值, res => ({ label: res.name, value: res.value })
  3. res 有几个数组,分别取值,res => res.aList.forEach 然后作各种遍历,不限于使用 forEach,map,for,或 lodash 等方法。

是否有这种数据可以被这样取值还能返回他们所需呢?我想到了 Proxy,在请求时拦截,返回一个 MockData, 取值时根据取值的方式返回不同的数据即可。

实现

为了方便直接对最外层进行遍历,我将这个 MockData 代理的目标定为一个 Array,并且由于数组中的每个对象都有可能继续被取值,所以数组中的每个对象也都会是一个 Proxy 对象

import dayjs from 'dayjs';

const createObj = i => ({
	date: dayjs().add(i, 'day').format('YYYY-MM-DD')
})

const random = () => Math.floor(Math.random() * 100)

const array = Array.from({ length: 10 }, (_, i) => {
	const obj = createObj(i)
	const proxyObj = new Proxy(obj, {
		get: (target, property, receiver) => {
			return random()
		}
	})
	return proxyObj
})

const mockData = new Proxy(array, {
	get: (target, property, receiver) => {
		if (property === 'value') {
			return random()
		}
		return mockData
	},
});

这只是一个最基础的架子,首先要让这个 MockData 支持数组自己的方法,例如 forEach,map 等,所以要判断 property 是否在 Array 的原型链上,并且为了能正常的返回值,还要判断该对象是否有这个值。

// 访问 Symbol 属性时保持一致
if (property in Symbol.prototype) {
	return Reflect.get(target, property, receiver);
}
// 为了能够正常使用 Array 的函数
if (property in Array.prototype) {
	return target[property];
}
// 为了能返回自身拥有的值
if ({}.hasOwnProperty.call(target, property)) {
	return Reflect.get(target, property, receiver)
}
// 直接向 MockData 取值时
if (property.includes('name')) {
	return `name${random()}`
}

array 的 obj 拦截器里也是要有这几个的。

还有就是让MockData 拥有它本身的一些行为

set: (target, property, value, receiver) => {
	return Reflect.set(target, property, value, receiver);
},

ownKeys: (target) => {
	return Reflect.ownKeys(target);
},

getOwnPropertyDescriptor: (target, property) => {
	return Reflect.getOwnPropertyDescriptor(target, property);
}

MockData 部分目前这样就够了,然后就是 array 中每个对象的细节处理

// 如果取值时包含 Name,则返回一个字符串
if (property.includes('name')) {
	return `示例${random()}`
}
// 对 obj 取数字值时,依然返回一个 obj
if (!Number.isNaN(Number(property))) {
	return proxyObj
}

源码

import dayjs from 'dayjs';

const createObj = i => ({
	date: dayjs().add(i, 'day').format('YYYY-MM-DD')
})

const random = () => Math.floor(Math.random() * 100)

const array = Array.from({ length: 10 }, (_, i) => {
    const obj = createObj(i)
    const proxyObj = new Proxy(obj, {
        get: (target, property, receiver) => {
            if (property === 'value') {
                return random()
            }
            if (property in Symbol.prototype) {
                return Reflect.get(target, property, receiver);
            }
            if (property in Array.prototype) {
                return Array.prototype[property].bind(array);
            }
            if (property.includes('Name')) {
                return `示例${random()}`
            }
            if ({}.hasOwnProperty.call(target, property)) {
                return Reflect.get(target, property, receiver)
            }
            if (!Number.isNaN(Number(property))) {
                return proxyObj
            }
            return random()
        }
    });
    return proxyObj
})

const mockData = new Proxy(array, {
    get: (target, property, receiver) => {
        if (property === 'value') {
            return random()
        }
        if (property in Symbol.prototype) {
            return Reflect.get(target, property, receiver);
        }
        if (property in Array.prototype) {
            return target[property];
        }
        if ({}.hasOwnProperty.call(target, property)) {
            return Reflect.get(target, property, receiver)
        }
        if (property.includes('name')) {
            return `name${random()}`
        }
        return mockData
    },
    set: (target, property, value, receiver) => {
        return Reflect.set(target, property, value, receiver);
    },
    ownKeys: (target) => {
        return Reflect.ownKeys(target);
    },
    getOwnPropertyDescriptor: (target, property) => {
        return Reflect.getOwnPropertyDescriptor(target, property);
    }
});

实践

根据之前的几个取值方式来看看最终能否得到符合格式的数据:

// res => res.map(item => ({ ...item, name: item.xxxname }))
mockData.map(item => ({ ...item, name: item.xxxname)
// [{date: '2024-07-30', name: 21}, ...]

// res => ({ label: res.name, value: res.value })
const data = { label: mockData.name, value: mockData.value }
// { label: name38, value: 29 }

// MockData 是对象,对某个值进行循环 或者 多次下标取值
const data = {a: mockData.aList.map(item => item.name), b: mockData.bList, c: mockData.cData[0][1] }
// { a: ['示例12', '示例52',...], b: mockData, c: {date: '2024-07-30'}}

说明

目前这个 MockData 还不算很成熟,只是暂时满足了我的使用,所以我抛出来提供一个思路,按理来说是可以封装起来,每次指定对象的数量,name的模版,增加唯一 id 等等优化和补全的,只不过暂时没时间(懒