Skip to content

Rollup 打包 React 组件库

rollup 是一个 JavaScript 模块打包器,在功能上要完成的事和webpack性质一样,就是将 小块代码编译成大块复杂的代码,例如 library 或应用程序。但应用程序webpack都是首选,相比之下,rollup更多是用于 library 打包,比如熟悉 的vuereactvuex、 等都是用 rollup 进行打包的。

webpack vs rollup

webpack 更适合对于应用的打包,rollup 更适合对于库的打包。

为什么不用 webpack

  • webpack 打包成ES module在目前版本5.74.0还是实验性质的 Api,见output-type, 而tree-shaking 是建立在ES module之上的,无论是作为一个 JS library 还 是组件库,这个特性在 2022 年的今天都是必备可少的(tree-shaking 按需加载)。而这个恰好是 rollup 的强项,要知道 tree-shaking 就是 rollup 率先提出的。
  • webpack 打包后得代码不精简,注入代码多,比如webpack打包的文件会添加一些自定义的模块加载代码__webpack_require__等工具函数,这些是一个library 库所不需要的。打包后的体积也比 rollup 的大。
  • rollup Api 更简单纯粹(只是相比 webpack, config option 还是挺多的,但核心api就那几个,其实大部分用不上),更容易配置出自己想要的 output

Webpack 对于代码分割、静态资源导入、热更新、dev server等有着先天优势,相比 rollup 大而全,所以更适合 App 打包。所以常说Rollup for libraries, Webpack for apps

webpack打包实现按需加载

其实 webpack 在不开启实验性质输出ES module而是输出cjs下,也能实现按需加载,但要借助 babel 插件,即像element-ui这样

js
import { Button } from 'element-ui';

打包输出的是 cjs,但是按固定的文件夹目录输出,即你的每个函数或者组件的入口都是 webpack 配置的entry,把每个组件打包成一个单独 的文件,再配合插件babel-plugin-component,就会转成

js
var button = require('element-ui/lib/button');
require('element-ui/lib/theme-chalk/button.css');

同时实现了js和css的按需加载。

老版本的 ant-design 也是类似的,使用babel-plugin-import按需加载 js 和 css 文件,最新的也是使用打包成 ES module tree-shaking了。 babel-plugin-import 插件通过引入固定路径的组件及组件 样式,替代手动 shaking 的过程。由此也可以确定打包后的文件路径(组件要求 lib/xx,样式文件要求 lib/xx/style/xx)和文件模 块CommonJs

js
import { Button } from 'antd';
ReactDOM.render(<Button>xxxx</Button>);
// then
var _button = require('antd/lib/button');
require('antd/lib/button/style/css');
ReactDOM.render(<_button>xxxx</_button>);

但都 2022 年了,这种方式直接 pass 吧。

正题rollup

first, 打包一个 js 文件

初始化

shell
mkdir "rollup-react-component-library"
cd rollup-react-component-library
pnpm init
pnpm add rollup -D
mkdir src

package.jsonscript 字段中添加并执行:

shell
"dev": "rollup -i src/index.js -o dist/bundle.js -f es"

在这段指令中:

  • -i指定要打包的文件,-i--input的缩写。

  • src/index.js-i的参数,即打包入口文件。

  • -o指定输出的文件,是--output.file--file的缩写。(如果没有这个参数,则直接输出到控制台)

  • dist/bundle.js-o的参数,即输出文件。

  • -f指定打包文件的格式,-f--format的缩写。

  • es-f的参数,表示打包文件使用 ES6 模块规范。

rollup 支持的打包文件的格式有amd, cjs, es\esm, iife, umd。其中,amd 为 AMD 标准,cjs 为 CommonJS 标准(NodeJs),esm\es 为 ES 模块标准,iife 为立即调用函数(浏览器), umd同时支持 amd、cjs 和 iife。

rollup 配置文件

在实际的工程中,都是使用配置文件,这不仅可以简化命令行操作,同时还能启用 rollup 的高级特性。

在项目根目录下创建rollup.config.js

js
export default {
  input: './src/index.js',
  output: [
    {
      file: './dist/my-lib-umd.js',
      format: 'umd',
      // 当入口文件有export时,'umd'格式必须指定name
      // 这样,在通过<script>标签引入时,才能通过name访问到export的内容。
      name: 'myLib',
    },
    {
      file: './dist/my-lib-es.js',
      format: 'es',
    },
    {
      file: './dist/my-lib-cjs.js',
      format: 'cjs',
    },
  ],
};

