OptionLoader 使用文档
📖 简介
optionLoader 是一个通用的下拉框选项加载工具,用于简化项目中大量重复的选项数据加载逻辑。
解决的问题
在实际开发中,我们经常需要:
- 调用 API 获取下拉框选项数据
- 将数据转换为
{ label, value }格式 - 填充到响应式变量中
- 处理加载错误
这些重复的代码分散在各个组件中,维护成本高。optionLoader 将这些逻辑统一封装,提供简洁的配置式 API。
🚀 快速开始
基础用法
import { ref } from 'vue'
import { createOptionLoaderWithPostProcess } from '@/utils/optionLoader'
import { getUserList } from '@/api/user'
// 1. 定义响应式变量
const refUserOptions = ref<OptionProps[]>([])
// 2. 创建加载器
const userOptionLoader = createOptionLoaderWithPostProcess({
api: getUserList,
mapper: v => ({
label: v.name,
value: v.id
}),
targetRef: refUserOptions
})
// 3. 在组件中使用
onMounted(() => {
userOptionLoader()
})
<template>
<a-select v-model:value="selectedUser" :options="refUserOptions" />
</template>
📚 核心 API
createOptionLoaderWithPostProcess
创建单个选项加载器。
类型定义
function createOptionLoaderWithPostProcess<T, R extends OptionProps[] = OptionProps[]>(
config: GenericOptionLoaderConfig<T, R>
): () => Promise<void>
配置参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
api | () => Promise<T[]> | ✅ | API 调用函数 |
mapper | (item: T) => OptionProps | ✅ | 数据映射函数 |
targetRef | Ref<R> | Ref<R>[] | ✅ | 目标响应式变量(单个或数组) |
postProcess | (data: R, rawData: T[]) => void | ❌ | 后处理函数 |
createOptionLoaders
批量创建多个选项加载器。
类型定义
function createOptionLoaders(
configs: { [key: string]: GenericOptionLoaderConfig<any> }
): {
loaders: Record<string, () => Promise<void>>
loadAll: () => Promise<void>
loadByKeys: (...keys: string[]) => Promise<void>
}
返回值
| 属性 | 类型 | 说明 |
|---|---|---|
loaders | Record<string, () => Promise<void>> | 加载器映射对象 |
loadAll | () => Promise<void> | 加载所有选项 |
loadByKeys | (...keys: string[]) => Promise<void> | 按 key 加载指定选项 |
💡 使用场景
场景1:单个选项加载
适用于只需要加载一个下拉框选项的场景。
// data.ts
import { ref } from 'vue'
import { createOptionLoaderWithPostProcess } from '@/utils/optionLoader'
import { getCurrencyList } from '@/api/currency'
export const refCurrencyOptions = ref<OptionProps[]>([])
export const currencyOptionLoader = createOptionLoaderWithPostProcess({
api: getCurrencyList,
mapper: v => ({
label: `${v.code} - ${v.name}`,
value: v.code
}),
targetRef: refCurrencyOptions
})
<!-- index.vue -->
<script setup lang="ts">
import { onMounted } from 'vue'
import { refCurrencyOptions, currencyOptionLoader } from './data'
onMounted(() => {
currencyOptionLoader()
})
</script>
<template>
<a-select
v-model:value="currency"
:options="refCurrencyOptions"
placeholder="请选择币种"
/>
</template>
场景2:批量加载选项
适用于页面需要加载多个下拉框选项的场景。
// data.ts
import { ref } from 'vue'
import { createOptionLoaders } from '@/utils/optionLoader'
import { getCurrencyList, getCountryList, getBankList } from '@/api'
export const refCurrencyOptions = ref<OptionProps[]>([])
export const refCountryOptions = ref<OptionProps[]>([])
export const refBankOptions = ref<OptionProps[]>([])
export const { loaders, loadAll, loadByKeys } = createOptionLoaders({
currency: {
api: getCurrencyList,
mapper: v => ({ label: v.code, value: v.code }),
targetRef: refCurrencyOptions
},
country: {
api: getCountryList,
mapper: v => ({ label: v.name, value: v.code }),
targetRef: refCountryOptions
},
bank: {
api: getBankList,
mapper: v => ({ label: v.bankName, value: v.bankCode }),
targetRef: refBankOptions
}
})
<!-- index.vue -->
<script setup lang="ts">
import { onMounted } from 'vue'
import { loadAll, loaders, loadByKeys } from './data'
onMounted(() => {
// 方式1:加载所有选项(并行请求)
loadAll()
// 方式2:单独加载某个选项
// loaders.currency()
// 方式3:加载指定的几个选项
// loadByKeys('currency', 'country')
})
</script>
场景3:多个下拉框共享同一数据源
适用于一次请求填充多个下拉框的场景。
// data.ts
export const refCurrencyOptions1 = ref<OptionProps[]>([])
export const refCurrencyOptions2 = ref<OptionProps[]>([])
export const currencyOptionLoader = createOptionLoaderWithPostProcess({
api: getCurrencyList,
mapper: v => ({ label: v.code, value: v.code }),
// 传入数组,一次请求填充多个 ref
targetRef: [refCurrencyOptions1, refCurrencyOptions2]
})
<template>
<div>
<a-select v-model:value="currency1" :options="refCurrencyOptions1" />
<a-select v-model:value="currency2" :options="refCurrencyOptions2" />
</div>
</template>
场景4:使用后处理函数
适用于需要在数据加载后执行额外逻辑的场景。
export const refUserOptions = ref<OptionProps[]>([])
export const refUserMap = ref<Map<string, User>>(new Map())
export const userOptionLoader = createOptionLoaderWithPostProcess({
api: getUserList,
mapper: v => ({
label: v.name,
value: v.id
}),
targetRef: refUserOptions,
// 后处理:构建用户 ID 到用户对象的映射
postProcess: (options, rawData) => {
refUserMap.value = new Map(
rawData.map(user => [user.id, user])
)
}
})
场景5:处理对象格式的 API 返回值
optionLoader 自动处理对象格式的返回值。
// API 返回格式:{ '1': 'USD', '2': 'CNY' }
export const currencyOptionLoader = createOptionLoaderWithPostProcess({
api: getCurrencyEnum, // 返回对象而非数组
mapper: v => ({ label: v, value: v }),
targetRef: refCurrencyOptions
})
// 自动转换为:
// [
// { label: 'USD', value: 'USD' },
// { label: 'CNY', value: 'CNY' }
// ]
🎯 最佳实践
1. 文件组织
推荐将选项加载器定义在独立的 data.ts 文件中:
src/views/yourModule/
├── index.vue # 主组件
├── data.ts # 选项加载器定义
└── components/ # 子组件
2. 命名规范
// 响应式变量:ref + 名称 + Options
const refCurrencyOptions = ref<OptionProps[]>([])
// 加载器:名称 + OptionLoader
const currencyOptionLoader = createOptionLoaderWithPostProcess({...})
// 批量加载器:解构命名
const { loaders, loadAll, loadByKeys } = createOptionLoaders({...})
3. 类型安全
// 定义 API 返回类型
interface Currency {
code: string
name: string
}
// 在 mapper 中会有类型提示
const currencyOptionLoader = createOptionLoaderWithPostProcess<Currency>({
api: getCurrencyList, // () => Promise<Currency[]>
mapper: v => ({
label: v.code, // v 的类型为 Currency
value: v.code
}),
targetRef: refCurrencyOptions
})
4. 错误处理
// 加载器内部已经处理了错误,不会抛出异常
onMounted(async () => {
// 可以安全调用,不需要 try-catch
await loadAll()
// 如果需要知道加载是否成功,可以检查 ref 的值
if (refCurrencyOptions.value.length === 0) {
console.warn('币种选项加载失败')
}
})
5. 性能优化
// ✅ 推荐:使用 loadAll() 并行加载
onMounted(() => {
loadAll() // 内部使用 Promise.all 并行请求
})
// ❌ 不推荐:串行加载
onMounted(async () => {
await loaders.currency()
await loaders.country() // 等待上一个完成
await loaders.bank()
})
⚠️ 注意事项
1. 不支持分页加载
optionLoader 适用于数据量较小的选项(几十到几百条),不支持分页加载。
// ❌ 不适用:大数据量的用户列表(需要分页)
const userOptionLoader = createOptionLoaderWithPostProcess({
api: getUserList, // 返回 10000+ 条数据
// ...
})
// ✅ 适用:小数据量的币种列表
const currencyOptionLoader = createOptionLoaderWithPostProcess({
api: getCurrencyList, // 返回 100 条左右
// ...
})
2. 不支持远程搜索
如果需要远程搜索功能,请使用 a-select 的 filterOption 或 onSearch 属性。
<template>
<!-- 本地搜索 -->
<a-select
v-model:value="currency"
:options="refCurrencyOptions"
show-search
:filter-option="filterOption"
/>
</template>
<script setup lang="ts">
const filterOption = (input: string, option: OptionProps) => {
return option.label.toLowerCase().includes(input.toLowerCase())
}
</script>
3. 组件销毁后数据不保留
optionLoader 将数据填充到组件的 ref 中,组件销毁后数据会丢失。
如果需要全局缓存,请使用 Pinia:
// stores/options.ts
import { defineStore } from 'pinia'
export const useOptionsStore = defineStore('options', () => {
const currencyOptions = ref<OptionProps[]>([])
const loadCurrency = async () => {
if (currencyOptions.value.length > 0) return // 已加载
const res = await getCurrencyList()
currencyOptions.value = res.map(v => ({ label: v.code, value: v.code }))
}
return { currencyOptions, loadCurrency }
})
🔧 高级用法
自定义选项格式
// 扩展 OptionProps 类型
interface CustomOptionProps extends OptionProps {
disabled?: boolean
extra?: any
}
const userOptionLoader = createOptionLoaderWithPostProcess<User, CustomOptionProps[]>({
api: getUserList,
mapper: v => ({
label: v.name,
value: v.id,
disabled: !v.isActive, // 禁用非活跃用户
extra: { email: v.email } // 额外数据
}),
targetRef: refUserOptions
})
条件加载
const { loaders, loadByKeys } = createOptionLoaders({
currency: { /* ... */ },
country: { /* ... */ },
bank: { /* ... */ }
})
onMounted(() => {
// 根据权限加载不同的选项
if (hasPermission('view_currency')) {
loaders.currency()
}
// 根据业务逻辑加载
if (formType.value === 'international') {
loadByKeys('currency', 'country')
} else {
loaders.bank()
}
})
动态刷新
const refreshOptions = async () => {
// 重新加载所有选项
await loadAll()
message.success('选项已刷新')
}
📊 性能对比
使用前
// 每个组件都要写这些重复代码
const refCurrencyOptions = ref<OptionProps[]>([])
const refCountryOptions = ref<OptionProps[]>([])
const refBankOptions = ref<OptionProps[]>([])
onMounted(async () => {
try {
const currencyRes = await getCurrencyList()
refCurrencyOptions.value = currencyRes.map(v => ({
label: v.code,
value: v.code
}))
} catch (error) {
console.error(error)
}
try {
const countryRes = await getCountryList()
refCountryOptions.value = countryRes.map(v => ({
label: v.name,
value: v.code
}))
} catch (error) {
console.error(error)
}
// ... 更多重复代码
})
代码量: ~40 行
使用后
const { loadAll } = createOptionLoaders({
currency: {
api: getCurrencyList,
mapper: v => ({ label: v.code, value: v.code }),
targetRef: refCurrencyOptions
},
country: {
api: getCountryList,
mapper: v => ({ label: v.name, value: v.code }),
targetRef: refCountryOptions
},
bank: {
api: getBankList,
mapper: v => ({ label: v.bankName, value: v.bankCode }),
targetRef: refBankOptions
}
})
onMounted(() => {
loadAll() // 并行加载
})
代码量: ~20 行 减少: 50% 性能: 并行请求,速度更快
🤝 可能的改进方向
- 加载状态管理:增加
loadingRef参数 - 错误状态管理:增加
errorRef参数 - 重试机制:增加
retry配置 - 请求取消:支持
AbortController
📝 更新日志
v1.0.0 (2024-02-10)
- ✨ 初始版本
- ✅ 支持单个/批量/按需加载
- ✅ 支持多目标填充
- ✅ 支持后处理函数
- ✅ 自动处理对象格式返回值
实现源码
/**
* write by Mephisto
*/
import type { Ref } from 'vue'
import type { OptionProps } from 'ant-design-vue/lib/select'
// 类型工具:从函数返回类型中提取数组元素类型
type ExtractApiItemType<T> = T extends () => Promise<(infer U)[]> ? U : never
export type OptionMapper<T> = (item: T) => OptionProps
export interface GenericOptionLoaderConfig<T, R extends OptionProps[] = OptionProps[]> {
/** API调用函数 */
api: () => Promise<T[]>
/** 数据映射函数 */
mapper: OptionMapper<T>
/**
* 目标Ref响应式变量 或者 Ref响应式变量集合
*/
targetRef: Ref<R> | Ref<R>[]
/**
* 后处理函数。比如拿到响应结果后去做一些其他的操作。
*/
postProcess?: (data: R, rawData: T[]) => void
}
// 改进版:支持类型推导的配置接口
export interface InferredOptionLoaderConfig<ApiFunc extends () => Promise<any[]>> {
/** API调用函数 */
api: ApiFunc
/** 数据映射函数 - 参数类型从 api 返回值自动推导 */
mapper: OptionMapper<ExtractApiItemType<ApiFunc>>
/**
* 目标Ref响应式变量 或者 Ref响应式变量集合
*/
targetRef: Ref<OptionProps[]> | Ref<OptionProps[]>[]
/**
* 后处理函数。比如拿到响应结果后去做一些其他的操作。
*/
postProcess?: (data: OptionProps[], rawData: ExtractApiItemType<ApiFunc>[]) => void
}
/**
* 定义单个选项加载配置(带类型推导)
* 这个辅助函数用于捕获 API 的类型信息,让 mapper 能自动推导参数类型
*/
export const defineOptionLoaderConfig = <ApiFunc extends () => Promise<any[]>>(
config: InferredOptionLoaderConfig<ApiFunc>
): InferredOptionLoaderConfig<ApiFunc> => {
return config
}
/**
* 选项加载器-创建单个
* @param optionLoaderConfig 配置项
*/
export const createOptionLoaderWithPostProcess = <T, R extends OptionProps[] = OptionProps[]>(
optionLoaderConfig: GenericOptionLoaderConfig<T, R>
) => {
return async () => {
try {
const { api, mapper, targetRef, postProcess } = optionLoaderConfig
const res = await api()
if (res) {
let options: OptionProps[] = []
// 如果 res 返回的是一个普通对象
// { '1': 'a101', '2': 'C006.01.01' }
if (!Array.isArray(res) && typeof res === 'object') {
options = Object.values(res).map(mapper)
} else {
options = res.map(mapper)
}
// 判断是单个还是多个
if (Array.isArray(targetRef)) {
targetRef.forEach(refTarget => {
refTarget.value = [...options] as R
})
} else {
targetRef.value = options as R
}
// 执行后处理(如果有)
if (postProcess) {
postProcess(options as R, res)
}
}
} catch (error) {
console.error(error)
}
}
}
/**
* 选项加载器-批量创建(改进版,支持类型推导)
* @example
* const configs = {
* user: {
* api: getUserList, // () => Promise<User[]>
* mapper: v => ({ label: v.name, value: v.id }), // v 自动推导为 User 类型
* targetRef: refUserOptions
* }
* }
* const { loaders, loadAll } = createOptionLoaders(configs)
*/
export const createOptionLoaders = <T extends Record<string, InferredOptionLoaderConfig<any>>>(
configs: T
) => {
// 使用 Record<keyof T, ...> 保留具体的 key 类型
const loaders = {} as Record<keyof T, () => Promise<void>>
// 创建选项加载器映射
for (const [key, config] of Object.entries(configs)) {
loaders[key as keyof T] = createOptionLoaderWithPostProcess(config as any)
}
const loadAll = async () => {
await Promise.all(Object.values(loaders).map(loader => loader()))
}
// 使用 keyof T 获得具体的 key 提示
const loadByKeys = async (...keys: (keyof T)[]) => {
await Promise.all(keys.map(key => loaders[key]()))
}
return {
/**
* 单独加载
* @example loaders.aaa()
*/
loaders,
/** 加载所有 */
loadAll,
/**
* 部分批量加载
* @example loadByKeys('key1', 'key2')
*/
loadByKeys
}
}
// 使用示例
// # 单个选项加载器
// export const dashBoardOptionLoader = createOptionLoaderWithPostProcess({
// api: queryUserDashboardList,
// mapper: v => ({
// label: v.dashboardName,
// value: v.id,
// dimensionList: v.dimensionList
// }),
// targetRef: refDashBoardOptions
// })
// 组件里用
// onMounted(() => {
// dashBoardOptionLoader()
// })
// # 批量创建加载器(新版 - 支持类型推导)
// const optionLoaderMap = {
// reportPlan: defineOptionLoaderConfig({
// api: queryUserReportConfigList,
// mapper: v => ({ label: v.label, value: v.value }), // v 自动推导类型
// targetRef: refReportPlanOptions
// }),
// currency: defineOptionLoaderConfig({
// api: queryCurrencyList,
// mapper: v => ({ label: v.code, value: v.code }), // v 自动推导类型
// targetRef: refCurrencyOptions
// })
// }
// export const { loaders, loadAll, loadByKeys } = createOptionLoaders(optionLoaderMap)
// 组件里用
// onMounted(() => {
// loadAll()
// loaders.reportPlan()
// loadByKeys('reportPlan', 'currency')
// })