Shadcn 源码阅读

date
Nov 25, 2024
slug
shadcn-source-code-reading
status
Published
tags
shadcn
tailwindcss
type
Post
URL
summary
源码阅读

克隆项目

ui
shadcn-uiUpdated Dec 20, 2024

初识外表

项目结构

先来看看项目的结构,就像看到一个人需要先看看他的外表
  • CONTRIBUTING.md
  • LICENSE.md
  • README.md
  • package.json
  • pnpm-lock.yaml
  • pnpm-workspace.yaml
  • tailwind.config.cjs
  • postcss.config.cjs
  • prettier.config.cjs
  • tsconfig.json
  • turbo.json
  • vitest.config.ts
  • vitest.workspace.ts
  • packages/
  • apps/
  • templates/
  • scripts/
初步可以直接判断这是一个基于Monorepo的项目

monorepo

Monorepo是一种软件开发策略,它在一个代码库中存储多个逻辑项目。这种方式提供了项目之间明确的关系,使得管理和协作变得更加高效。
有如下的一些优势:
  • 简化代码管理:通过集中存储代码,Monorepo 能更轻松地跟踪变更、维护一致性,以及实施版本控制。
  • 增强团队协作:统一代码库便于共享与审查,促进开发人员间的知识共享与沟通。
  • 优化工具使用:统一工具设置可简化测试和部署,提升整体流程效率。
  • 支持代码共享与重用:通过集中共享的库和组件,减少重复代码,促进一致性和快速开发。
  • 简化依赖管理:集中化的项目结构让依赖管理更加直观,减少冲突与兼容性问题。
  • 灵活性与可扩展性:适合规模较大和复杂度增长的项目,以支持未来的需求变化。
  • 显著减少沟通负担:避免跨多个代码库的协调工作,加快开发迭代速度。

pnpm

pnpm 对Monorepo有着天然的支持,pnpm 加入了 workspace 的概念,我们只需要在项目的根目录创一个pnpm-workspace.yaml 文件,在此文件中我们可以定义 workspace 的范围,shadcn中是这样定义的
这样一来,就可以利用 workspace 的功能去对某些包使用筛选的功能(www 目录在 apps 下),更多的特性可以查看官方文档

Turborepo

Turborepo 是适用于 JavaScript 和 TypeScript 代码库的高性能构建系统。它专为扩展 monorepo 而设计,也使单包工作区中的工作流更快。所以这里又是一个为 monorepo 服务的,这个是一个构建工具。
Monorepo 有很多优点 - 但它们难以扩展。每个工作区都有自己的测试套件、自己的 linting 和自己的构建过程。单个 monorepo 可能有数千个任务要执行
notion image
Turborepo 可以极大地提高构建的速度,对任务进行并行的处理,有自己的一个缓存机制,在远程 CI 的的时候可以极大提高效率,减少远程构建的时间。Turborepo 还有增量构建的机制,每次构建不需要全部重新构建一次,总之,Turborepo 可以解决 monorepo 中许多存在的问题,所有技术的出现都是服务于业务,用于解决问题的。
notion image
来看看缓存命中的效果,非常炫酷,这不但在本地有,远程也有,如果对缓存机制感兴趣,可以看看官方文档
notion image
Pipeline 是 Turborepo 的核心概念,允许开发者定义并执行跨多个包(packages)的自定义任务序列。通过在 turbo.json 配置文件中定义任务管道,Turborepo 能够基于任务依赖关系,自动优化执行顺序,实现任务的并行处理,同时使用缓存加速重复任务的执行。
在 pipeline 中,每一个 key 都对应一个在 package.json 中定义的 script 脚本命令。这些 key 也是通过 turbo run 执行任务时使用的脚本名称。例如,通过 turbo run <key> 可以触发对应 Pipeline 中任务的执行。
如此我们就可以理解在package.json 中关于 turbo 的命令是怎么来的了,下面是 shadcn 中的 turbo.json ,其中dependsOn 代表任务的依赖关系,"^build" 代表每个包在执行 build 时,依赖于其它包的 build 任务,且按依赖顺序执行。
其他的就不做过多的解释了,这些字段都很简单,不用看文档都看得懂。

Kodiak