使用 Rollup 的配置文件,可以使用rollup --config或者rollup -c指令。

json5
//修改package.json的script字段
{
  dev: 'rollup -c', // 默认使用rollup.config.js
  dev: 'rollup -c my.config.js', // 使用自定义的配置文件,my.config.js
}

可以看出,同样的入口文件,es格式的文件体积最小。

在没有其他开发依赖,其实这样一个简单的 Js Sdk 的打包就配置完成的了。但现实往往没那么简单啊。

rollup 插件

rollup 插件只有插件扩展功能,相当于兼顾了 webpack 的loaderplugin,常用插件:

  • @rollup/plugin-node-resolve, 使用node resolution algorithm 查找 模块,让 rollup 能够识别node_modules的第三方模块。这些dependencies应该在package.json中。
  • @rollup/plugin-commonjs, rollup 默认是不支持 CommonJS 模块的,将 CommonJS 的模块转换为 ES2015 供 rollup 处理。所以 应该搭配@rollup/plugin-node-resolve使用,去打包那些 CommonJS dependencies
  • @rollup/plugin-babel, rollup 的集成 babel 插件,即使用 babel 编译文件
  • @rollup/plugin-typescript, 更好的集成 ts 的插件,比如可以做 ts=>js
  • rollup-plugin-visualizer, 类似 webpack-bundle-analyzer,但展示的 bundle-size 图没有 webpack 那个好用
  • rollup-plugin-terser, terser代码压缩
  • rollup-plugin-postcss,处理 css,支持 css 文件的加载、css 加前缀、css 压缩、scss/less 等
  • rollup-plugin-peer-deps-external,自动提取 package.json 中的依赖,再放入 external 配置中
  • @rollup/plugin-replace,打包时替换设置的字符串
  • rollup-plugin-dts 生成.d.ts文件
  • 官方插件列表

项目基本配置说明

这里最终的产出是一个按需加载的 React 组件库,使用的技术即react、typescript、@emotion/react, Material UI

  • 这里 css 方案采用的 css-in-js,打包会比其他方案更简单, React组件库的 css 方案没有一种占统治地位,现有的各种方案都有各 自的优缺点,因此选择一种合适的样式方案需要综合考虑很多方面。推荐文 章React 组件库 CSS 样式方案分析。后面会讲一下怎么处理要单独输出 style 文件。
  • Material UI 引入是想更好的测试 tree-shaking,二次封装组件库也是实际业务常见的需求。

组件结构,button 简单示列:

md
|-- src 
    |-- Button 
    | |-- Button.tsx 
    | |-- index.ts
    | |-- types.d.ts 
|-- index.ts

Button.tsx

tsx
import * as React from 'react';
import { css } from '@emotion/react';

import type { ButtonProps } from './types';

const color = 'white';

const cssBtn = css`
  padding: 8px;
  background-color: hotpink;
  font-size: 14px;
  border-radius: 4px;
  &:hover {
    color: ${color};
  }
  border: 1px solid gray;
  transition: color 0.3s ease-in-out;
`;

const Button: React.FC<ButtonProps> = () => <button css={cssBtn}>Hover to change color.</button>;

export default Button;

types.d.ts

ts
export type ButtonProps = {
  text: string;
};

index.ts

ts
export { default } from './Button';
export * from './Button';
export type { ButtonProps } from './types';

src/index.ts

ts
export { default as Button } from './Button';
export * from './Button';
// export type { ButtonProps } from './Button';

typescript配置

这里打包时输出的类型文件时使用tsc构建输出的,

tsconfig.type.json

json5
{
  "extends": "./tsconfig.json",
  "include": ["src"],
  "exclude": ["**/*__tests__"],
  "compilerOptions": {
    // 允许输出
    "noEmit": false,
    "declaration": true,
    // 只输出类型
    "emitDeclarationOnly": true,
    "rootDir": "./src",
    "outDir": "./dist"
  }
}

package.json

