前言
对于级联选择器
来说,在开发中常常使用,比如地区的选择
、分类的选择
等。但是对于如何使用,我们通常是根据项目的需求来决定。
有几种情况
- 对于数据不多时。每次都查询接口来获取
Cascader
的全部数据。可以进一步优化,当第一次查询时将数据记录到redux
中。后续使用时从redux
中取出。这应该是常见的做法。 - 对于数据量大时。查询全部数据就不适用了。我们会使用
动态加载选项
。也可以进一步的优化,将每一次请求的数据保存到redux
中,并进行拼接。在需要加载选项时,判断是否需要请求获取。
第一种情况不管是编辑还是回显,都很好处理。在第二种情况中,编辑也很好处理,但是回显不好处理,也有两种方式:
(1)当需要回显时,后端将选项的
label
也一并返回。(2)后端提供一个接口,通过
节点id
,返回同级节点及其祖先同级节点
,然后再和现有的节点进行拼接处理。还有一个注意点,在后面会提到
。
在前一段开发中,也是遇到的Cascader
的问题。跟后端讨论了下最终选择了2-(2)
这个方案。
后面有详细代码地址。
正文
初始化
在redux
中存储一个是否初始化的标志。在没有初始化时,进行初始化。
import { getBrothersAndAncestors, getChildrenData, ResData } from '@/components/Cascader/data';
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { RootState } from '.';
export interface Option {
parentId: string,
value: string,
label: string,
isLeaf: boolean,
children?: Option[]
}
let isLoading = false;
// 初始化
export const initFetch = createAsyncThunk('data/initFetch', async () => {
if (isLoading) {
return Promise.reject('loading')
}
isLoading = true;
const response = await getChildrenData()
let result: Option[] = []
response.forEach((item) => {
result.push({
parentId: '0',
value: item.id,
label: item.label,
isLeaf: item.isLeaf,
})
})
isLoading = false;
return result;
});
const initialState: {
isInit: boolean,
treeData: Option[],
loading: boolean
} = {
isInit: false,
treeData: [],
loading: false
}
export const treeSlice = createSlice({
name: 'treeSlice',
initialState,
reducers: {
changeInit: (state, action) => {
state.isInit = action.payload
},
setTree: (state, action) => {
state.treeData = action.payload
}
},
extraReducers: (builder) => {
builder
.addCase(initFetch.pending, (state) => {
state.loading = true;
})
.addCase(initFetch.fulfilled, (state, action) => {
state.isInit = true;
state.treeData = action.payload;
state.loading = false;
})
}
})
export const { changeInit, setTree } = treeSlice.actions
export default treeSlice.reducer
在封装组件中
useEffect(() => {
if (isInit === false) {
dispatch(initFetch())
}
}, [])
获取子节点
在获取子节点时,我们需要一个方法来判断是否需要请求接口。
/**
* @description 查找分类
* @param {Option[]} list 需要查找的分类列表
* @param {string} findId 要查找的分类id
* @returns {Object} 包含data和listId的对象
* @property {Option} data 找到的分类
* @property {string[]} listId 找到的分类的id列表
*/
const findItem = (list: Option[], findId: string) => {
let result: Option = null as unknown as Option;
let listId: string[] = [];
const fn = (tempList: Option[], findId: string, tempListId: string[]) => {
for (let i = 0; i < tempList.length; i++) {
let item = tempList[i]
if (item.value === findId) {
listId = [...tempListId, item.value]
result = item;
break;
}
}
for (let i = 0; i < tempList.length; i++) {
let item = tempList[i]
if (item.children && item.children.length > 0 && result === null) {
fn(item.children, findId, [...tempListId, item.value])
}
}
}
fn(list, findId, listId)
return {
data: result,
listId: listId
}
}
/**
* @description 当前节点的是否查询过子节点
* @param findId 要查找的id
* @returns boolean 是否有查询过
*/
const isQueryChild = useCallback((parentId: string) => {
let parent = findItem(treeData, parentId).data
if (!parent.isLeaf && !!parent.children && parent.children.length > 0) {
return true
}
return false
}, [treeData])
/** 获取子数据*/
const getChildData = useCallback((parentId: string) => {
let tempList = _.cloneDeep(treeData)
let result = findItem(tempList, parentId).data
return getChildrenData(parentId).then(data => {
let list = data.map(item => {
return {
parentId: parentId,
value: item.id,
label: item.label,
isLeaf: item.isLeaf,
children: [],
};
});
result.children = list;
dispatch(setTree(tempList))
})
}, [treeData])
在封装组件中判断是否需要请求接口
const loadData = (selectedOptions: Option[]) => {
const targetOption = selectedOptions[selectedOptions.length - 1];
if (!isQueryChild(targetOption.value)) {
getChildData(targetOption.value);
}
};
获取同级及祖先同级节点
检查节点是否存在
const checkIdExists = useCallback((id: string): Promise<string[]> => {
return new Promise((resolve, reject) => {
let { data, listId } = findItem(treeData, id)
if (data === null) {
dispatch(parentsFetch(id)).then((data) => {
if (data.type.toString() === parentsFetch.fulfilled.toString()) {
resolve(findItem(data.payload as Option[], id).listId)
}
}).catch(() => {
})
} else {
resolve(listId)
}
})
}, [treeData])
查询同级节点及祖先同级节点
,并和现有的节点进行拼接
/** 获取分类祖籍*/
export const parentsFetch = createAsyncThunk('data/parentsFetch', async (id: string, thunkAPI) => {
if (isLoading) {
return Promise.reject('loading')
}
isLoading = true;
const state = thunkAPI.getState() as RootState;
const mergeArrays = (arr1: Option[], arr2: ResData[], parentId: string = ''): Option[] => {
const result: Option[] = [];
let idSet = new Set();
let uniqueIds: string[] = []
arr1.forEach(item => {
idSet.add(item.value)
})
arr2.forEach((item: any) => {
idSet.add(item.id)
})
for (let id of idSet) {
uniqueIds.push(id as string)
}
uniqueIds.forEach(id => {
const item1 = arr1.find((item) => item.value === id);
const item2 = arr2.find((item) => item.id === id);
const mergedItem: Option = {
parentId: parentId,
value: id,
label: item1 ? item1.label : item2!.label,
children: [],
isLeaf: item2 ? item2.isLeaf : item1!.isLeaf,
};
if (item1 && item1.children) {
mergedItem.children = mergeArrays(item1.children, item2 ? item2.children || [] : [], id);
} else if (item2 && item2.children) {
mergedItem.children = mergeArrays(item1 ? item1.children || [] : [], item2.children, id);
}
result.push(mergedItem);
});
return result;
}
let response = await getBrothersAndAncestors(id)
let result = mergeArrays(state.tree.treeData, response)
isLoading = false;
return result;
})
export const treeSlice = createSlice({
name: 'treeSlice',
initialState,
reducers: {
changeInit: (state, action) => {
state.isInit = action.payload
},
setTree: (state, action) => {
state.treeData = action.payload
}
},
extraReducers: (builder) => {
builder
.addCase(parentsFetch.pending, (state) => {
state.loading = true;
})
.addCase(parentsFetch.fulfilled, (state, action) => {
state.treeData = action.payload;
state.loading = false;
})
}
})
封装组件中
//当初始化完成,并且value有值时,获取子节点数据
useEffect(() => {
if (!!value && isInit && selfValue[selfValue.length - 1] != value && !loading) {
checkIdExists(`${value}`).then((data: string[]) => {
setSelfValue(data);
});
}
}, [value, isInit, loading]);
注意问题
在上面核心代码中可以看到,在初始化请求和查询同级节点及祖先同级节点
时,会有这么一行代码。这是因为有可能一个页面中同时出现多个Cascader
,这样防止
重复请求。
isLoading = true;
这一段和上面配合起一个队列的的作用,既避免重复请求,又防止漏请求。
useEffect(() => {
if (!!value && isInit && selfValue[selfValue.length - 1] != value && !loading) {
checkIdExists(`${value}`).then((data: string[]) => {
setSelfValue(data);
});
}
}, [value, isInit, loading]);
结语
感兴趣的可以去试试。