前端开发模式Skill frontend-patterns

这个技能专注于前端开发的最佳实践模式,特别针对Next.js App Router、Clerk认证、shadcn/Radix UI组件和PostHog分析集成。它用于构建用户界面组件、创建页面、实现认证流程或添加分析事件,确保一致的用户体验模式和可访问性标准。关键词包括前端开发、Next.js、认证、UI模式、分析。

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

名称: 前端模式 描述: 适用于Next.js App Router、Clerk认证、shadcn/Radix UI和PostHog分析的前端模式。在构建UI组件、创建页面、实现认证流程或添加分析事件时使用。确保一致的用户体验模式和可访问性标准。

前端模式技能

目的

通过为Next.js App Router、认证、shadcn/ui组件和分析建立的模式,确保一致的前端开发。

何时适用此技能

  • 构建新的UI组件或页面
  • 实现认证流程
  • 添加带验证的表单
  • 集成分析事件
  • 创建受保护/认证的路由
  • 使用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

认证模式

服务器组件认证检查

import { auth } from "@clerk/nextjs/server";

export default async function Page() {
  const { userId } = await auth();
  // userId 是字符串 | 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>;
}

管理员验证

// 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>

分析模式

事件命名约定

使用带类别前缀的snake_case:

// 用户操作
"user_signed_up";
"user_signed_in";
"user_profile_updated";

// 功能使用
"feature_dark_mode_toggled";
"feature_export_clicked";

// 支付
"payment_checkout_started";
"payment_completed";
"subscription_upgraded";

// 导航
"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>;
}

可访问性清单

所有组件必需

  • [ ] 键盘导航:所有交互元素可通过Tab聚焦
  • [ ] 焦点指示器:可见焦点环(Tailwind: focus:ring-2
  • [ ] 颜色对比:文本最小4.5:1
  • [ ] 替代文本:所有图像有描述性alt文本
  • [ ] 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>

常见错误避免

不要这样做

// 交互组件缺失'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');  // 使用refs代替

// 内联样式(使用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();
}

// 使用refs访问DOM
const inputRef = useRef<HTMLInputElement>(null);

// Tailwind类
<div className="mt-5">

参考

  • UI模式docs/patterns/ui/
  • 组件库components/ui/(shadcn/ui)
  • 功能标志config/features.ts