🎢从零打造React组件库!Rollup+TS+React19保姆级上车指南(二)🚀

366 阅读8分钟

摘要

"旨在通过从0到1式,通过一个个依赖,搭建起来一个正常的react + rollup项目,帮大家理解项目运行机制,以及基于前端工程化的基础搭建流程"

在上一篇文章中,我们已经完成了一个最基础的react项目,基于rollup打包构建,以及开发模式的服务监听,但离组件库的成品效果还差一些

分析

组件库最少所需具备的特性:

  • 组件仓库
  • 开发模式:全量打包(支持提供热更新、实时预览的开发体验)
  • 生产模式:多入口打包,进行组件批次的打包构建,优化构建产物,生成多种模块格式的发布包,支持业务项目按需引入组件
  • 需要支持处理ts、css、tsx编译特性

一、🚦 组件仓库规范定义

|-- src
    |-- components
        |-- index.ts // 组件声明,暴露
        |-- Button // 组件模块定义
            |-- Button.tsx // 组件核心逻辑封装
            |-- index.ts // 组件注册/暴露
            |-- style
                |-- index.css // 组件样式模块

index.ts

export * from './Button'
export * from './Input'

Button.tsx

import React from 'react'
import './style/index.css'

export interface ViButtonProps {
    children: React.ReactNode
    onClick: () => void
}

const ViButton: React.FC<ViButtonProps> = ({ children, onClick }) => {
    return (
        <button className={'violet-btn'} onClick={onClick}>
            {children}
        </button>
    )
}
export default ViButton

Button/index.ts

export { default as ViButton } from './Button'
export type { ViButtonProps } from './Button'

二、📦 开发模式打包规范

开发的时候我们需要对每个组件进行打包,同样的,需要输出esm/cjs规范的打包产物(esm支持import模块语法进行使用)

// 基础插件配置
const basePlugins = [
  replace({ /* 环境变量注入 */ }),
  babel({ /* Babel 转换 */ }),
  resolve({ /* 模块解析 */ }),
  commonjs() /* CommonJS 转换 */
]

// 开发环境插件 - fn避免重复调用(如果是对象,isProduction ? 1 : devConfig这个判断的时候就会执行serve,需要用方法返回来避免调用)
const getDevPlugins = () => {
    return [
        // 开发模式的时候,本地需要解析css,所以需要这个插件,生产环境不需要(使用gulp进行打包)
        postcss({
            modules: false, // 禁用 CSS Modules
            extract: false, // 提取为单独文件
            inject: true, // 注入到 JavaScript 文件中,会强制使用css.js的形式进行css依赖注入
            minimize: false, // 生产环境压缩
            plugins: [
                autoprefixer()
            ]
        }),
        // 我们可以搭建一个本地服务进行辅助开发,并热更新dist打包产物
        serve({
            open: true,
            contentBase: [
                path.resolve(__dirname),
                path.resolve(__dirname, 'dist'),
                path.resolve(__dirname, 'public')
            ],
            port: 3000,
            host: 'localhost'
        }),
        livereload({
            watch: 'dist'
        })
    ]
}

// 生产环境特有配置
const productionPlugins = [
  typescript({ /* TypeScript 编译 */ })
]

关于esm与cjs

在现代 JavaScript 开发中,esm(ES Modules)和 cjs(CommonJS)是两种常见的模块化系统,它们在构建和发布 JavaScript 组件库时扮演着非常重要的角色。理解这两种格式的作用和区别,有助于你决定如何发布你的组件库,使其适配不同的使用场景。

ESM(ECMAScript Modules)

ESM 是基于 ES6(ECMAScript 2015)引入的模块化系统,也是现代 JavaScript 标准的模块化方式。它通过 import 和 export 关键字来定义模块的导入和导出。

