|
1 | 1 | ---
|
2 |
| -title: Examples |
| 2 | +title: 示例 |
3 | 3 | description: Using openapi-typescript in real-world applications
|
4 | 4 | ---
|
5 | 5 |
|
6 |
| -# Examples |
| 6 | +# 示例 |
7 | 7 |
|
8 |
| -::: warning |
| 8 | +openapi-typescript生成的类型是通用的,可以以各种方式使用。虽然这些示例不够全面,但希望它们能激发你如何在应用程序中使用这些类型的想法。 |
9 | 9 |
|
10 |
| -This article is a stub. Please help [expand it](https://github.com/drwpow/openapi-typescript/tree/main/docs/zh/)! |
| 10 | +## 数据获取 |
11 | 11 |
|
| 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 **能够** 捕获的明显错误(在你的堆栈中可能有更多具有类型的东西,而你并不了解!)。 |
12 | 53 | :::
|
| 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