0%

Shadcn Blocks + Tailwind——UI 组件库集成指南

昨天偶然发现了 shadcn/ui 的 Blocks 功能,简直打开了新世界的大门!一行命令就能生成完整的页面组件,比自己从头搭建快了不知道多少倍。

shadcn/ui 简介

  shadcn/ui 是由 Shaundai 开发的现代化 UI 组件库,不同于传统的组件库如 Ant Design 或 Material-UI,shadcn/ui 采用了一种革命性的设计理念——将组件代码直接复制到你的项目中。这种方式让开发者完全掌控组件代码,可以任意定制样式和逻辑,同时享受高质量组件带来的便利。

  经过几年的发展,shadcn/ui 已经从最初的手工复制粘贴演进到了自动化工具,现在可以通过 CLI 命令一键添加组件。更令人兴奋的是,它推出了 Blocks 功能,可以一键生成完整的页面组件,大大提升了开发效率。

shadcn/ui 的核心优势

  1. 完全可控: 组件代码在你的项目中,想怎么改就怎么改
  2. 零运行时: 不会在 bundle 中增加额外的库代码
  3. 按需使用: 只包含你需要的组件
  4. 类型安全: 完整的 Typescript 支持
  5. 无障碍: 符合 WCAG 标准的组件实现
  6. 定制性强: 使用 Tailwind Css,样式定制灵活

Blocks 功能介绍

  Blocks 是 shadcn/ui 的最新功能,它提供了一系列预制的页面组件和布局,涵盖了常见的应用场景。每个 Block 都是一个完整的组件,包含了所需的所有子组件和样式。你可以将它们看作是”页面积木”,通过组合不同的 Blocks 来快速构建完整的应用界面。

Blocks 的主要类别

类别描述典型用途
Landing着陆页组件产品介绍、营销页面
Dashboard仪表盘组件管理后台、数据可视化
Authentication认证组件登录、注册、找回密码
Forms表单组件数据录入、设置页面
Data Display数据展示组件表格、卡片、列表
Marketing营销组件产品特性、定价、FAQ

Blocks 实战演示

安装和初始化

1
2
3
4
5
6
7
8
9
10
# 创建新项目 npm create vite@latest my-app -- --template React-TS
cd my-app
npm install

# 安装 Tailwind Css
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

# 配置 tailwind.config.JS(如上一篇文章所述)
# 配置 src/index.Css(如上一篇文章所述)

安装 shadcn/ui 和 Blocks

1
2
3
4
5
# 安装 shadcn/ui CLI
npx shadcn-ui@latest init

# 交互式配置,选择 Typescript、Default 样式等
# 安装 Blocks 依赖 npx shadcn-ui@latest add blocks

使用 Landing Blocks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
// src/pages/LandingPage.tsx
import {
Page,
Block,
HeroSection,
FeaturesSection,
TestimonialsSection,
PricingSection,
CtaSection,
FooterSection
} from '@/components/block';

export default function LandingPage() {
return (
<Page>
{/* 英雄区域 */}
<HeroSection
title="打造现代 Web 应用"
subtitle="使用 shadcn/ui 和 Blocks 快速构建美观、可访问的用户界面"
ctaText="开始使用"
ctaLink="/dashboard"
secondaryCtaText="了解更多"
secondaryCtaLink="/features"
/>

{/* 特性展示 */}
<FeaturesSection
title="核心功能"
description="专为现代 Web 应用设计的功能特性"
features={[
{
title: "高性能",
description: "优化的渲染性能和用户体验",
icon: "⚡"
},
{
title: "可访问性",
description: "遵循 WCAG 标准支持键盘导航",
icon: "♿"
},
{
title: "响应式设计",
description: "适配各种屏幕尺寸和设备",
icon: "📱"
}
]}
/>

{/* 客户评价 */}
<TestimonialsSection
testimonials={[
{
name: "张三",
role: "前端开发",
content: "使用 shadcn/ui 我们的开发效率提升了50%",
avatar: "/avatars/1.png"
},
{
name: "李四",
role: "产品经理",
content: "用户界面的一致性得到了很大改善",
avatar: "/avatars/2.png"
}
]}
/>

{/* 定价 */}
<PricingSection
plans={[
{
name: "基础版",
price: "免费",
description: "适合个人开发者",
features: ["最多3个项目", "基础组件", "社区支持"]
},
{
name: "专业版",
price:99/",
description: "适合小团队",
features: ["无限项目", "高级组件", "优先支持", "自定义域名"],
highlight: true
},
{
name: "企业版",
price:299/",
description: "适合大型团队",
features: ["SAML SSO", "专属支持", "定制开发"]
}
]}
/>

{/* 行动号召 */}
<CtaSection
title="准备好开始了吗?"
subtitle="立即注册开始构建你的应用"
ctaText="免费试用"
ctaLink="/register"
/>

{/* 页脚 */}
<FooterSection
links={[
{ name: "关于我们", href: "/about" },
{ name: "联系方式", href: "/contact" },
{ name: "隐私政策", href: "/privacy" }
]}
copyright="© 2024 公司名称. 保留所有权利."
/>
</Page>
);
}

