【发布时间】:2021-10-17 22:57:55
【问题描述】:
在 React + Redux 前端中,我目前正在尝试将 HATEOAS 集成到整个流程中。应用程序最初启动时除了后端可以找到的基本 URI 之外没有任何知识,后端会将 URI 添加到前端请求的每个资源。
应用程序本身目前能够上传一个存档文件,其中包含后端应根据前端通过 multipart/form-data 上传传递给后端的某些配置处理的文件。上传完成后,后端将为上传创建一个新任务并开始处理上传,这会导致计算一些值,这些值最终会存储在数据库中,后端的控制器负责公开处理某些文件的各种资源。
我目前使用 ReduxJS 工具包和切片结合异步 thunk 来管理和访问 Redux 存储。存储本身有一个专门用于后端 API 返回的内容的部分,即全局链接部分、页面信息等,并将存储每次调用的结果并稍作重新映射。
我在这里面临的一个挑战是,在最初请求负责呈现所有任务的组件时,该组件首先需要在后端 API 中查找预定义链接关系名称的链接,并且在连续调用时应该重用可用的信息。在这里,我想出了一个像这样的动作创建函数:
type HalLinks = { [rel: string]: HalLink };
const requestLinks = async (uri: string, state: ApiState): Promise<[HalLinks, APILinkResponse | undefined]> => {
let links: APILinkResponse | undefined;
let rels: { [rel: string]: HalLink; };
if (!state.links || Object.keys(state.links).length === 0) {
console.debug(`Requesting links from ${uri}`);
const linkLookup = await axios(uri, requestConfig);
links = linkLookup.data;
if (links) {
rels = links._links;
} else {
throw new Error('Cannot resolve links in response');
}
} else {
links = undefined;
rels = state.links;
}
return [rels, links];
}
const lookupUriForRelName = (links: HalLinks, relName: string): HalLink | undefined => {
if (links) {
if (relName in links) {
return links[relName];
} else {
return undefined;
}
} else {
return undefined;
}
}
const requestResource = async (link: HalLink, pageOptions?: PageOptions, filterOptions?: FilterOptions) => {
let href: string;
if (link.templated) {
const templated: URITemplate = utpl(link.href);
href = fillTemplateParameters(templated, pageOptions, filterOptions);
} else {
href = link.href;
}
console.debug(`Attempting to request URI ${href}`)
const response = await axios.get(href, requestConfig);
return response.data;
}
const lookup = async <T> (state: ApiState, uri: string, relName: string, pageOptions?: PageOptions, filterOptions?: FilterOptions): Promise<[T, APILinkResponse | undefined]> => {
const [rels, links] = await requestLinks(uri, state);
const link: HalLink | undefined = lookupUriForRelName(rels, relName);
if (link) {
const data = await requestResource(link, pageOptions, filterOptions);
return [data, links];
} else {
throw new Error('No link relation for relation name ' + relName + ' found');
}
}
export const requestTasksAsync = createAsyncThunk<[APITasksResponse, APILinkResponse | undefined], { uri: string; pageOptions?: PageOptions; filterOptions?: FilterOptions; }>(
'api/tasks',
async ({ uri, pageOptions, filterOptions }: { uri: string, pageOptions?: PageOptions, filterOptions?: FilterOptions }, { getState }) => {
const state: ApiState = getState() as ApiState;
const TASK_REL = 'http://acme.com/rel/tasks'; // RFC 8288 compliant link relation extension according to section 2.1.2
return await lookup<APITasksResponse>(state, uri, TASK_REL, pageOptions, filterOptions);
}
);
在上面的代码中,我主要查找从由初始 URI 标识的资源返回的链接,以防它们不存在于 Redux 存储或状态中或已过时。如果我们在跳过此步骤之前收集了链接,而是重用了 state/store 中可用的信息(所有这些都发生在 requestLinks(...) 函数中)。一旦链接可用,我们最终可以请求为我们想要获取的任务信息提供服务的资源。
上面的 action creator 的 reducer 如下所示:
export const apiSlice = createSlice({
name: 'api',
initialState,
reducers: {
...
},
extraReducers: (builder) => {
builder
...
.addCase(requestTasksAsync.fulfilled, (state, action) => {
const parts: [APITasksResponse, APILinkResponse | undefined] = action.payload;
const tasks: APITasksResponse = parts[0];
const linksResponse: APILinkResponse | undefined = parts[1];
// update any link information received in preceeding calls ot
// the performed actin
if (linksResponse) {
processsLinks(linksResponse._links, state);
}
processsLinks(tasks._links, state);
processPage(tasks.page, state);
processTasks(tasks._embedded.tasks, state);
state.status = StateTypes.SUCCESS;
})
...
}
});
基本上两个 HTTP 响应被获取和处理。如果我们在不必再次查找之前已经检索到这些 URI,因此链接响应是未定义的,我们只需使用 if 语句将其过滤掉。
各个进程函数只是减少reducer中代码的辅助函数。 IE。 processTasks 函数只是将给定 taskId 的任务添加到状态中存在的记录中:
const processTasks = (tasks: TaskType[], state: ApiState) => {
for (let i = 0; i < tasks.length; i++) {
let task: TaskType = tasks[i];
state.tasks[task.taskId] = task;
}
}
根据是使用自定义链接关系还是由 IANA 指定的链接关系(即up、next、prev、...),将链接分为全局 URI 和组件 URI。
const processsLinks = (links: { [rel: string]: HalLink }, state: ApiState) => {
if (links) {
Object.keys(links).forEach(rel => {
if (rel.startsWith('http')) {
if (!state.links[rel]) {
console.debug(`rel ${rel} not yet present in state.links. Adding it with URI ${links[rel]}`);
state.links[rel] = links[rel];
} else {
console.debug(`rel ${rel} already present in state with value ${state.links[rel]}. Not going to add value ${links[rel]}`);
}
} else {
if (state.current) {
console.debug(`Updateing rel ${rel} for current component to point to URI ${links[rel]}`);
state.current.links[rel] = links[rel];
} else {
state.current = { links: { [rel]: links[rel] } };
console.debug(`Created new object for current component and assigned it the link for relation ${rel} with uri ${links[rel]}`);
}
}
});
}
}
在 TaskOverview 组件中,操作基本上是使用以下代码分派的:
const [pageOptions, setPageOptions] = useState<PageOptions | undefined>();
const [filterOptions, setFilterOptions] = useState<TaskFilterOptions | undefined>()
const state: StateTypes = useAppSelector(selectState);
const current = useAppSelector(selectCurrent);
const tasks: Record<string, TaskType> = useAppSelector(selectTasks);
const dispatch = useAppDispatch();
useEffect(() => {
...
// request new tasks if we either have not tasks yet or options changed and we
// are not currently loading them
if (StateTypes.IDLE === state) {
// lookup tasks
dispatch(requestTasksAsync({uri: "http://localhost:8080/api", pageOptions: pageOptions, filterOptions: filterOptions}));
}
...
}, [dispatch, state, tasks, pageOptions, filterOptions])
上面的代码有效。我能够根据链接关系查找 URI 并获取正确的 URI 以检索任务资源公开的数据并将这些信息存储到 Redux 存储中。然而,这一切都让人感觉非常笨拙,因为我必须从动作创建中返回两个响应对象,因为我既不能从非功能性组件中发出调度,也不能改变动作本身的状态。
理想情况下,我希望在异步 thunk 中以某种方式调度操作,并在数据在商店中可用时通知我的回调,但我想这是不可能的。由于我对 React/Redux 还很陌生,我想知道是否有更好的方法可以基于某些依赖项触发操作,并且在之前没有加载这些依赖项的情况下?我知道虽然我可以简单地将操作拆分为单独的操作,然后在相应的组件中执行调用,但感觉就像它会 a) 将切片负责的一些状态管理逻辑拖到相应的组件中b) 复制一些代码。
【问题讨论】:
标签: reactjs typescript redux redux-toolkit