Skip to content

Commit bc3b91b

Browse files
committed
docs(chinese): translate examples
1 parent ba1aca5 commit bc3b91b

File tree

1 file changed

+221
-4
lines changed

1 file changed

+221
-4
lines changed

docs/zh/examples.md

Lines changed: 221 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,229 @@
11
---
2-
title: Examples
2+
title: 示例
33
description: Using openapi-typescript in real-world applications
44
---
55

6-
# Examples
6+
# 示例
77

8-
::: warning
8+
openapi-typescript生成的类型是通用的,可以以各种方式使用。虽然这些示例不够全面,但希望它们能激发你如何在应用程序中使用这些类型的想法。
99

10-
This article is a stub. Please help [expand it](https://github.com/drwpow/openapi-typescript/tree/main/docs/zh/)!
10+
## 数据获取
1111

12+
可以使用**自动生成类型的fetch包装器**简单而安全地获取数据:
13+
14+
- [openapi-fetch](/openapi-fetch/)(推荐)
15+
- [openapi-typescript-fetch](https://www.npmjs.com/package/openapi-typescript-fetch)[@ajaishankar](https://github.com/ajaishankar)
16+
17+
::: tip
18+
一个良好的fetch包装器**不应使用泛型**。泛型需要更多的输入,并且可能隐藏错误!
19+
:::
20+
21+
## Hono
22+
23+
[Hono](https://hono.dev/) 是一个现代的用于 Node.js 的服务器框架,可以轻松部署到网络中(例如 [Cloudflare Workers](https://developers.cloudflare.com/workers/)),就像部署到标准容器一样。它还内置了 TypeScript,因此非常适合生成的类型。
24+
25+
[使用 CLI 生成类型之后](/zh/introduction),为每个端点传递适当的 `paths` 响应:
26+
27+
```ts
28+
import { Hono } from "hono";
29+
import { components, paths } from "./path/to/my/types";
30+
31+
const app = new Hono();
32+
33+
/** /users */
34+
app.get("/users", async (ctx) => {
35+
try {
36+
const users = db.get("SELECT * from users");
37+
return ctx.json<
38+
paths["/users"]["responses"][200]["content"]["application/json"]
39+
>(users);
40+
} catch (err) {
41+
return ctx.json<components["schemas"]["Error"]>({
42+
status: 500,
43+
message: err ?? "An error occurred",
44+
});
45+
}
46+
});
47+
48+
export default app;
49+
```
50+
51+
::: tip
52+
在服务器环境中进行类型检查可能很棘手,因为通常会查询数据库并与 TypeScript 无法内省的其他端点通信。但是使用泛型将使你能够注意到 TypeScript **能够** 捕获的明显错误(在你的堆栈中可能有更多具有类型的东西,而你并不了解!)。
1253
:::
54+
55+
## Mock-Service-Worker (MSW)
56+
57+
如果你正在使用 Mock Service Worker (MSW) 来定义 API 的模拟数据,你可以使用一个 小巧、自动类型化的封装 来包裹 MSW,这样当你的 OpenAPI 规范发生变化时,你可以轻松解决 API 模拟数据中的冲突。最终,你可以对应用程序的 API 客户端和 API 模拟数据具有相同的信心水平。
58+
59+
使用 `openapi-typescript` 和一个 fetch 的包装器,比如 `openapi-fetch`,可以确保我们应用程序的 API 客户端不会与 OpenAPI 规范冲突。
60+
61+
然而,虽然你可以轻松解决 API 客户端的问题,但你必须手动记住调整 API 模拟,因为没有机制提醒你有冲突。
62+
63+
我们推荐使用以下的包装器,它与 `openapi-typescript` 完美配合:
64+
65+
- [openapi-msw](https://www.npmjs.com/package/openapi-msw) by [@christoph-fricke](https://github.com/christoph-fricke)
66+
67+
## 测试模拟
68+
69+
测试出现误报的最常见原因之一是模拟数据与实际 API 响应不同步。
70+
71+
`openapi-typescript` 提供了一种极好的方法来防范这种情况,而且付出的努力很小。下面是一个示例,演示如何编写一个帮助函数,对所有模拟数据进行类型检查以符合你的 OpenAPI 架构(我们将使用 [vitest](https://vitest.dev/)/[vitest-fetch-mock](https://www.npmjs.com/package/vitest-fetch-mock),但相同的原理也适用于任何设置):
72+
73+
假设我们想要按照以下对象结构编写模拟数据,以便一次性模拟多个端点:
74+
75+
```ts
76+
{
77+
[pathname]: {
78+
[HTTP method]: { status: [status], body: { …[some mock data] } };
79+
}
80+
}
81+
```
82+
83+
使用我们生成的类型,我们可以推断出任何给定路径 + HTTP 方法 + 状态码的**正确数据结构**。示例测试如下:
84+
85+
::: code-group [my-test.test.ts]
86+
87+
```ts
88+
import { mockResponses } from "../test/utils";
89+
90+
describe("My API test", () => {
91+
92+
93+
it("mocks correctly", async () => {
94+
mockResponses({
95+
"/users/{user_id}": {
96+
// ✅ 正确的 200 响应
97+
get: { status: 200, body: { id: "user-id", name: "User Name" } },
98+
// ✅ 正确的 403 响应
99+
delete: { status: 403, body: { code: "403", message: "Unauthorized" } },
100+
},
101+
"/users": {
102+
// ✅ 正确的 201 响应
103+
put: { 201: { status: "success" } },
104+
},
105+
});
106+
107+
// 测试 1: GET /users/{user_id}: 200
108+
await fetch("/users/user-123");
109+
110+
// 测试 2: DELETE /users/{user_id}: 403
111+
await fetch("/users/user-123", { method: "DELETE" });
112+
113+
// 测试 3: PUT /users: 200
114+
await fetch("/users", {
115+
method: "PUT",
116+
body: JSON.stringify({ id: "new-user", name: "New User" }),
117+
});
118+
119+
// 测试清理
120+
fetchMock.resetMocks();
121+
});
122+
});
123+
```
124+
125+
:::
126+
127+
_注意:此示例使用原始的 `fetch()` 函数,但可以将任何 fetch 包装器(包括 [openapi-fetch](/openapi-fetch/))直接替换,而不需要进行任何更改。_
128+
129+
而能够实现这一点的魔法将存储在 `test/utils.ts` 文件中,可以在需要的地方复制 + 粘贴(为简单起见进行隐藏):
130+
131+
<details>
132+
<summary>📄 <strong>test/utils.ts</strong></summary>
133+
134+
135+
::: code-group [test/utils.ts]
136+
137+
```ts
138+
import type { paths } from "./api/v1"; // 由 openapi-typescript 生成
139+
// 设置
140+
// ⚠️ 重要:请更改这个!这是所有 URL 的前缀
141+
const BASE_URL = "https://myapi.com/v1";
142+
// 结束设置
143+
// 类型帮助程序 —— 忽略这些;这只是使 TS 查找更好的工具,无关紧要。
144+
type FilterKeys<Obj, Matchers> = {
145+
[K in keyof Obj]: K extends Matchers ? Obj[K] : never;
146+
}[keyof Obj];
147+
type PathResponses<T> = T extends { responses: any } ? T["responses"] : unknown;
148+
type OperationContent<T> = T extends { content: any } ? T["content"] : unknown;
149+
type MediaType = `${string}/${string}`;
150+
type MockedResponse<T, Status extends keyof T = keyof T> =
151+
FilterKeys<OperationContent<T[Status]>, MediaType> extends never
152+
? { status: Status; body?: never }
153+
: {
154+
status: Status;
155+
body: FilterKeys<OperationContent<T[Status]>, MediaType>;
156+
};
157+
/**
158+
* 模拟 fetch() 调用并根据 OpenAPI 架构进行类型检查
159+
*/
160+
export function mockResponses(responses: {
161+
[Path in keyof Partial<paths>]: {
162+
[Method in keyof Partial<paths[Path]>]: MockedResponse<
163+
PathResponses<paths[Path][Method]>
164+
>;
165+
};
166+
}) {
167+
fetchMock.mockResponse((req) => {
168+
const mockedPath = findPath(
169+
req.url.replace(BASE_URL, ""),
170+
Object.keys(responses),
171+
)!;
172+
// 注意:这里的类型我们使用了懒惰的方式,因为推断是不好的,而且这有一个 `void` 返回签名。重要的是参数签名。
173+
if (!mockedPath || !(responses as any)[mockedPath])
174+
throw new Error(`No mocked response for ${req.url}`); // 如果未模拟响应,则抛出错误(如果希望有不同的行为,则删除或修改)
175+
const method = req.method.toLowerCase();
176+
if (!(responses as any)[mockedPath][method])
177+
throw new Error(`${req.method} called but not mocked on ${mockedPath}`); // 类似地,如果响应的其他部分没有模拟,则抛出错误
178+
if (!(responses as any)[mockedPath][method]) {
179+
throw new Error(`${req.method} called but not mocked on ${mockedPath}`);
180+
}
181+
const { status, body } = (responses as any)[mockedPath][method];
182+
return { status, body: JSON.stringify(body) };
183+
});
184+
}
185+
// 匹配实际 URL(/users/123)与 OpenAPI 路径(/users/{user_id} 的辅助函数)
186+
export function findPath(
187+
actual: string,
188+
testPaths: string[],
189+
): string | undefined {
190+
const url = new URL(
191+
actual,
192+
actual.startsWith("http") ? undefined : "http://testapi.com",
193+
);
194+
const actualParts = url.pathname.split("/");
195+
for (const p of testPaths) {
196+
let matched = true;
197+
const testParts = p.split("/");
198+
if (actualParts.length !== testParts.length) continue; // 如果长度不同,则自动不匹配
199+
for (let i = 0; i < testParts.length; i++) {
200+
if (testParts[i]!.startsWith("{")) continue; // 路径参数({user_id})始终算作匹配
201+
if (actualParts[i] !== testParts[i]) {
202+
matched = false;
203+
break;
204+
}
205+
}
206+
if (matched) return p;
207+
}
208+
}
209+
```
210+
211+
:::
212+
213+
::: info 补充说明
214+
上面的代码相当复杂!在大多数情况下,这是大量的实现细节,你可以忽略。 `mockResponses(…)` 函数签名是所有重要的魔法发生的地方,你会注意到这个结构与我们的设计之间有直接的链接。从那里,代码的其余部分只是使运行时按预期工作。
215+
:::
216+
217+
```ts
218+
export function mockResponses(responses: {
219+
[Path in keyof Partial<paths>]: {
220+
[Method in keyof Partial<paths[Path]>]: MockedResponse<
221+
PathResponses<paths[Path][Method]>
222+
>;
223+
};
224+
});
225+
```
226+
227+
</details>
228+
229+
现在,每当你的架构更新时,所有的模拟数据都将得到正确的类型检查 🎉。这是确保测试具有弹性和准确性的重要步骤。

0 commit comments

Comments
 (0)