解析 ‘Modern Library Bundle’:如何发布一个同时兼容 ESM、CJS 和 RSC 协议的 React 组件库?

构建与发布通用React组件库:ESM、CJS、RSC 兼容性深度解析

在现代前端开发的复杂生态系统中,构建和发布一个React组件库已经不再仅仅是输出一些JavaScript文件那么简单。随着模块系统的演进,以及React Server Components (RSC) 的出现,一个优秀的组件库必须能够同时兼容多种环境和协议:ESM (ECMAScript Modules)、CJS (CommonJS) 和 RSC。这不仅是为了最大化库的可用性,也是为了确保在不同项目类型(如传统的CRA应用、Next.js、Remix等)中都能发挥最佳性能和开发体验。

今天,我们将深入探讨如何从零开始,构建一个能够完美支持这三种模块协议的React组件库。我们将涵盖从项目结构、核心概念、构建工具选择、配置细节,到最终发布的全过程。

模块化演进与多协议兼容的必要性

在深入技术细节之前,我们首先需要理解为什么多协议兼容性变得如此重要。这与JavaScript模块系统的历史演进和React框架自身的发展密切相关。

CommonJS (CJS)

CommonJS 是Node.js早期采用的模块系统,它允许开发者在服务器端组织代码。其特点是同步加载模块,使用 require() 导入和 module.exports 导出。

  • 优点: 简单易用,在Node.js环境中成熟稳定,大量npm包至今仍以CJS格式发布。
  • 缺点: 无法进行静态分析(如Tree-shaking),加载同步阻塞,不适用于浏览器环境(需要打包工具转换)。

ECMAScript Modules (ESM)

ESM 是JavaScript语言层面官方定义的模块系统,通过 importexport 关键字实现。它旨在成为浏览器和Node.js通用的模块标准。

  • 优点: 支持静态分析,实现Tree-shaking,提升打包效率和运行时性能;异步加载,更适合浏览器环境;是未来的标准。
  • 缺点: 在Node.js中引入初期存在兼容性问题(.mjs 文件扩展名、"type": "module"package.json 中),但在新版本Node.js中已得到良好支持。

React Server Components (RSC)

RSC 是React 18及以上版本引入的一项突破性特性,它允许开发者在服务器上渲染组件,并将结果作为轻量级数据传递给客户端。这显著减少了客户端JavaScript包的大小,提高了初始加载性能。

  • 核心理念: 区分“服务器组件”(Server Components)和“客户端组件”(Client Components)。
  • 服务器组件: 在服务器上渲染,不发送给客户端。它们可以访问后端资源(数据库、文件系统),但不能使用React Hooks(如 useState, useEffect)或处理浏览器事件。
  • 客户端组件: 在客户端渲染,与传统React组件行为一致,可以交互,使用Hooks。
  • 兼容性挑战: RSC 对组件库提出了新的要求。一个组件库可能包含服务器端逻辑、客户端交互逻辑,或者两者皆有。正确地标记和打包这些组件,以便RSC环境能够区分和优化,是关键。'use client' 指令是其核心。

多协议兼容的必要性

特性/协议 CJS ESM RSC (客户端组件) RSC (服务器组件)
导入/导出 require/module.exports import/export import/export import/export
环境 Node.js 浏览器/Node.js 浏览器 Node.js (服务器)
Tree-shaking
性能 一般 较好 较好 最佳 (减少JS)
兼容性 广泛 现代环境 现代React框架 现代React框架

一个通用的React组件库需要:

  1. 支持Node.js项目: 确保旧有或纯Node.js项目能够 require 你的库。
  2. 支持现代Web项目: 确保现代前端打包工具能够 import 你的库,并进行Tree-shaking。
  3. 支持RSC环境: 确保你的组件在Next.js、Remix等支持RSC的框架中能够正确工作,区分客户端和服务器端逻辑,并利用RSC的性能优势。

忽略其中任何一个都可能限制你的库的采用率和可用性。

构建工具链的选择

