Web Artifacts Builder 完整指南
本文档包含使用 Web Artifacts Builder 创建复杂 claude.ai artifacts 的完整技术细节。如果您是初次接触,建议先阅读 📋 概览。
目录
技术栈详解
核心技术
┌─────────────────────────────────────┐
│ 开发环境 (Vite) │
│ ┌──────────────────────────────┐ │
│ │ React 18 + TypeScript │ │
│ │ │ │
│ │ ┌────────────────────────┐ │ │
│ │ │ shadcn/ui 组件库 │ │ │
│ │ │ (40+ 组件) │ │ │
│ │ └────────────────────────┘ │ │
│ │ │ │
│ │ ┌────────────────────────┐ │ │
│ │ │ Tailwind CSS 3.4.1 │ │ │
│ │ │ (实用类样式) │ │ │
│ │ └────────────────────────┘ │ │
│ └──────────────────────────────┘ │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 打包工具 (Parcel) │
│ - 编译 TypeScript │
│ - 处理 Tailwind CSS │
│ - 解析路径别名 (@/) │
│ - 内联所有资源 │
└─────────────────────────────────────┘
│
▼
📄 bundle.html
(自包含的 artifact)
版本要求
| 技术 | 版本 | 说明 |
|---|---|---|
| Node.js | ≥18.0.0 | 必需 |
| React | 18.x | 自动安装 |
| TypeScript | 5.x | 自动安装 |
| Vite | 自动选择 | 根据 Node 版本 |
| Tailwind CSS | 3.4.1 | 固定版本 |
| Parcel | 最新 | 打包时安装 |
完整工作流程
阶段 1:项目初始化
运行初始化脚本
bash scripts/init-artifact.sh <project-name>
参数:
<project-name>:项目名称(小写字母、数字、连字符)
执行过程:
[1/7] 创建项目目录
└─ 创建 <project-name>/
[2/7] 初始化 Vite + React + TypeScript
├─ 运行 npm create vite@latest
├─ 选择 React 模板
└─ 选择 TypeScript 变体
[3/7] 安装依赖
└─ npm install
[4/7] 安装 Tailwind CSS
├─ npm install -D tailwindcss postcss autoprefixer
└─ npx tailwindcss init -p
[5/7] 配置 shadcn/ui
├─ 安装 @shadcn/ui CLI
├─ 初始化配置
└─ 设置主题和别名
[6/7] 预装 shadcn/ui 组件
└─ 安装 40+ 常用组件及依赖
[7/7] 配置路径别名
├─ 更新 tsconfig.json
├─ 更新 vite.config.ts
└─ 创建 @/ 别名指向 src/
✅ 项目初始化完成!
生成的项目结构
<project-name>/
├── src/
│ ├── main.tsx # 应用入口
│ ├── App.tsx # 主组件
│ ├── App.css # 样式
│ ├── index.css # 全局样式 + Tailwind
│ ├── vite-env.d.ts # Vite 类型定义
│ ├── components/
│ │ └── ui/ # shadcn/ui 组件
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── dialog.tsx
│ │ ├── input.tsx
│ │ ├── select.tsx
│ │ └── ... (40+ 组件)
│ └── lib/
│ └── utils.ts # 工具函数 (cn, clsx)
├── public/ # 静态资源
├── index.html # HTML 模板
├── package.json # 依赖配置
├── tsconfig.json # TypeScript 配置
├── vite.config.ts # Vite 配置
├── tailwind.config.js # Tailwind 配置
├── postcss.config.js # PostCSS 配置
├── components.json # shadcn/ui 配置
└── README.md
关键配置文件
vite.config.ts:
import path from 'path'
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
})
tsconfig.json(关键部分):
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
tailwind.config.js:
module.exports = {
darkMode: ["class"],
content: [
"./index.html",
"./src/**/*.{ts,tsx,js,jsx}",
],
theme: {
extend: {
// shadcn/ui 主题配置
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
// ... 更多颜色
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
},
},
plugins: [require("tailwindcss-animate")],
}
阶段 2:开发 Artifact
基础开发流程
# 进入项目目录
cd <project-name>
# 启动开发服务器
npm run dev
# 浏览器访问 http://localhost:5173
编辑 App.tsx
基础示例:
import { useState } from 'react'
import { Button } from '@/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card'
function App() {
const [count, setCount] = useState(0)
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 p-8">
<div className="max-w-md mx-auto">
<Card>
<CardHeader>
<CardTitle>计数器应用</CardTitle>
<CardDescription>
点击按钮增加计数
</CardDescription>
</CardHeader>
<CardContent>
<div className="text-center">
<p className="text-6xl font-bold text-blue-600 mb-4">
{count}
</p>
<p className="text-sm text-gray-500">
当前计数
</p>
</div>
</CardContent>
<CardFooter className="flex gap-2">
<Button
onClick={() => setCount(count + 1)}
className="flex-1"
>
增加
</Button>
<Button
onClick={() => setCount(0)}
variant="outline"
className="flex-1"
>
重置
</Button>
</CardFooter>
</Card>
</div>
</div>
)
}
export default App
使用 shadcn/ui 组件
表单示例:
import { useState } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
function ContactForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
type: '',
message: ''
})
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
console.log('提交数据:', formData)
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<Label htmlFor="name">姓名</Label>
<Input
id="name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="请输入您的姓名"
/>
</div>
<div>
<Label htmlFor="email">邮箱</Label>
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
placeholder="your@email.com"
/>
</div>
<div>
<Label htmlFor="type">咨询类型</Label>
<Select
value={formData.type}
onValueChange={(value) => setFormData({ ...formData, type: value })}
>
<SelectTrigger id="type">
<SelectValue placeholder="请选择" />
</SelectTrigger>
<SelectContent>
<SelectItem value="general">一般咨询</SelectItem>
<SelectItem value="support">技术支持</SelectItem>
<SelectItem value="sales">销售咨询</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="message">留言</Label>
<Textarea
id="message"
value={formData.message}
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
placeholder="请输入您的留言"
rows={4}
/>
</div>
<Button type="submit" className="w-full">
提交
</Button>
</form>
)
}
对话框示例:
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogFooter,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
function ConfirmDialog() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="destructive">删除</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>确认删除?</DialogTitle>
<DialogDescription>
此操作无法撤销。确定要删除这个项目吗?
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline">取消</Button>
<Button variant="destructive">确认删除</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
状态管理
使用 React Context:
// src/context/AppContext.tsx
import { createContext, useContext, useState, ReactNode } from 'react'
interface AppContextType {
user: { name: string; email: string } | null
setUser: (user: { name: string; email: string } | null) => void
theme: 'light' | 'dark'
setTheme: (theme: 'light' | 'dark') => void
}
const AppContext = createContext<AppContextType | undefined>(undefined)
export function AppProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<{ name: string; email: string } | null>(null)
const [theme, setTheme] = useState<'light' | 'dark'>('light')
return (
<AppContext.Provider value={{ user, setUser, theme, setTheme }}>
{children}
</AppContext.Provider>
)
}
export function useApp() {
const context = useContext(AppContext)
if (!context) {
throw new Error('useApp must be used within AppProvider')
}
return context
}
在 main.tsx 中使用:
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import { AppProvider } from './context/AppContext.tsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<AppProvider>
<App />
</AppProvider>
</React.StrictMode>,
)
阶段 3:打包 Artifact
运行打包脚本
bash scripts/bundle-artifact.sh
执行过程:
[1/5] 检查 index.html
✅ 找到 index.html
[2/5] 安装打包依赖
├─ npm install -D parcel
├─ npm install -D @parcel/config-default
├─ npm install -D parcel-resolver-tspaths
└─ npm install -D html-inline
[3/5] 创建 Parcel 配置
└─ 生成 .parcelrc
[4/5] 使用 Parcel 构建
├─ 编译 TypeScript
├─ 处理 Tailwind CSS
├─ 解析 @/ 路径别名
└─ 生成 dist/ 目录
[5/5] 内联所有资源
├─ 运行 html-inline
├─ 内联 JavaScript
├─ 内联 CSS
└─ 生成 bundle.html
✅ 打包完成!文件位置: bundle.html
Parcel 配置(.parcelrc)
{
"extends": "@parcel/config-default",
"resolvers": ["parcel-resolver-tspaths", "..."]
}
说明:
@parcel/config-default:使用默认配置parcel-resolver-tspaths:支持 TypeScript 路径别名(@/)
打包输出
dist/ 目录结构:
dist/
├── index.html # 带外部资源引用的 HTML
├── index.[hash].js # 编译后的 JavaScript
└── index.[hash].css # 编译后的 CSS
bundle.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Artifact</title>
<style>
/* 所有 CSS 内联在这里 */
/* 包括 Tailwind、shadcn/ui 和自定义样式 */
</style>
</head>
<body>
<div id="root"></div>
<script>
// 所有 JavaScript 内联在这里
// 包括 React、组件代码和依赖
</script>
</body>
</html>
阶段 4:分享 Artifact
在 Claude 对话中分享
读取 bundle.html:
cat bundle.html发送给用户: 直接将文件内容作为代码块发送
用户查看: Claude 会将其渲染为交互式 artifact
文件大小考虑
典型大小:
- 简单应用(1-2 组件):~200-500 KB
- 中等应用(5-10 组件):~500 KB - 1 MB
- 复杂应用(20+ 组件):1-3 MB
优化建议:
- 移除未使用的 shadcn/ui 组件
- 精简 Tailwind CSS(使用 purge)
- 避免大型第三方库
设计和样式指南
避免 "AI 风格"
问题模式
// ❌ 避免:所有内容居中
function BadLayout() {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
{/* 所有内容都居中 */}
</div>
</div>
)
}
// ❌ 避免:紫色渐变
<div className="bg-gradient-to-r from-purple-500 to-pink-500">
// ❌ 避免:统一圆角
<Button className="rounded-xl" />
<Card className="rounded-xl" />
<Input className="rounded-xl" />
推荐模式
// ✅ 推荐:实用的布局
function GoodLayout() {
return (
<div className="min-h-screen bg-gray-50">
<header className="bg-white shadow">
<nav className="max-w-7xl mx-auto px-4 py-4">
{/* 导航内容 */}
</nav>
</header>
<main className="max-w-7xl mx-auto px-4 py-8">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* 网格布局 */}
</div>
</main>
</div>
)
}
// ✅ 推荐:品牌色系统
const colors = {
primary: 'blue', // 主色
secondary: 'green', // 辅助色
accent: 'orange', // 强调色
neutral: 'gray', // 中性色
}
// ✅ 推荐:语义化圆角
<Button className="rounded-md" /> // 中等圆角
<Card className="rounded-lg" /> // 大圆角
<Badge className="rounded-full" /> // 完全圆角
Tailwind CSS 最佳实践
响应式设计
function ResponsiveCard() {
return (
<Card className="
w-full
md:w-1/2
lg:w-1/3
p-4
md:p-6
lg:p-8
">
<h2 className="
text-xl
md:text-2xl
lg:text-3xl
font-bold
">
响应式标题
</h2>
</Card>
)
}
组合实用类
// ✅ 使用 cn() 工具函数组合类
import { cn } from '@/lib/utils'
function DynamicButton({ variant, className }: ButtonProps) {
return (
<button
className={cn(
// 基础样式
"px-4 py-2 rounded-md font-medium transition-colors",
// 条件样式
variant === 'primary' && "bg-blue-600 text-white hover:bg-blue-700",
variant === 'secondary' && "bg-gray-200 text-gray-900 hover:bg-gray-300",
// 自定义样式
className
)}
>
按钮
</button>
)
}
常见开发任务
添加新组件
安装单个组件:
npx shadcn-ui@latest add <component-name>
# 示例
npx shadcn-ui@latest add dropdown-menu
npx shadcn-ui@latest add date-picker
npx shadcn-ui@latest add data-table
查看可用组件:
npx shadcn-ui@latest add
# 会显示所有可用组件列表
创建自定义组件
// src/components/CustomCard.tsx
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
import { ReactNode } from 'react'
interface CustomCardProps {
title: string
icon?: ReactNode
children: ReactNode
}
export function CustomCard({ title, icon, children }: CustomCardProps) {
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
{icon}
{title}
</CardTitle>
</CardHeader>
<CardContent>
{children}
</CardContent>
</Card>
)
}
添加图标
安装 lucide-react:
npm install lucide-react
使用图标:
import { Search, User, Settings, X } from 'lucide-react'
function IconExample() {
return (
<div className="flex gap-2">
<Search className="h-4 w-4" />
<User className="h-5 w-5 text-blue-600" />
<Settings className="h-6 w-6 text-gray-500" />
<X className="h-4 w-4 hover:text-red-600 cursor-pointer" />
</div>
)
}
数据获取
import { useState, useEffect } from 'react'
function DataFetching() {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
fetch('https://api.example.com/data')
.then(res => res.json())
.then(data => {
setData(data)
setLoading(false)
})
.catch(err => {
setError(err.message)
setLoading(false)
})
}, [])
if (loading) return <div>加载中...</div>
if (error) return <div>错误: {error}</div>
return <div>{/* 渲染数据 */}</div>
}
完整示例
示例 1:待办事项应用
import { useState } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Checkbox } from '@/components/ui/checkbox'
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
import { X } from 'lucide-react'
interface Todo {
id: number
text: string
completed: boolean
}
function TodoApp() {
const [todos, setTodos] = useState<Todo[]>([])
const [input, setInput] = useState('')
const addTodo = () => {
if (input.trim()) {
setTodos([...todos, {
id: Date.now(),
text: input,
completed: false
}])
setInput('')
}
}
const toggleTodo = (id: number) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
))
}
const deleteTodo = (id: number) => {
setTodos(todos.filter(todo => todo.id !== id))
}
return (
<div className="min-h-screen bg-gray-50 p-8">
<Card className="max-w-2xl mx-auto">
<CardHeader>
<CardTitle>待办事项</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex gap-2">
<Input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && addTodo()}
placeholder="添加新任务..."
/>
<Button onClick={addTodo}>添加</Button>
</div>
<div className="space-y-2">
{todos.map(todo => (
<div
key={todo.id}
className="flex items-center gap-2 p-3 bg-white rounded-lg border"
>
<Checkbox
checked={todo.completed}
onCheckedChange={() => toggleTodo(todo.id)}
/>
<span className={`flex-1 ${todo.completed ? 'line-through text-gray-400' : ''}`}>
{todo.text}
</span>
<Button
variant="ghost"
size="sm"
onClick={() => deleteTodo(todo.id)}
>
<X className="h-4 w-4" />
</Button>
</div>
))}
</div>
{todos.length === 0 && (
<p className="text-center text-gray-400 py-8">
暂无任务,添加一个吧!
</p>
)}
</CardContent>
</Card>
</div>
)
}
export default TodoApp
示例 2:数据表格
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import { Badge } from '@/components/ui/badge'
interface User {
id: number
name: string
email: string
role: 'admin' | 'user'
status: 'active' | 'inactive'
}
const users: User[] = [
{ id: 1, name: '张三', email: 'zhang@example.com', role: 'admin', status: 'active' },
{ id: 2, name: '李四', email: 'li@example.com', role: 'user', status: 'active' },
{ id: 3, name: '王五', email: 'wang@example.com', role: 'user', status: 'inactive' },
]
function UserTable() {
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead>姓名</TableHead>
<TableHead>邮箱</TableHead>
<TableHead>角色</TableHead>
<TableHead>状态</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{users.map(user => (
<TableRow key={user.id}>
<TableCell>{user.id}</TableCell>
<TableCell className="font-medium">{user.name}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>
<Badge variant={user.role === 'admin' ? 'default' : 'secondary'}>
{user.role === 'admin' ? '管理员' : '用户'}
</Badge>
</TableCell>
<TableCell>
<Badge variant={user.status === 'active' ? 'default' : 'outline'}>
{user.status === 'active' ? '活跃' : '停用'}
</Badge>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)
}
故障排除
常见问题
1. 打包后样式丢失
原因:Tailwind 未正确扫描文件
解决:
// tailwind.config.js
module.exports = {
content: [
"./index.html",
"./src/**/*.{ts,tsx,js,jsx}", // 确保包含所有文件
],
// ...
}
2. 路径别名不工作
原因:配置不一致
解决:
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
// vite.config.ts
import path from 'path'
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
})
3. Parcel 构建失败
原因:缺少 .parcelrc 配置
解决:运行打包脚本会自动创建
或手动创建:
{
"extends": "@parcel/config-default",
"resolvers": ["parcel-resolver-tspaths", "..."]
}
参考资源
官方文档
- shadcn/ui: https://ui.shadcn.com
- React: https://react.dev
- TypeScript: https://www.typescriptlang.org
- Vite: https://vitejs.dev
- Tailwind CSS: https://tailwindcss.com
- Parcel: https://parceljs.org
组件示例
- shadcn/ui 示例: https://ui.shadcn.com/examples
- Tailwind 组件: https://tailwindui.com/components
工具
- lucide-react 图标: https://lucide.dev
- Radix UI: https://www.radix-ui.com