json
{
  "script": {
    "build:type": "tsc -b --force --verbose tsconfig.type.json"
  }
}
  • tsc: -b 即代表--build,tsc -b还支持其它一些选项:
    • --verbose:打印详细的日志(可以与其它标记一起使用)
    • --dry: 显示将要执行的操作但是并不真正进行这些操作
    • --clean: 删除指定工程的输出(可以与--dry一起使用)
    • --force: 把所有工程当作非最新版本对待
    • --watch: 观察模式(可以与--verbose一起使用)

babel

babel配置可见后续@rollup/plugin-babel小节。

rollup配置

先安装rollup两个必用到基础插件,作用见上插件列表说明。

shell
pnpm add @rollup/plugin-node-resolve @rollup/plugin-commonjs -D

怎么处理Typescript

借助 @babel/preset-typescript 的能力,直接转译 ts 代码,一步到位,而不是采用 @rollup/plugin-typescript 先将 ts 转成 js 然后再交给 @rollup/plugin-babel 再转换一次,增加了适配成本。

如何排除公共依赖不打包仅组件中

使用 external 配置,将不需要打包依赖加进去;有些时候会使用依赖的子一级文件 ,所以这个 external 可以是一个函数,其参数是引用的依赖地址,返回值是一个 boolean true 则表示,这个依赖不会被打包进去. 可以使用rollup-plugin-peer-deps-external插件。

@rollup/plugin-babel

用于转换 es6 语法、ts、react-jsx。安装

shell
pnpm add @rollup/plugin-babel -D
pnpm add @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript -D

babel.config.js配置如下

js
module.exports = {
  presets: [
    '@babel/preset-env',
    [
      '@babel/preset-react',
      {
        runtime: 'automatic',
        importSource: '@emotion/react',
      },
    ],
    '@babel/preset-typescript',
  ],
  plugins: ['@emotion', '@babel/plugin-transform-runtime'],
};

这里@babel/plugin-transform-runtime 用于避免污染全局函数(不是必须要用到,但作为类库最好要加上)。 rollup 中的配置如下

js
import babel from '@rollup/plugin-babel';

export default {
  plugins: [
    babel({
      extensions,
      babelHelpers: 'runtime',
      exclude: 'node_modules/**',
    }),
  ],
};
plugin-transform-runtime

A plugin that enables the re-use of Babel's injected helper code to save on codesize.

为了浏览器向下兼容,会使用 @babel/preset-env 对每个文件的 ES6 语法进行转换,我们称之为辅助函数。

但样这做存在一个问题。在正常的前端工程开发的时候,少则几十个 js 文件,多则上千个。如果每个文件里都使用了 class 类语法, 那会导致每个转换后的文件上部都会注入这些相同的函数声明。这会用构建工具打包出来的包非常大,包含大量重复代码。一个思路就是 ,把这些函数声明都放在一个npm包里,需要使用的时候直接从这个包里引入到我们的文件里。这样即使上千个文件,也会从相同地包 里引用这些函数。通过 rollup/webpack 这一类的构建工具打包的时候,只会把使用到的 npm 包里的函数引入一次,这样就做到了复 用,减少了体积。

安装 @babel/runtime包提供辅助函数模块,且是安装到dependencies,安装 Babel 插件 @babel/plugin-transform-runtime 来 自动替换辅助函数。

shell
pnpm add @babel/runtime
pnpm add @babel/plugin-transform-runtime -D

效果如下:即如是这样导入 help 函数的import _defineProperty from '@babel/runtime/helpers/defineProperty';

完整配置

package.json

json
{
  "files": ["dist"],
  "main": "dist/index.cjs.js",
  "module": "dist/index.esm.js",
  "types": "dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/index.esm.js",
      "require": "./dist/index.cjs.js"
    }
  },
  "sideEffects": false,
  "scripts": {
    "copy-d.ts": "copyfiles -u 1 src/**/*.d.ts dist",
    "build:type": "tsc -b --force --verbose tsconfig.type.json && pnpm run copy-d.ts",
    "build:dev": "pnpm run build:type && rollup -c -w --environment NODE_ENV:development",
    "build:all": "rm -rf dist && pnpm run build:type && rollup -c --environment NODE_ENV:production"
  },
  "peerDependencies": {
    "react": ">=16.8.0",
    "react-dom": ">=16.8.0"
  },
  "dependencies": {
    "@babel/runtime": "^7.18.9",
    "@emotion/react": "^11.10.4",
    "@emotion/styled": "^11.10.4",
    "@mui/icons-material": "^5.10.3",
    "@mui/material": "^5.10.3"
  }
}
  • files: 定义你的 NPM 包中要包含哪些文件
  • main: 定义 CommonJS 入口,main 是一个当打包工具或运行时不支持 package.json#exports 时的兜底方案;如果打包工具或运行 时支持 package exports,则不会使用 mainmain 应该指向一个兼容 CommonJS 格式的产出;它应该与 package exports 中的 require 保持一致。
  • types: 定义 TypeScript 类型
  • exports: 为你的库定义公共 API
  • sideEffects: 来允许 treeshaking, false代表所有模块都是“纯”的,没有副作用
  • peerDependencies:如果你依赖别的框架或库,将它设置为 peer dependency

