连接 Viewer 远端行数据
维护期(已弃用)
@ahoo-wang/fetcher-viewer 已进入维护期(弃用),仅维护现有功能,不再新增功能。数据视图能力的后续演进由 @ahoo-wang/fetcher-view-engine 承担,新项目请使用 View Engine。本页保留供存量项目维护参考;两者模型与 API 不同,迁移需要适配。
创建一个 React 页面,渲染默认视图、加载数据并响应分页/排序。Viewer 管理展示状态,应用提供数据和持久化回调。
1. 准备应用
使用支持 CSS 打包的 React 应用,安装 @ahoo-wang/fetcher-viewer 及其声明的 peer 依赖。包入口列出了完整安装契约;React、Ant Design、图标、dayjs 和 Fetcher peers 都必须解析到兼容版本。
本例要求同源应用提供 POST /users/paged,接收 { condition, pagination: { index, size }, sort },返回 JSON { list: [{ id, name }], total }。请实现该路由或将 load 适配至真实服务。这是应用契约,不是 Viewer 创建的端点。后端必须执行授权并验证查询。countUrl 是必填的定义元数据,但本例不会调用它。
2. 添加完整页面
保存为 UsersPage.tsx,通过现有 React root/路由挂载 <UsersPage />:
import { useCallback, useEffect, useRef, useState } from 'react';
import { Fetcher, ResultExtractors } from '@ahoo-wang/fetcher';
import { all, type PagedList } from '@ahoo-wang/fetcher-wow';
import {
Viewer,
type FieldDefinition,
type ViewDefinition,
type ViewState,
type ViewChangeAction,
} from '@ahoo-wang/fetcher-viewer';
interface User {
id: string;
name: string;
}
const fields: FieldDefinition[] = [
{ name: 'id', label: 'ID', type: 'text', primaryKey: true },
{
name: 'name',
label: 'Name',
type: 'text',
primaryKey: false,
sorter: true,
},
];
const definition: ViewDefinition = {
id: 'users',
name: 'Users',
fields,
availableFilters: [],
dataUrl: '/users/paged',
countUrl: '/users/count',
};
const defaultView: ViewState = {
id: 'default',
name: 'All users',
definitionId: 'users',
type: 'PERSONAL',
source: 'SYSTEM',
isDefault: true,
filters: [],
columns: fields.map(field => ({
key: field.name,
name: field.name,
fixed: field.primaryKey,
hidden: false,
})),
tableSize: 'middle',
pageSize: 20,
condition: all(),
sorter: [],
};
const defaultViews = [defaultView];
const api = new Fetcher();
type QueryArgs = Parameters<ViewChangeAction>;
export function UsersPage() {
const [data, setData] = useState<PagedList<User>>({ list: [], total: 0 });
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string>();
const pending = useRef<AbortController | null>(null);
const lastQuery = useRef<QueryArgs>([all(), 1, 20, []]);
const load = useCallback(async (...args: QueryArgs) => {
lastQuery.current = args;
pending.current?.abort();
const controller = new AbortController();
pending.current = controller;
const [condition, index, size, sorter] = args;
setLoading(true);
setError(undefined);
try {
const page = await api.post<PagedList<User>>(
definition.dataUrl,
{
signal: controller.signal,
body: { condition, pagination: { index, size }, sort: sorter },
},
{ resultExtractor: ResultExtractors.Json },
);
if (!controller.signal.aborted) setData(page);
} catch (cause) {
if (!controller.signal.aborted) {
setError(cause instanceof Error ? cause.message : 'Loading failed');
}
} finally {
if (!controller.signal.aborted) setLoading(false);
}
}, []);
useEffect(() => {
void load(...lastQuery.current);
return () => pending.current?.abort();
}, [load]);
return (
<section aria-label="Users">
{error && (
<div role="alert">
{error}{' '}
<button onClick={() => void load(...lastQuery.current)}>Retry</button>
</div>
)}
<Viewer<User>
defaultViews={defaultViews}
defaultView={defaultView}
definition={definition}
dataSource={data}
loading={loading}
pagination={{}}
enableRowSelection
onLoadData={load}
onSwitchView={view => {
void load(view.condition, 1, view.pageSize, view.sorter);
}}
/>
</section>
);
}初始 effect 加载第一页。切换视图通过 onSwitchView 按目标视图的条件、页大小和排序重新加载第一页;仅提供 onLoadData 不会覆盖视图切换。后续变化通过 onLoadData 提供条件、从 1 开始的页码、大小和 sorter,回调将其转换为应用请求体。新请求取消前一个请求,已取消的响应不能覆盖新数据。卸载时取消活动请求。
3. 验证加载、空和失败状态
将应用路由模拟为 { list: [{ id: '42', name: 'Ada' }], total: 1 },检查可见数据行、选择、排序和发出的查询。返回空 list、零 total 来检查表格空状态。返回 HTTP 错误,检查提示和对最后一次查询的重试。延迟两个响应并快速翻页:应只有当前响应更新表格。本例失败时保留之前的数据行,并在上方显示错误。
再使用超过 20 行且各页内容不同的数据:翻到第 2 页后点击视图面板的 All users,确认请求的 pagination.index 为 1,且可见数据行重新变为第一页,而不只是页码变化。
JSON 泛型不验证服务端数据;需要时应在应用边界验证不可信响应。本流程的 TypeScript 检查不能替代浏览器交互检查,Storybook 提供组件示例。
4. 按需添加过滤与保存视图
初始视图有意不设置过滤器。根据过滤契约向 availableFilters 和已保存 filters 添加条目。内置日期时间行为通过注册表的 datetime 解析;DateTimeFilter 不是包根的具名导出,不要导入内部文件。
持久化视图变更时提供 onCreateView、onUpdateView、onDeleteView,只有保存成功后才使用服务端确认的视图调用成功回调,否则 UI 应保留现有状态。参见保存视图生命周期。本例只定义一个系统默认视图,不执行远端视图写入。
5. 按契约选择远端组合
只有服务端实现定义接口与 Wow 保存视图契约,并且已设置必要客户端 Fetcher 配置时,才使用 FetcherViewer。仅有通用分页端点并不足够。参见模型与状态及 View 与 Viewer了解所有权边界。
Viewer.tsx:57 说明数据与持久化责任。
采用 FetcherViewer 前确认
上面的完整适配器只把业务分页端点接到 Viewer。先通过本地示例验证界面,再替换数据加载。若采用 FetcherViewer,还需要兼容的视图定义、Wow 命令和视图快照查询服务;仅有 POST /users/paged 不足以运行。
远端行查询发送 PagedQuery,把 internalCondition 与交互条件组合。租户、所有者和 definitionId 必须一致,后端仍需授权。创建/更新视图会等待命令阶段并重新读取快照,只有版本足够新且身份一致才确认;这不是所有查询的读己之写保证,也不代表投影延迟有上界。删除没有相同的版本确认流程。
身份切换时组件会隔离旧界面工作,但已发送的命令不会因此从服务端撤销。请结合Wow 集成、FetcherViewer 参考与状态和资源核对实际部署契约。