构建一个多协议兼容的组件库需要一套强大的工具链。以下是我们推荐的核心工具:

  1. TypeScript: 强烈推荐使用TypeScript来编写组件库。它提供类型安全、更好的代码可维护性、以及对现代JavaScript特性的良好支持。
  2. 构建工具 (Bundler):
    • Rollup: 对于库的构建,Rollup 通常优于 Webpack。它专注于生成优化的、Tree-shakable 的 ESM 模块,并且配置灵活,非常适合生成多种输出格式。
    • esbuild: 以其极快的速度著称,可以作为Rollup的替代或与Rollup结合使用。对于简单的库构建,esbuild 可能更直接。
  3. Babel (可选,但常用): 虽然TypeScript编译器本身可以进行JSX转换和一些ESNext特性的降级,但Babel提供了更丰富的插件生态系统,例如用于优化代码、宏、或特定框架转换。
  4. package.jsonexports 字段: 这是实现多协议兼容性的核心机制,用于定义不同环境下的模块入口点。

在本次讲座中,我们将以 TypeScript + Rollup 为核心,详细讲解其配置和使用。

项目结构与设计哲学

为了实现多协议兼容,我们的项目结构需要支持不同模块类型的输出,并清晰地区分客户端和服务器端代码。

示例项目结构

my-universal-component-library/
├── src/
│   ├── components/
│   │   ├── Button/
│   │   │   ├── index.ts        # 聚合导出
│   │   │   ├── Button.tsx      # 客户端组件 (默认入口)
│   │   │   └── Button.server.tsx # 服务器组件 (可选)
│   │   ├── Link/
│   │   │   ├── index.ts
│   │   │   └── Link.tsx
│   │   └── index.ts            # 聚合导出所有组件
│   ├── utils/
│   │   ├── client-only-util.ts # 仅限客户端的工具函数
│   │   └── server-only-util.ts # 仅限服务器的工具函数
│   └── index.ts                # 库的公共入口
├── dist/                         # 构建输出目录
│   ├── esm/                      # ESM 输出
│   │   ├── index.mjs
│   │   ├── components/
│   │   │   └── Button/
│   │   │       ├── index.mjs
│   │   │       └── Button.mjs
│   │   └── ...
│   ├── cjs/                      # CJS 输出
│   │   ├── index.cjs
│   │   ├── components/
│   │   │   └── Button/
│   │   │       ├── index.cjs
│   │   │       └── Button.cjs
│   │   └── ...
│   ├── types/                    # TypeScript 类型定义
│   │   ├── index.d.ts
│   │   └── ...
│   └── react-server/             # RSC 特定的客户端组件入口
│       ├── components/
│       │   └── Button/
│       │       └── index.mjs   # 带有 'use client' 的客户端组件
│       └── ...
├── package.json
├── tsconfig.json
├── rollup.config.js
└── ...

设计原则

  1. TypeScript优先: 所有源代码都使用 .ts.tsx
  2. 清晰的职责分离:
    • 客户端组件: 包含交互逻辑,使用 useState, useEffect 等 Hooks。通常作为库的默认导出。
    • 服务器组件 (如果需要): 不含交互逻辑,可以访问服务器端资源。通常作为单独的文件或命名导出。
    • 工具函数: 区分哪些工具函数只能在客户端运行,哪些只能在服务器运行。
  3. 'use client' 指令: 这是 RSC 兼容性的核心。任何需要在客户端渲染和交互的组件,都必须在其文件顶部声明 'use client'。构建工具需要确保这个指令被保留。
  4. package.json exports 字段: 精心配置 exports,以指导打包工具根据上下文(ESM、CJS、RSC)选择正确的入口文件。

TypeScript 配置 (tsconfig.json)

tsconfig.json 是 TypeScript 项目的基石。对于一个组件库,我们需要进行一些特定的配置。