更多的详细自动可参 考打包 JavaScript 库的现代化指南

  • copyfiles: 后续会讲到,主要是 tsc build 时不会处理.d.ts文件,单独复制。
  • tsc: -b 即代表--build,tsc -b还支持其它一些选项:
    • --verbose:打印详细的日志(可以与其它标记一起使用)
    • --dry: 显示将要执行的操作但是并不真正进行这些操作
    • --clean: 删除指定工程的输出(可以与--dry一起使用)
    • --force: 把所有工程当作非最新版本对待
    • --watch : 观察模式(可以与--verbose一起使用)
  • rollup: -w-watch监听模式
  • rollup: --environment NODE_ENV:development, 设置环境变量environment-values

rollup.config.js

js
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import babel from '@rollup/plugin-babel';
import replace from '@rollup/plugin-replace';
import externalDep from 'rollup-plugin-peer-deps-external';
import { terser } from 'rollup-plugin-terser';
import visualizer from 'rollup-plugin-visualizer';

const input = './src/index.ts';
const extensions = ['.ts', '.tsx', '.js', '.jsx'];
const isProd = process.env.NODE_ENV === 'production';

export default {
    input,
    output: [
      {
        file: 'dist/index.esm.js',
        format: 'esm',
        sourcemap: true,
      },
      {
        file: 'dist/index.cjs.js',
        format: 'cjs',
        sourcemap: true,
      },
    ],
    plugins: [
      // 设置打入output file,包括peerDependencies和dependencies
      externalDep({
        // dependencies也不打包进output
        includeDependencies: true,
      }),
      resolve({
        extensions,
      }),
      commonjs(),
      babel({
        extensions,
        babelHelpers: 'runtime',
        exclude: 'node_modules/**',
      }),
      // production时压缩
      isProd &&
      terser({
        output: {
          comments: false,
        },
        compress: {
          // drop_console: true,
        },
      }),
      !isProd &&
      visualizer({
        filename: 'rollup-bundle-report.html',
      }),
    ],
};

输出大概是这样:

虽然是输出的单文件的打包文件,但支持 es module,也是支持 tree-shaking 的。这样一个组件库的打包就基本完成了。

如何支持 scss/less

组件的样式方案是样式和逻辑分离,这种方案的优点有:

  • 适用性广泛,可以支持组件库使用者的各种开发环境。

  • 不限制组件库的技术栈,同一套样式可以用在基于多个框架的组件库上。

  • 无需考虑对 SSR(服务端渲染)的支持,对外提供的是 CSS 文件,因此 SSR 流程完全交给组件库的使用者控制。

  • 可以直接对外提供 less、sass 等源文件,便于外部覆盖变量,实现主题定制或换肤等功能。但是这种方案也有一些问题:

  • 需要使用者手动引入样式文件。如果直接引入了完整的 CSS 文件,而在实际使用中并没有用到组件库里的全部组件,就会造成一些无 用的样式被打包进项目中。

  • 让组件库支持 CSS 按需引入的功能会比较复杂,既需要组件库的开发者在打包流程和产物上进行处理,又需要使用者按照一定规则引 入样式文件。首先组件库开发者需要定一套样式文件的目录组织规范,使其能在打包流程中支持以组件为单位打包样式文件,之后使用 者就可以按需手动引入对应组件的样式文件。对于具有特定目录组织规范的组件库,目前已经有插件可以在编译阶段辅助生成引入样式 的 import 语句,例如 babel-plugin-importunplugin-vue-components 等。如开篇讲的列子

  • 如果组件库内部的组件存在引用关系,为了实现按需引入,打包出来的组件的样式可能会存在冗余。

