如何构建您自己的 Shadcn UI 注册表

发布: (2026年1月31日 GMT+8 18:25)
7 min read
原文: Dev.to

Source: Dev.to

Naud

在历时 4 周构建了 30+ 着陆页区块(hero、pricing、FAQ、CTA、testimonial)后,我终于破解了完美的 Shadcn 注册表设置。

一次关于如何建模区块(Hero、Pricing、FAQ 等),将它们连接到 registry.json,渲染服务器友好预览,并为 npx shadcn add 做准备的完整演练。

1. 介绍

Shadcn UI 附带了官方的组件和区块注册表,但你也可以构建自己的注册表,以适配你的布局和设计系统。

经过 4 周的迭代,我构建了一个面向登录页的注册表,其中包含 30+ 区块,如 hero、pricing、FAQ、CTA 等——全部在 registry.json 中描述,并在通用的 /blocks/[name] 页面中渲染,支持实时预览和语法高亮代码。

指标: 30+ 区块,覆盖 12 类,超过 50 k 行 Tailwind,100 % 与服务器组件兼容。

我们将介绍:

  • 单个区块(Hero01)的结构
  • 该区块在 registry.json 中的描述方式
  • 通用的 Next.js 页面 /blocks/[name] 如何按名称渲染任意区块
  • BlockProvider 如何在不触及应用全局主题的情况下注入主题、屏幕尺寸和预览行为
  • 该设计如何与 React Server Components 和 Shadcn CLI 良好协作

2. 什么是 Shadcn “registry block”?

在高层次上,Shadcn 注册项是指向一个或多个代码文件并描述其依赖关系的 JSON 元数据。

TypeRoleExample value
Component低层原语(按钮,输入)registry:component
Block页面区块(hero,pricing,FAQ)registry:block

在我的注册表中: 30 个页面区块(hero ×5,pricing ×3,features ×5,CTA ×4,等)采用 registry:block 建模。

指标: 每个区块平均包含 3–5 个 Shadcn 原语buttonbadgecard),通过 registryDependencies

Source:

3. 具体实例:Hero01

下面是我的实际 Hero01 —— 一个两列布局的 Hero,包含徽章、标题、描述、两个 CTA 按钮和一张图片:

interface Hero01Props {
  badge?: string;
  heading?: string;
  description?: string;
  buttons?: {
    primary?: { text: string; url: string; icon?: React.ReactNode };
    secondary?: { text: string; url: string; icon?: React.ReactNode };
  };
  image?: string;
  className?: string;
}

关键设计决策

  • 仅限营销属性 – 没有低层次的布局调节
  • 纯展示组件 – 0 hooks,0 数据获取
  • 支持服务器组件 – 没有 use client 指令
  • 组合 Shadcn – 使用 ButtonBadge + Tailwind 网格

用法

指标: Hero01 = 187 行12 个 Tailwind 实用类2 个 Shadcn 组件

Hero01 预览

4. 在 registry.json 中描述块

我的 hero-01 注册项:

{
  "name": "hero-01",
  "type": "registry:block",
  "title": "Hero 01",
  "description": "Two-column hero with badge + CTAs",
  "dependencies": ["lucide-react"],
  "registryDependencies": ["button", "badge"],
  "categories": ["hero"],
  "meta": { "image": "/r/previews/hero-01.webp" },
  "files": [
    {
      "path": "src/registry/blocks/hero-01/hero.tsx",
      "target": "components/hero-01.tsx"
    }
  ]
}

关键字段

  • registryDependencies → CLI 自动安装 button + badge
  • categories → 为 /blocks?category=hero 过滤提供动力
  • files.target → 可预测的 components/hero-01.tsx 位置

指标: 30 个块 = 12 个类别共 47 个依赖(npm + registry)。

5. BlockProvider:隔离的预览魔法

Problem: Preview 30+ blocks with theme switching + responsive breakpoints without breaking the site theme.

Solution: BlockProvider creates a self‑contained preview universe:

interface BlockContextValue {
  block: SerializableRegistryBlock;
  theme: Theme;
  screenSize: ScreenSize; // 'mobile' | 'tablet' | 'desktop'
  setTheme: (theme: Theme) => void;
  setScreenSize: (size: ScreenSize) => void;
}