// tsconfig.json
{
  "compilerOptions": {
    "target": "es2018",                       // 编译目标JS版本,Rollup会进一步处理
    "module": "esnext",                      // 模块系统使用ESNext,以便Tree-shaking
    "lib": ["dom", "dom.iterable", "esnext"], // 包含DOM和ESNext的类型定义
    "jsx": "react-jsx",                      // JSX转换方式,无需在每个文件顶部import React
    "declaration": true,                     // 生成类型声明文件 (.d.ts)
    "declarationMap": true,                  // 生成声明映射文件 (source maps for .d.ts)
    "sourceMap": true,                       // 生成源码映射文件
    "outDir": "./dist/types",                // 类型声明文件的输出目录
    "strict": true,                          // 启用所有严格类型检查选项
    "esModuleInterop": true,                 // 启用ES模块和CJS模块之间的兼容性
    "skipLibCheck": true,                    // 跳过库文件的类型检查
    "forceConsistentCasingInFileNames": true, // 强制文件名大小写一致
    "allowSyntheticDefaultImports": true,    // 允许从没有默认导出的模块中默认导入
    "resolveJsonModule": true,               // 允许导入 .json 文件
    "isolatedModules": true,                 // 确保每个文件都可以安全地独立转译
    "baseUrl": ".",                          // 基础URL,用于模块解析
    "paths": {                               // 路径别名 (可选)
      "@/*": ["src/*"]
    }
  },
  "include": ["src/**/*.ts", "src/**/*.tsx"], // 包含的源文件
  "exclude": ["node_modules", "dist"]          // 排除的目录
}

关键配置解释:

  • target: "es2018": 虽然我们最终会输出多种JS版本,但这里设置一个相对现代的ES版本,让TypeScript专注于类型检查,实际的降级工作交给Rollup/Babel。
  • module: "esnext": 告诉TypeScript保留ES模块语法,这对于Rollup的Tree-shaking至关重要。
  • jsx: "react-jsx": 使用新的JSX转换方式,无需手动 import React
  • declaration: true, outDir: "./dist/types": 这是生成 .d.ts 类型定义文件的关键,确保消费者能获得完整的类型提示。

Rollup 配置 (rollup.config.js)

Rollup 是我们构建流程的核心。我们将配置它来生成 ESM、CJS 和 RSC 兼容的输出。

首先,安装必要的Rollup插件:

npm install --save-dev rollup @rollup/plugin-typescript @rollup/plugin-node-resolve @rollup/plugin-commonjs rollup-plugin-dts @rollup/plugin-terser @rollup/plugin-babel @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript

rollup.config.js 会相对复杂一些,因为它需要处理多种输出格式和插件。

// rollup.config.js
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
import terser from '@rollup/plugin-terser';
import { babel } from '@rollup/plugin-babel';
import dts from 'rollup-plugin-dts';
import path from 'path';

// 定义需要打包的入口点,例如 src/index.ts 和 src/components/**/*.ts(x)
const input = [
  'src/index.ts',
  'src/components/Button/index.ts',
  'src/components/Link/index.ts',
  // ... 其他组件或模块的入口
];

// 定义外部依赖,这些依赖不会被打包到库中,而是作为peerDependencies
const externals = [
  'react',
  'react-dom',
  'react/jsx-runtime', // 用于新的JSX转换
  // ... 其他 peerDependencies
];

const createOutput = (format, dir, preserveUseClient = false) => {
  const outputOptions = {
    dir: dir,
    format: format,
    sourcemap: true,
    preserveModules: true, // 保留模块结构,方便Tree-shaking
    entryFileNames: `[name].${format === 'es' ? 'mjs' : 'cjs'}`,
    chunkFileNames: `[name].${format === 'es' ? 'mjs' : 'cjs'}`,
    exports: 'named', // 导出方式,对于ESM和CJS都适用
    globals: {
      react: 'React',
      'react-dom': 'ReactDOM',
      'react/jsx-runtime': 'jsxRuntime',
    },
  };

  if (preserveUseClient) {
    // 对于RSC兼容的输出,我们需要特殊处理 'use client' 指令
    // Rollup本身不会移除它,但其他插件(如terser)可能会。
    // 这里确保在每个文件的顶部添加或保留它。
    outputOptions.banner = (chunkInfo) => {
      // 检查源文件是否包含 'use client'
      // 这是一个简化的检查,更严谨的可能需要AST分析
      const sourcePath = chunkInfo.facadeModuleId;
      if (sourcePath && sourcePath.includes('.client.')) { // 约定命名规则
        return `'use client';n`;
      }
      return '';
    };
  }
  return outputOptions;
};