如何配置

安装插件,rollup-plugin-postcss支持 css 文件的加载、css 加前缀、css 压缩、对 scss/less 的支持,用这个就能搞定。

shell
pnpm add postcss rollup-plugin-postcss --dev

添加到 rollup

js
import postcss from 'rollup-plugin-postcss';
export default {
  // input: ...,
  // output: ...,
  plugins: [postcss()],
};

这样子只会在 umd 下生效,css 样式生成style标签内联到head中。在工程中使用一般需要额外引入样式文件。所以需要抽离单独的 css 文件。rollup-plugin-postcss配置即可,另外还要启用modules, 即CSS modules,隔离组件库样式。

js
postcss({
  extract: 'index.css',
  // 压缩
  minimize: true,
  // Enable CSS modules
  modules: true,
});

启用CSS modules后,样式引用要改为:

tsx
import * as React from 'react';
import type { AlertProps } from './types';
import css from './style.css';

const Alert: React.FC<AlertProps> = () => {
  return <div className={css.alert}>Alert</div>;
};

export default Alert;

输出的className才能和 index.css 中匹配

index.css

css
.style_alert__ZRjcy {
  background: #535bf2;
}
  • 额外配置
  1. css modules typescript 报错,新增文件global.d.ts:
ts
declare module '*.css' {
  const css: { [key: string]: string };
  export default css;
}
  1. 在测试包时,import 'rollup-react-component-library/dist/index.css'引入distindex.css时 webpack 出现错误找不到模块;

参考问题Declare .css in package.json exports,package.json 新增配置即可

json
{
  "exports": {
    ".": {
      "import": "./dist/index.esm.js",
      "require": "./dist/index.cjs.js"
    },
    "./dist/index.css": {
      "import": "./dist/index.css",
      "require": "./dist/index.css"
    }
  },
  "sideEffects": ["*.sass", "*.scss", "*.css"]
}
  1. sideEffects 要从false => 如上,不然 webpack会把import的css tree-shaking 掉

css 加前缀

借助autoprefixer插件来给css3的一些属性加前缀。安装pnpm i autoprefixer@8.0.0 -D,配置:

js
import postcss from 'rollup-plugin-postcss';
import autoprefixer from 'autoprefixer';
export default {
  plugins: [
    postcss({
      plugins: [autoprefixer()],
    }),
  ],
};

使用autoprefixer除了以上配置,还需要配置browserslist,有 2 种方式,一种是建立.browserslistrc文件,另一种是直接在 package.json 里面配置,我们在 package.json 中,添加browserslist字段。

json
{
  "browserslist": ["defaults", "not ie < 8", "last 2 versions", "> 1%", "iOS 7", "last 3 iOS versions"]
}

css 压缩

即开启minimize选项。实际上使用的cssnano压缩。

支持 scss/less

项目中一般不会写 css,而是用scssless等预编译器。rollup-plugin-postcss默认集成了对scss、less、stylus的支持。但额外需要 安装依赖,如官网所示:

  • For Sass install node-sass: pnpm add node-sass --dev
  • For Stylus Install stylus: pnpm add stylus --dev
  • For Less Install less: pnpm add less --dev

以上的样式和逻辑分离方案才基本可用,可以发现比css-in-js麻烦多了,而且现在还未实现样式的按需加载,要想使用者使用方便,就要按组件目录结构输出样式目录,用于sideEffects配置和babel插件(这块开头有讲过)。

组件按目录结构输出

rollup-plugin-postcss => rollup-plugin-styles

上面的rollup-plugin-postcss 这个插件只会把所有组件的样式抽离抽离出一个单独的文件,适合输出到dist文件夹,但这个场景下, 在查找一番后,要使用rollup-plugin-styles 这个插件,它可以与 以下会用到的rollup output的preserveModules 做适配,即输出按照组件维度拆分的 css 文件,不过要想正确地实现这个功能需要做一些配置。

shell
pnpm add rollup-plugin-styles -D

output.preserveModules

这里组件按原始目录结构输出使用 output.preserveModules , output.preserveModulesRoot,中文官网还没有配置,所以文档还是看英文的最新,关于这个配置,看了挺多 比较常见的组件库,都没有用这个配置输出,一般都是用rollup输出单一打包文件,后来是翻到一下文章有提到这样也可以实现,而这个配置主要是