使用 Dashboard Blocks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
// src/pages/Dashboard.tsx
import {
DashboardLayout,
DashboardHeader,
DashboardSidebar,
DashboardMain,
DashboardCard,
DashboardTable,
DashboardChart
} from '@/components/block';

export default function Dashboard() {
return (
<DashboardLayout>
<DashboardSidebar />
<DashboardMain>
<DashboardHeader
title="仪表盘"
breadcrumbs={['仪表盘', '概览']}
/>

<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
<DashboardCard
title="总用户数"
value="1,234"
trend="+12%"
trendType="positive"
icon="👥"
/>
<DashboardCard
title="活跃用户"
value="892"
trend="+8%"
trendType="positive"
icon="🔥"
/>
<DashboardCard
title="收入"
value="¥24,567"
trend="+15%"
trendType="positive"
icon="💰"
/>
<DashboardCard
title="转化率"
value="23.4%"
trend="-2%"
trendType="negative"
icon="📊"
/>
</div>

<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="lg:col-span-1">
<DashboardChart
title="用户增长趋势"
type="line"
data={[
{ month: '1月', users: 1200 },
{ month: '2月', users: 1900 },
{ month: '3月', users: 3000 },
{ month: '4月', users: 2500 },
{ month: '5月', users: 3500 }
]}
/>
</div>

<div className="lg:col-span-1">
<DashboardTable
title="最近活动"
columns={[
{ header: '用户', accessor: 'user' },
{ header: '操作', accessor: 'action' },
{ header: '时间', accessor: 'time' }
]}
data={[
{ user: '张三', action: '登录', time: '2分钟前' },
{ user: '李四', action: '下单', time: '5分钟前' },
{ user: '王五', action: '注册', time: '10分钟前' }
]}
/>
</div>
</div>
</DashboardMain>
</DashboardLayout>
);
}

使用 Authentication Blocks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// src/pages/LoginPage.tsx
import {
AuthLayout,
LoginForm,
SocialLogin,
ForgotPasswordForm
} from '@/components/block';

export default function LoginPage() {
return (
<AuthLayout title="登录到您的账户">
<LoginForm
onSubmit={async (values) => {
// 处理登录逻辑 console.log(values);
}}
onForgotPasswordClick={() => {
// 处理忘记密码
}}
/>

<SocialLogin
providers={[
{ name: 'Google', icon: '/google.svg' },
{ name: 'GitHub', icon: '/github.svg' },
{ name: 'Microsoft', icon: 'microsoft.svg' }
]}
onProviderClick={(provider) => {
// 处理社交登录 console.log(provider);
}}
/>

<div className="mt-4 text-center text-sm text-muted-foreground">
没有账户?{' '}
<a href="/register" className="underline underline-offset-4">
立即注册
</a>
</div>
</AuthLayout>
);
}

自定义 Blocks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
// src/components/block/custom-blocks.tsx
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';

// 自定义的产品展示 Block
interface ProductShowcaseBlockProps {
products: Array<{
id: string;
name: string;
description: string;
price: string;
features: string[];
popular?: boolean;
}>;
}