Why it works

  • ✅ Theme changes stay inside /blocks/hero-01
  • ✅ Screen‑size toggle = instant CSS viewport classes
  • 30+ blocks share one preview system
  • ✅ Global site theme remains completely untouched

Metric: Provider handles 5 themes × 3 breakpoints = 15 preview variations per block.

6. Server components + registry = ⚡ 性能

服务器优先设计

  • Blocks = 服务器组件(无 use client
  • Registry JSON = 静态、构建时可读取的
  • Preview shell = 最小化的客户端边界

结果

✅ Bundle size: Hero01 = 2.1 KB (gzipped)
✅ TTFB: /blocks/hero-01 = 89 ms
✅ Static pages: 30+ generated at build time

预览外壳(BlockProvider 及其控件)仅在需要时保持在客户端。

Source:

7. 整个注册表的单一路由

通过使用单一的动态路由(pages/blocks/[name].tsxapp/blocks/[name]/page.tsx),每个区块都可以按需渲染:

// pages/blocks/[name].tsx
import { getBlockByName } from '@/registry';
import BlockProvider from '@/components/BlockProvider';

export default async function BlockPage({ params }) {
  const block = await getBlockByName(params.name);
  return (
    <BlockProvider block={block}>
      {/* Render the block component dynamically */}
    </BlockProvider>
  );
}
  • 不需要为每个区块单独创建页面。
  • 让代码库保持 DRY(不要重复自己),并且添加新区块只需更新 registry.json 即可。

TL;DR

  • 定义 每个 UI 部分为 registry:block,写入 registry.json
  • 实现 区块为仅接受营销层级属性的服务器组件。
  • 渲染 任意区块时,使用单一的动态路由,并在 BlockProvider 中包装,以实现独立的主题和视口预览。
  • 享受 快速的构建、极小的 bundle,以及与 Shadcn CLI 完美契合的工作流。

祝构建愉快! 🚀

目录: /blocks/[name]

单个 Next.js 页面驱动 所有 30+ 区块

export async function generateStaticParams() {
  return getBlocks().map(block => ({ name: block.name })); // 30+ pages!
}

export default async function BlockPage({ params }) {
  const block = getBlock(params.name);
  const { component, ...serializableBlock } = block; // Server‑safe

  return (
    <BlockProvider block={serializableBlock}>
      {/* Render the block component */}
    </BlockProvider>
  );
}

自动生成: /blocks/hero-01, /blocks/pricing-01, /blocks/cta-01, …

指标: 22 个静态页面, 100 % 服务器渲染, 完整的 SEO 元数据。

8️⃣ CLI 集成

npx shadcn add @shadcnship/hero-01

我的注册表完全遵循 Shadcn 架构,因此 CLI 集成无缝进行:

# Direct URL (works now)
npx shadcn add https://mysite.com/r/hero-01.json

# Namespaced (after PR approval)
npx shadcn add @shadcnship/hero-01

CLI 的操作:

  • 复制 hero.tsxcomponents/hero-01.tsx
  • 安装 lucide-react
  • 自动安装 button + badge

总计 30 秒

9️⃣ 经验教训(4 周后 + 30 个区块)

✅ 做

  • 仅使用 营销属性headingbuttons)开始。
  • 严格使用 registryDependencies
  • 保持区块 服务器组件优先
  • 在扩展区块之前构建预览系统 之前

❌ 不要

  • 暴露布局属性(paddinggap)。
  • 混合预览和站点主题状态。
  • 在预览页面中硬编码区块名称。
  • 忘记提供用于覆盖的 className 属性。

最大收获: 通用 /blocks/[name] + BlockProvider = 每个区块零额外代码

获取完整代码

30+ 生产就绪的区块,带预览系统、主题切换和注册表:

👉 GitHub:

👉 Live demo:

感谢您的时间。

祝使用愉快! 🚀

Back to Blog

相关文章

阅读更多 »

开发了我的第一个作品集。

!Forem 标志https://media2.dev.to/dynamic/image/width=65,height=,fit=scale-down,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2...