export default [
  // 1. ESM 输出
  {
    input: input,
    output: createOutput('es', 'dist/esm'),
    external: externals,
    plugins: [
      resolve(),
      commonjs(),
      typescript({ tsconfig: './tsconfig.json' }),
      babel({
        babelHelpers: 'bundled',
        presets: [
          ['@babel/preset-env', { modules: false, targets: { esmodules: true } }],
          '@babel/preset-react',
          '@babel/preset-typescript',
        ],
        plugins: [
          // 添加其他Babel插件,如 styled-components 或 emotion
        ],
        extensions: ['.js', '.jsx', '.ts', '.tsx'],
      }),
      terser(), // 压缩ESM输出
    ],
  },

  // 2. CJS 输出
  {
    input: input,
    output: createOutput('cjs', 'dist/cjs'),
    external: externals,
    plugins: [
      resolve(),
      commonjs(),
      typescript({ tsconfig: './tsconfig.json' }),
      babel({
        babelHelpers: 'bundled',
        presets: [
          ['@babel/preset-env', { modules: 'cjs' }], // CJS 模块
          '@babel/preset-react',
          '@babel/preset-typescript',
        ],
        plugins: [
          // 同上
        ],
        extensions: ['.js', '.jsx', '.ts', '.tsx'],
      }),
      terser(), // 压缩CJS输出
    ],
  },

  // 3. RSC 兼容的客户端组件输出 (ESM)
  // 这部分输出是专门为支持RSC的环境准备的,它必须是ESM格式,并且保留 'use client' 指令
  {
    input: input, // 同样处理所有组件,但只标记客户端组件
    output: createOutput('es', 'dist/react-server', true), // preserveUseClient = true
    external: externals,
    plugins: [
      resolve(),
      commonjs(),
      typescript({ tsconfig: './tsconfig.json' }),
      babel({
        babelHelpers: 'bundled',
        presets: [
          ['@babel/preset-env', { modules: false, targets: { esmodules: true } }],
          '@babel/preset-react',
          '@babel/preset-typescript',
        ],
        plugins: [
          // 确保 'use client' 不被移除
          // 可以在 babel 插件中配置 'pragma' 选项,或使用自定义 babel 插件
        ],
        extensions: ['.js', '.jsx', '.ts', '.tsx'],
      }),
      // 注意:对于RSC兼容的输出,terser可能会移除 'use client'。
      // 需要配置 terser 禁用对指示符的移除,或者在 banner 中重新注入
      // terser({
      //   format: {
      //     preamble: "'use client'", // 这是一个全局的注入,不够精确
      //   },
      // }),
    ],
  },

  // 4. 类型声明文件输出 (.d.ts)
  {
    input: input,
    output: {
      dir: 'dist/types',
      format: 'es',
    },
    plugins: [dts()],
  },
];

Rollup 配置解释:

  • input: 定义了 Rollup 需要处理的入口文件。对于一个组件库,通常是 src/index.ts 以及所有独立的组件文件。preserveModules: true 结合 dir 输出,可以保留源文件目录结构,这对于 Tree-shaking 和调试都很有利。
  • external: 声明外部依赖(如 react, react-dom)。这些模块不会被打包进你的库,而是由消费你的库的应用提供。这通常对应 peerDependencies
  • createOutput 函数: 封装了不同输出格式(ESM, CJS)的通用配置。
    • format: es (ESM) 或 cjs (CommonJS)。
    • dir: 输出目录,例如 dist/esmdist/cjs
    • entryFileNames, chunkFileNames: 确保输出文件使用正确的扩展名(.mjs 用于 ESM,.cjs 用于 CJS)。
    • preserveUseClient: 这是 RSC 兼容输出的关键。我们通过 banner 选项在每个被标记为客户端组件的文件顶部重新注入 'use client'更健壮的方法是,在编写组件时,直接将 'use client' 放在文件顶部,并通过构建工具确保其不被移除。
  • 插件 (Plugins):
    • @rollup/plugin-node-resolve: 允许 Rollup 解析 node_modules 中的模块。
    • @rollup/plugin-commonjs: 将 CommonJS 模块转换为 ES 模块,以便 Rollup 处理。
    • @rollup/plugin-typescript: 使用 tsconfig.json 编译 TypeScript 文件。
    • @rollup/plugin-babel: 对编译后的 JavaScript 进行进一步的降级和特性转换。对于 ESM 输出,@babel/preset-env 配置 modules: false;对于 CJS 输出,配置 modules: 'cjs'
    • @rollup/plugin-terser: 压缩生成的代码,减少文件大小。注意:terser 默认会移除所有注释,包括 'use client'。你需要配置 terser 以保留特定的注释,或者像上面 banner 示例那样重新注入。一个更精确的方法是使用 terser({ format: { comments: /@preserve|@lic|use client/i } }),或者在构建客户端组件时完全跳过 terser,让消费应用自行优化。
    • rollup-plugin-dts: 专门用于生成 .d.ts 类型声明文件。它会聚合所有源文件的类型信息并输出到指定目录。

