太長了;博士

>

如果您只想切入正題,可以在 GitHub 上查看、克隆和分叉最終產品:

>

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

今年年初,我被一家新公司聘用,幫助完成一個新的(但尚未公開)專案。為此,我們想要進入設計系統和元件庫。

這個話題本身對我們來說並不新鮮,但我們自己實施這個話題卻很新鮮。我的任務是找到一個設置

  • 讓我們在 monorepo 中工作,其中我們的 ui 庫是一個包,而我們的應用程式是另一個包

  • ui 函式庫必須是可搖樹的,因為我們非常專注於效能

root
 ∟ packages
    ∟ app
    ∟ ui-library

讓我詳細闡述第二點,因為這點非常重要。如果您要建立一個元件庫並直接捆綁所有內容,您可能最終會建立一個 CommonJS (CJS) 格式的單一檔案。

CommonJS 和 ES 模組

如今,我們有多種 JS 文件格式,其中大多數仍在積極使用。您可以在 @igredible 撰寫的這篇非常好的文章中了解不同的格式

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 的格式。它可以從 Node.js 12 開始原生使用,目前被標記為實驗性的

不管怎樣,使用 Webpack/Babel 或 TypeScript 你都會熟悉這種格式。這是允許您編寫的格式

import X from "y";

export Z;

👆 ESM

而不是 CJS 👇

const X = require("y")

module.exports = Z;

那麼為什麼這很重要呢?

因為樹搖晃!

問題

如果您將 ui 庫捆綁在一個包含以下內容的 CJS 檔案中:

  • 一個標題

  • 一個按鈕

  • 一張卡和

  • 一個影像

並且您只需將庫中的一個元件導入到您的應用程式中,整個庫就會被載入和捆綁。這意味著即使您只在應用程式中使用按鈕,整個 ui 庫(包括標題、卡片和圖像)最終都會出現在您的捆綁包中,並使您的應用程式變得更大載入時間、解析和執行時間可能會增加

解決方案

……當然是搖樹。 ES 模組使捆綁程式可以對您的程式碼進行 tree-shake。如果我沒記錯的話,這是因為 ESM 語法允許捆綁器靜態檢查程式碼的哪些部分被使用,哪些部分沒有使用,這對於require來說更困難,因為它可以以更動態的方式使用,就像這樣

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

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

概括

簡而言之,如果你想建立一個元件庫,你應該讓它成為可搖樹的,如果你想這樣做,你必須使用 ESM。

還有其他方法可以實現這一點。 Material UI 和 Ant Design 走的是不同的方向。

他們不是建立一個導出所有元件的單一捆綁包,而是實際上建立了無數個微小的捆綁包,每個元件一個。所以而不是

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

你會這樣做

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

請注意,您從/core/Button包內的檔案(一個小包)載入按鈕。

確實有效,但需要特定的捆綁設置,如果您不小心,就有很大的風險為每個元件一遍又一遍地捆綁重複的程式碼

現在有些人可能有使用 MaterialUI 和 Ant Design 的經驗,並注意到你可以做到這一點

import { DatePicker, message } from 'antd';

一切似乎都正常,但這只是一個技巧。 Ant 要求您安裝babel-plugin-import並使用create-react-app瘋狂設置,這需要您重新連接您的react-scripts 。這個 babel 插件的作用是自動翻譯這個

import { DatePicker, message } from 'antd';

進入這個

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

      ↓ ↓ ↓ ↓ ↓ ↓

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

😧

底線仍然是:

對於搖樹來說,這些都不是必要的。

方法

最後,設定可以很簡單。對於我將使用的庫

  • Rollup

  • TypeScript

為了建立一個完整的設置,我將加入

  • 用於開發元件的StoryBook

  • 使用該庫的Next.js應用程式

我會將所有內容放在 monorepo 中。這將幫助我們建立程式碼,並且我們將擁有一個專案,該專案被分成單獨的非整體包,同時具有熱模組重新加載,並且在開發時無需手動步驟。

太長了;博士

>

如果您只想切入正題,可以在 GitHub 上查看、克隆和分叉最終產品:

>

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

因此,首先我們必須建立一個 monorepo。我不會解釋每一行程式碼,請隨時在評論中詢問我,我會很樂意嘗試回答。另外,我將使用 *nix 命令來編寫此內容,因為我使用的是 Mac。

因此,為了建立一個 monorepo,我將使用帶有 2 個套件、 appui-library紗線工作區:

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

您現在應該有一個像這樣的資料夾結構

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

在程式碼編輯器中開啟您的專案並編輯package.json

刪除main欄位並新增private: trueworkspaces: ["packages/*"]所以它看起來像這樣:

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

