從零到一構建並打包 React + TypeScript + Less元件庫教程(一、專案初始化搭建+程式碼規範整合)

风希落發表於2024-11-14

本系列涉及的內容如下:

  1. 元件庫基礎搭建,react + ts + less
  2. 專案規範,包括但不限於 prettier、eslint、stylelint、husky、lint-staged、commitlint
  3. pnpm monorepo + turborepo 整合
  4. gulp + webpack 構建 esm、cjs 和 umd
  5. storybook 文件整合

此係列不包含釋出 npm 和構建 CI 流程。

本系列目錄如下:

  1. 專案初始化搭建+程式碼規範整合
  2. 元件庫多產物編譯及文件編寫

此係列不是所謂的最佳實踐,流行元件庫經過迭代都非常完善,不是一人力能完成的,而本系列只是將功能上的核心進行了總結,不足之處依然很多

  • 閱讀本系列需要對元件庫的構成有一定的瞭解,不多,一點點即可,純小白可能會有點難入手。
  • 後續有時間會追加一篇 React Icon 元件庫的構建。

本篇的內容可直接檢視 commithttps://github.com/json-q/rc-library-templete/commit/edaa1adb6772babf475612a754566f0dbefa52fb

初始化專案

mkdir react-library-templete
cd react-library-templete
pnpm init

package.json 初始化工作

由於是 pnpm monorepo 專案,必須使用 pnpm,可以在 package.json 中做一下限制

  "scripts": {
    "preinstall": "npx only-allow pnpm",
  },
  "packageManager": "pnpm@9.12.3",
  "engines": {
    "node": ">=20"
  },
  • packageManager 可以不寫,主要作用是為了約束開發都是用這個版本,避免包管理工具版本不一致導致的相容問題
  • engines 指定專案使用的 node 版本

注意: 一旦宣告 packageManager,在執行 install 時,會提示你是否替換成指定版本,此替換為全域性的版本替換,之前的安裝版本將被覆蓋

安裝基礎依賴

使用 TypeScript + Less 編寫,肯定要安裝 typescript 和 less,如果涉及到 node 相關的,還可以再安裝一個 @types/node 型別提示

pnpm i typescript less @types/node -D

專案規範

整合 eslint

需提前安裝 ESLint 外掛

pnpm dlx @eslint/create-config

image

eslint 基本結構有了,再裝一些輔助外掛

pnpm i eslint-plugin-react-hooks eslint-plugin-jsx-a11y eslint-plugin-react-refresh -D
  • eslint-plugin-jsx-a11y 是一個無障礙訪問的 eslint 檢測,可按需安裝
  • eslint-plugin-react-refresh 檢測 react 元件是否可以安全重新整理,可按需安裝

eslint.config.mjs 中新增這幾個外掛

import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
import pluginReact from "eslint-plugin-react";
import reactRefresh from 'eslint-plugin-react-refresh';
import jsxA11y from 'eslint-plugin-jsx-a11y';
import reactHooks from 'eslint-plugin-react-hooks';