.kodiak.toml 是 Kodiak 的配置文件,它的作用是定义如何自动合并 GitHub 的 Pull Requests。通过灵活的规则,Kodiak 可以帮助团队自动化繁琐的合并任务,同时确保 PR 的质量和符合团队的代码策略需求,这尤其适用于 monorepo 中管理多个模块的 PR 合并
更多可以看官方仓库,这是一个github app

更多

对于其他的配置文件就不一一看了,剩下的都是一些基本操作了
  • .commitlintrc.json:设定 commit 的提交规范
  • .editorconfig:统一编辑器的代码风格
  • .eslintrc.json:代码检查
  • .gitignore:git 的忽略文件
  • .npmrc:npm 配置文件
  • .nvmrc:nvm 配置文件,约定 node 版本
  • .prettierignore:代码格式化工具的忽略文件
  • postcss.config.cjs:csss 转换和处理工具配置文件
  • tailwind.config.cjs:tailwind 配置文件
  • vitest.config.ts:测试工具的配置文件
 
此时你可能会好奇,这个项目的编译打包工具是什么? 此时就需要介绍 tsup!

Tsup

这是一个基于 esbuild 的打包工具,来看看它的速度
notion image
这里可选的编译/打包工具:
  • Transformer(编译):babeljs、TypeScript、esbuild
  • Bundler(打包):Rollup、esbuild、webpack、tsup、unbuild
两者区别:编译(a → a'、b → b')、打包(ab → c,All in One)
tsup可以快速的、方便的、开箱即用的构建出 esm、cjs 格式的包,同时还有类型,对于 shadcn 这个项目来说,tsup 完全够用,用不上 webpack,rollup 之类的打包工具,毕竟配置很繁琐
来看看shadcn中 package 下的 cli 的配置就知道了
是的,就这么简单

深入内核

如此,我们大概了解了这个项目的架子是怎么样的,现在我们需要抛出一些问题,逐步去了解这个项目,这个项目的内核大概是这个样子的
通过阅读官方的贡献文档,我们可以直接知道这些目录的作用是什么
Path 路径
Description 描述
apps/www/app
网站的 Next.js 应用程序
apps/www/components
网站的 React 组件
apps/www/content
网站的内容
apps/www/registry
组件的注册表
packages/cli
shadcn-ui 软件包

如何开发一个新的组件

首先我们需要知道 shadcn 是 Radix 进行开发的,然后阅读一下官方的贡献文档,直接就可以知道如果要开发一个新的组件,是在 registry 目录下进行开发,比如 button
notion image
这样你可以很容易反推该目录下其他的子目录的作用,block 是一些大组件的组合,是非原子的;example 是每个组件的一些演示实例,其他就不多说了
在贡献文档中你可能会注意到一句话,也就是 shadcn 是基于注册系统来进行开发的,那么什么是注册系统,正如这个目录的名字一样,这个我们放到最后去解释,这也是 shadcn 最核心的地点,cli 工具以及官方文档都离不开这个注册系统的实现

如何编写官方文档

mdx

文档是使用 MDX 编写的。可以在 apps/www/content/docs 目录中找到文档文件,那么对于 button 这个组件来说,是如何编写的,可以直接找到相关的 button.mdx 文件来看看
我省略了一些部分,可以看到比较重要的是ComponentPreview的实现,因为这个组件可以我们编写的组件直接渲染在文档
notion image

component-preview

直接全局搜索就可以找到这个组件的代码,就在 apps/www/components里面

import部分

我们一步步对这个组件进行拆解,先看看 import 部分,疑惑的可能是这个地方
  • import { Index } from "@/__registry__"
当我们跟踪__registry__过去的时候,就会发现这样的一句话,这个脚本是由build-registry.ts生成,这个脚本有接近 1000 行的代码,还是非常恐怖的,这个脚本正是这个注册中心的核心,我们稍后解释

类型定义

再来看看接口定义的部分,可以看到它继承了 div 的属性,并且自定义了一些入参

组件实现

如下是组件中的一些变量
  • codeString:用于点击复制按钮的时候,复制组件的代码
  • Codes:在 button 中并没有 children,你可能会疑惑,这个明明没有 children,那么是从哪里来的呢,我们可以尝试打印一下 children,看看是什么