优点:

  • 静态分析:ESM 是静态的,这意味着导入和导出在编译时就能确定,这使得现代构建工具(如 Webpack、Rollup)能够进行更好的优化(比如 tree-shaking)。

  • 更好的支持未来的 JavaScript 特性:ESM 是 JavaScript 官方的模块系统,未来的 JavaScript 特性(如异步加载模块)将基于 ESM。

  • 浏览器原生支持:现代浏览器原生支持 ESM,可以直接通过 <script type="module"> 引入模块,而不需要打包。

使用场景:

  • 适用于现代 JavaScript 环境,如前端开发(React、Vue、Angular 等现代框架)。
  • 适用于使用 tree-shaking(消除未使用代码)进行优化的项目。
  • 在使用 import 和 export 时,能够减少额外的构建步骤,浏览器能够直接理解。

eg:

// 导出
export const MyComponent = () => {
  return <div>Hello World</div>;
};

// 导入
import { MyComponent } from 'my-library';

CJS(CommonJS)

CJS 是最早的模块化规范,它在 Node.js 环境中非常流行。它使用 require 和 module.exports 来导入和导出模块。

优点:

  • 兼容性:CJS 是 Node.js 的标准模块化格式,因此在大多数 Node.js 项目中都可以直接使用。

  • 动态导入:CJS 支持在代码中动态加载模块(如 require() 语句可以在任何地方调用),这对于一些复杂的动态逻辑来说非常有用。

使用场景:

  • 适用于 Node.js 后端应用程序 和一些老旧的 CommonJS 环境,或者不支持 ESM 的环境。
  • 某些老旧的构建工具和库仍然只支持 CJS 格式。

eg:

// 导出
module.exports = {
  MyComponent: () => {
    return 'Hello World';
  }
};

// 导入
const { MyComponent } = require('my-library');

运行

pnpm dev

image.png

image.png

三、📦 生产模式打包规范

生产模式下,我们需要对多组件进行打包,但是不同的是,css我们不使用rolluo的插件postcss进行打包,转而使用gulp进行打包(源自antd项目打包流程借鉴),为什么使用Gulp呢?

一、Gulp特性:

Gulp 是一个任务自动化工具,提供了 强大的插件系统灵活的任务配置,适用于许多复杂的 CSS 处理任务。而 Rollup 本身是一个模块打包器,尽管也能处理 CSS,但它并不是专门为 CSS 处理设计的,它的 CSS 处理能力相较于 Gulp 更加基础。

它的优点:

  • 任务自动化和流水线处理:Gulp 可以用来处理多种任务,像是压缩 CSS、CSS 分离、自动添加前缀、编译 Sass/LESS 等,非常灵活。
  • 插件扩展性:Gulp 拥有丰富的插件生态系统,比如 gulp-postcss,gulp-sass,gulp-cssnano 等,能够灵活组合多个插件来实现不同的 CSS 处理需求。
  • 并行任务执行:Gulp 允许并行执行多个任务,这对于大项目的构建非常有帮助。

二、Rollup对css的支持

Rollup 是一个专注于 模块打包 的工具,虽然它支持通过 rollup-plugin-postcss 来处理 CSS,但 Rollup 的设计初衷并不是为了处理复杂的 CSS 任务,而是主要处理模块化 JavaScript。Rollup 对 CSS 的处理更多是为了将其作为模块导入、按需提取,或者是打包成一个最终文件,而不是执行复杂的样式任务。

Rollup 的 CSS 处理能力:

  • 简单的 CSS 打包:通过 rollup-plugin-postcss,Rollup 可以处理 CSS、将其提取为独立文件、自动添加前缀等。
  • 按需提取:Rollup 支持通过 extract 配置将 CSS 提取到独立文件,但它的处理逻辑相对较简单,不如 Gulp 灵活。
  • 不支持复杂的任务流水线:比如,自动将 Sass 或 LESS 编译成 CSS,或者需要多个 CSS 处理步骤的组合,Rollup 处理起来并不如 Gulp 那么容易。

三、结合使用 Gulp 和 Rollup

通常的做法是,使用 Gulp 来进行 资源处理,如 CSS、Sass/LESS 编译、图像压缩、文件合并等,然后将处理后的资源交给 Rollup 进行 模块打包。这种做法的优点是能够利用 Gulp 强大的任务处理和 Rollup 强大的模块打包功能。

