跳到主要内容

如何开发自己的一个 shadcn 组件

· 阅读需 13 分钟
Oxygen
Front End Engineer

距离上一篇介绍shadcn文章已过去一年之久啦😅。在这一年多的时间内,随着AI Generate Web技术的快速发展,shadcn凭借其 AI 友好的组件开发模式,生态发展得极为庞大。shadcn本身也经过了一些架构调整,新增了一些特性,比如add命令支持第三方registry以及支持build命名等。本篇在解析shadcn CLI 的基础上详细介绍一下如何加入shadcn组件生态。

shadcn add

上一篇讲到shadcn add命令会默认从shadcn文档同域的地址获取registry.json文件,以解析shadcn组件库的组件目录结构和组件代码。当时shadcn add命令只支持通过定义process.env.COMPONENTS_REGISTRY_URL来自定义registry.json的域,这种方式最大的局限性就是在使用的时候只能指定一个第三方域的地址,当你要使用shadcn add添加不同域的组件时,就要不断修改process.env.COMPONENTS_REGISTRY_URL

所以shadcn add命令最大的改进就是支持通过components参数来指定要添加的组件的地址,这样第三方组件就能通过本地编写registry.json自由地将自己的某个单一的组件或者整个组件库分享出去。

支持这一特性的代码也很简单,如下所示,在add命令执行的第一步(源码)会判断components是否为一个URL,如果是则请求该json内容。

const options = addOptionsSchema.parse({
components,
cwd: path.resolve(opts.cwd),
...opts,
})

let itemType: z.infer<typeof registryItemTypeSchema> | undefined
let registryItem: any = null

if (
components.length > 0 &&
// 判断 add 命令的第一个参数是否为 url
(isUrl(components[0]) || isLocalFile(components[0]))
) {
registryItem = await getRegistryItem(components[0], "")
itemType = registryItem?.type
}

isUrl判断是否为远端地址

function isUrl(path: string) {
try {
new URL(path)
return true
} catch (error) {
return false
}
}

如果是远端地址,则fetch请求json,获取json内容定义的组件源码等内容,后面就跟上一篇的介绍的流程大体一致,就不多赘述了。

function getRegistryItem(name: string, style: string) {
try {
...

// Handle URLs and component names
const [result] = await fetchRegistry([
isUrl(name) ? name : `styles/${style}/${name}.json`,
])

return registryItemSchema.parse(result)
} catch (error) {
logger.break()
handleError(error)
return null
}
}

shadcn build

shadcn buildshadcn2.3.0中新增的命名,用来构建registry.json文件,也就是让你的组件库支持使用shadcn add命令添加到其他项目内部。

shadcn build命令的逻辑非常简单(源码位置)总结来说就分为 4 步:

image-20250727160853198

查找并解析 registry.json

该命令默认会从项目根目录获取registry.json文件,也可以通过registrycwd参数来指定registry.json文件的路径(一般用于monorepo项目)。

const options = buildOptionsSchema.parse({
cwd: path.resolve(opts.cwd),
registryFile: registry,
outputDir: opts.output,
})

// 检查指定的目录是否存在,并返回解析后的本地 registry.json 和输出文件目录绝对路径
const { resolvePaths } = await preFlightBuild(options)
// 读取 registry.json
const content = await fs.readFile(resolvePaths.registryFile, "utf-8")

// 使用 zod 校验 registry.json 的结构是否符合 registry schema 结构
// https://ui.shadcn.com/schema/registry-item.json
const result = registrySchema.safeParse(JSON.parse(content))

if (!result.success) {
logger.error(
`Invalid registry file found at ${highlighter.info(
resolvePaths.registryFile
)}.`
)
process.exit(1)
}

该命名使用zod进行registry.json结构的校验,判断其是否符合shadcn约束的定义结构。shadcn官方对于registry.json的约束,可以在shadcn的文档中查看——registry.json,这里就不一一介绍了。

遍历文件路径

根据registry.json中注册的items字段,可以找到定义项目内部组件的文件路径字段files,这些在shadcn的文档中都有详细介绍,参考这里registry-item.json

for (const registryItem of registry.items) {
...
}

读取组件代码并写入到registry.json

shadcn build这个命令主要就是为了将组件源码写入到registry.json中,从而使得第三方开发者在为自己的组件库编写registry.json时无需将组件源码写入registry.json,避免繁琐的流程。

for (const registryItem of result.data.items) {
if (!registryItem.files) {
continue
}

for (const file of registryItem.files) {
file["content"] = await fs.readFile(
path.resolve(resolvePaths.cwd, file.path),
"utf-8"
)
}
}

将registry.json写入输出目录

build命令最后默认将registry.json内容写入到项目根目录的public目录下,这样做的目的主要是因为现在大部分的组件开发框架都会将public目录做为默认的静态不编译文件目录,在项目打包的时候支持拷贝目录内部的文件到打包目录下。如果一个组件库会发布文档网站,那么registry.json就可以直接在文档网站的域内访问到,也不用为registry.json再单独折叠其他域名地址了。

当然build命令也支持使用output参数修改registry.json写入的目录路径。

await fs.writeFile(
path.resolve(resolvePaths.outputDir, `${registry.name}.json`),
JSON.stringify(result.data, null, 2)
)

加入shadcn生态

如果你想编写一个组件或者组件库,让其支持shadcn的生态,能够使用shadcn add命令在各个项目之间共享,最简单的方式是基于shadcn提供的nextjs模板项目直接开发。

你可以直接在 GitHub 上基于这个模板项目创建仓库并克隆到本地直接开始开发你的组件,这个项目的结构如下所示:

├── 📄 README.md
└── 📂 app/ // nextjs 的路由文件,用来编写组件开发文档
│ ├── 📄 favicon.ico
│ ├── 📄 globals.css
│ ├── 📄 layout.tsx
│ ├── 📄 page.tsx
└── 📂 components/ // 第三方通用组件,使用 shadcn add 添加其他第三方组件辅助开发
├── 📄 components.json
│ ├── 📄 open-in-v0-button.tsx
└── 📂 lib/
│ ├── 📄 utils.ts
└── 📂 public/ // 输出组件注册的 registry.json 文件,构建文档的时候会直接拷贝
│ └── 📂 r/
│ ├── 📄 hello-world.json
└── 📂 registry/ // 组件存放目录
│ └── 📂 new-york/
│ └── 📂 blocks/ // 块级复杂组件
│ └── 📂 hello-world/
│ ├── 📄 hello-world.tsx
│ └── 📂 ui/ // 单个组件
│ ├── 📄 button.tsx
└── 📄 tsconfig.json
├── 📄 registry.json // 注册组件,必须自己编写

当你在编写完组件push到 GitHub 后,可以直接在 Vercel 上绑定你的仓库并构建发布。

然后别人就能通过 Vercel 给你分配的域名地址或者你自定义的域名地址访问到你开发的组件的registry.json,并且使用shadcn add命令添加你开发的组件到本地。

基于shadcn这套流程最大的便捷之处就是你无需去选择工具打包你的组件库,你只需要编写好组件的registry.json即可。

参考项目

歌词组件仓库

使用 Vercel 导入项目并一键部署,现在就可以使用访问我的歌词组件的registry.json文件了,并且支持在项目中使用shadcn add https://shadcn-lyrics.vercel.app/r/lyrics.json来添加组件。

image-20250727215325524