Product Ready 标准
ProductReady 的产品就绪质量标准和最佳实践 - 第一次就做对
Product Ready 标准
ProductReady 按照 product-ready 标准构建。本指南解释了样板代码中融入的质量实践以及如何在构建过程中保持它们。
为什么这很重要:遵循这些标准意味着更少的错误、更容易维护、更好的性能和更快乐的用户。交付质量,快速交付。
📋 产品定义标准
"Product Ready" 在你编写代码之前就开始了。 定义良好的产品可以防止浪费工程工作。
1. 产品需求 (PRD)
不要只是"开始编码"。定义你正在构建什么以及为什么。
- 问题陈述:你正在解决什么痛点?
- 用户故事:"作为[用户],我想[行动],以便[好处]"
- 成功指标:你如何知道它是否有效?
2. 理想客户画像 (ICP)
知道你为谁构建。
- 人口统计:角色、行业、公司规模
- 心理特征:目标、恐惧、动机
- 购买触发因素:什么让他们寻找解决方案?
3. 营销策略
构建时考虑到分发。
- 价值主张:一句话解释你的产品
- 渠道:你的用户在哪里?
- 发布计划:你将如何获得前 10 个用户?
✅ Product Ready 检查清单
在发布之前,请使用此综合清单验证您的应用程序是否真正"Product Ready"。这反映了 monorepo 中 App Status 表格的质量标准。
图例:✅ 100% 评分必需 | ⭐ 强烈推荐 | 📝 可选但有价值
1. 产品与设计 ("灵魂")
| 要求 | 验证方法 | 描述 |
|---|---|---|
| 产品规格 | 文件检查: apps/XXX/spec/*.md | 必需: product-design, icp-guide, marketing-guide, onboarding-story, design-system, vi |
| 视觉识别 | 视觉检查 | 品牌颜色和排版在所有页面保持一致 |
| 设计系统 | 代码检查 | 无硬编码颜色/间距;使用 global.css 中的 CSS 变量 |
| 主题同步 | 视觉检查 | 亮色/暗色模式切换完美无故障 |
| 品牌资产 | URL 检查 | /logo.svg 和 /brand/ 资产可访问 |
2. 用户体验 ("身体")
| 要求 | 验证方法 | 描述 |
|---|---|---|
| 落地页 | 视觉检查 | 首页在 5 秒内清晰阐述价值主张 |
| 认证体验 | 交互检查 | 注册/登录流程顺畅,有错误处理 |
| 演示模式 | URL 检查 | 通过 ?demo=1 即时访问应用,无需登录 |
| 仪表板 | 视觉检查 | 登录后加载带有侧边栏/导航的受保护布局 |
| 移动端响应 | 视觉检查 | 布局在移动端 (375px 宽度) 适配正确 |
3. 资产与元数据 ("面孔")
| 要求 | 验证方法 | 描述 |
|---|---|---|
| Favicon | URL 检查 | /favicon.ico 加载正确 |
| 应用图标 | URL 检查 | /icon.svg (或 png) 加载正确 |
| Open Graph | URL 检查 | /opengraph-image 生成正确的社交预览 |
| 截图 | 文件检查 | public/screenshots/ 包含高清营销图片 |
| Manifest | URL 检查 | /manifest.webmanifest 返回有效的 JSON |
4. 信任与法律 ("西装")
| 要求 | 验证方法 | 描述 |
|---|---|---|
| 关于页面 | URL 检查 | /about App Store 风格的产品展示页 |
| 品牌指南 | URL 检查 | /brand 品牌指南,包含 logo、颜色、字体规范 |
| 隐私政策 | URL 检查 | /privacy 页面存在且可访问 |
| 服务条款 | URL 检查 | /terms 页面存在且可访问 |
| 联系/支持 | 视觉检查 | 页脚或帮助中心提供联系方式 |
5. 工程与运维 ("大脑")
| 要求 | 验证方法 | 描述 |
|---|---|---|
| 类型安全 | 构建检查 | pnpm typecheck 通过且无错误 |
| SEO 站点地图 | URL 检查 | /sitemap.xml 返回有效的 XML |
| SEO Robots | URL 检查 | /robots.txt 存在并正确允许/禁止爬虫 |
| AI 爬虫 | URL 检查 | /llms.txt 针对 LLM 的爬虫指令 |
| 健康检查 | URL 检查 | /api/health 返回 JSON 状态 |
6. 内容与国际化 ("声音")
| 要求 | 验证方法 | 描述 |
|---|---|---|
| 文档 | URL 检查 | /docs 或 /help 正确渲染内容 |
| 本地化 | URL 检查 | /en, /zh 等前缀工作正常 (如果支持多语言) |
| 翻译 | 视觉检查 | 切换语言时 UI 文本正确变更 |
7. 能力与集成 ("肌肉")
| 能力 | 验证方法 | 描述 |
|---|---|---|
| 支付能力 | 交互检查 | 可以启动结账流程 (即使在测试模式下) |
| 开放 API | URL 检查 | /api/openapi 或 /api/docs 可访问 |
| MCP 网关 | URL 检查 | /sse/mcp 端点连接成功 |
| 管理后台 | URL 检查 | /admin 对授权用户可访问 |
| 邮件投递 | 交互检查 | 事务性邮件 (欢迎、魔术链接) 到达收件箱 |
8. 质量保证 ("盾牌")
| 要求 | 验证方法 | 描述 |
|---|---|---|
| E2E 测试 | 命令检查 | pnpm test:e2e 通过关键流程 |
| 单元测试 | 命令检查 | pnpm test 通过业务逻辑测试 |
| 代码检查 | 命令检查 | pnpm lint 通过且无警告 |
🎨 设计质量标准
设计系统架构
ProductReady 遵循严格的设计层次结构以确保一致性:
- 品牌识别:你的颜色、排版和声音 ("灵魂")
- 设计令牌:值的语义名称(例如
primary-color,spacing-md) - CSS 变量:
global.css中的技术实现 - 组件:使用变量的 UI 元素
🚨 组件库完整性 (KUI)
切勿直接修改 packages/kui/*。
packages/kui 是共享的基础库。修改它可能会导致多个应用程序崩溃。
正确的定制方式:
- 导入组件:
import { Button } from 'kui/button'; - 在应用中创建包装器:
apps/XXX/src/components/ui/my-button.tsx - 通过 CSS 变量或 className 属性应用自定义样式。
CSS 变量(不要硬编码!)
始终使用 CSS 变量表示颜色、间距等:
// ✅ 好:使用 CSS 变量
<div className="bg-primary text-primary-foreground">
<button className="bg-accent hover:bg-accent/90">
// ❌ 差:硬编码颜色
<div className="bg-blue-500 text-white">
<button className="bg-green-600 hover:bg-green-700">深色模式支持
所有 UI 必须在深色模式下工作:
/* global.css */
:root {
--background: 0 0% 100%; /* 白色 */
--foreground: 222 47% 11%; /* 深色文本 */
}
.dark {
--background: 224 71% 4%; /* 深色背景 */
--foreground: 213 31% 91%; /* 浅色文本 */
}测试深色模式:在浏览器中切换或使用 ?theme=dark
响应式设计
移动优先方法:
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
{/* 移动端 1 列,平板 2 列,桌面 3 列 */}
</div>测试平台:
- iPhone (375px 宽度)
- iPad (768px)
- 桌面 (1280px+)
📊 可访问性标准
WCAG 2.1 AA 合规
最低要求:
✅ 颜色对比度:
- 普通文本:4.5:1
- 大文本 (18px+):3:1
- 使用 WebAIM 对比度检查器测试
✅ 键盘导航:
// 所有交互元素必须可通过键盘访问
<button onClick={handleClick}>点击我</button> ✅
<div onClick={handleClick}>点击我</div> ❌✅ 焦点指示器:
button:focus-visible {
outline: 2px solid hsl(var(--primary));
outline-offset: 2px;
}✅ 图片替代文本:
<img src="/logo.png" alt="ProductReady Logo" />✅ 语义 HTML:
✅ <nav>, <main>, <article>, <button>
❌ <div className="nav">, <div onClick={...}>🔗 演示模式标准
ProductReady 包括演示模式 (?demo=1) 用于无需数据库的测试:
// src/lib/demo.ts
export function isDemoMode(searchParams?: URLSearchParams): boolean {
return searchParams?.get('demo') === '1';
}
// 在 tRPC 路由器中
export const tasksRouter = createTRPCRouter({
list: publicProcedure
.input(z.object({ demo: z.boolean().optional() }))
.query(async ({ ctx, input }) => {
if (input.demo) {
return { tasks: getDemoTasks() }; // 模拟数据
}
return { tasks: await ctx.db.select().from(tasks) };
}),
});好处:
- ✅ 即时测试(无需数据库设置)
- ✅ 演示和展示
- ✅ 无需后端的 UI 开发
🎯 Lighthouse 分数 (目标)
在 Chrome DevTools 中运行 Lighthouse 审计:
最低分数:
- ⚡ 性能: 90+
- ♿ 可访问性: 95+
- ✅ 最佳实践: 95+
- 🔍 SEO: 95+
常见问题和修复:
| 问题 | 修复 |
|---|---|
| 大图片 | 使用 Next.js <Image> 进行优化 |
| 未使用的 JavaScript | 使用动态导入进行代码拆分 |
| 缺少替代文本 | 添加到所有 <img> 标签 |
| 缺少元标签 | 在 layout.tsx 元数据中添加 |
📝 文档标准
代码注释
为函数和组件使用 TSDoc:
/**
* 创建带有 AI 生成描述的新任务
* @param input - 任务标题和优先级
* @returns 带有生成描述的创建任务
* @throws {Error} 如果 AI 服务不可用
*/
export async function createTaskWithAI(
input: CreateTaskInput
): Promise<Task> {
const description = await generateDescription(input.title);
return db.insert(tasks).values({ ...input, description });
}README 更新
保持 README 最新:
- ✅ 添加主要功能时更新
- ✅ 保持设置说明准确
- ✅ 记录新的环境变量
- ✅ 添加到新文档的链接
🔄 维护标准
依赖更新
每月:
# 检查更新
pnpm outdated
# 交互式更新
pnpm update -i
# 更新后测试
pnpm typecheck
pnpm lint
pnpm test
pnpm build安全审计
每周:
# 检查漏洞
pnpm audit
# 自动修复
pnpm audit --fix变更日志纪律
为以下创建变更日志:
- ✅ 新功能
- ✅ 破坏性更改
- ✅ 错误修复(重要)
- ✅ 性能改进
- ✅ 安全更新
格式:apps/productready/changelog/YYYYMMDD-description.md
🎯 核心质量原则
1. 处处类型安全
TypeScript 严格模式在代码投入生产之前捕获错误。
// ✅ 好:完全类型安全
interface CreateTaskInput {
title: string;
description?: string;
priority: 'low' | 'medium' | 'high' | 'urgent';
}
function createTask(input: CreateTaskInput): Promise<Task> {
// TypeScript 确切知道 input 包含什么
return db.insert(tasks).values(input).returning();
}
// ❌ 差:使用 `any` 失去所有安全性
function createTask(input: any) {
// 不知道 input 包含什么 - 运行时错误!
return db.insert(tasks).values(input);
}好处:
- ✅ 编辑器中的自动完成
- ✅ 捕获拼写错误和缺失属性
- ✅ 重构安全且简单
- ✅ 自我文档化的代码
2. 使用 Zod 进行运行时验证
TypeScript 只在编译时检查。Zod 在运行时验证(当数据来自用户、API 或数据库时)。
import { z } from 'zod';
// 定义架构
const CreateTaskSchema = z.object({
title: z.string().min(1).max(255),
description: z.string().optional(),
priority: z.enum(['low', 'medium', 'high', 'urgent']).default('medium'),
});
// 在 tRPC 中使用
export const tasksRouter = createTRPCRouter({
create: protectedProcedure
.input(CreateTaskSchema) // ✅ 验证输入
.mutation(async ({ input }) => {
// 这里的 input 保证是有效的
return db.insert(tasks).values(input);
}),
});Zod 捕获的内容:
- 无效的数据类型(字符串 vs 数字)
- 缺少必需字段
- 超出允许范围的值
- 格式错误的电子邮件、URL 等
3. VO/DTO/PO 模式
在不同层的数据中分离关注点:
| 层 | 类型 | 目的 | 示例 |
|---|---|---|---|
| 数据库 (PO) | 持久对象 | 原始数据库架构 | 包括密码哈希、内部 ID |
| API (DTO) | 数据传输对象 | 输入/输出验证 | 用户提供邮箱、密码 |
| UI (VO) | 视图对象 | 前端显示 | 显示用户名、邮箱、头像 |
// 数据库层 (PO) - 永远不要直接暴露!
const userPO = await db.query.users.findFirst({
where: eq(users.id, userId)
});
// 包含: passwordHash, stripeCustomerId 等
// API 层 (DTO) - 验证输入
const CreateUserDTO = z.object({
email: z.string().email(),
password: z.string().min(8),
name: z.string(),
});
// UI 层 (VO) - 可以安全地返回到前端
interface UserVO {
id: string;
email: string;
name: string;
avatar: string | null;
createdAt: string; // ISO 字符串,不是 Date 对象
}
// 返回之前将 PO → VO 转换
function toUserVO(po: UserPO): UserVO {
return {
id: po.id,
email: po.email,
name: po.name,
avatar: po.image,
createdAt: po.createdAt.toISOString(),
};
}安全性:永远不要将数据库对象直接返回到前端。始终通过 VO 转换。
🔒 安全标准
认证和授权
ProductReady 使用具有安全默认设置的 Better Auth:
✅ 已配置的内容:
- 密码哈希 (bcrypt)
- 会话管理 (httpOnly cookies)
- CSRF 保护
- 安全 cookie 标志(仅生产环境)
你的责任:
// ✅ 始终对敏感操作使用 protectedProcedure
export const tasksRouter = createTRPCRouter({
delete: protectedProcedure // 需要身份验证
.input(z.object({ id: z.number() }))
.mutation(async ({ ctx, input }) => {
// ctx.session.user 保证存在
const userId = ctx.session.user.id;
// ✅ 删除前检查所有权
const task = await db.query.tasks.findFirst({
where: eq(tasks.id, input.id),
});
if (!task || task.userId !== userId) {
throw new Error('未授权');
}
await db.delete(tasks).where(eq(tasks.id, input.id));
}),
});环境变量
永远不要提交秘密!
# ✅ 好:使用 .env 文件(git 忽略)
PG_DATABASE_URL="postgresql://..."
BETTER_AUTH_SECRET="..."
# ❌ 差:代码中硬编码
const secret = "super-secret-key-123"; // 永远不要这样做!公共变量的前缀:
# ✅ 在浏览器中可用
NEXT_PUBLIC_APP_NAME="ProductReady"
# ✅ 仅服务器(安全)
PG_DATABASE_URL="postgresql://..."
STRIPE_SECRET_KEY="sk_live_..."📊 性能标准
数据库查询
避免 N+1 查询 - 使用连接而不是循环:
// ❌ 差:N+1 查询 (1 + N 个查询)
const tasks = await db.select().from(tasks);
for (const task of tasks) {
const user = await db.select().from(users)
.where(eq(users.id, task.userId)); // N 个查询!
}
// ✅ 好:使用连接的单个查询
const tasksWithUsers = await db
.select({
task: tasks,
user: users,
})
.from(tasks)
.leftJoin(users, eq(tasks.userId, users.id));分页
始终分页大列表:
export const tasksRouter = createTRPCRouter({
list: protectedProcedure
.input(z.object({
page: z.number().min(1).default(1),
limit: z.number().min(1).max(100).default(20),
}))
.query(async ({ ctx, input }) => {
const offset = (input.page - 1) * input.limit;
const tasks = await ctx.db
.select()
.from(tasks)
.limit(input.limit)
.offset(offset);
return { tasks, page: input.page, limit: input.limit };
}),
});连接池
对 Vercel/无服务器至关重要!
使用池化数据库连接:
# Neon - 使用端口 6543(池化)而不是 5432
PG_DATABASE_URL="postgresql://user:pass@host:6543/db"
# Supabase - 使用"连接池"字符串
PG_DATABASE_URL="postgresql://...pooler.supabase.com:6543/..."🧪 测试标准
E2E 测试 (Playwright)
ProductReady 包括冒烟测试。为关键流程添加更多:
// tests/e2e/auth.spec.ts
import { test, expect } from '@playwright/test';
test.describe('身份验证', () => {
test('用户可以注册', async ({ page }) => {
await page.goto('/signup');
await page.fill('input[name="email"]', 'test@example.com');
await page.fill('input[name="password"]', 'password123');
await page.fill('input[name="name"]', '测试用户');
await page.click('button[type="submit"]');
// 应该重定向到仪表板
await expect(page).toHaveURL('/dashboard');
});
});运行测试:
pnpm test:e2e # 无头模式
pnpm test:e2e:ui # 交互模式单元测试 (Vitest)
测试业务逻辑和实用程序:
// src/lib/utils.test.ts
import { describe, it, expect } from 'vitest';
import { formatCurrency } from './utils';
describe('formatCurrency', () => {
it('正确格式化 USD', () => {
expect(formatCurrency(1000, 'USD')).toBe('$1,000.00');
});
it('处理零', () => {
expect(formatCurrency(0, 'USD')).toBe('$0.00');
});
});📐 代码质量标准
代码检查和格式化
ProductReady 使用 Biome(比 ESLint + Prettier 更快):
# 格式化和检查
pnpm lint
# 自动修复问题
pnpm lint --write预提交钩子:使用 Lefthook 配置(每次提交时运行)
TypeScript 严格性
所有应用必须启用严格模式:
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true
}
}组件模式
保持组件小而专注:
// ✅ 好:单一职责
function TaskCard({ task }: { task: Task }) {
return (
<div className="card">
<h3>{task.title}</h3>
<p>{task.description}</p>
</div>
);
}
function TaskList({ tasks }: { tasks: Task[] }) {
return (
<div className="grid gap-4">
{tasks.map(task => <TaskCard key={task.id} task={task} />)}
</div>
);
}
// ❌ 差:太多职责
function TaskPage() {
// 在一个组件中获取、过滤、排序、渲染
// 500 行代码...
}🚀 生产标准
健康检查端点
ProductReady 包括用于监控的 /api/health:
// src/app/api/health/route.ts
export async function GET() {
return Response.json({
status: 'healthy',
timestamp: new Date().toISOString(),
service: 'productready',
version: process.env.VERSION || 'unknown',
buildNumber: process.env.NEXT_PUBLIC_BUILD_NUMBER || '0',
});
}用于:
- 正常运行时间监控(UptimeRobot、Better Uptime)
- 负载均衡器健康检查
- 部署验证
错误跟踪
推荐:Sentry 或类似工具
// src/app/layout.tsx
import * as Sentry from '@sentry/nextjs';
if (process.env.NODE_ENV === 'production') {
Sentry.init({
dsn: process.env.SENTRY_DSN,
tracesSampleRate: 0.1,
});
}