name: 前端模式 description: 用于 Next.js App Router、Clerk 认证、shadcn/Radix UI 和 PostHog 分析的前端模式。在构建 UI 组件、创建页面、实现认证流程或添加分析事件时使用。确保一致的 UX 模式和可访问性标准。
前端模式技能
目的
使用 Next.js App Router、Clerk 认证、shadcn/ui 组件和 PostHog 分析的既定模式,确保一致的前端开发。
何时应用此技能
应用此技能当:
- 构建新的 UI 组件或页面
- 实现认证流程
- 添加带验证的表单
- 集成 PostHog 分析事件
- 创建受保护的/认证的路由
- 使用 shadcn/ui 或 Radix 组件
Next.js App Router 模式
服务器组件与客户端组件
// 服务器组件(默认) - 用于:
// - 数据获取
// - 认证检查
// - SEO 关键内容
// app/dashboard/page.tsx
import { auth } from "@clerk/nextjs/server";
export default async function DashboardPage() {
const { userId } = await auth();
// 服务器端获取数据...
}
// 客户端组件 - 用于:
// - 交互性(onClick、onChange)
// - 浏览器 API(localStorage、window)
// - 钩子(useState、useEffect)
// app/dashboard/_components/interactive-widget.tsx
("use client");
import { useState } from "react";
export function InteractiveWidget() {
const [count, setCount] = useState(0);
// 交互逻辑...
}
受保护页面
关键:始终在认证页面上使用 export const dynamic = 'force-dynamic':
// app/dashboard/[page]/page.tsx
import { auth } from "@clerk/nextjs/server";
import { redirect } from "next/navigation";
// 必需 - 构建时认证上下文不可用
export const dynamic = "force-dynamic";
export default async function ProtectedPage() {
const { userId } = await auth();
if (!userId) {
redirect("/sign-in");
}
// 渲染受保护内容...
}
路由组织
app/
├── (auth)/ # 认证路由(登录、注册)
│ ├── sign-in/[[...sign-in]]/page.tsx
│ └── sign-up/[[...sign-up]]/page.tsx
├── (marketing)/ # 公开营销页面
│ ├── page.tsx # 主页
│ └── pricing/page.tsx
├── dashboard/ # 受保护的用户区域
│ ├── page.tsx
│ └── _components/ # 页面特定组件
└── admin/ # 仅管理员区域
└── page.tsx
Clerk 认证模式
认证启用模式
// 服务器组件认证检查
import { auth } from '@clerk/nextjs/server';
export default async function Page() {
const { userId } = await auth();
// userId 是 string | null
}
// 客户端组件认证
"use client"
import { useUser, useAuth } from '@clerk/nextjs';
export function UserProfile() {
const { user, isLoaded, isSignedIn } = useUser();
const { signOut } = useAuth();
if (!isLoaded) return <Skeleton />;
if (!isSignedIn) return <SignInPrompt />;
return <div>欢迎, {user.firstName}!</div>;
}
认证禁用模式(功能切换)
当通过功能标志禁用认证时,提供优雅回退:
// 检查功能标志
import { FEATURES } from '@/config/features';
export function AuthWrapper({ children }) {
if (!FEATURES.AUTH_ENABLED) {
// 显示演示/访客体验
return <GuestExperience>{children}</GuestExperience>;
}
return <AuthenticatedWrapper>{children}</AuthenticatedWrapper>;
}
管理员验证
// app/admin/page.tsx
import { auth } from "@clerk/nextjs/server";
import { redirect } from "next/navigation";
export const dynamic = "force-dynamic";
export default async function AdminPage() {
const { userId, orgId, orgRole } = await auth();
if (!userId) {
redirect("/sign-in");
}
// 验证管理员角色
const ADMIN_ORG_ID = process.env.CLERK_ADMIN_ORG_ID;
const ADMIN_ROLE = "org:admin";
if (orgId !== ADMIN_ORG_ID || orgRole !== ADMIN_ROLE) {
redirect("/admin-denied");
}
// 渲染管理员内容...
}
shadcn/ui 组件模式
导入约定
// 始终使用 @/components/ui 路径别名
import { Button } from "@/components/ui/button";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
表单模式(React Hook Form + Zod)
"use client"
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { Button } from '@/components/ui/button';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
import { Input } from '@/components/ui/input';
const FormSchema = z.object({
email: z.string().email('无效邮箱'),
name: z.string().min(1, '名称必填'),
});
type FormData = z.infer<typeof FormSchema>;
export function MyForm() {
const form = useForm<FormData>({
resolver: zodResolver(FormSchema),
defaultValues: { email: '', name: '' },
});
async function onSubmit(data: FormData) {
// 处理提交...
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>名称</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">提交</Button>
</form>
</Form>
);
}
按钮变体
// 主要操作
<Button>保存更改</Button>
// 次要操作
<Button variant="secondary">取消</Button>
// 破坏性操作
<Button variant="destructive">删除</Button>
// 幽灵/微妙
<Button variant="ghost">了解更多</Button>
// 链接样式
<Button variant="link" asChild>
<Link href="/docs">文档</Link>
</Button>
// 加载状态
<Button disabled={isLoading}>
{isLoading ? '保存中...' : '保存'}
</Button>
PostHog 分析模式
事件命名约定
使用带类别前缀的下划线命名:
// 用户操作
"user_signed_up";
"user_signed_in";
"user_profile_updated";
// 功能使用
"feature_dark_mode_toggled";
"feature_export_clicked";
// 支付
"payment_checkout_started";
"payment_completed";
"subscription_upgraded";
// 内容
"content_video_watched";
"content_pdf_downloaded";
// 导航
"page_viewed";
"cta_clicked";
事件跟踪
"use client"
import { usePostHog } from 'posthog-js/react';
export function TrackableButton() {
const posthog = usePostHog();
function handleClick() {
posthog?.capture('cta_clicked', {
button_text: '立即开始',
page: '/pricing',
variant: 'primary',
});
}
return <Button onClick={handleClick}>立即开始</Button>;
}
页面浏览跟踪
// 通过 PostHogProvider 自动跟踪(已配置)
// 针对 SPA 的手动跟踪:
"use client";
import { usePathname } from "next/navigation";
import { usePostHog } from "posthog-js/react";
import { useEffect } from "react";
export function PageViewTracker() {
const pathname = usePathname();
const posthog = usePostHog();
useEffect(() => {
if (pathname && posthog) {
posthog.capture("$pageview", { path: pathname });
}
}, [pathname, posthog]);
return null;
}
功能标志
"use client"
import { useFeatureFlagEnabled } from 'posthog-js/react';
export function FeatureFlaggedComponent() {
const showNewFeature = useFeatureFlagEnabled('new-checkout-flow');
if (showNewFeature) {
return <NewCheckoutFlow />;
}
return <LegacyCheckoutFlow />;
}
可访问性检查表
所有组件必需
- [ ] 键盘导航:所有交互元素可通过 Tab 键聚焦
- [ ] 聚焦指示器:可见的聚焦环(Tailwind:
focus:ring-2) - [ ] 颜色对比度:文本至少 4.5:1
- [ ] 替代文本:所有图像都有描述性替代文本
- [ ] ARIA 标签:表单输入有标签或 aria-label
- [ ] 错误状态:表单错误通知屏幕阅读器
模式
// 可访问按钮
<Button aria-label="关闭对话框">
<X className="h-4 w-4" />
</Button>
// 可访问表单字段
<FormItem>
<FormLabel htmlFor="email">邮箱</FormLabel>
<FormControl>
<Input id="email" type="email" aria-describedby="email-error" />
</FormControl>
<FormMessage id="email-error" />
</FormItem>
// 键盘用户的跳过链接
<a href="#main-content" className="sr-only focus:not-sr-only">
跳转到主要内容
</a>
响应式设计模式
Tailwind 断点
// 移动优先方法
<div className="
px-4 // 移动端:16px 内边距
md:px-6 // 平板:24px 内边距
lg:px-8 // 桌面:32px 内边距
">
// 响应式网格
<div className="
grid
grid-cols-1 // 移动端:1 列
md:grid-cols-2 // 平板:2 列
lg:grid-cols-3 // 桌面:3 列
gap-4
">
// 在断点处隐藏/显示
<div className="hidden md:block">仅桌面可见</div>
<div className="md:hidden">仅移动端可见</div>
容器模式
// 标准容器
<div className="container mx-auto px-4 md:px-6">
{/* 内容 */}
</div>
// 最大宽度约束
<div className="max-w-4xl mx-auto px-4">
{/* 较窄内容如文章 */}
</div>
常见错误避免
不要这样做
// ❌ 交互组件缺失 'use client'
import { useState } from 'react'; // 会报错!
// ❌ 在服务器组件中使用钩子
export default async function Page() {
const [state, setState] = useState(); // 会报错!
}
// ❌ 认证页面缺失 force-dynamic
export default async function ProtectedPage() {
const { userId } = await auth(); // 可能在构建时失败!
}
// ❌ 直接操作 DOM
document.getElementById('foo'); // 改用 ref
// ❌ 内联样式(使用 Tailwind)
<div style={{ marginTop: '20px' }}> // 使用 className="mt-5"
改为这样做
// ✅ 正确的客户端组件
"use client"
import { useState } from 'react';
// ✅ 带认证的服务器组件
export const dynamic = 'force-dynamic';
export default async function Page() {
const { userId } = await auth();
}
// ✅ 使用 ref 访问 DOM
const inputRef = useRef<HTMLInputElement>(null);
// ✅ Tailwind 类
<div className="mt-5">
权威参考
- UI 模式:
docs/patterns/ui/authenticated-page.md- 受保护页面模式form-with-validation.md- React Hook Form + Zoddata-table.md- 服务器端分页表格marketing-page.md- 公共营销页面
- 组件库:
components/ui/(shadcn/ui) - PostHog 设置:
lib/posthog/ - 功能标志:
config/features.ts