export function ProductShowcaseBlock({ products }: ProductShowcaseBlockProps) {
return (
<section className="py-12 px-4">
<div className="container mx-auto max-w-6xl">
<div className="text-center mb-12">
<h2 className="text-3xl font-bold mb-4">我们的产品</h2>
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
专为现代企业和开发者设计的高质量产品和服务
</p>
</div>

<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{products.map((product) => (
<Card
key={product.id}
className={product.popular ? "border-primary shadow-lg" : ""}
>
{product.popular && (
<Badge className="absolute top-4 right-4 transform -translate-y-1/2">
热门
</Badge>
)}

<CardHeader>
<CardTitle className="flex items-center justify-between">
{product.name}
{product.popular && (
<span className="text-xs bg-primary text-primary-foreground px-2 py-1 rounded-full">
推荐
</span>
)}
</CardTitle>
<div className="text-2xl font-bold text-primary">
{product.price}
</div>
</CardHeader>

<CardContent>
<p className="text-muted-foreground mb-4">
{product.description}
</p>

<ul className="space-y-2 mb-6">
{product.features.map((feature, index) => (
<li key={index} className="flex items-center">
<svg className="w-4 h-4 mr-2 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
</svg>
{feature}
</li>
))}
</ul>

<Button
className="w-full"
variant={product.popular ? "default" : "outline"}
>
选择此套餐
</Button>
</CardContent>
</Card>
))}
</div>
</div>
</section>
);
}

// 自定义的 FAQ Block
interface FAQBlockProps {
faqs: Array<{
question: string;
answer: string;
}>;
}

export function FAQBlock({ faqs }: FAQBlockProps) {
return (
<section className="py-12 px-4 bg-muted">
<div className="container mx-auto max-w-4xl">
<div className="text-center mb-12">
<h2 className="text-3xl font-bold mb-4">常见问题</h2>
<p className="text-lg text-muted-foreground">
解答您可能遇到的问题
</p>
</div>

<div className="space-y-4">
{faqs.map((faq, index) => (
<div
key={index}
className="bg-background rounded-lg border p-6 hover:shadow-sm transition-shadow"
>
<h3 className="text-lg font-semibold mb-2">{faq.question}</h3>
<p className="text-muted-foreground">{faq.answer}</p>
</div>
))}
</div>
</div>
</section>
);
}

高级 Blocks 使用技巧

动态 Blocks 组合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// src/pages/DynamicPage.tsx
import { useState } from 'React';
import { Block } from '@/components/block';

interface DynamicPageBuilderProps {
blockConfigs: Array<{
type: string;
props: Record<string, any>;
}>;
}

export default function DynamicPageBuilder({ blockConfigs }: DynamicPageBuilderProps) {
const [blocks, setBlocks] = useState(blockConfigs);

// 根据配置动态渲染 Blocks
const renderBlock = (config: any) => {
switch (config.type) {
case 'hero':
return <Block.HeroSection {...config.props} />;
case 'features':
return <Block.FeaturesSection {...config.props} />;
case 'pricing':
return <Block.PricingSection {...config.props} />;
default:
return null;
}
};

return (
<div>
{blocks.map((block, index) => (
<div key={index}>
{renderBlock(block)}
</div>
))}
</div>
);
}

Blocks 的主题定制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// src/components/block/theme-provider.tsx
import { createContext, useContext, useState, useEffect } from 'React';

interface ThemeContextType {
theme: 'light' | 'dark' | 'system';
setTheme: (theme: 'light' | 'dark' | 'system') => void;
}

const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark' | 'system'>('system');

useEffect(() => {
const root = window.document.documentElement;
root.classList.remove('light', 'dark');

if (theme === 'system') {
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
root.classList.add(systemTheme);
} else {
root.classList.add(theme);
}
}, [theme]);

return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}

export function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}

// 自定义主题适配的 Block 组件 export function ThemedHeroBlock(props: any) {
const { theme } = useTheme();

return (
<div className={`bg-${theme === 'dark' ? 'gray-900' : 'white'} text-${theme === 'dark' ? 'white' : 'gray-900'}`}>
<HeroSection {...props} />
</div>
);
}

Blocks 的性能优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 优化大量 Blocks 的渲染性能 import { memo, useMemo } from 'React';
import { LazyLoadComponent } from 'React-lazy-load-image-component';