客户端组件和服务器组件的示例

为了更好地说明,我们来看一个 Button 组件的例子。

src/components/Button/Button.tsx (客户端组件)

// src/components/Button/Button.tsx
'use client'; // 明确标记为客户端组件

import React, { useState } from 'react';

interface ButtonProps {
  children: React.ReactNode;
  onClick?: () => void;
}

export function Button({ children, onClick }: ButtonProps) {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(c => c + 1);
    onClick?.();
  };

  return (
    <button
      onClick={handleClick}
      style={{
        padding: '10px 20px',
        fontSize: '16px',
        cursor: 'pointer',
        backgroundColor: '#007bff',
        color: 'white',
        border: 'none',
        borderRadius: '5px',
      }}
    >
      {children} clicked {count} times
    </button>
  );
}

src/components/Button/index.ts (聚合导出)

这个文件聚合导出了 Button.tsx 中的客户端组件。

// src/components/Button/index.ts
export * from './Button';

src/index.ts (库的入口)

// src/index.ts
export * from './components/Button';
export * from './components/Link';
// ... 导出所有公共组件和工具函数

在Rollup的 input 配置中,我们只需包含 src/index.ts 和所有组件的 index.ts 文件。Button.tsx 会被 Button/index.ts 导入。

package.jsonexports 字段:模块解析的指挥棒

package.json 中的 exports 字段是实现多协议兼容性的核心。它允许你为不同的环境(CJS、ESM、RSC)定义不同的模块入口点,从而指导打包工具和Node.js选择最合适的模块文件。

// package.json
{
  "name": "my-universal-component-library",
  "version": "1.0.0",
  "description": "A React component library compatible with ESM, CJS, and RSC.",
  "license": "MIT",
  "main": "dist/cjs/index.cjs",                     // CJS 兼容的入口 (传统Node.js require)
  "module": "dist/esm/index.mjs",                   // ESM 兼容的入口 (传统打包工具 import)
  "types": "dist/types/index.d.ts",                 // TypeScript 类型声明主入口
  "files": [
    "dist"                                          // 发布时包含 dist 目录
  ],
  "scripts": {
    "build": "rollup -c",
    "prepublishOnly": "npm run build"
  },
  "peerDependencies": {
    "react": ">=18.0.0",
    "react-dom": ">=18.0.0"
  },
  "devDependencies": {
    // ... 开发依赖
  },
  "exports": {
    ".": {                                          // 库的根入口
      "import": {                                   // ESM 环境
        "types": "./dist/types/index.d.ts",
        "react-server": "./dist/react-server/index.mjs", // RSC 服务器端渲染时使用的客户端组件入口
        "default": "./dist/esm/index.mjs"           // 默认的 ESM 入口
      },
      "require": {                                  // CJS 环境
        "types": "./dist/types/index.d.ts",
        "default": "./dist/cjs/index.cjs"           // 默认的 CJS 入口
      },
      "types": "./dist/types/index.d.ts",           // 兜底的类型声明
      "default": "./dist/esm/index.mjs"             // 兜底的 ESM 入口
    },
    "./package.json": "./package.json",             // 允许直接导入 package.json
    "./components/Button": {                        // 单独为 Button 组件定义入口
      "import": {
        "types": "./dist/types/components/Button/index.d.ts",
        "react-server": "./dist/react-server/components/Button/index.mjs",
        "default": "./dist/esm/components/Button/index.mjs"
      },
      "require": {
        "types": "./dist/types/components/Button/index.d.ts",
        "default": "./dist/cjs/components/Button/index.cjs"
      },
      "types": "./dist/types/components/Button/index.d.ts",
      "default": "./dist/esm/components/Button/index.mjs"
    },
    // ... 为其他组件或子路径添加类似的 exports
    "./*": {                                        // 通配符匹配,处理未明确定义的子路径
      "import": {
        "types": "./dist/types/*.d.ts",
        "react-server": "./dist/react-server/*.mjs",
        "default": "./dist/esm/*.mjs"
      },
      "require": {
        "types": "./dist/types/*.d.ts",
        "default": "./dist/cjs/*.cjs"
      },
      "types": "./dist/types/*.d.ts",
      "default": "./dist/esm/*.mjs"
    }
  }
}

