zod与SSG:静态站点生成验证

zod与SSG:静态站点生成验证

【免费下载链接】zod TypeScript-first schema validation with static type inference 【免费下载链接】zod 项目地址: https://gitcode.com/GitHub_Trending/zo/zod

还在为静态站点生成(SSG)中的数据验证头疼吗?每次构建时都担心外部API数据格式变化导致构建失败?zod作为TypeScript优先的schema验证库,为SSG场景提供了完美的解决方案。

读完本文,你将获得:

  • ✅ SSG场景下数据验证的核心痛点解析
  • ✅ zod在构建时验证的最佳实践方案
  • ✅ 完整的Next.js、Gatsby、Nuxt集成示例
  • ✅ 性能优化与错误处理策略
  • ✅ 类型安全的全栈开发体验

为什么SSG需要数据验证?

静态站点生成(Static Site Generation)在现代前端开发中越来越流行,但数据源的不可控性带来了巨大挑战:

mermaid

传统解决方案往往在运行时才发现数据问题,而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中的集成,我们实现了:

  1. 构建时安全:在编译阶段捕获数据错误,避免运行时问题
  2. 类型同步:自动生成TypeScript类型,保持前后端类型一致
  3. 开发体验:详细的错误信息和快速的反馈循环
  4. 维护性:集中化的验证逻辑,易于维护和扩展

关键最佳实践

实践要点说明示例
Schema集中管理所有schema统一存放,便于维护和复用schemas/目录
渐进式验证从基础验证开始,逐步添加业务规则先验证类型,再验证业务逻辑
错误处理结构化错误日志,便于调试和监控ValidationLogger类
性能考虑避免不必要的验证,复用schema实例缓存常用schema
测试覆盖为重要schema编写单元测试Jest + zod测试

zod与SSG的结合为现代前端开发提供了坚实的数据验证基础,让开发者能够 confidently 构建可靠的静态站点应用。

【免费下载链接】zod TypeScript-first schema validation with static type inference 【免费下载链接】zod 项目地址: https://gitcode.com/GitHub_Trending/zo/zod

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值