如果您是前端工程师,您可能遇到过这样的情况:您被要求在服务后端部分的 API 之前开始实现某个功能功能存在。工程师通常会转向模拟来实现并行开发(这意味着该功能的前端和后端部分是并行开发的)。
然而,Mocking 可能会带来一些缺点。第一个也是最明显的一点是模拟可能会偏离实际实现,导致它们不可靠。第二个问题是模拟通常很冗长;对于包含大量数据的模拟,可能不清楚某个模拟响应实际上在模拟什么。
以下数据是您可能在代码库中找到的一些数据的示例:
type Order = { orderId: string; customerInfo: CustomerInfo; // omitted these types for brevity orderDate: string; items: OrderItem[]; paymentInfo: PaymentInfo; subtotal: number; shippingCost: number; tax: number; totalAmount: number; status: 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled'; trackingNumber: string | null; }; const mockOrders: Order[] = [ { orderId: "ORD-2024-001", customerInfo: { id: "CUST-1234", name: "Alice Johnson", email: "alice.j@email.com", shippingAddress: { street: "123 Pine Street", city: "Portland", state: "OR", zipCode: "97201", country: "USA" } }, orderDate: "2024-03-15T14:30:00Z", items: [ { productId: "PROD-789", name: "Organic Cotton T-Shirt", quantity: 2, pricePerUnit: 29.99, color: "Navy", size: "M" }, { productId: "PROD-456", name: "Recycled Canvas Tote", quantity: 1, pricePerUnit: 35.00, color: "Natural" } ], paymentInfo: { method: "credit_card", status: "completed", transactionId: "TXN-88776655" }, subtotal: 94.98, shippingCost: 5.99, tax: 9.50, totalAmount: 110.47, status: "shipped", trackingNumber: "1Z999AA1234567890" }, // Imagine more objects here, with various values changed... ];
我每天使用的数据看起来很像这样。订单数组或某种以客户为中心的信息,具有嵌套值,可帮助填充详细说明各种信息的表格、弹出窗口和卡片。
作为一名负责维护严重依赖此类模拟的应用程序的工程师,您可能会问“响应模拟中的这个特定对象是什么?”。我经常发现自己滚动浏览数百个示例,就像上面的示例一样,但不确定每个对象的用途是什么。
随着我对自己作为工程师的身份越来越有信心,我给自己下定决心要解决上述问题;如果每个模拟都可以更轻松地展示其目的怎么办?如果工程师只需要编写他们打算模拟的行怎么办?
在使用一些代码和一个名为 Zod 的库时,我发现了以下称为 parse 的方法,它尝试根据已知类型验证传入数据:
const stringSchema = z.string(); stringSchema.parse("fish"); // => returns "fish" stringSchema.parse(12); // throws error
这是一个灵光乍现的时刻; Zod 文档中的这个小例子正是我一直在寻找的!如果解析方法可以接受一个值并返回它,那么如果我传入一个值,我就会得到它。我也已经知道我可以为 Zod 模式定义默认值。如果传递一个空对象将返回一个完整的对象及其值怎么办?你瞧,确实如此;我可以在 Zod 模式上定义默认值,并返回默认值:
const UserSchema = z.object({ id: z.string().default('1'), name: z.string().default('Craig R Broughton'), settings: z.object({ theme: z.enum(['light', 'dark']), notifications: z.boolean() }).default({ theme: 'dark', notifications: true, }) }); const user = UserSchema.parse({}) // returns a full user object
现在我有了一种生成对象的方法,但它仍然不是我想要的。我真正想要的是一种仅写下我正在“嘲笑”的确切行的方法。一个简单的解决方案可能如下所示:
const UserSchema = z.object({ id: z.string().default('1'), name: z.string().default('Craig R Broughton'), settings: z.object({ theme: z.enum(['light', 'dark']), notifications: z.boolean() }).default({ theme: 'dark', notifications: true, }) }); const user = UserSchema.parse({}) const overridenUser = {...user, ...{ name: "My new name", settings: {}, // I would need to write every key:value for settings :( } satisfies Partial<z.infer<typeof UserSchema>>} // overrides the base object
然而这也有其自身的缺陷;如果我希望覆盖的值本身就是一个对象或数组怎么办?然后,我必须手动输入该功能之前所需的每一行才能继续工作并按预期被嘲笑,这违背了我们正在进行的解决方案的目的。
很长一段时间以来,这就是我所得到的,直到最近我再次尝试改进上述内容。第一步是定义“API”;我希望我的用户如何与此功能交互?
type Order = { orderId: string; customerInfo: CustomerInfo; // omitted these types for brevity orderDate: string; items: OrderItem[]; paymentInfo: PaymentInfo; subtotal: number; shippingCost: number; tax: number; totalAmount: number; status: 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled'; trackingNumber: string | null; }; const mockOrders: Order[] = [ { orderId: "ORD-2024-001", customerInfo: { id: "CUST-1234", name: "Alice Johnson", email: "alice.j@email.com", shippingAddress: { street: "123 Pine Street", city: "Portland", state: "OR", zipCode: "97201", country: "USA" } }, orderDate: "2024-03-15T14:30:00Z", items: [ { productId: "PROD-789", name: "Organic Cotton T-Shirt", quantity: 2, pricePerUnit: 29.99, color: "Navy", size: "M" }, { productId: "PROD-456", name: "Recycled Canvas Tote", quantity: 1, pricePerUnit: 35.00, color: "Natural" } ], paymentInfo: { method: "credit_card", status: "completed", transactionId: "TXN-88776655" }, subtotal: 94.98, shippingCost: 5.99, tax: 9.50, totalAmount: 110.47, status: "shipped", trackingNumber: "1Z999AA1234567890" }, // Imagine more objects here, with various values changed... ];
上面的 API 将允许用户指定他们选择的模式,然后提供适当的覆盖并返回一个用户对象!当然,我们希望正确考虑数组以及单个对象。为此,对传入的覆盖类型进行简单的类型检查就足够了:
const stringSchema = z.string(); stringSchema.parse("fish"); // => returns "fish" stringSchema.parse(12); // throws error
上面的代码实际上与之前相同,但是现在它在内部封装了解析,因此用户不必手动执行此操作或了解有关 Zods 解析方法的详细信息。正如您通过阅读包含的 if/else 语句可能猜到的那样,我们还通过使用递归构建器函数来解决嵌套对象和数组的保存问题,该函数解析每个值并返回 Zod 模式中指定的默认值。 🎜>
上面的内容有点让人费解,但结果是用户可以执行以下操作:
const UserSchema = z.object({ id: z.string().default('1'), name: z.string().default('Craig R Broughton'), settings: z.object({ theme: z.enum(['light', 'dark']), notifications: z.boolean() }).default({ theme: 'dark', notifications: true, }) }); const user = UserSchema.parse({}) // returns a full user object
这已经是一篇相当长的文章了,所以让我们以我们所有努力工作的结果来结束吧。让我们回顾一下第一个模拟,以及如何使用 zodObjectBuilder 编写它。首先让我们定义我们的类型和默认值,并将结果模式传递到 zodObjectBuilder 中:
const UserSchema = z.object({ id: z.string().default('1'), name: z.string().default('Craig R Broughton'), settings: z.object({ theme: z.enum(['light', 'dark']), notifications: z.boolean() }).default({ theme: 'dark', notifications: true, }) }); const user = UserSchema.parse({}) const overridenUser = {...user, ...{ name: "My new name", settings: {}, // I would need to write every key:value for settings :( } satisfies Partial<z.infer<typeof UserSchema>>} // overrides the base object
const UserSchema = z.object({ id: z.string().default('1'), name: z.string().default('Craig R Broughton'), settings: z.object({ theme: z.enum(['light', 'dark']), notifications: z.boolean() }).default({ theme: 'dark', notifications: true, }) }); const user = zodObjectBuilder({ schema: UserSchema, overrides: { name: 'My new name', settings: { theme: 'dark' } } // setting is missing the notifications theme :( }); // returns a full user object with the overrides
通过这个小演示,我们已经完成了第一篇文章的结尾:) 我希望您喜欢阅读这段探索改进模拟的旅程。 zodObjectBuilder 仍在构建中,但它很好地满足了我最小化模拟对象的需求。如果您想尝试当前版本,可以在 https://www.npmjs.com/package/@crbroughton/ts-utils 找到它,其中包含该功能。
以上是我如何尝试用 Zod 改进模拟的详细内容。更多信息请关注PHP中文网其他相关文章!