前端开发模式技能Skill frontend-patterns

前端开发模式技能旨在指导开发者使用 Next.js App Router、Clerk 认证、shadcn/ui 组件和 PostHog 分析的既定最佳实践,确保前端开发一致性、可访问性和用户体验。适用于构建 UI 组件、实现认证流程、添加表单验证、集成分析事件等场景,提升开发效率和代码质量。关键词:前端开发,Next.js,Clerk 认证,shadcn/ui,PostHog 分析,响应式设计,可访问性,模式,最佳实践。

前端开发 0 次安装 0 次浏览 更新于 3/15/2026

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 + Zod
    • data-table.md - 服务器端分页表格
    • marketing-page.md - 公共营销页面
  • 组件库components/ui/(shadcn/ui)
  • PostHog 设置lib/posthog/
  • 功能标志config/features.ts