Instead of creating as few chunks as possible, this mode will create separate chunks for all modules using the original module names as file names. Requires the output.dir option. Tree-shaking will still be applied, suppressing files that are not used by the provided entry points or do not have side effects when executed. This mode can be used to transform a file structure to a different module format.

和常规模式尽可能创建更少的chunk不一样,这个模式会按原始的模块划分,分别输出文件,要求output.dir必须声明,即输出的是目录,之前的out.file就不能用了,且Tree-shaking依然适用。

实现主要思路是查找每个component的入口,并放入input数组,再开启preserveModules模式。

rollup.esm.js具体配置

js
// 同之前
import styled from 'rollup-plugin-styles';

const extensions = ['.ts', '.tsx', '.js', '.jsx'];
const isProd = process.env.NODE_ENV === 'production';

const entryFile = 'src/index.ts';
// 可以时在多一级也可以,'src/components'
const componentDir = 'src';
const componentEntryFiles = fs
  .readdirSync(path.resolve(componentDir), {
    withFileTypes: true,
  })
  .filter((dirent) => dirent.isDirectory() && /^[A-Z]\w*/.test(dirent.name))
  .map((dirent) => `${componentDir}/${dirent.name}/index.ts`);

export default [
  {
    input: [entryFile, ...componentEntryFiles],
    output: [
      {
        dir: 'esm',
        format: 'esm',
        sourcemap: true,
        preserveModules: true,
        preserveModulesRoot: 'src',
        exports: 'named',
        assetFileNames: ({ name }) => {
          // console.log(name);
          // 抽离后的样式文件会作为 asset 输出,这里可以配置一下 样式文件的输出位置(为 babel-plugin-import 做准备)
          const { ext, dir, base } = path.parse(name);
          // console.log(ext);

          if (ext !== '.css') return '[name].[ext]';
          // 输出到style目录,便于tree-shaking
          return path.join(dir, 'style', base);
        },
      },
    ],
    plugins: [
      // 其他插件一样
      styled({
        // 抽出css,而不是打包进js
        mode: 'extract',
        // use: ['less'],
        // less: {
        //   javascriptEnabled: true,
        // },
        extensions: ['.less', '.css'],
        minimize: true,
        // CSS Modules
        modules: false,
        sourceMap: true,
        url: {
          inline: true,
        },
        onExtract: (data) => {
          // 以下操作用来确保每个组件目录只输出一个 index.css,实际上每一个子级组件都会输出样式文件,index.css 会包含所有子组件的样式
          const { css, name, map } = data;
          const { base } = path.parse(name);
          if (base !== 'index.css') return false;
          return true;
        },
      }),
    ],
  },
];

rollup-plugin-styles文档不全,目前发现使用css module的模式下,代码并没有抽离,目前还是使用的常规class。

另外还需要修改package.json才能使输出生效

json
{
  "files": ["dist", "esm"],
  "main": "dist/index.cjs.js",
  "module": "esm/index.js",
  "types": "dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./esm/index.js",
      "require": "./dist/index.cjs.js"
    }
  },
  "sideEffects": [
    "esm/**/style/*",
    "lib/**/style/*",
    "*.css"
  ]
}

再搭配babel-plugin-import即可,可以看到整个实现真的麻烦,所以还是选择css-in-js把,也有支持零运行的框架,比如Vanilla-extract

如何调试

  1. monorepo架构下的项目,比如pnpm+workspace,直接安装即可

比如在本地把pkg1安装到 pkg2

shell
pnpm install @sysuke/pkg1 --filter @sysuke/pkg2
  1. pnpm/yarn link, 不好用,会出现Invalid Hook Call Warning的错误
  2. webpack alias

webpack.config.js

shell
webpack: {
  resolve: {
    alias: {
      'react': path.resolve(__dirname, 'node_modules/react')
    }
  }
}
  1. yalc

可参考文章居然有比 npm link 更好的调试?

总结

使用rollup打包一个可用的react组件库基本实现,源代码见rollup-react-component-library,其实一些常见的组件库,如muiant-design, naive-ui等,在打包成es module,按模块 打包输出时,都是用的其他方式,比如babel,tsc等,后面会补一篇文章去解析其他开源的组件库是如何打包的。

踩坑记录

rollup插件是数组,有的插件使用是有顺序的

