作为一个工程师,你会构建一些东西。
你正在构建的东西可能会有输入——数据被注入的点。
假设你正在构建一个名为 mycli 的CLI:
mycli run <something>
这里,<something>
是输入。它告诉程序该做什么。它在运行时是完全未知的,它甚至可能不是存在的。
如果你正在构建一个面向公众的API,你的输入可能会暴露在web上的公共端口上:
GET https://somesite.com/api/user/:id
POST https://somesite.com/api/user/:id
我想到了几个可能的输入:
-
路径中的
:id
参数 -
您使用了哪个REST方法(
GET
或POST
) -
请求的标头
-
添加到请求中的搜索参数:
?hello=world
-
请求体(for
POST
)
所有这些在运行时都是未知的,因为如果这些API暴露给外界,任何人都可以ping通它们。如果它们没有得到验证,许多甚至可能成为攻击的载体。
当你不信任输入时
在这些情况下,你都不相信进入应用程序的数据。对于这些情况,你应该使用Zod。
对于脚本,您可以解析 process.argv
:
import {z} from 'zod'
// Make a schema for the arguments
const ArgSchema = z.tuple([z.any(), z.any(), z.string()])
// Use it to parse process.argv
const [, , name] = ArgSchema.parse(process.argv)
// Log it to the console safely!
console.log(name)
process.argv通常包含两个无用的参数,然后是您想要提取的动态参数。对于更复杂的情况,您可能希望使用 commander — 但是对于简单的脚本,这非常有效。
这里使用Zod的好处是 name
被推断为 string
,而不需要任何其他工作。
对于公共API,您可以创建Zod模式来确保请求体和头是正确的。
import {z} from 'zod'
import {Request, Response} from 'express'
const CreateUserSchema = z.object({
body: z.object({
// Ensures that the email exists, and is an email
email: z.string().email(),
}),
headers: z.object({
// Ensures that the authorization header is present
authorization: z.string(),
}),
})
const handleCreateUser = (req: Request, res: Response) => {
// Parse the request
const result = CreateUserSchema.safeParse(req)
// If something was missing, send back an error
if (!result.success) {
res.status(400).send(result.error)
return
}
const {email} = result.data.body
// Create the user
}
这里使用 .safeParse
是为了不抛出错误,而是返回 400
并将错误传递回去。Zod抛出了非常好的、可读的错误,因此我们可以确保用户知道哪里出了问题。
多亏了Zod,你可以确保应用程序中的未知输入是经过验证和安全的。如果我正在构建一个带有未知输入的应用程序,我会立即添加Zod。
更多未知输入的例子:
-
表单——登录表单、CRUD表单……这些都是Zod的伟大用例
-
Websocket连接
-
localStorage
-用户可以操作它,否则它可能已经过期。
当你“有点”相信输入的时候
对于应用程序中不可信的输入,Zod的用例是显而易见的。但还有其他类型的输入是你“有点”信任的。
我想到的例子是第三方服务。如果你的应用依赖于调用一个你无法控制的第三方API,你应该用Zod验证这个API吗?
如果API改变了形状,可能会在应用程序中造成细微的错误。作为一名工程师,我已经经历过很多次了:在意识到API返回了一些我没有预料到的东西之前,假设我的代码是错误的。
用Zod验证数据仍然会在你的应用程序中导致一个错误——但是这个错误会在数据进入你的应用程序时被抛出。这使得它更容易调试和修复。
在这种情况下,为什么不验证呢? 如果考虑到包的大小,Zod是12kb的压缩包,这对一些应用来说有点大了。验证也不可避免地比不验证稍微慢一些。因此,如果关注关键路径性能,您可能希望跳过Zod。
然而,在我职业生涯中构建的大多数应用程序中,健壮性是关键问题。“不可能的数据”——或者你不期望的数据——可能是我整个开发生涯中最常见的bug原因。所以我会验证任何不可信的输入。
一些更“有点”值得信赖的输入:
-
公共API,比如GitHub和YouTube
-
API由组织中的其他团队控制(这是有争议的,但根据文化,我会考虑将Zod作为一种选择)
当你能控制输入的时候
我们来看最后一种情况。想象一下,你正在构建一个全栈应用程序,使用流行的框架,如Remix, Next.js, SvelteKit或next。
您希望从前端加载一些数据。您ping一个API端点(由于它是面向公众的,因此可能已经通过Zod进行了验证)。你会得到一些数据。您是否应该在前端使用Zod验证该数据?
这是一个棘手的问题。我们完全控制API端点——我们负责部署到它,并且它与前端同步部署。然而,仍然有可能发生以下顺序:
-
用户在我们的页面上启动浏览器会话(不刷新)。假设这是一个金融应用,通过ping API,图表每10秒刷新一次。
-
当他们浏览时,我们重新部署我们的应用程序,对后端进行了一些突破性的更改。
-
用户仍然没有刷新页面,但现在他们ping的后端已经过期了。书页开始破裂。
这种前端和后端之间的“版本漂移”比你想象的更常见,特别是考虑到一些团队部署的频率。如果我们将Zod放在前端,我们就能够在任何版本漂移发生时立即出错,并提示用户刷新页面。
然而,在这些情况下,我通常选择不使用Zod。有了版本漂移,应用程序通常只是一次浏览器刷新,就能获得更好的体验。由于没有任何安全敏感的内容暴露给前端代码,因此漏洞的爆炸半径相对较小。
这是有争议的,尽管每当一个版本漂移发生时,你可能想要检查所有的数据进入你的应用程序的错误。
总结
当你的应用有你不信任的输入时,使用Zod。
当你的应用有你信任但无法控制的输入时,用Zod验证它们。
当你的应用程序有你信任和控制的输入时,我通常不会与Zod验证它们。
欢迎关注公众号:文本魔术,了解更多