Tree Shaking in React How to write a tree-shakable component library

2025-06-07

React 中的 Tree Shaking如何编写可摇树的组件库

总结

如果您只想开门见山,可以在 GitHub 上查看、克隆和分叉最终产品:

https://github.com/LukasBombach/tree-shakable-component-library

今年年初,我被一家新公司聘用,负责一个尚未公开的新项目。我们想研究设计系统和组件库。

这个话题本身对我们来说并不陌生,但我们自己实现它却很困难。我接到的任务是找到一个设置,

  • 让我们在 monorepo 中工作,其中我们的 UI 库是一个包,我们的应用程序是另一个包
  • UI 库必须是可摇树的,因为我们非常注重性能


root
 ∟ packages
    ∟ app
    ∟ ui-library


Enter fullscreen mode Exit fullscreen mode

第二点非常重要,我再详细解释一下。如果你要创建一个组件库,然后直接把所有东西都打包起来,你最终很可能只会创建一个 CommonJS(CJS)格式的文件。

CommonJS 和 ES 模块

如今,市面上有多种 JS 文件格式,其中大多数仍在积极使用。您可以阅读@iggredible的这篇精彩文章,了解这些不同的格式。

https://dev.to/iggredible/what-the-heck-are-cjs-amd-umd-and-esm-ikm

非深入版本是,有一种常用的格式,恰巧名为 CommonJS(或 CJS),还有一种大多数人都熟悉的新型格式,即 ES 模块(ESM)。

CJS 是 Node.js 传统上使用的语言。ESM 较新且标准化(CJS 不是),未来可能会成为 Node.js 的语言格式。ESM自 Node.js 12 起即可原生使用,目前处于实验阶段

无论如何,使用 Webpack/Babel 或 TypeScript 的你都会熟悉这种格式。这种格式允许你编写



import X from "y";

export Z;


Enter fullscreen mode Exit fullscreen mode

👆 ESM

而不是 CJS👇



const X = require("y")

module.exports = Z;


Enter fullscreen mode Exit fullscreen mode

那么这到底为什么重要呢?

因为 tree-shaking!

问题

如果你将 UI 库捆绑在一个包含以下内容的 CJS 文件中,假设

  • 标题
  • 一个按钮
  • 一张卡片和
  • 一张图片

而且你只需从库中导入一个组件到你的应用中,整个库都会被加载并打包。这意味着,即使你只在应用中使用了按钮,你的整个 UI 库,包括标题、卡片和图片,最终都会被打包到你的应用中,这会让你的应用变得非常庞大。加载时间、解析时间和执行时间都可能会大幅增加

解决方案

……当然是 tree-shaking。ES模块让打包工具能够对代码进行 tree-shaking。如果我没记错的话,这是因为 ESM 语法允许打包工具静态地检查代码的哪些部分被使用了,哪些部分没有被使用,这比较难,require因为它可以以更动态的方式使用,比如这样



var my_lib;
if (Math.random()) {
    my_lib = require('foo');
} else {
    my_lib = require('bar');
}

if (Math.random()) {
    exports.baz = "🤯";
}


Enter fullscreen mode Exit fullscreen mode

概括

简而言之,如果您想创建一个组件库,您应该使其可进行 tree-shakable,如果您想做到这一点,您必须使用 ESM。

还有其他方法可以实现这一点。Material UI 和 Ant Design 则走的是不同的方向。

他们并没有创建一个导出所有组件的 bundle,而是创建了无数个小 bundle,每个组件一个。所以



import { Button } from '@material-ui';


Enter fullscreen mode Exit fullscreen mode

你会这样做



import Button from '@material-ui/core/Button';


Enter fullscreen mode Exit fullscreen mode

请注意,您从包内的文件(小包)中加载按钮/core/Button

确实有效,但需要特定的捆绑设置,如果您不小心,则可能会面临为每个组件一遍又一遍地捆绑重复代码的巨大风险

现在有些人可能有使用 MaterialUI 和 Ant Design 的经验,并且注意到你可以这样做



import { DatePicker, message } from 'antd';


Enter fullscreen mode Exit fullscreen mode

一切似乎都正常,但这只是一个小技巧。Ant 要求你安装babel-plugin-import,并使用一个疯狂的设置,这create-react-app需要你重新连接你的react-scripts。这个 babel 插件的作用是自动翻译这个



import { DatePicker, message } from 'antd';


Enter fullscreen mode Exit fullscreen mode

进入这个



import { Button } from 'antd';
ReactDOM.render(<Button>xxxx</Button>);

      ↓ ↓ ↓ ↓ ↓ ↓

var _button = require('antd/lib/button');
ReactDOM.render(<_button>xxxx</_button>);


Enter fullscreen mode Exit fullscreen mode

😧

底线仍然是:

对于 tree-shaking 来说,这些都不是必需的。

如何

最后,设置起来很简单。我将使用

  • Rollup
  • TypeScript

为了创建完整的设置,我将添加

  • StoryBook用于开发组件
  • Next.js使用该库的应用程序