exports 字段详解:

  • "." (Root Export): 定义了当消费者直接导入你的库时(例如 import { Button } from 'my-universal-component-library';)的入口。
    • import 条件: 用于 ESM 环境。
      • "react-server": 这是 RSC 特有的条件。当一个 React Server Component 尝试导入你的库时,会优先匹配这个条件。它应该指向包含 'use client' 标记的客户端组件的 ESM 版本。这个路径下的组件将被视为客户端组件,并在服务器端渲染时被特殊的RSC运行时处理。
      • "types": 优先为 ESM 导入提供类型声明。
      • "default": 如果其他条件都不匹配,则回退到这个 ESM 入口。
    • require 条件: 用于 CJS 环境。
      • "types": 为 CJS 导入提供类型声明。
      • "default": 默认的 CJS 入口。
    • 顶级 "types""default": 作为备用,当没有特定条件匹配时使用。
  • 子路径导出 ("./components/Button"): 允许消费者直接导入库的特定子模块(例如 import Button from 'my-universal-component-library/components/Button';)。这对于 Tree-shaking 和按需加载非常有用。配置方式与根入口类似,为每个子路径提供 ESM、CJS 和 RSC 兼容的路径。
  • *`"./"` (Wildcard Export):** 通配符导出可以简化为每个子路径重复配置的工作。它会匹配所有未明确定义的子路径,并将其映射到相应的构建输出。
  • 优先级: exports 字段中的条件会按顺序匹配,第一个匹配的条件会被使用。这意味着 react-server 应该放在 import 条件的顶部,以确保RSC环境能够正确识别。

main, module, types 字段与 exports 的关系:

  • exports 字段存在时,它会优先于 main, module, types 字段。
  • 但为了向后兼容旧的打包工具或Node.js版本,建议仍然保留 main (CJS)、module (ESM) 和 types (TypeScript类型声明) 字段。这些字段可以作为 exports 的回退。

发布流程与注意事项

构建脚本

package.json 中添加构建脚本:

{
  "scripts": {
    "build": "rollup -c",
    "prepublishOnly": "npm run build" // 在发布前自动执行构建
  }
}

执行 npm run build 会根据 rollup.config.js 生成所有输出文件。

files 字段

package.json 中定义 files 字段,以确保只有必要的文件被发布到 npm registry,避免发布不必要的源文件、测试文件等。

{
  "files": [
    "dist"
  ]
}

Peer Dependencies

你的组件库很可能依赖于 reactreact-dom。这些通常不应该被打包进你的库,而是由使用你库的应用提供。因此,它们应该被声明为 peerDependencies

{
  "peerDependencies": {
    "react": ">=18.0.0",
    "react-dom": ">=18.0.0"
  },
  "devDependencies": {
    "react": "^18.2.0", // 在开发时安装,方便本地测试
    "react-dom": "^18.2.0",
    // ... 其他开发工具
  }
}

发布到 npm

一切准备就绪后,通过以下命令发布你的库:

npm publish --access public // 如果是公开库

版本管理

遵循 Semantic Versioning (SemVer) 规范 (MAJOR.MINOR.PATCH) 来管理你的库的版本。

  • MAJOR: 当你做了不兼容的 API 更改时。
  • MINOR: 当你新增了功能,但保持向后兼容时。
  • PATCH: 当你做了向后兼容的 bug 修复时。

总结与展望

构建一个同时兼容 ESM、CJS 和 RSC 的 React 组件库是一项复杂但至关重要的任务。通过精心配置 TypeScript 和 Rollup,并充分利用 package.json 中的 exports 字段,我们可以为不同环境下的消费者提供优化的模块入口。

这不仅确保了库的最大可用性,更重要的是,它为未来的React应用(特别是基于RSC的框架)提供了更好的性能和开发体验。随着前端生态的不断演进,组件库的通用性和适应性将成为其成功的关键。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注