zod与SSG:静态站点生成验证
还在为静态站点生成(SSG)中的数据验证头疼吗?每次构建时都担心外部API数据格式变化导致构建失败?zod作为TypeScript优先的schema验证库,为SSG场景提供了完美的解决方案。
读完本文,你将获得:
- ✅ SSG场景下数据验证的核心痛点解析
- ✅ zod在构建时验证的最佳实践方案
- ✅ 完整的Next.js、Gatsby、Nuxt集成示例
- ✅ 性能优化与错误处理策略
- ✅ 类型安全的全栈开发体验
为什么SSG需要数据验证?
静态站点生成(Static Site Generation)在现代前端开发中越来越流行,但数据源的不可控性带来了巨大挑战:
传统解决方案往往在运行时才发现数据问题,而zod让我们能够在构建时就捕获这些错误。
zod在SSG中的核心优势
构建时类型安全
zod最大的价值在于将运行时验证转换为构建时类型检查:
// 定义文章schema
const ArticleSchema = z.object({
id: z.string(),
title: z.string().min(1, "标题不能为空"),
content: z.string(),
publishDate: z.string().datetime(),
tags: z.array(z.string()).optional(),
metadata: z.object({
description: z.string().max(160),
image: z.string().url().optional()
})
});
// 类型自动推断
type Article = z.infer<typeof ArticleSchema>;
错误早发现早处理
通过zod的safeParse方法,我们可以在构建阶段优雅处理数据错误:
const validateArticles = (rawData: unknown) => {
const result = z.array(ArticleSchema).safeParse(rawData);
if (!result.success) {
console.error('数据验证失败:');
result.error.issues.forEach(issue => {
console.error(`- ${issue.path.join('.')}: ${issue.message}`);
});
process.exit(1); // 构建失败
}
return result.data; // 类型安全的数据
};
主流SSG框架集成实战
Next.js + zod完整示例
// lib/schemas.ts
import { z } from 'zod';
export const PostSchema = z.object({
id: z.string(),
slug: z.string(),
title: z.string(),
excerpt: z.string(),
content: z.string(),
date: z.string().datetime(),
author: z.object({
name: z.string(),
avatar: z.string().url()
}),
tags: z.array(z.string())
});
export type Post = z.infer<typeof PostSchema>;
// lib/posts.ts
import { PostSchema, type Post } from './schemas';
export async function getPosts(): Promise<Post[]> {
const response = await fetch('https://api.example.com/posts');
const rawData = await response.json();
const result = z.array(PostSchema).safeParse(rawData);
if (!result.success) {
throw new Error(`Posts数据验证失败: ${JSON.stringify(result.error.format())}`);
}
return result.data;
}
// pages/blog/index.tsx
import { GetStaticProps } from 'next';
import { getPosts } from '../../lib/posts';
export const getStaticProps: GetStaticProps = async () => {
try {
const posts = await getPosts();
return { props: { posts } };
} catch (error) {
console.error('构建时数据获取失败:', error);
return { notFound: true };
}
};
Gatsby配置方案
// gatsby-node.ts
import { z } from 'zod';
const ProductSchema = z.object({
id: z.string(),
name: z.string(),
price: z.number().positive(),
category: z.string(),
inStock: z.boolean(),
variants: z.array(z.object({
color: z.string(),
size: z.string(),
sku: z.string()
})).optional()
});
exports.createPages = async ({ graphql, actions }) => {
const { data } = await graphql(`
query {
allExternalProducts {
nodes
}
}
`);
const result = z.array(ProductSchema).safeParse(data.allExternalProducts.nodes);
if (!result.success) {
throw new Error(`产品数据验证失败: ${result.error.message}`);
}
const products = result.data;
products.forEach(product => {
actions.createPage({
path: `/products/${product.id}`,
component: require.resolve('./src/templates/Product.tsx'),
context: { product }
});
});
};
Nuxt 3组合式API集成
// composables/useValidation.ts
import { z } from 'zod';
export const UserSchema = z.object({
id: z.string(),
email: z.string().email(),
name: z.string(),
role: z.enum(['admin', 'user', 'guest']),
preferences: z.object({
theme: z.enum(['light', 'dark']),
notifications: z.boolean()
}).optional()
});
export const useUserValidation = () => {
const validateUser = (data: unknown) => {
return UserSchema.safeParse(data);
};
return { validateUser };
};
// pages/users/[id].vue
<script setup>
const { id } = useRoute().params;
const { data: userData } = await useFetch(`/api/users/${id}`);
const { validateUser } = useUserValidation();
const validationResult = validateUser(userData.value);
if (!validationResult.success) {
throw createError({
statusCode: 500,
message: '用户数据格式错误'
});
}
const user = validationResult.data;
</script>
高级验证模式
条件验证与业务规则
const OrderSchema = z.object({
id: z.string(),
status: z.enum(['pending', 'processing', 'shipped', 'delivered']),
items: z.array(z.object({
productId: z.string(),
quantity: z.number().int().positive(),
price: z.number().positive()
})),
shippingAddress: z.object({
street: z.string(),
city: z.string(),
zipCode: z.string()
}).optional(),
trackingNumber: z.string().optional()
}).refine((data) => {
// 只有已发货的订单需要有物流单号
if (data.status === 'shipped' || data.status === 'delivered') {
return data.trackingNumber !== undefined;
}
return true;
}, {
message: "已发货订单必须提供物流单号",
path: ["trackingNumber"]
});
异步数据验证
const validateWithExternalCheck = z.string().refine(async (email) => {
// 构建时检查邮箱是否已注册
const response = await fetch(`https://api.example.com/check-email?email=${email}`);
const data = await response.json();
return data.available;
}, {
message: "邮箱已被注册"
});
// 在getStaticProps中使用
export const getStaticProps: GetStaticProps = async () => {
const email = "user@example.com";
const result = await validateWithExternalCheck.safeParseAsync(email);
if (!result.success) {
// 处理验证失败
}
return { props: {} };
};
性能优化策略
Schema复用与缓存
// schemas/index.ts - 集中管理所有schema
import { z } from 'zod';
// 基础schema
export const BaseSchema = z.object({
id: z.string(),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime()
});
// 扩展schema
export const UserSchema = BaseSchema.extend({
name: z.string(),
email: z.string().email()
});
export const PostSchema = BaseSchema.extend({
title: z.string(),
content: z.string(),
authorId: z.string()
});
// 复用验证逻辑
export const validateData = <T extends z.ZodTypeAny>(
schema: T,
data: unknown
): z.infer<T> => {
const result = schema.safeParse(data);
if (!result.success) {
throw new Error(`验证失败: ${result.error.message}`);
}
return result.data;
};
构建时验证配置
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
// 启用严格模式确保构建时类型检查
typescript: {
ignoreBuildErrors: false,
},
// 构建时环境变量验证
env: {
API_URL: process.env.API_URL,
// 添加环境变量验证
},
};
// 环境变量验证
const envSchema = z.object({
API_URL: z.string().url(),
DATABASE_URL: z.string().url(),
NODE_ENV: z.enum(['development', 'production', 'test'])
});
const env = envSchema.safeParse(process.env);
if (!env.success) {
console.error('环境变量配置错误:');
env.error.issues.forEach(issue => {
console.error(`- ${issue.path}: ${issue.message}`);
});
process.exit(1);
}
module.exports = nextConfig;
错误处理与监控
结构化错误日志
interface ValidationError {
timestamp: string;
environment: string;
schema: string;
issues: Array<{
path: string[];
message: string;
expected?: string;
received?: string;
}>;
rawData?: any;
}
export class ValidationLogger {
static logError(error: z.ZodError, schemaName: string, data?: any) {
const errorLog: ValidationError = {
timestamp: new Date().toISOString(),
environment: process.env.NODE_ENV || 'development',
schema: schemaName,
issues: error.issues.map(issue => ({
path: issue.path,
message: issue.message,
expected: issue.expected,
received: issue.received
})),
rawData: data
};
// 发送到日志服务
console.error(JSON.stringify(errorLog, null, 2));
// 开发环境下显示详细错误
if (process.env.NODE_ENV === 'development') {
console.error('原始数据:', data);
}
}
}
CI/CD集成验证
# .github/workflows/validate.yml
name: Data Validation
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm ci
- name: Run validation script
run: npm run validate-data
env:
API_URL: ${{ secrets.API_URL }}
NODE_ENV: test
# package.json
{
"scripts": {
"validate-data": "ts-node scripts/validate-data.ts",
"build": "npm run validate-data && next build"
}
}
实战:电商网站商品数据验证
// schemas/product.ts
import { z } from 'zod';
export const PriceSchema = z.object({
amount: z.number().positive(),
currency: z.enum(['CNY', 'USD', 'EUR']),
originalAmount: z.number().positive().optional(),
discount: z.number().min(0).max(100).optional()
});
export const VariantSchema = z.object({
id: z.string(),
sku: z.string(),
name: z.string(),
price: PriceSchema,
attributes: z.record(z.string(), z.any()),
stock: z.number().int().min(0),
images: z.array(z.string().url())
});
export const ProductSchema = z.object({
id: z.string(),
name: z.string().min(1).max(200),
description: z.string(),
category: z.string(),
brand: z.string(),
price: PriceSchema,
variants: z.array(VariantSchema).min(1),
specifications: z.record(z.string(), z.any()),
tags: z.array(z.string()),
seo: z.object({
title: z.string(),
description: z.string().max(160),
keywords: z.array(z.string())
}),
status: z.enum(['active', 'draft', 'archived'])
}).refine((product) => {
// 确保至少有一个变体有库存
return product.variants.some(variant => variant.stock > 0);
}, {
message: "产品必须至少有一个有库存的变体"
});
// 使用示例
export const validateProductImport = (jsonData: any) => {
try {
const products = z.array(ProductSchema).parse(jsonData);
// 额外的业务逻辑验证
const duplicateSKUs = new Set();
products.forEach(product => {
product.variants.forEach(variant => {
if (duplicateSKUs.has(variant.sku)) {
throw new Error(`重复的SKU: ${variant.sku}`);
}
duplicateSKUs.add(variant.sku);
});
});
return products;
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(`商品数据验证失败: ${error.message}`);
}
throw error;
}
};
总结与最佳实践
通过zod在SSG中的集成,我们实现了:
- 构建时安全:在编译阶段捕获数据错误,避免运行时问题
- 类型同步:自动生成TypeScript类型,保持前后端类型一致
- 开发体验:详细的错误信息和快速的反馈循环
- 维护性:集中化的验证逻辑,易于维护和扩展
关键最佳实践
| 实践要点 | 说明 | 示例 |
|---|---|---|
| Schema集中管理 | 所有schema统一存放,便于维护和复用 | schemas/目录 |
| 渐进式验证 | 从基础验证开始,逐步添加业务规则 | 先验证类型,再验证业务逻辑 |
| 错误处理 | 结构化错误日志,便于调试和监控 | ValidationLogger类 |
| 性能考虑 | 避免不必要的验证,复用schema实例 | 缓存常用schema |
| 测试覆盖 | 为重要schema编写单元测试 | Jest + zod测试 |
zod与SSG的结合为现代前端开发提供了坚实的数据验证基础,让开发者能够 confidently 构建可靠的静态站点应用。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