/** @type {import('eslint').Linter.Config[]} */
export default [
  {files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"]},
  {languageOptions: { globals: globals.browser}},
  pluginJs.configs.recommended,
  ...tseslint.configs.recommended,
  pluginReact.configs.flat.recommended,
  jsxA11y.flatConfigs.recommended,
  {
    plugins: {
      'react-hooks': reactHooks,
      'react-refresh': reactRefresh,
    },
    rules: {
      ...reactHooks.configs.recommended.rules,
      'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
    },
  },
  {
    ignores: [
      '**/dist/*',
      '**/es/*',
      '**/lib/*',
      'node_modules',
      '*.woff',
      '.ttf',
      '.vscode',
      '.idea',
      '.husky',
      '.local',
      '/bin',
      '.eslintcache',
      '.stylelintcache',
    ],
  },
];

eslint 配置過程中,可以新建一個 ts 檔案,寫一些不規範程式碼,時刻檢視 eslint 的檢測是否會失效,因為由於寫法錯誤也會導致 eslint 的配置失效。

整合 Prettier

需提前安裝 Prettier 外掛

pnpm i prettier eslint-plugin-prettier eslint-config-prettier -D

新建 .prettierrc.cjs

/** @type {import("prettier").Config} */
module.exports = {
  printWidth: 120,
  useTabs: false,
  singleQuote: true,
  proseWrap: 'never',
};

新建 .prettierignore

**/dist/*
**/es/*
**/lib/*

**/.local
**/node_modules/**

**/*.svg
**/*.sh

.eslintcache
.stylelintcache

此時你可以發現 .prettierrc.cjs 被 eslint 報錯了

image

這是由於 module 是 node 環境的全域性變數,而 eslint 不識別 node 環境。在 eslint.config.mjs 中的 globals 屬性新增 node 全域性變數

export default [
  // ...
  {languageOptions: { globals: {...globals.browser,...globals.node}}},
  // ...
];

此時就沒問題了

prettier 和 eslint 的整合

前邊寫過的都省略掉了,只列出更改的部分。該部分讓 eslint 和 prettier 做了整合,並新增了一項 eslint 關閉校驗的規則 @typescript-eslint/no-explicit-any,即此時是允許使用 any 的(預設禁止使用 any)

// ...
import eslintConfigPrettier from 'eslint-config-prettier';
import prettierRecommended from 'eslint-plugin-prettier/recommended';

/** @type {import('eslint').Linter.Config[]} */
export default [
  // ...
  eslintConfigPrettier,
  prettierRecommended,
  {
    // ...
    rules: {
      // ...
      '@typescript-eslint/no-explicit-any': 'off',
    },
  },
];

不出意外的話,此時就能看到 eslint.config.mjs 中存在大量的 prettier 報錯提示。

image

在根目錄新建一個 .vscode 資料夾,在資料夾內部新建 settings.json 檔案

{
  "search.exclude": {
    "**/node_modules": true,
    "dist": true,
    "es": true,
    "lib": true,
    "storybook-static": true,
    "pnpm-lock.yaml": true
  },
  "editor.codeActionsOnSave": {
    "source.fixAll": "explicit"
  },
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "stylelint.validate": ["css", "less", "scss", "sass", "html"]
}

其實主要就是自動儲存格式化,然後回到 eslint 的檔案,儲存就會自動格式化,如果格式化後依然報錯,可以嘗試重啟 vscode。

編碼問題

在 windows 上預設是 CRLF 編碼,而 prettier 認為這是一種錯誤的編碼方式,格式化也不行。

image

  • 安裝 EditorConfig for VS Code 外掛
  • 根目錄新建 .editorconfig,寫入內容,再次儲存,就可以看到編碼已經變成 LF
# http://editorconfig.org
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false

[Makefile]
indent_style = tab

使用 .gitattributes 解決多環境編碼不一致問題

editconfig 只是解決本地開發的編碼問題,當你提交到 git 後,再拉取一個全新的,如果是 win 系統,依然是 CRLF。

為了解決這個問題,可以在程式碼提交時做格式上的統一,在根目錄新建 .gitattributes,統一編碼為 LF,且檔案類的不做處理

* text=auto eol=lf

*.{ico,png,jpg,jpeg,gif,webp,svg,woff,woff2} binary

Stylelint 整合

由於使用 less 開發,所以就額外安裝了 less 的解析和校驗外掛

pnpm i stylelint stylelint-config-recommended stylelint-config-standard-less stylelint-prettier stylelint-order stylelint-config-recess-order postcss-less -D

這幾個我就不過多介紹了,其中 stylelint-orderstylelint-config-recess-order 就是 css 排序外掛和排序規則,兩個結合使用(可選安裝)。

根目錄新建檔案 .stylelintrc.cjs

/** @type {import("stylelint").Config} */
module.exports = {
  plugins: ['stylelint-order'],
  customSyntax: 'postcss-less',
  extends: [
    'stylelint-config-recommended',
    'stylelint-prettier/recommended',
    'stylelint-config-recess-order',
    'stylelint-config-standard-less',
  ],
  rules: {
     // 規則自定義即可
    'color-function-notation': null,
    'less/no-duplicate-variables': null,
  },
  ignoreFiles: ['**/.js', '/*.jsx', '/.tsx', '**/.ts', '**/dist/**', '**/es/**', '**/lib/**', '**/node_modules/**'],
};

完畢之後可以隨便建一個測試的 less 檔案,寫一些內容來測試外掛是否生效

image

由此可見,排序外掛是生效了,此時儲存程式碼就可以自動修復排序規則,那這個 stylelint 的配置應該也沒什麼問題。

如果沒有生效,嘗試重啟 vscode,且每次更改 stylelint 的配置後,都建議重啟驗證

scripts 新增 fix 命令(可忽略)

package.json 新增 script 命令

  "scripts": {
    "preinstall": "npx only-allow pnpm",
    "lint:eslint": "eslint . --fix --cache",
    "lint:stylelint": "stylelint \"**/*.{less,css}\" --fix --cache"
  },

monorepo 基本結構搭建

這裡不介紹 monorepo 單體倉庫的作用,能看到該文章的應該都對其有一定的瞭解。

該專案的多包倉庫思路是:

  • packages 資料夾作為元件開發的核心模組
    • 多個子包都需要 tsconfig 的配置,那就可以把這個配置抽離出來
    • icon 元件庫作為一個單獨的包
    • 其它廣泛使用的元件為元件庫核心包(常用 util hook 什麼的也可以抽離,這裡不再做更細緻劃分)
  • storybook 文件作為單獨的模組
  • site 元件庫網站作為單獨模組(可選)

此時結構目錄應該是

- packages
    - components
    - icons
    - tsconfig
- site
- storybook

目錄新建完畢後,根目錄新建 pnpm-workspace.yaml

packages:
  - packages/*
  - site
  - storybook

此時 monorepo 的基本目錄結構就已經搭建好了

初始化 packages 目錄的子包

其中 sitestorybook 先設為 TODO(待辦),優先整理 packages 下的子包

packages 下的包統一初始化執行 pnpm init 命令

tsconfig

進入 tsconfig 目錄,將 package.json 中的 name 修改為你喜歡的包名,後續都是以 name 欄位的名字安裝依賴,由於專案內使用,設定成 private 即可,我設定的 rclt 就是 react-component-library-templete 的簡寫。

{
  "name": "rclt-tsconfig",
  "version": "0.0.1",
  "description": "",
  "keywords": [],
  "author": "",
}

json 內容僅供參考

新建 base.json 檔案

{
  "$schema": "https://json.schemastore.org/tsconfig",
  "compilerOptions": {
    "strict": true,
    "composite": false,
    "declaration": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "allowSyntheticDefaultImports": true,
    "allowImportingTsExtensions": true,
    "allowJs": true,
    "inlineSources": false,
    "isolatedModules": true,
    "module": "ES2020",
    "moduleResolution": "Bundler",
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "preserveWatchOutput": true,
    "skipLibCheck": true,
    "strictNullChecks": true
  },
  "exclude": ["dist", "build", "es", "lib", "node_modules"]
}

新建 react-library.json 檔案

{
  "$schema": "https://json.schemastore.org/tsconfig",
  "extends": "./base.json",
  "compilerOptions": {
    "module": "ES6",
    "target": "ES5",
    "jsx": "react",
    "lib": ["DOM", "ES2015"]
  }
}

此時就可以在根目錄新增 rclt-tsconfig 依賴

  "devDependencies": {
    "rclt-tsconfig": "workspace:*",
  }

執行 pnpm i 安裝,新建一個 tsconfig.json 來使用這個包

{
  "extends": "rclt-tsconfig/base.json",
  "compilerOptions": {
    "noEmit": true
  },
  "include": ["packages/**/*.tsx", "packages/**/*.ts"]
}