// 使用 memo 避免不必要的重渲染 const MemoizedFeatureBlock = memo(({ features }: { features: any[] }) => {
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{features.map((feature, index) => (
<div key={feature.id} className="text-center">
<div className="bg-primary/10 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4">
{feature.icon}
</div>
<h3 className="text-xl font-semibold mb-2">{feature.title}</h3>
<p className="text-muted-foreground">{feature.description}</p>
</div>
))}
</div>
);
});

MemoizedFeatureBlock.displayName = 'MemoizedFeatureBlock';

// 对于长页面使用懒加载 export function OptimizedLandingPage() {
return (
<div>
<HeroSection />
<LazyLoadComponent>
<FeaturesSection />
</LazyLoadComponent>
<LazyLoadComponent>
<TestimonialsSection />
</LazyLoadComponent>
<LazyLoadComponent>
<PricingSection />
</LazyLoadComponent>
<FooterSection />
</div>
);
}

与传统 UI 库的对比

shadcn/ui vs Material-UI

特性shadcn/uiMaterial-UI
安装方式复制粘贴到项目npm install
定制性完全可定制需要主题定制
包体积按需引入整体较大
Typescript内置支持需要额外配置
组件数量精选高质量数量庞大
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Material-UI 使用方式 import { Button, Card, Typography } from '@mui/material';

function MyComponent() {
return (
<Card>
<Typography variant="h5">标题</Typography>
<Button variant="contained">按钮</Button>
</Card>
);
}

// shadcn/ui 使用方式 import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';

function MyComponent() {
return (
<Card>
<CardHeader>
<CardTitle>标题</CardTitle>
</CardHeader>
<CardContent>
<Button>按钮</Button>
</CardContent>
</Card>
);
}

shadcn/ui vs Ant Design

特性shadcn/uiAnt Design
设计语言现代简洁企业级风格
样式系统Tailwind Css自定义 Css
国际化需要额外配置内置支持
学习曲线较低中等

最佳实践

1. Blocks 的组织结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 推荐的目录结构
// src/
// components/
// block/
// landing/
// hero.tsx
// features.tsx
// pricing.tsx
// dashboard/
// layout.tsx
// card.tsx
// chart.tsx
// auth/
// login.tsx
// register.tsx
// shared/
// button.tsx
// input.tsx

2. Blocks 的配置化管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/config/page-config.TS
export const pageConfigs = {
landing: {
hero: {
title: '欢迎来到我们的平台',
subtitle: '快速、高效、美观的解决方案',
ctaText: '开始使用',
ctaLink: '/signup'
},
features: {
title: '为什么选择我们',
features: [
{
title: '高性能',
description: '优化的渲染性能',
icon: '⚡'
}
]
}
}
};

3. Blocks 的测试策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/__tests__/blocks.test.tsx
import { render, screen } from '@testing-library/React';
import { HeroSection } from '@/components/block';

describe('HeroSection', () => {
it('renders correctly', () => {
render(
<HeroSection
title="Test Title"
subtitle="Test Subtitle"
ctaText="Test CTA"
/>
);

expect(screen.getByText('Test Title')).toBeInTheDocument();
expect(screen.getByText('Test Subtitle')).toBeInTheDocument();
expect(screen.getByText('Test CTA')).toBeInTheDocument();
});
});

总结

  • shadcn/ui 的 Blocks 功能极大提升了页面开发效率
  • 通过预构建的组件块,可以快速搭建完整页面
  • Blocks 保持了 shadcn/ui 一贯的高度定制性
  • 适合快速原型开发和产品上线
  • 支持主题定制和国际化
  • 良好的 Typescript 和无障碍支持

周末用 Blocks 搭建了一个 SaaS 产品的着陆页,从设计到上线只用了半天时间!这种开发效率在过去是不敢想象的。有了 Blocks,我们终于可以从繁琐的 UI 细节中解放出来,专注于业务逻辑本身了。

扩展阅读

  • shadcn/ui 官方文档
  • Tailwind Css 文档
  • Building with shadcn/ui Blocks
  • Modern UI Development Patterns

参考资料

  • shadcn/ui GitHub: https://github.com/shadcn/ui
  • Radix UI: https://www.radix-ui.com/
  • Tailwind Css: https://tailwindcss.com/
  • Figma Design System: https://www.figma.com/community/file/example
bulb