notion image
可以看到这里多了一些 props,接下来就需要去溯源这个 props 是怎么来的,我们只要去查找某个属性是在哪里 set 的就可以了,我们去看一个特别的属性:__rawString__
可以发现在一个名为contentlayer.config.js中的文件设置了这个属性,你可以打印一下codeEl.children?.[0].value
notion image

contentlayer

Contentlayer 是一个专门为开发者设计的内容处理框架,主要用于现代化网站开发中的内容管理,有如下的几个优势
  • 将内容文件(如 Markdown、MDX)转换为类型安全的 JSON 数据
  • 提供完整的类型提示和自动补全
  • 支持实时内容更新(热重载)
  • 与主流框架(如 Next.js)无缝集成
shadcn 的官方文档正是使用了这样的一个内容管理框架,contentlayer.config.js 主要是对所有的 mdx 进行处理的配置文件
这个配置文件有个重要的方法就是makeSource
在处理MDX的逻辑当中,包含两类插件:
  • remark插件:用于处理Markdown语法
    • remarkGfm:支持GitHub风格的Markdown
    • codeImport:处理代码导入
  • rehype插件:用于处理HTML语法,按顺序执行以下处理:
    • rehypeSlug:生成标题的锚点链接
    • rehypeComponent:处理组件相关
rehypeComponent是一个自定义的插件,里面有一个visit用于暴露事件,可以在里面处理dom,如果你了解 webpack 的 babel,就会知道这是差不多的,我们可以在里面去操作 AST
我们深入其中会发现有一个地方对ComponentPreview 做了一个判断,后面的逻辑就不继续说了,还是有点复杂的,主要目的都是往这个组件的属性里面疯狂塞东西,
还有一个插件是rehypeNpmCommand,主要用于生成安装代码,所以这里非常巧妙,MdxComponent 是用于展示 contentlayer 中内容的最终的组件
现在我们来梳理一下文档的完整的展示逻辑,以 button 为例子
1. 在注册中心 registry 中开发 button 组件
  1. 通过buildRegistry脚本来进行组件的注册,最后会生成__registry__目录,存放各种组件的映射表
  1. 使用 contentlayer 对 mdx 中的ComponentPreview进行处理,在元素中加入源码、安装脚本等属性
  1. contentlayer的脚本生成.contentlayer目录,也就是转换为了 json
notion image
  1. mdxComponent读取属性,展示button的demo,这里的 preview,code 都来自于上面的 json,json 是contentlayer处理的
notion image

componentPreview的实现

这个组件支持如下的一些功能
  • 可以切换预览和代码视图
  • 支持主题切换
  • 包含代码复制功能
  • 可以显示组件的实时预览
  • 支持响应式设计
  • 如果 type === "block",会渲染一个特殊的图片预览块,包含:
    • 明暗主题下的不同图片
    • 响应式iframe展示
  • 否则会渲染标准预览界面,包含:
    • 预览/代码切换标签页
    • 样式切换器
    • 复制按钮
    • 组件实时预览区域
整体的实现并不复杂,但是有一个组件ThemeWrapper 这个的实现大有门道,这是 theme 页面 实现在线切换各种颜色主题的核心,感兴趣可以去源码阅读,这里不做解释

注册中心的实现

回到build-registry.mts这个脚本,通过粗略的阅读可以知道,这个注册中心的目的就是去描述各种文件的信息,比如button,描述 button 在哪个文件,依赖于什么组件
这个是__registry__目录下的 index.tsx,也是 cli 和官方文档主要引入的东西
有了这么一组信息,对于官方文档来说,可以直接找到 button 组件的位置并引入,然后生成组件代码,对于 cli 来说,cli 可以通过这个去安装相关的依赖到指定的目录
如此以来 cli 的逻辑我们可以不阅读就猜出来了,cli 就是去注册中心里面找信息,然后把组件给安装好
该脚本就是对这个目录下的各种注册配置进行遍历,然后去生成注册配置信息,这里无需再去关心这个脚本到底是怎么写的了,就是遍历然后拼接字符串而已,而不同的注册配置生成的字符串不一样,详细可以去阅读,到这里已经足够了
notion image

© Jayden 2024 - 2025