不要問我為什麼不使用命令安裝,問就是沒找到根目錄安裝子包的命令(不會)

components

rclt-tsconfig 連結到 components 包下,以下命令的意思就是從本地找 rclt-tsconfig 安裝到 rclt-components

  • rclt-components 是 components 包 package.json 中的 name
  • -workspace 是從本地查詢
  • 暴力一點的話直接在 package.json 中寫入包依賴,比如 "rclt-tsconfig": "workspace:*",再執行 pnpm install 即可。
pnpm --filter rclt-components add rclt-tsconfig -D -workspace

新建 tsconfig.json

{
  "extends": "rclt-tsconfig/react-library.json",
  "compilerOptions": { "emitDeclarationOnly": true }, // 只生成宣告檔案
  "include": ["src"]
}

沒問題的話,點 extends 的路徑連結是可以跳到 json 檔案的

提交規範

這些依賴的整合都是在根目錄進行的,記得安裝的時候帶上 -w 來指定在根工作區安裝。

husky + lint-staged 整合

安裝依賴之前的準備工作

在此之前,先把我們的專案 git 初始化一下,執行 git init 命令,最好再建一個 git 倉庫,關聯原生代碼,這部分我就不贅述了,大家應該都會。

根目錄新建 .gitignore,以下是我的檔案內容,大家可自行修改忽略檔案(我個人習慣不上傳 lock 依賴,除非特殊情況需要鎖定版本)

