快速创建组件库🚀(使用 Vite 的库模式)
如果您正在管理多个 React 应用程序并希望用户界面保持一致,那么您迟早会发现您需要一个组件库。
当我第一次想要创建一个 React 组件库时,我花了很多时间才找到一个满足我所有要求并且不太复杂的设置。
像这样的指南可以让我省去很多精力去处理这些事情。希望它能像帮助我一样帮助到你。
这篇文章介绍了如何设置和发布 React 组件库,包括配置构建过程以及将包发布到 npm,以便您和/或其他人可以使用它。
我尽力使所有配置保持简单和简洁,尽可能使用默认设置。
完成后,您可以像安装其他 npm 包一样安装您的库:
npm install @username/my-component-library
并像这样使用它:
import { Button } from `@username/my-component-library`;
function MyComponent() {
return <Button>Click me!</Button>
}
在我们开始之前
在我们深入研究实施细节之前,我想详细说明一些有关库设置的技术细节。
🌳 完全可摇树
对我来说,只有必要的代码才能最终出现在最终的应用程序中,这一点尤为重要。导入组件时,它只包含必要的 JS 和 CSS 样式。很酷吧?
🦑 编译的 CSS 模块
这些组件使用CSS 模块进行样式设置。构建库时,这些样式将被转换为普通的 CSS 样式表。这意味着使用应用程序甚至不需要支持 CSS 模块。
作为奖励,编译 CSS 模块可以避免兼容性问题,并且该包可以在支持 CSS 模块命名导入的环境和不支持的环境中使用。
🧁 如果您有兴趣使用vanilla-extract代替 CSS 模块,您可以在文章底部找到一个带有 vanilla extract 的分支。
😎 TypeScript
虽然该库是用TypeScript编写的,但它也可以在任何“普通”的 JavaScript 项目中使用。如果您以前从未使用过 TypeScript,不妨尝试一下。它不仅能迫使您编写更简洁的代码,还能帮助您的 AI 编程助手提供更好的建议 😉
好了,读得够多了,现在让我们来玩得开心点吧!
1. 设置一个新的 Vite 项目
如果你从未使用过Vite,可以将其视为Create React App的替代品。只需几个命令就可以开始使用。
npm create vite@latest
? Project name: › my-component-library
? Select a framework: › React
? Select a variant: › TypeScript
cd my-component-library
npm i
就是这样,您的新 Vite/React 项目已准备就绪。
2. 基本构建设置
现在,您可以运行npm run dev
并浏览 Vite 提供的 URL。在处理库时,您可以在此处轻松导入库并实际查看组件。您可以将src
文件夹中的所有代码视为您的演示页面。
实际的库代码将位于另一个文件夹中。让我们创建这个文件夹并将其命名为lib
。您也可以使用其他名称,但这lib
是一个不错的选择。
库的主入口点将是一个名为的文件main.ts
。lib
安装库时,您可以导入从该文件导出的所有内容。
📂my-component-library
+┣ 📂lib
+┃ ┗ 📜main.ts
┣ 📂public
┣ 📂src
…
Vite 图书馆模式
此时,如果您使用npm run build
Vite 构建项目,则会将其中的代码编译src
到dist
文件夹中。这是 Vite 的默认行为。
目前,您仅将演示页面用于开发目的。因此,暂时无需编译项目的这一部分。您需要编译并发布 中的代码lib
。
这就是 Vite 的库模式发挥作用的地方。它是专为构建/转译库而设计的。要激活此模式,只需在 vite.config.ts 中指定库的入口点即可。
像这样:
import { defineConfig } from 'vite'
+ import { resolve } from 'path'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
+ build: {
+ lib: {
+ entry: resolve(__dirname, 'lib/main.ts'),
+ formats: ['es']
+ }
}
})
💡 默认格式为
'es'
和'umd'
。对于你的组件库来说,你只需要 es 即可。这也消除了添加 属性的必要性name
。💡 如果您的 TypeScript linter 抱怨
'path'
并__dirname
只需安装节点的类型:npm i @types/node -D
TypeScript 和库模式
Vite 创建的文件夹tsconfig.json
仅包含 文件夹src
。要为新创建的lib
文件夹启用 TypeScript,您需要将其添加到 TypeScript 配置文件中,如下所示:
- "include": ["src"],
+ "include": ["src", "lib"],
src
尽管和文件夹都需要启用 TypeScript ,但构建库时lib
最好不要包含。src
为了确保lib
在构建过程中仅包含目录,您可以专门为构建创建一个单独的 TypeScript 配置文件。
💡 实现此单独的配置有助于避免当您直接从
dist
演示页面上的文件夹导入组件且这些组件尚未构建时出现 TypeScript 错误。⚠️ 对于 Vite 5,请阅读我对新 Typscript 配置结构的评论。
📂my-component-library
┣ …
┣ 📜tsconfig.json
+┣ 📜tsconfig-build.json
…
唯一的区别是,构建配置仅包含lib
目录,而默认配置同时包含lib
和src
📜tsconfig-build.json
{
"extends": "./tsconfig.json",
"include": ["lib"]
}
要用于tsconfig-build.json
构建,您需要将配置文件传递tsc
给 package.json 中的构建脚本:
"scripts": {
…
- "build": "tsc && vite build",
+ "build": "tsc --p ./tsconfig-build.json && vite build",
最后,您还需要将文件vite-env.d.ts
从复制src
到lib
。如果没有此文件,Typescript 在构建时会丢失 Vite 提供的一些类型定义(因为我们不再包含它们src
)。
现在您可以npm run build
再次执行,这就是您将在 dist 文件夹中看到的内容:
📂dist
┣ 📜my-component-library.js
┗ 📜vite.svg
💡 默认情况下,输出文件的名称与
name
package.json 中的属性相同。您可以在 Vite 配置中更改它(build.lib.fileName
),但我们稍后会对此进行其他处理。
该文件vite.svg
位于您的dist
文件夹中,因为 Vite 会将目录中的所有文件复制public
到输出文件夹。让我们禁用此行为:
build: {
+ copyPublicDir: false,
…
}
您可以在此处阅读更详细的解释:为什么文件 vite.svg 位于 dist 文件夹中?
构建类型
由于这是一个 Typescript 库,您也需要在包中附带类型定义。幸运的是,有一个 Vite 插件可以做到这一点:vite-plugin-dts
npm i vite-plugin-dts -D
默认情况下,dts
将为 和 生成类型src
,lib
因为这两个文件夹都包含在项目的 中.tsconfig
。这就是为什么我们需要传递一个配置参数:include: ['lib']
。
// vite.config.ts
+import dts from 'vite-plugin-dts'
…
plugins: [
react(),
+ dts({ include: ['lib'] })
],
…
💡它也可以工作
exclude: ['src']
或使用不同的 Typescript 配置文件进行构建。
为了测试一下,让我们在你的库中添加一些实际代码。打开lib/main.ts
并导出一些内容,例如:
lib/main.ts
export function helloAnything(thing: string): string {
return `Hello ${thing}!`
}
然后运行npm run build
并编译你的代码。如果你的文件夹内容dist
如下所示,那么一切就绪了🥳:
📂dist
┣ 📜main.d.ts
┗ 📜my-component-library.js
💡 不要害羞,打开文件看看程序为您做了什么!
3.没有组件的 React 组件库是什么?
我们做这一切并非只是为了导出一个helloAnything
函数。所以,让我们在库里加点肉🍖(或者豆腐🌱,或者两者都加)。
让我们来看看三个非常常见的基本组件:按钮、标签和文本输入。
📂my-component-library
┣ 📂lib
+┃ ┣ 📂components
+┃ ┃ ┣ 📂Button
+┃ ┃ ┃ ┗ 📜index.tsx
+┃ ┃ ┣ 📂Input
+┃ ┃ ┃ ┗ 📜index.tsx
+┃ ┃ ┗ 📂Label
+┃ ┃ ┗ 📜index.tsx
┃ ┗ 📜main.ts
…
这些组件的一个非常基本的实现:
// lib/components/Button/index.tsx
export function Button(props: React.ButtonHTMLAttributes<HTMLButtonElement>) {
return <button {...props} />
}
// lib/components/Input/index.tsx
export function Input(props: React.InputHTMLAttributes<HTMLInputElement>) {
return <input {...props} />
}
// lib/components/Label/index.tsx
export function Label(props: React.LabelHTMLAttributes<HTMLLabelElement>) {
return <label {...props} />
}
最后从库的主文件中导出组件:
// lib/main.ts
export { Button } from './components/Button'
export { Input } from './components/Input'
export { Label } from './components/Label'
如果你npm run build
再一次注意到,转换后的文件my-component-library.js
现在有78kb 😮
上述组件的实现包含 React JSX 代码,因此react
(和react/jsx-runtime
)也被捆绑。
由于此库将在已安装 React 的项目中被使用,因此您可以外部化此依赖项以从包中删除代码:
//vite.config.ts
build: {
…
+ rollupOptions: {
+ external: ['react', 'react/jsx-runtime'],
+ }
}
4.添加一些样式
正如开头提到的,这个库将使用CSS 模块来设置组件的样式。
Vite 默认支持 CSS 模块。您只需创建以 结尾的 CSS 文件即可.module.css
。
📂my-component-library
┣ 📂lib
┃ ┣ 📂components
┃ ┃ ┣ 📂Button
┃ ┃ ┃ ┣ 📜index.tsx
+ ┃ ┃ ┃ ┗ 📜styles.module.css
┃ ┃ ┣ 📂Input
┃ ┃ ┃ ┣ 📜index.tsx
+ ┃ ┃ ┃ ┗ 📜styles.module.css
┃ ┃ ┗ 📂Label
┃ ┃ ┣ 📜index.tsx
+ ┃ ┃ ┗ 📜styles.module.css
┃ ┗ 📜main.ts
…
并添加一些基本的 CSS 类:
/* lib/components/Button/styles.module.css */
.button {
padding: 1rem;
}
/* lib/components/Input/styles.module.css */
.input {
padding: 1rem;
}
/* lib/components/Label/styles.module.css */
.label {
font-weight: bold;
}
并在您的组件中导入/使用它们,例如:
import styles from './styles.module.css'
export function Button(props: React.ButtonHTMLAttributes<HTMLButtonElement>) {
const { className, ...restProps } = props
return <button className={`${className} ${styles.button}`} {...restProps} />
}
⛴️ 展现你的风格
编译库后,您会注意到分发文件夹中有一个新文件:
📂dist
┣ …
┣ 📜my-component-library.js
+ ┗ 📜style.css
但该文件存在两个问题:
- 您需要在使用应用程序中手动导入该文件。
- 它是一个包含所有组件的所有样式的文件。
导入 CSS
CSS 文件无法轻易地在 JavaScript 中导入。因此,CSS 文件是单独生成的,允许库用户决定如何处理该文件。
但是,如果我们假设使用该库的应用程序具有可以处理 CSS 导入的捆绑器配置,该怎么办?
为了使其正常工作,转译后的 JavaScript 包必须包含 CSS 文件的 import 语句。我们将使用另一个 Vite 插件 ( vite-plugin-lib-inject-css ),它完全不需要任何配置就能满足我们的需求。
npm i vite-plugin-lib-inject-css -D
// vite.config.ts
+import { libInjectCss } from 'vite-plugin-lib-inject-css'
…
plugins: [
react(),
+ libInjectCss(),
dts({ include: ['lib'] })
],
…
构建库并查看捆绑的 JavaScript 文件的顶部(dist/my-component-library.js
):
// dist/my-component-library.js
import "./main.css";
…
💡 您可能会注意到 CSS 文件名已从 style.css 更改为 main.css。发生此更改是因为插件为每个块生成一个单独的 CSS 文件,在这种情况下,块的名称来自入口文件的文件名。
拆分 CSS
但还有第二个问题:当你从库中导入某些内容时,main.css
所有 CSS 样式都会被导入,并且所有 CSS 样式最终都会出现在你的应用程序包中。即使你只导入了按钮。
该libInjectCSS
插件为每个块生成一个单独的 CSS 文件,并在每个块的输出文件的开头包含一个导入语句。
因此,如果您拆分 JavaScript 代码,最终您将得到单独的 CSS 文件,这些文件只有在导入相应的 JavaScript 文件时才会被导入。
一种方法是将每个文件都变成 Rollup 的入口点。而且,Rollup 文档中有一个推荐的方法,效果非常好:
📘 如果您想将一组文件转换为另一种格式,同时保持文件结构和导出签名,推荐的方法 - 而不是使用可能对导出进行树形摇动以及发出插件创建的虚拟文件的 output.preserveModules - 是将每个文件变成一个入口点。
因此让我们将其添加到您的配置中。
首先安装glob
,因为它是必需的:
npm i glob -D
然后将你的 Vite 配置更改为:
// vite.config.ts
-import { resolve } from 'path'
+import { extname, relative, resolve } from 'path'
+import { fileURLToPath } from 'node:url'
+import { glob } from 'glob'
…
rollupOptions: {
external: ['react', 'react/jsx-runtime'],
+ input: Object.fromEntries(
+ glob.sync('lib/**/*.{ts,tsx}', {
+ ignore: ["lib/**/*.d.ts"],
+ }).map(file => [
+ // The name of the entry point
+ // lib/nested/foo.ts becomes nested/foo
+ relative(
+ 'lib',
+ file.slice(0, file.length - extname(file).length)
+ ),
+ // The absolute path to the entry file
+ // lib/nested/foo.ts becomes /project/lib/nested/foo.ts
+ fileURLToPath(new URL(file, import.meta.url))
+ ])
+ )
}
…
💡 glob 库可帮助您指定一组文件名。在本例中,它会选择所有以
.ts
或结尾的文件.tsx
,并忽略Glob Wikipedia 中的*.d.ts
文件。
现在,你的文件夹根目录下有一堆 JavaScript 和 CSS 文件dist
。虽然能用,但看起来不太美观,不是吗?
// vite.config.ts
rollupOptions: {
…
+ output: {
+ assetFileNames: 'assets/[name][extname]',
+ entryFileNames: '[name].js',
+ }
}
…
再次编译该库,所有 JavaScript 文件现在应该位于您之前创建的相同文件夹结构中,lib
并包含它们的类型定义。CSS 文件位于名为 assets 的新文件夹中。
再次编译该库,所有 JavaScript 文件现在应该位于你创建的相同文件夹结构中,lib
并包含它们的类型。CSS 文件则位于一个名为“assets”的新文件夹中。🙌
注意,主文件的名称已从“my-component-library.js”更改为“main.js”。太棒了!
4. 发布软件包前的最后几个步骤
您的构建设置现已准备就绪,在发布您的包之前只需考虑一些事项。
该package.json
文件将与您的软件包文件一起发布。您需要确保它包含有关该软件包的所有重要信息。
主文件
每个 npm 包都有一个主要入口点,默认情况下该文件位于index.js
包的根目录中。
您的库的主入口点现在位于dist/main.js
,因此需要在 中进行设置package.json
。类型的入口点也同样如此:dist/main.d.ts
// package.json
{
"name": "my-component-library",
"private": true,
"version": "0.0.0",
"type": "module",
+ "main": "dist/main.js",
+ "types": "dist/main.d.ts",
…
定义要发布的文件
您还应该定义哪些文件应该打包到您的分发包中。
// package.json
…
"main": "dist/main.js",
"types": "dist/main.d.ts",
+ "files": [
+ "dist"
+ ],
…
💡 无论设置如何,某些文件(例如
package.json
或)README
始终包含在内:阅读文档
依赖项
现在看一下你的dependencies
:现在应该只有两个react
和react-dom
和几个devDependencies
。
你也可以将这两个移动到devDepedencies
。另外,将它们添加为,以便peerDependencies
使用应用程序知道必须安装 React 才能使用此包。
// package.json
- "dependencies": {
+ "peerDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
…
}
💡 请参阅此 StackOverflow 答案,了解有关不同类型依赖项的更多信息:链接
副作用
为了防止 CSS 文件被消费者的 tree-shaking 操作意外删除,您还应该将生成的 CSS 指定为副作用:
// package.json
+ "sideEffects": [
+ "**/*.css"
+ ],
sideEffects
您可以在webpack 文档中阅读更多相关信息。(该字段最初来自 Webpack,现已发展成为一种通用模式,现在也得到了其他打包工具的支持)
确保包已构建
您可以使用特殊的生命周期脚本prepublishOnly
来保证您的更改始终在包发布之前构建:
// package.json
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
…
+ "prepublishOnly": "npm run build"
},
5. 演示页面及部署
如果你想在演示页面上试用一下组件,可以直接从项目根目录导入组件。这是因为你package.json
指向了已编译的主文件dist/main.ts
。
src/App.tsx
…
import { Button, Label, Input } from '../';
…
要发布你的软件包,你只需要运行npm publish
。如果你想将你的软件包发布给公众,你必须private: false
在你的 中进行设置package.json
。
您可以在我的这些文章中阅读有关发布包的更多信息,包括在本地项目中安装它(无需发布):
常见问题解答
我可以使用香草精代替 CSS 模块吗?
这是一个使用香草精的分支:https://github.com/receter/my-component-library/tree/vanilla-extract
为了能够继续测试您的库,npm run dev
有必要添加一个ignore
for "lib/**/*.css.ts"
invite.config.ts
以避免vanillaExtractPlugin()
对已编译的文件进行操作。
我对最新版本的 create vite 遇到了问题
我尚未更新本文,按照本指南操作后,您可能会遇到最新版本的问题create vite
。不过,我创建了一个分支,并进行了一些修改,以便与vite@5.4.4兼容:
https://github.com/receter/my-component-library/tree/revision-1
我可以从输出中删除 CSS 导入吗?
是的,您可以轻松删除该vite-plugin-lib-inject-css
插件(以及随后sideEffects
的package.json
)
完成后,您将获得一个包含所有必需类的已编译样式表dist/assets/style.css
。在您的应用程序中导入/使用此样式表,就可以了。
当然,您将失去 CSS treeshaking 功能,该功能通过在每个组件内仅导入所需的 CSS 来实现。
我在这里发布了一个演示此更改的分支:https://github.com/receter/my-component-library/tree/no-css-injection
这适用于 Next.js 吗?
从 Next.js 13.4 开始,可以从外部 npm 包导入 CSS:
https://github.com/vercel/next.js/discussions/27953#discussioncomment-5831478
如果您使用旧版本的 Next.js,则可以安装next-transpile-modules
这是一个 Next.js 演示仓库:https://github.com/receter/my-nextjs-component-library-consumer
错误:找不到模块“ajv/dist/core”
vite-plugin-dts@4
如果您与 结合使用,则会发生此错误--legacy-peer-deps
。解决方法是手动安装ajv@8
或停止使用--legacy-peer-deps
。
npm i ajv@8
https://github.com/qmhc/vite-plugin-dts/issues/388
如何将 Storybook 用于我的图书馆?
要安装 Storybook,请运行npx storybook@latest init
并开始添加您的故事。
如果您在文件夹中添加故事,lib
您还需要确保.stories.tsx
从 glob 模式中排除所有文件,以便故事不会出现在您的捆绑包中。
glob.sync('lib/**/*.{ts,tsx}', { ignore: 'lib/**/*.stories.tsx'})
我在这里发布了 Storybook 的分支:https://github.com/receter/my-component-library/tree/storybook
要构建 Storybook,您需要禁用该插件。否则运行时libInjectCss
会出错(感谢@codalf解决了这个问题!)TypeError: Cannot convert undefined or null to object
npm run build-storybook
更新 2024.03.26:此问题(#15)vite-plugin-lib-inject-css
已在版本中修复2.0.0
,不再需要修复。
感谢阅读!
如果您没有继续学习或者某些内容不太清楚,您可以在我的GitHub 个人资料上找到带有工作示例的完整源代码:
- https://github.com/receter/my-component-library
- https://github.com/receter/my-component-library/tree/revision-1(适用于最新 Vite 版本的修订版)
- https://github.com/receter/my-component-library-consumer
- https://www.npmjs.com/package/@receter/my-component-library
祝您好运,它对您有所帮助,我愿意倾听您分享的任何想法。
文章来源:https://dev.to/receter/how-to-create-a-react-component-library-using-vites-library-mode-4lma