普通视图

发现新文章,点击刷新页面。
今天 — 2025年11月26日首页

从0-1封装一个React组件

作者 龙颜
2025年11月25日 18:28

第一步:初始化与安装依赖

创建一个空文件夹并初始化:

mkdir my-button
cd my-button
pnpm init

接下来安装依赖。对于组件库,核心原则是:

  1. react 和 react-dom 应该是 Peer Dependencies(宿主环境提供),而不是打包进去。
  2. 构建工具和类型定义是 Dev Dependencies

执行下面命令

# 1. 安装构建工具、node类型定义、TS、Sass、类型定义生成插件 和 自动注入样式
pnpm add -D vite@5 @types/node typescript sass vite-plugin-dts vite-plugin-lib-inject-css

# 2. 安装 React 的类型定义 (开发时需要用到类型提示)
pnpm add -D @types/react @types/react-dom

# 3. (可选) 如果你开发时需要用到 react 的具体代码提示,也可以装一下,但最终不会打包
pnpm add -D react react-dom

第二步:手动创建配置文件

1. 创建 tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,
    "moduleResolution": "bundler" /* Vite 5 推荐 */,
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx" /* 关键:支持 React */,
    "strict": true,
    "declaration": true /* 生成类型文件 */,
    "declarationDir": "dist"
  },
  "include": ["src"]
}

2. 创建 vite.config.ts

// 导入 Vite 配置函数和所需插件
import { defineConfig } from "vite";
import dts from "vite-plugin-dts";
import { libInjectCss } from "vite-plugin-lib-inject-css";
import { resolve } from "path";

// 使用 defineConfig 定义 Vite 构建配置
export default defineConfig({
  // 配置使用的插件
  plugins: [
    // 注入 CSS 到库中的插件
    libInjectCss(),
    // 生成类型声明文件(.d.ts)的插件
    dts({
      include: ["src/**/*.ts", "src/**/*.tsx"], // 包含的 TypeScript 文件类型
      outDir: "dist", // 输出目录
      rollupTypes: true, // 使用 Rollup 打包类型
    }),
  ],
  // 构建配置
  build: {
    // 库模式配置
    lib: {
      entry: resolve(__dirname, "src/index.ts"), // 库的入口文件
      name: "MyButton", // UMD 格式的全局变量名
      fileName: (format) => `index.${format}.js`, // 输出文件名格式
    },
    // Rollup 打包配置
    rollupOptions: {
      // 外部化依赖,不打包进库
      external: ["react", "react-dom", "react/jsx-runtime"],
      output: {
        // 配置 UMD 格式的全局变量名
        globals: {
          react: "React",
          "react-dom": "ReactDOM",
        },
      },
    },
    sourcemap: true, // 生成 sourcemap 便于调试
    emptyOutDir: true, // 构建前清空输出目录
  },
});

第三步:构建目录结构与源码

my-button/
├── src/
│ ├── components/ 
│ │ └── MyButton/ 
│ │   ├── index.tsx 
│ │   └── index.module.scss 
│ └── index.ts <-- 统一出口 
├── package.json 
├── tsconfig.json 
└── vite.config.ts

编写组件 src/components/Button/index.tsx

import styles from "./index.module.scss";

export interface MyButtonProps {
  label: string;
  onClick?: () => void;
}

export const MyButton = ({ label, onClick }: MyButtonProps) => {
  return (
    <button className={styles["my-btn"]} onClick={onClick}>
      {label}
    </button>
  );
};

编写样式 src/components/Button/index.module.scss

@use "sass:color";

.my-btn {
  background-color: #007bff;
  color: white;
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  &:hover {
    background-color: color.adjust(#007bff, $lightness: -10%);
  }
}

编写入口 src/index.ts

export * from "./components/MyButton";

第四步:配置 package.json (关键)

{
  "name": "my-button",
  "version": "1.0.0",
  "description": "A lightweight React component library",
  "main": "dist/index.umd.js",
  "module": "dist/index.es.js",
  "types": "dist/index.d.ts",
  "files": [
    "dist"
  ],
  "exports": {
    ".": {
      "import": "./dist/index.es.js",
      "require": "./dist/index.umd.js",
      "types": "./dist/index.d.ts"
    },
    "./index.css": "./dist/index.css"
  },
  "sideEffects": [
    "**/*.css"
  ],
  "scripts": {
    "build": "tsc && vite build"
  },
  "peerDependencies": {
    "react": ">=18.0.0",
    "react-dom": ">=18.0.0"
  },
  "keywords": [
    "my-button"
  ],
  "author": "hql",
  "license": "ISC",
  "packageManager": "pnpm@10.14.0",
  "devDependencies": {
    // ... 这里是刚才 pnpm add -D 安装的那些
  }
}

在项目代码中使用

import { Button } from 'my-button';

function App() {
  return <Button label="点击我" onClick={() => alert('Works!')} />;
}

本地调试的时候可以在项目中,使用Vite Alias 映射

alias: { 
    // 关键配置:将包名映射到组件库的【源码入口】 
    'my-button': '系统路径/组件包的文件夹名称(my-button)/src/components/MyButton/index.tsx',
},

修改业务项目的 tsconfig.json (关键)

{
  "compilerOptions": {
    // ...其他配置
    "baseUrl": ".", // 启用 paths 必须配置 baseUrl
    "paths": {
      "my-button": [
        // 这里也要填绝对路径
        "系统路径/组件包的文件夹名称(my-button)/src/index.ts"
      ]
    }
  }
}

效果

  • 无需打包:不需要在组件库里运行 pnpm build
  • 实时热更:在 my-button 里改了 SCSS 颜色,Ctrl+S 保存,my-app 页面毫秒级自动刷新样式。
  • 源码调试:在浏览器的 DevTools 里看到的源码是 TS 原文件,而不是打包后的 JS,断点调试非常方便。
  • 避免 React 冲突:因为直接编译源码,组件库会直接使用业务项目的 React 实例,完美避开 "Invalid Hook Call" 问题。

第五步:打包与本地验证

  1. 打包 pnpm build
  2. 本地模拟发布 (最稳妥的测试方式) pnpm pack
  3. 在业务项目中测试,找一个你本地其他的 React 项目(或者随便新建一个测试项目):
# 假设你的 tgz 文件路径是 /Users/xxx/code/my-button/my-button-1.0.0.tgz
pnpm add /绝对路径/my-button-1.0.0.tgz

在代码中使用

import { Button } from 'my-button';

function App() {
  return <Button label="点击我" onClick={() => alert('Works!')} />;
}

第六阶段:发布到 NPM

1. 准备工作

确保 package.json 中的 name 是唯一的(去 npmjs.com 搜一下)。 确保没有私有配置(如 .npmrc 指向了公司私有源),发布需要指向官方源:

npm config set registry https://registry.npmjs.org/

2. 登录与发布

# 登录 npm (如果没有账号需先注册)
npm login

# 升级版本号 (patch: 1.0.0 -> 1.0.1)
npm version patch

# 发布
npm publish

3. 验证

发布成功后,在你的业务项目中把之前的本地引用改回 npm 引用:

pnpm remove my-button
pnpm add my-button
❌
❌