# Dependencies
node_modules
.pnp
.pnp.js

# Local env files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

# Testing
coverage

# Turbo
.turbo

# Vercel
.vercel

# Build Outputs
.next/
out/
build
dist
es
lib


# Debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Misc
.DS_Store
*.pem

# cache
.eslintcache
.stylelintcache

# lock
pnpm-lock.yaml

整合依賴

安裝並初始化 husky

pnpm i husky lint-staged -D -w
# 初始化 husky
npx husky init

執行完畢 script 中會新增一個 husky 命令,根目錄會生成 .husky 檔案,其中有一個 pre-commit 指令碼檔案,寫入以下內容

npx lint-staged

這件事就是在程式碼 commit 之前,執行 lint-staged 做一些事情,比如檢測程式碼規範,格式化程式碼等,接下來就幹這個事。

根目錄新建 .lintstagedrc

{
  "packages/**/*.{js,jsx,ts,tsx}": ["eslint --fix --cache", "prettier --write"],
  "packages/**/package.json": ["prettier --write"],
  "packages/**/*.{css,less}": ["stylelint --fix --cache", "prettier --write"],
  "**/*.md": ["prettier --write"]
}

內容應該都能看得懂,就是匹配到的檔案執行相應的命令

commitlint 整合

pnpm i @commitlint/cli @commitlint/config-conventional -D -w

根目錄新建 commitlint.config.js

module.exports = {
  ignores: [(commit) => commit.includes('init')],
  extends: ['@commitlint/config-conventional'],
};

.husky 資料夾下新建 commit-msg

npx --no-install commitlint --edit $1

測試整合是否正常工作

我們把 lint-staged 和 commitlint 準備工作都完成了,如何測試呢?

首先測試 lint-staged,即寫一段被 eslint 警告的程式碼進行提交:

// pakcgaes/components/src/index.ts

const a = 1; // 'a' is assigned a value but never used.

然後執行 commit(初次執行程式碼校驗時間可能較長)

image

上圖可以看到,lint 校驗沒透過,無法 commit,此時再把 const a = 1; 刪除掉,消除 eslint 的報錯,再次執行 commit

image

上圖可知,這次程式碼的校驗已經透過,但是 commitlint 丟擲了錯誤,提示 commit 資訊不規範,此時修改一下再提交

image

此時提交就可以正常透過了,接下來推送到遠端倉庫即可。

相關文章