Design System 101 - 專案建置
- 文章發表於
前言
接下來將會介紹如何建置一個 Design System 的 Repo,本篇會分成兩個部分:
- 第一部分: 會著重於 repo 的基本設置,包含 Monorepo 的建置,以及如何使用 pnpm 來管理 packages。
- 第二部分: 加入 Template, Storybook 與單元測試。
技術選型
以下將會列出這次建立 Design System Repo 所使用到的技術,大部分都是目前比較主流的,而會採用這些技術,一部分是參考了很多公司的 Design System 所整理出來的,另一部分則是在現在的公司都不會碰到這些技術,想要藉由這個機會當作練習。
如果有任何錯誤、問題或是建議,都歡迎給我回饋!
pnpm
pnpm 是一個 package manager,跟 npm、yarn 一樣到工具,但是它有一些特別的地方,就跟官方文件所說的一樣:
pnpm is a fast, disk space efficient package manager
就是快!而且省空間!
當然它也解決了一直以來 npm 與 yarn 為人詬病的問題,
假設現在我們有一個專案 app 其安裝 package-a 與 package-b 作為相依套件,而 package-a 跟 package-b 都有 package-c 作為其相依套件
npm 的重複安裝問題
在 npm@3 之前,node_modules 就會像是這樣:
node_modules├── package-a│ └── node_modules│ └── package-c└── package-b└── node_modules└── package-c
會發現 package-c 被安裝了兩次,是因為 npm 會將所有的 package 都安裝在專案的 node_modules 裡,而不管這個 package 是不是已被其他 package 所依賴,最終 node_modules 就會像是巨無霸一樣。
npm@3 與 yarn 的幻影依賴問題
npm@3 與 yarn 則會將所有的 package 安裝在專案的 node_modules 裡,但是會將相依套件的 package 移到上層的 node_modules 裡,達到 node_modules 結構扁平化,這樣就不會有重複安裝的問題了。
node_modules├── package-a├── package-b└── package-c
但因為這樣的扁平化結構,就會使 app 可以直接使用 package-c,進而導致幻影依賴的產生。
假設 app/index.js
剛好直接引用 package-c,那麼假設今天 package-a 升級,裡面使用到的 package-c 有重大 API 更新,這樣 app 使用的 package-c 就會產生問題。
為什麼要用 pnpm
然而上述的問題,pnpm 都解決了,其方法就是將所有 package 先存到 .store 的倉庫裡面,然後產生像樹狀結構的 node_modules,而 pnpm 不是像 npm 那樣把每個套件所需的東西都複製一份,而是透過軟連結 symlink 的方式,把相依的套件連結到 store 裡面的套件,這樣就不會有重複安裝的問題,也不會有幻影依賴的問題。
node_modules├── package-a│ └── node_modules│ └── package-c -(symlink)-> .store/package-c@1.0.0└── package-b└── node_modules└── package-c -(symlink)-> .store/package-c@1.0.0
這也是這次想要嘗試使用 pnpm 的原因!
Monorepo 工具 - turbo
什麼是 Monorepo
通常 Monorepo 是指一個 repo 同時有多個 packages,而這些 packages 可以是 library、app 或是 documentation 等等。
為什麼要用 Monorepo
之所以這次用 monorepo 是因為之後我們 design system 的架構會是這樣,會分成多個 packages:
@ui/design-tokens
: 裡面會包含所有的 color、typography、spacing、border-radius 等等。@ui/components
: 裡面會包含所有的 components。@ui/a11y
: 裡面會包含所有元件對應的 accessibility 相關的東西。@ui/core
: 裡面會包含所有的 utils、hooks、context 等等。
而目前最常見的 monorepo 就是透過 yarn workspace + lerna 或是 turbo + pnpm,這次我們會使用 turbo + pnpm 來管理 monorepo。
版本管理 changeset
changesets 主要是做兩件事:
Changesets hold two key bits of information: a version type (following semver), and change information to be added to a changelog.
這次 Design System 就會透過 changeset 來管理 packages 的版本號,以及產生 changelog。
專案建置
以下是專案建置的步驟 (由於將所有設定步驟列出來會佔太多片幅,所以只會列出重點 commit):
- 建立 REAMDE.md & gitignore
- 建立 pnpm workspace & setup turbo, changeset
- tsconfig 設定
- 新增 commitlint, husky, lint-staged
- eslint 設定
方法二
如果沒有特別的客製化可以直接用 Turbo CLI 來建立專案。Template 連結: turbo-design-system-template:
npx create-turbo@latest -e design-system
Plop - 產生元件模板
在開發組件的時候,為了避免反覆的複製貼上一些基礎的設定,通常我們會透過 Plop 來產生組件的模板,也可以讓整個團隊的組件開發流程更加一致。
Plop is a little tool that saves you time and helps your team build new files with consistency.
安裝 Plop
> design-system/ pnpm add -Dw plop
建立組件模板
這時候可以根據開發者的喜好建立組件模板,每個團隊的設定不盡相同,以下是筆者的檔案結構 Plop Template
plop-templates/components├── src│ ├── {{component}}.tsx // 組件的主要程式碼│ ├── {{component}}.test.tsx // 組件的測試程式碼│ ├── {{component}}.stories.tsx // 組件的 storybook│ └── index.ts└── CHANGELOG.md // 主要用來記錄組件的變更└── README.md // 組件的說明文件└── tsconfig.json // 組件的 tsconfig└── packages.json // 組件的 packages.json
設定 Plop
先建立一個 plopfile.js 的檔案,並且在裡面設定我們的 plop,以下是筆者的設定 Plopfile.js
> design-system/ touch plopfile.js
最後再加上一個 script,讓我們可以透過 pnpm generate
來產生組件模板。
// packages.json{"scripts": {"generate": "plop",}}
產生組件模板
> design-system/ pnpm generate
之後回答完問題,就會產生一個組件模板拉~
Storybook 以及測試環境建置
Storybook 元件開發環境
Storybook 就是一個 UI 組件開發的協助工具,可以讓開發者在開發組件的時候,可以快速的看到組件的樣子,並且可以透過不同的 props 或是 story 來測試組件的各種狀態。
安裝 Storybook
> design-system/ pnpm add storybook @storybook/addon-a11y @storybook/addon-actions @storybook/addon-essentials @storybook/addon-interactions @storybook/addon-links @storybook/react-webpack5 @storybook/react -Dw
@storybook/addon-a11y
: 幫助我們檢查組件是否符合無障礙規範 (accessibility)@storybook/addon-actions & @storybook/addon-interactions
: 用來觸發組件的事件@storybook/addon-links
: 幫助我們在 storybook 上面跳轉到其他 story
設定 Storybook
建立一個 .storybook 的資料夾,並且在裡面建立一個 main.js 與 preview.js 的檔案,用來設定 storybook。
> design-system/ mkdir .storybook> design-system/ touch ./storybook/main.js ./storybook/preview.js
詳細內容可以參考筆者的設定 ./storybook
Jest & React Testing Library - 單元測試
最後則是建立單元測試的環境,這邊我們使用 Jest 與 React Testing Library 來建立單元測試的環境。
> design-system/ pnpm add -Dw ts-jest jest @testing-library/dom @testing-library/jest-dom @testing-library/react @testing-library/user-event @types/jest jest-environment-jsdom
這次測試環境建置主要是用 ts-jest 來幫助我們在 typescript 環境下建立測試環境,並且使用 jest-environment-jsdom 來幫助我們在測試環境下使用 jsdom。
設定 Jest
建立一個 jest.config.js 的檔案,並且在裡面設定 jest。這是筆者設定的 jest.config.js 以及 custom-env.ts
> design-system/ touch jest.config.js
以及在 package.json 中加上一個 script,讓我們可以透過 pnpm test
來執行測試。
// packages.json{"scripts": {"test": "jest --passWithNoTests",}}
最後再 pnpm test
一下,就可以測試跑成功拉~