Purikura的页面系统
为什么这么设计五层架构
架构不是为了炫技,而是为了解决一个简单的问题:如何让修改变得容易。
模型页面系统 (/m/[modelId]) 展示了这个理念。它通过五层分层架构,让页面生成变成了编辑 JSON 文件这么简单的事。不需要改代码,不需要重新部署组件,只需要修改数据。
这就是数据驱动开发的核心:把变化的部分抽象成数据,把不变的部分固化成代码。
五层架构的设计思路
第一层:类型定义层
类型定义层的职责是定义数据的形状。它不关心数据从哪来,也不关心数据怎么用,只关心数据应该长什么样。
// lib/ui-i18n-types.ts
export interface ShowcaseItem {
id: string;
title: string;
promptText: string;
mediaUrl: string;
mediaType: 'image' | 'video';
mediaAlt: string;
metadata: ShowcaseItemMetadata;
useCase?: string; // 可选字段
proTip?: string; // 可选字段
}
关键在于可选字段。useCase? 和 proTip? 的问号不是随便加的,它代表了架构的灵活性:有些模型需要这些字段,有些不需要。类型系统通过可选字段支持这种差异,而不是强制所有数据都一样。
你可能会想:为什么不定义多个接口,比如 BasicShowcaseItem 和 AdvancedShowcaseItem?
答案是:可选字段更灵活。如果用联合类型,你需要在每个使用的地方做类型判断。而可选字段只需要一个条件渲染:
{item.useCase && <div>{item.useCase}</div>}
这是架构设计的权衡:复杂的类型系统 vs 简单的条件渲染。我们选择后者。
第二层:数据层
数据层存储实际内容。它的形式是 JSON 文件,这不是偶然的选择。
{
"model_info": {
"id": "kie-sora2",
"name": "Sora 2",
"tagline": "OpenAI Sora - Text-to-Video AI Generator (2025)",
"vendor": "OpenAI",
"category": "text-to-video"
},
"example_showcase": {
"title": "Scroll down to see OpenAI Sora 2 video examples",
"items": [
{
"id": "sora2-pillow-fight-001",
"title": "Epic Pillow Fight Battle",
"promptText": "...",
"mediaUrl": "https://s.purikura.io/...",
"mediaType": "video"
}
]
}
}
JSON 的优势在于:非技术人员可以编辑。你不需要懂 React,不需要懂 TypeScript,只需要知道如何填写字段。这降低了维护成本,也降低了出错概率。
嵌套结构支持复杂的内容组织,但整体还是扁平的。model_info、example_showcase、faq 每个都是独立的模块,可以单独修改,也可以完全删除。
第三层:组件层
组件层负责把数据渲染成 UI。它的设计原则是:永远不假设数据一定存在。
// components/example-showcase/index.tsx
{item.useCase && (
<div className="mb-4">
<h5 className="text-rose-300 font-medium text-sm mb-1">Use Case</h5>
<p className="text-slate-300 text-sm">{item.useCase}</p>
</div>
)}
{item.proTip && (
<div className="bg-pink-500/10 border border-pink-500/30 rounded-lg p-3">
<h5 className="text-pink-300 font-medium text-sm mb-1">💡 Pro Tip</h5>
<p className="text-slate-300 text-sm">{item.proTip}</p>
</div>
)}
条件渲染是这一层的核心。item.useCase && 不仅仅是一个语法糖,它代表了一种设计哲学:组件应该自动适配数据。如果数据存在就渲染,如果不存在就跳过。
这种设计让组件具有通用性。同一个组件可以处理不同结构的数据,只要数据符合类型定义。
对于 SEO 重要但视觉不需要的信息,使用 sr-only 类:
{metadata && (
<div className="sr-only" aria-hidden="true">
Model: {metadata.model}, Resolution: {metadata.resolution},
Generation Time: {metadata.generationTime}
</div>
)}
这样搜索引擎能读到元数据,但用户界面保持简洁。这是一种折衷:保留 SEO 价值但不影响视觉体验。
组件层还实现了渐进增强。服务端渲染静态内容用于 SEO,客户端渲染动态交互用于体验。两者互不干扰,也互相补充。
第四层:加载器层
加载器层的任务是把 JSON 文件读取进来,并确保它符合类型定义。
// lib/model-content.ts
export async function getModelContent(modelId: string) {
try {
const content = await import(`@/content/models/${modelId}.json`);
validateModelContent(content);
return content;
} catch (error) {
console.error(`Failed to load model content: ${modelId}`, error);
return null;
}
}
这一层看起来简单,但它是类型安全的关键。JSON 文件本身是无类型的,加载器通过运行时验证把它转换成有类型的数据。
错误处理也在这一层。如果文件不存在,如果格式错误,都在这里优雅地处理,而不是让错误传播到上层。
第五层:页面层
页面层是整个架构的入口。它从 URL 中提取参数,调用加载器获取数据,然后把数据传递给组件。
// app/m/[modelId]/page.tsx
export default async function ModelPage({ params }: { params: { modelId: string } }) {
const content = await getModelContent(params.modelId);
if (!content) {
notFound();
}
return (
<>
<ModelHero content={content.model_info} />
{content.example_showcase && (
<ExampleShowcase content={content.example_showcase} locale="en" />
)}
{content.faq && (
<FAQSection content={content.faq} />
)}
</>
);
}
这一层的代码几乎不需要修改。新增模型?创建一个 JSON 文件。新增字段?组件会自动处理。这就是零代码修改的含义。
动态路由生成更进一步:
export async function generateStaticParams() {
const modelIds = await getAvailableModelIds();
return modelIds.map((modelId) => ({ modelId }));
}
系统会自动扫描 content/models/ 目录,为每个 JSON 文件生成一个静态页面。你不需要手动维护路由列表。
架构验证
理论上的设计需要实践验证。这个架构经过三个测试场景:
场景 1:添加新模型
创建 content/models/runway-gen3.json 文件。结果:页面自动生成,无需修改代码。
这验证了数据驱动的核心:变化被隔离在数据层,代码层完全不受影响。
场景 2:不同数据结构
有些模型有 useCase 字段,有些没有。结果:组件自动适配,不会显示空白或报错。
这验证了条件渲染的有效性:组件能够处理可选字段。
场景 3:不同媒体类型
混合使用 image 和 video。结果:组件根据 mediaType 自动选择正确的渲染方式。
这验证了类型驱动的设计:通过联合类型 'image' | 'video',让组件知道如何处理不同的媒体。
设计原则总结
这个架构体现了几个设计原则:
关注点分离:数据、类型、组件、加载、页面各自独立。修改一个不影响其他。
类型安全:从 TypeScript 接口到运行时验证,确保数据一致性。
条件渲染:永远不假设数据存在,让组件自动适配。
渐进增强:先保证 SEO,再增强交互。两者独立但互补。
数据驱动:把变化的部分(内容)和不变的部分(逻辑)分开。
这些原则不是为了追求完美,而是为了让修改变得容易。当你需要添加新模型时,你只需要创建一个 JSON 文件。这就是架构的价值。
五层架构不是免费的。它增加了初始开发的复杂度,也增加了学习成本。
如果你只需要创建一个页面,这个架构是过度设计。但如果你需要创建十个、二十个相似的页面,这个架构会节省大量时间。
架构设计是权衡:前期投入 vs 长期收益。这个架构选择了后者。
技术栈
这个架构基于现代前端技术栈:
- Next.js 14:App Router 和 Server Components 支持服务端渲染
- TypeScript:类型安全和接口定义
- Framer Motion:滚动驱动的动画效果
- Tailwind CSS:原子化的样式系统
- React:组件化和条件渲染
每个技术都有明确的作用。TypeScript 提供类型安全,Next.js 提供路由和渲染,Tailwind 提供样式,Framer Motion 提供动画。它们共同支撑了这个数据驱动的架构。
Related Posts
Articles you might also find interesting
离屏渲染:照片捕获为什么需要独立的 canvas
实时流与静态合成的本质冲突,决定了系统必须分离。理解这种分离,就理解了架构设计中最重要的原则。
集中式配置:让 Reddit 组件脱离重复泥潭
当同一份数据散落在多个文件中,维护成本呈指数级增长。集中式配置不是技术选择,而是对抗熵增的必然手段。
双重导出管道的架构选择
在用户生成内容场景中,速度与质量的权衡决定了导出架构。理解两种不同管道的设计逻辑,能够更准确地把握产品体验的边界。
继承基础配置
配置不需要重复书写。继承机制让每个层次只表达自己的差异。
重复数据的迁移实践:从 N 个文件到 1 个真相源
当同一份 Reddit posts 配置散落在多个文件中,维护成本以文件数量指数增长。迁移到集中式配置不是技术选择,而是对复杂度的清算。
让错误浮现
Next.js 构建悬挂问题的根源不在工具,而在掩盖。严格类型检查不是负担,而是质量的守护者。
Studio 前端架构:从画布到组件的设计思考
深入 Purikura Studio 前端架构设计,探讨 DOM-based 画布、状态管理和组件化的实践经验
Context 驱动的认证状态管理
认证系统的核心不在登录按钮,而在状态同步。如何让整个应用感知用户状态变化,决定了用户体验的流畅度。
定价界面优化的三层方法
数据诚实、决策引导、视觉精调——三层递进的优化方法。从移除虚假功能到帮助用户选择,再到像素级修复,每一步都在解决真实问题
约束驱动设计:为何选择内存追踪
在 Cloudflare Workers 环境中实现追踪系统,持久化和内存存储之间的权衡不是技术偏好,而是约束驱动的必然选择。
依赖注入
依赖注入不是关于框架或工具,而是关于控制权的转移。理解这个转移,就理解了软件设计的核心原则。
让文档跟着代码走
文档过时是熵增的必然。对抗衰败的方法不是更频繁的手工维护,而是让文档"活"起来——跟随代码自动更新。三种文档形态,三种生命周期。
动态元数据生成:让机器读懂你的页面
generateMetadata 不只是填写表单。它决定了搜索引擎、社交平台、AI 系统如何理解和呈现你的内容。
tsc --noEmit:即时类型反馈
类型错误不应该等到构建时才发现。最快的反馈来自最简单的命令。
Google Fonts 官方集成
Next.js 提供了 next/font 模块,让字体加载变得简单且性能优化。Google Fonts 是最直接的商用免费字体选择。
在运行的系统上生长新功能
扩展不是推倒重来,而是理解边界,找到生长点。管理层作为观察者和调节器,附着在核心系统上,监测它,影响它,但不改变它的运行逻辑。
全局安装 TypeScript
当 tsc 命令未找到时,你缺少的不是命令,而是编译器本身。
分层修复
生产问题没有银弹。P0 止血,P1 加固,P2 优化。优先级不是排序,而是在不确定性下的决策框架。
多厂商 AI 调度:统一混乱的供应商生态
当你依赖第三方 AI 服务时,单点故障是最大的风险。多厂商调度不只是技术架构,更是对不确定性的应对策略。
next-intl localePrefix:默认语言不显示前缀
理解 next-intl 中 localePrefix 配置的设计哲学,以及为什么默认语言不应在 URL 中显现。
next-intl 的服务端与客户端协同机制
理解 next-intl 如何在 Next.js App Router 中协调服务端渲染和客户端交互,以及为什么需要显式设置 locale。
减少 Next.js 启动时的工作量
开发服务器启动缓慢不是偶然。它在做的事太多了。
Props Drilling
数据在组件树中层层传递,每一层都不需要它,却必须经手。这不是技术债,是架构对真实需求的误解。
分布式 Workers 的解耦设计
通过微服务架构和队列系统,实现高可用的 AI 任务处理。从单体到分布式,每个设计决策都是对复杂度的权衡。
React Context Provider
Context 不是状态管理。它是数据传递的通道。这个区别决定了你应该如何使用它。