比如 @rollup/plugin-babel就要求 @rollup/plugin-commonjs必须在前面,所以要好好看插件文档再使用。

rollup-plugin-dts输出单个文件的类型文件,不适合组件库这种类型输出很多的library,不如使用tsc,还能按目录输出,无其他依赖配置也简单

tsc --build ...构建输出的类型文件,并不会处理.d.ts文件

即如果 src 的类型文件是.d.ts结尾的,编译构建时tsc不会处理,导致文件不会复制到outDir。解决方式主要有 2 种

  • .d.ts重命名为.ts, 正确的方式,类型文件不该.d.ts,一般全局的才用这个。
  • 构建阶段单独处理,复制文件到输出目录,这里使用的copyfiles
json
{
  "scripts": {
    "copy-d.ts": "copyfiles -u 1 src/**/*.d.ts dist"
  }
}

其实类型文件不应该以.d.ts声明,除了能限制在.d.ts中不写非类型的代码,其他的基本上都是不好的地方,比如.d.ts是全局共享的,不需要exportimport就能使用。

TypeScript与ECMAScript 2015一样,任何包含顶级import或者export的文件都被当成一个模块。相反地,如果一个文件不带有顶级的import或者export声明,那么它的内容被视为全局可见的(因此对模块也是可见的)(全局就是以tsconfig.json文件为根目录的所有文件都能访问到) 所以在.d.ts如果使用了exportimport就失去了global type的属性。

tsc 编译的后,映射的路径(即配置的path )不会处理,将导致编译后的代码找不到模块。

TypeScript 编译时并不会重写 module pathspath是设计用来让 typescript 理解其他 bundle 工具的路径别名,比如 webpack 的 resolve alias,项目中我选的以下方案的 tsc-alias,只需配置到script,本demo暂未用到。

不止是peerDependenciesexternal,dependencies的依赖也需要。

在最开始使用rollup打包时,只把peerDependencies设置到external了,发现打包体积比较大,且在使用output.preserveModules时有问题,打包的结果还会多个 node_modules目录,当时完全找不到头绪,后来看了一些组件库源码,比如 ant-design:

其实dependencies也要external掉(当然不是amd格式时),使用时这些依赖也会安装。

如果在已经使用了babel + @babel/preset-typescript时, rollup-plugin-typescript2插件不需要,转换 ts 同样的事没必要做 2 次。

@babel/runtime

  • 应该作为dependency,并且当作external
  • 配置到external, external: ["@babel/runtime"] 这样配置是不工作的,得external: [/@babel\/runtime/]或者一个函数(external: id => id.includes('@babel/runtime'),@emotion/react也是
  • @babel/plugin-transform-runtimeuseESModules7.13.0就遗弃了,不用去考虑 打包时esmcjs这块有影响,去区分对待

文档其实这块写得很清楚@rollup/plugin-babel.babelHelpers.runtime

sideEffects

package.json 中声明 sideEffects 属性

在一个纯粹的 ESM 模块世界中,识别出哪些文件有副作用很简单。然而,我们的项目无法达到这种纯度,所以,此时有必要向 webpack 的 compiler 提供提示哪些代码是“纯粹部分”。 —— 《webpack 文档》

注意:样式部分是有副作用的!即不应该被 tree-shaking!

若是直接声明 sideEffectsfalse,那么打包时将不包括样式!所以应该像下面这样配置:

json
{
  "sideEffects": ["*.sass", "*.scss", "*.css"]
}

@rollup/plugin-multi-entry不好用,自己写脚本处理多入口吧

简单试了下,会有些额外的输出,比如这个_virtual文件夹。

  • entryFileName不支持函数
  • index.ts 输出到一个_virtual文件夹,不知道怎么出现的, 可能用这个插件@rollup/plugin-virtual解决
  • 只输出了 entry index ,component 目录没有按原始目录输出文件

output.preserveModules也不太好用

虽然用这个配置成功按模块输出,但有的情况下有问题,比如上面也提到过,dependencies不排除(external)时,会多输出文件夹,比如node_modules

当把dependencies打包入output时,比如会发现react同时打入了react.development.jsreact.production.js

使用@rollup/plugin-replace解决,参考react官网,Optimizing Performance

Reference

Last updated:

In case I don't see you. Good afternoon, good evening, and good night.