Gulp 和 Rollup 的结合:

  1. Gulp 负责:

    • 处理 CSS(例如编译 Sass、压缩、自动添加前缀等)。
    • 其他资源(如图片、字体)的优化和压缩。
  2. Rollup 负责:

    • 打包模块化的 JavaScript 代码。
    • 将打包后的 CSS 提取到单独文件中,或者将其内联到 JavaScript 中。

这种组合能够让你充分发挥两者的优势,既能享受 Gulp 灵活的任务自动化,又能利用 Rollup 高效的 JavaScript 模块打包。

rollup.config.js

const productionPlugins = [
    typescript({
        tsconfig: './tsconfig.json', // 主配置,用于 JS 构建
        declaration: false,          // 禁止 Rollup 插件生成类型
    })
]

const resultConfig = {
    input: [
        'src/main.tsx',
        'src/components/index.ts'
        // ...getComponentOutput() // 直接使用函数返回的数组,不需要额外的解构赋值
    ],
    output: [
        {
            dir: 'dist/esm',
            format: 'esm',
            preserveModules: true, // 保留模块结构
            preserveModulesRoot: 'src' // 保留模块的根目录
        },
        {
            dir: 'dist/cjs',
            format: 'cjs',
            preserveModules: true, // 保留模块结构
            preserveModulesRoot: 'src' // 保留模块的根目录
        }
    ],
    plugins: [
        ...basePlugins,
        ...(isProduction ? productionPlugins : getDevPlugins())
    ],
    // 不这样设置:打包结果会把 .pnpm 的软链接结构直接保留到了输出目录里,会产生virtual目录和node_modules目录
    external: isProduction ? ((id) =>
        /^react/.test(id) ||
        /^react-dom/.test(id) ||
        /\.css$/.test(id)  // 新增:排除所有CSS文件
    ) : []
}
export default resultConfig

构建脚本如下:

  "scripts": {
    "clean": "rm -rf dist",
    "dev": "cross-env NODE_ENV=development rollup -c -w",
    "build:types": "tsc -p tsconfig.build.json",
    "build": "pnpm clean && pnpm build:css && cross-env NODE_ENV=production rollup -c --no-watch && pnpm build:types",
    "build:css": "gulp"
  },

gulpfile.js

需要安装依赖:

pnpm add gulp gulp-postcss -D

多任务进行结合打包,es和cjs产物的css样式输出,这边的路径需要提前约定规范

import gulp from 'gulp';
import postcss from 'gulp-postcss';
import autoprefixer from 'autoprefixer';
import cssnano from 'cssnano';
// import path from 'path';

function processESMCSS() {
return gulp.src('src/components/**/style/*.css')
  .pipe(postcss([autoprefixer(), cssnano()]))
  .pipe(gulp.dest((file) => 'dist/esm/components'));
}

function processCJSCSS() {
return gulp.src('src/components/**/style/*.css')
  .pipe(postcss([autoprefixer(), cssnano()]))
  .pipe(gulp.dest((file) => 'dist/cjs/components'));
}

export default gulp.series(
gulp.parallel(processESMCSS, processCJSCSS)
);

运行 pnpm build,生成打包产物,并生成ts的类型声明文件(方便ide进行编译提示检查)

image.png

四、测试构建

运行link进行全局软链注册

pnpm link --global

在业务项目中进行使用

pnpm link violet-ui --global

image.png

打包产物只会有这个组件,不包含全量资源(后续其他组件模块可依赖babel-plugin-import辅助css模块的注入,这里由于组件内部显式引入css所以自带了,并不需要)

image.png

四、🚀 演进方向

  • 文档系统 :集成 Storybook 或 Dumi
  • 主题系统 :实现类似 Ant Design 的动态主题
  • 测试体系 :添加 Jest + React Testing Library
  • Monorepo :迁移到 pnpm workspace

github地址参考:github.com/chenhebin/v…