您現在擁有一個包含appui-library套件的Yarn Workspaces MonoRepocd進入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

現在打開packages/ui-library中的package.json刪除main的字段並加入以下字段, scriptsmainmoduletypespeerDependencies ,所以package.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"
  }
}

在你的ui-library資料夾中加入rollup.config.tstsconfig.json

touch rollup.config.ts
touch tsconfig.json

rollup.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,
    }),
  ],
};

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"]
}

現在這是我將做一些解釋的部分,因為這確實是它的核心。 rollup 配置已設置,以便使用rollup-plugin-typescript2插件來載入和轉換所有 TypeScript 檔案。截至今天,這個仍然比官方的@rollup/plugin-typescript更合適,因為後者無法發出 TypeScript 定義檔。這意味著我們的 UI 庫不會向消費者匯出任何類型(噓!)。我們將一個選項傳遞給名為useTsconfigDeclarationDirtypescript插件。這告訴插件使用tsconfig.json中的declarationDir選項。我們設定的所有其他 TypeScript 選項都已從tsconfig.json中讀取。這意味著我們透過 Rollup 執行 TypeScript,但所有與 TypeScript 相關的設定都駐留在tsconfig.json中。

Rollup 剩下要做的就是捆綁我們的檔案。我們也可以在這裡應用捆綁器所做的任何其他事情,例如縮小。現在我們只是建立一個 ES 模組,但此設定允許您在其上進行建置。現在我們如何建立 ES 模組?為此,我們有以下 2 個輸出設定:

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

這告訴 rollup 實際上要建立 2 個包,一個採用 CJS 格式,一個採用 ESM 格式。我們從package.json中取得這些檔案的檔案名,這樣它們始終保持同步。

好的,但是為什麼選擇 CJS?我很高興假裝你問了。當您使用您的程式庫時,如果您的package.json中沒有有效的main條目並且該條目必須採用 CJS 格式,則 Node.js 和其他捆綁器將無法辨識(即假裝它根本不存在)。此外,這將為您提供向後相容性,但沒有樹搖動功能。

有趣的部分是es的條目。我們從package.jsonmodule條目中取得檔案名稱。像 Webpack 和 Rollup 這樣的捆綁器將辨識該條目,並且在正確設定時使用它並期望其後面有一個 ES 模組(同時忽略main條目)。

和...

就是這樣!

好吧,我們確實想測試一下。讓我們試試看:

在你的終端機中,你應該仍然位於ui-library資料夾中。您可以輸入pwd來確認,這將顯示您目前的工作目錄。

如果你在那裡,請輸入

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

那應該已經建立了文件

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

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

在你的專案中。編輯它們如下

索引.ts

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

按鈕.tsx

import React from "react";

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

🎉 🎉 🎉 現在你可以執行了🎉 🎉 🎉

yarn build

現在有一個名為lib新資料夾。其中您有 1 個資料夾和 2 個檔案。打開index.esm.js 。您應該會看到庫的 ES 模組格式建置:

import React from 'react';

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

export { Button };

🎉🎉🎉

消費它

好了,現在我們終於可以收穫勞動成果了。我們將在 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

將 Next scripts新增到package.json中,就像您從 Next 中知道的那樣:

{
  "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"
  }
}

並像這樣實現你的pages/index.tsx

索引.tsx

import { Button } from "ui-library";

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

export default HomePage;

現在剩下要做的就是啟動您的專案並查看您的按鈕是否在那裡:

yarn dev

你現在應該看到這個:

下一個應用程式的元件在螢幕上可見

好吧,對於一個可見的小東西來說這是一個漫長的旅程。但現在你確實擁有了很多:

  • 您有一個 monorepo,其中包含適用於您的 ui 庫和應用程式的單獨獨立包

  • 您的應用程式可以使用任何基於 JS 的技術來實現

  • 您的 monorepo 中可以有多個使用元件庫的應用程式

  • 您的 UI 庫是可樹搖動的,並使用 TypeScript 進行輸入

  • 您可以在建置設置的基礎上進行建置,並將 Rollup cosmos 中的任何內容應用到其中

獎金

熱模組重新加載有效!如果你同時做

cd packages/app
yarn dev

cd packages/ui-library
yarn build -w

您可以在庫中編輯元件,它們將被監視並重新捆綁,您的 Next 應用程式將辨識您的 monorepo 中的這些變更並自動更新!

如果您想節省一些時間,我已經在以下位置建立了一個演示專案

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

其中我還加入了StoryBook 。在該專案的自述文件中,我還加入了一些說明,您可以在其中親自查看 tree-shaking 以確保其有效。

快樂編碼✌️


原文出處:https://dev.to/lukasbombach/how-to-write-a-tree-shakable-component-library-4ied


共有 0 則留言