Skip to content

2.3 请求 Client 与 Mock 场景 ​

首页按阶段取列表,详情页按 ID 取一条,“我的记录”按已保存 ID 恢复多条。让三个页面读取同一份 Mock 数组,会把查询规则和测试场景散进 page。

本节把边界收在这些文件:

text
apps/web/src/api/recommendation-client.ts
apps/web/src/api/recommendation-client.test.ts
apps/web/src/api/mock-recommendation-client.ts
apps/web/src/api/recommendation-client-provider.ts
apps/web/src/api/recommendation-client-provider.test.ts
apps/web/src/composables/recommendation-request.ts
apps/web/src/composables/recommendation-request.test.ts
docs/api-contract.md

三个查询就是 page 的完整依赖 ​

ts
export interface RecommendationClient {
  list(stageId: StageId): Promise<readonly Recommendation[]>;
  get(id: RecommendationId): Promise<Recommendation | undefined>;
  listByIds(ids: readonly RecommendationId[]): Promise<readonly Recommendation[]>;
}
  • 首页切换 stage 后调用 list(stageId);
  • 详情页先用 isRecommendationId(query.id) 收窄,再调用 get(id);
  • “我的记录”把收藏和历史 ID 交给 listByIds(ids),不请求完整目录。

五个 RecommendationId 是封闭 contract。Client 不能接受服务端临时增加的第六个 ID,再让它一路流进 store 或 route。

Page 只 import recommendation-client-provider.ts:

ts
export function createAppRecommendationClient(
  scenario: unknown = "success",
): RecommendationClient {
  return createMockRecommendationClient({
    scenario: parseRecommendationScenario(scenario),
  });
}

Provider 是替换 adapter 的单点。页面不 import Mock adapter,也不 import data/recommendations.ts。

Runtime parser 拒绝看似合法的对象 ​

return value as Recommendation[] 没有验证运行时输入。parseRecommendationList(value: unknown) 必须检查:

  • recommendation ID 非空且属于固定五项;同一响应不能有重复 ID;
  • stageIds 非空且只包含四个 StageId;
  • 七个字段都是对象自己的 data property,不能来自 prototype 或 getter;
  • 外层和 stageIds 都是 dense array,稀疏 index 直接失败;
  • descriptor trap 抛错、getter、异常 Proxy 都归一为 invalid-response;
  • 成功结果、每个 item 和嵌套 stageIds 都冻结。

关键读取不使用普通属性访问:

ts
function readOwnDataProperty(object: object, key: PropertyKey): unknown {
  if (!Object.hasOwn(object, key)) {
    return invalidResponse();
  }

  const descriptor = Object.getOwnPropertyDescriptor(object, key);
  if (!descriptor || !Object.hasOwn(descriptor, "value")) {
    return invalidResponse();
  }

  return descriptor.value;
}

数组也逐个读取 own index。Array(1) 的 length 是 1,但 index 0 不存在,不能被 .map() 静默跳过。

Review fixture 应包含 blank/unknown ID、重复 ID、继承字段、getter、sparse array 和会抛错的 Proxy。失败时修 parser,不能把 fixture 换成普通 object 或删 assertion。

Mock 负责四种可重复场景 ​

ts
export type RecommendationScenario = "success" | "empty" | "slow" | "retry";
ScenarioAdapter 行为
success返回对应 list / get / listByIds 的已解析结果
empty返回冻结空数组或 undefined
slow默认等待 800 ms 后返回
retry第一次请求抛 network,后续请求成功

未知 scenario 回 success。listByIds 对输入 ID 按首次出现顺序去重,再返回对应内容。

Delay 在 test 中注入为 0。Retry 使用 adapter closure 保存“是否应失败”,不会把次数状态放到 page。

共享 controller 挡住 stale result ​

首页、详情和“我的记录”共用 useRecommendationRequest<T>()。每次 load 增加 token:

ts
const token = ++requestToken;
status.value = "loading";
errorMessage.value = "";

const nextData = await loader();
if (token !== requestToken) return;

data.value = nextData;
status.value = "success";

Catch 分支也检查 token。旧请求晚到的 success 或 error 都不能覆盖最新结果。retry() 复用最近一次 loader;新 load 立刻清理旧错误文案。

Controller 把 network、server、timeout、invalid-response 映射成固定页面文案。阶段 2 没有 HTTP 请求,也没有 timeout 计时器。阶段 3 可以在 provider 后替换 transport adapter,保留这三个 client 方法和 page 调用形状。

用三组定向测试批准边界 ​

bash
npm run test:web -- \
  recommendation-client.test.ts \
  recommendation-client-provider.test.ts \
  recommendation-request.test.ts
npm run typecheck:web

Fresh 结果:

text
src/api/recommendation-client.test.ts (45 tests)
src/api/recommendation-client-provider.test.ts (2 tests)
src/composables/recommendation-request.test.ts (9 tests)
Test Files  3 passed (3)
Tests       56 passed (56)
vue-tsc --noEmit: exit 0

45 项守 parser、固定 ID、Mock scenario 与三个 client 方法;2 项守 provider 的 scenario 注入;9 项守 stale success、stale error、retry 最近 loader、loading 清理旧错误和安全文案。Page import Mock 或 data 时,即使这 56 项全绿,依赖边界仍然不合格。

下一节:2.4 页面状态与浏览器验收。

晨悦 AI 实践手册