我会把所有东西都放到一个 Monorepo 里。这能帮助我们构建代码,最终我们会拥有一个单一的项目,这个项目会被拆分成多个独立的非单体式包,同时支持热模块重载,开发过程中无需任何手动操作。

总结

如果您只想开门见山,可以在 GitHub 上查看、克隆和分叉最终产品:

https://github.com/LukasBombach/tree-shakable-component-library

首先,我们需要创建一个 Monorepo。我不会解释每一行代码,欢迎在评论区提问,我会尽力解答。另外,由于我使用的是 Mac,所以我会使用 *nix 命令来编写代码。

因此,为了创建一个 monorepo,我将使用带有 2 个包的 yarn 工作区,app并且ui-library



mkdir myproject
cd myproject
yarn init -y
mkdir -p packages/app
mkdir -p packages/ui-library


Enter fullscreen mode Exit fullscreen mode

你现在应该有一个像这样的文件夹结构



root
 ∟ package.json
 ∟ packages
    ∟ app
    ∟ ui-library


Enter fullscreen mode Exit fullscreen mode

在代码编辑器中打开你的项目并编辑package.json
删除main字段并添加private: true和,workspaces: ["packages/*"]使其看起来像这样:



{
  "name": "myproject",
  "version": "1.0.0",
  "license": "MIT",
  "private": true,
  "workspaces": [
    "packages/*"
  ]
}



Enter fullscreen mode Exit fullscreen mode

现在您已经有了Yarn Workspaces MonoRepo包含这些包appui-library. cdinto 的文件packages/ui-library,创建一个包并添加以下依赖项:



cd packages/ui-library
yarn init -y
yarn add -DE \
  @rollup/plugin-commonjs \
  @rollup/plugin-node-resolve \
  @types/react \
  react \
  react-dom \
  rollup \
  rollup-plugin-typescript2 \
  typescript


Enter fullscreen mode Exit fullscreen mode

现在打开package.json里面删除packages/ui-library字段main并添加以下字段scripts,,,,,,所以看起来像这样:mainmoduletypespeerDependenciespackage.json



{
  "name": "ui-library",
  "version": "1.0.0",
  "license": "MIT",
  "scripts": {
    "build": "rollup -c rollup.config.ts"
  },
  "main": "lib/index.cjs.js",
  "module": "lib/index.esm.js",
  "types": "lib/types",
  "devDependencies": {
    "@rollup/plugin-commonjs": "11.0.2",
    "@rollup/plugin-node-resolve": "7.1.1",
    "@types/react": "16.9.19",
    "react": "16.12.0",
    "react-dom": "16.12.0",
    "rollup": "1.31.0",
    "rollup-plugin-typescript2": "0.25.3",
    "typescript": "3.7.5"
  },
  "peerDependencies": {
    "react": ">=16.8",
    "react-dom": ">=16.8"
  }
}


Enter fullscreen mode Exit fullscreen mode

在你的ui-library文件夹中添加一个rollup.config.ts和一个tsconfig.json



touch rollup.config.ts
touch tsconfig.json


Enter fullscreen mode Exit fullscreen mode

汇总.config.ts



import commonjs from "@rollup/plugin-commonjs";
import resolve from "@rollup/plugin-node-resolve";
import typescript from "rollup-plugin-typescript2";
import pkg from "./package.json";

export default {
  input: "components/index.ts",
  output: [
    {
      file: pkg.main,
      format: "cjs",
    },
    {
      file: pkg.module,
      format: "es",
    },
  ],
  external: ["react"],
  plugins: [
    resolve(),
    commonjs(),
    typescript({
      useTsconfigDeclarationDir: true,
    }),
  ],
};



Enter fullscreen mode Exit fullscreen mode

tsconfig.json



{
  "compilerOptions": {
    "declaration": true,
    "declarationDir": "lib/types",
    "esModuleInterop": true,
    "moduleResolution": "Node",
    "jsx": "react",
    "resolveJsonModule": true,
    "strict": true,
    "target": "ESNext"
  },
  "include": ["components/**/*"],
  "exclude": ["components/**/*.stories.tsx"]
}


Enter fullscreen mode Exit fullscreen mode

现在到了我要做一些解释的部分,因为这确实是它的核心。rollup 配置的设置使得它将使用rollup-plugin-typescript2插件加载和转译所有 TypeScript 文件。截至目前,这个配置仍然比官方的更合适,@rollup/plugin-typescript因为后者无法发出 TypeScript 定义文件。这意味着我们的 UI 库不会向消费者导出任何类型(嘘!)。我们向typescript插件传递了一个名为 的选项useTsconfigDeclarationDir。这个选项告诉插件使用declarationDir中的选项tsconfig.json。我们设置的所有其他 TypeScript 选项都将从 中读取tsconfig.json。这意味着我们通过 Rollup 运行 TypeScript,但所有 TypeScript 相关设置都位于 中tsconfig.json

Rollup 剩下要做的就是打包文件。我们也可以在这里应用打包工具的其他功能,比如最小化。目前我们只创建一个 ES 模块,但这个设置允许你在此基础上进行构建。现在我们如何创建 ES 模块?为此,我们有以下两个输出设置:



{
  output: [
    {
      file: pkg.main,
      format: "cjs",
    },
    {
      file: pkg.module,
      format: "es",
    },
  ],
}


Enter fullscreen mode Exit fullscreen mode

这告诉 rollup 实际上要创建两个 bundle,一个是 CJS 格式,一个是 ESM 格式。我们从 中获取这两个 bundle 的文件名package.json,这样它们就能始终保持同步。

好的,但是为什么要使用 CJS 选项呢?我很高兴我假装你问了这个问题。当你使用你的库时,Node.js 和其他打包器将无法识别(也就是说,假装它根本不存在)main你的库中没有有效的条目,package.json并且该条目必须是 CJS 格式。此外,这可以为你提供向后兼容性,但没有 tree-shaking 功能。

有趣的部分是 的条目。我们从的条目es中获取文件名。像 Webpack 和 Rollup 这样的打包工具会识别该条目,并在正确设置后使用它,并期望其后面有一个 ES 模块(同时忽略该条目)。modulepackage.jsonmain

和...

就是这样!

好吧,我们确实想测试一下。那就来试试吧:

在终端中,你应该仍然位于该ui-library文件夹中。你可以输入 来确认pwd,它将显示你当前的工作目录。

如果你在那里输入



mkdir -p components/Button
touch components/index.ts
touch components/Button/Button.tsx


Enter fullscreen mode Exit fullscreen mode

这应该已经创建了文件

  • packages/ui-library/components/Button/Button.tsx
  • packages/ui-library/components/index.ts

在你的项目中。按如下方式编辑它们

索引.ts



export { default as Button } from "./Button/Button";


Enter fullscreen mode Exit fullscreen mode

按钮.tsx



import React from "react";

export default () => <button>I SHOULD BE HERE</button>;


Enter fullscreen mode Exit fullscreen mode

🎉 🎉 🎉 现在你可以跑了 🎉 🎉 🎉



yarn build


Enter fullscreen mode Exit fullscreen mode

现在有一个名为 的新文件夹lib。其中有 1 个文件夹和 2 个文件。打开index.esm.js。您应该看到一个 ES 模块格式的库构建版本:



import React from 'react';

var Button = () => React.createElement("button", null, "I SHOULD BE HERE");

export { Button };


Enter fullscreen mode Exit fullscreen mode

🎉 🎉 🎉

食用它

好了,现在我们终于可以收获劳动成果了。我们将在 monorepo 中创建一个 Next.js 应用,并使用我们之前提到的类型化 tree-shook 库。

因此,从您的ui-library文件夹cd进入您的app文件夹并创建下一个应用程序:



cd ../app
yarn init -y
yarn add -E next react react-dom
yarn add -DE @types/node typescript
mkdir pages
touch pages/index.tsx


Enter fullscreen mode Exit fullscreen mode

将下一步添加scripts到您的package.json下一步,就像您从下一步中知道的那样:



{
  "name": "app",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "dev": "next",
    "build": "next build",
    "start": "next start"
  },
  "dependencies": {
    "next": "9.2.1",
    "react": "16.12.0",
    "react-dom": "16.12.0"
  },
  "devDependencies": {
    "@types/node": "13.7.0",
    "typescript": "3.7.5"
  }
}
```

And implement your {% raw %}`pages/index.tsx` like so

**index.tsx**

```tsx
import { Button } from "ui-library";

function HomePage() {
  return (
    <div>
      Welcome to Next.js! Check out my <Button />
    </div>
  );
}

export default HomePage;
```

Now all that is left to do is start your project and see if your button is there:

```bash
yarn dev
```

You should see this now:

![Next App with Component visible on the screen](https://dev-to-uploads.s3.amazonaws.com/i/szaf3c8b6kze7qcm910r.png)

Ok, that was a long ride for a small visible thing. But now you *do* have a lot:

* You have a monorepo with separate independent packages for your ui library and your app(s)
* Your app can be implemented with any JS based technology
* You can have multiple apps in your monorepo comsuming your component library
* Your UI library is tree-shakable and typed with TypeScript
* You can build on your build setup and apply anything from the Rollup cosmos to it

## Bonus

**Hot-Module-Reloading works!** If you in parallel do

```bash
cd packages/app
yarn dev
```

and

```bash
cd packages/ui-library
yarn build -w
```

you can edit your components in your library, they will be watched and rebundled, your Next app will recognize these changes in your monorepo and update automatically too!

If you want to save some time, I have set up a demo project at

https://github.com/LukasBombach/tree-shakable-component-library/

in which I have also added **StoryBook**. In the readme of that project I have also added some instruction in which you can see the tree-shaking for yourself to make sure it works.

Happy coding ✌️
Enter fullscreen mode Exit fullscreen mode
文章来源:https://dev.to/lukasbombach/how-to-write-a-tree-shakable-component-library-4ied
PREV
50 个快捷键 ⌨ 每个 Intellij 用户必知!✔ 实验一下!⚗ 结论 🔎
NEXT
尼加拉瓜 GitLab 的工作人员 100% 远程工作人员 100% 远程工作人员 准备好实际的工作清单 🎉🎉🎉 2022 年 4 月更新