博客 / 詳情

返回

【前端工程化】一文看懂現代Monorepo(npm)工程

引言:本文介紹了目前流行的 pnpm workspace + changesets + turborepo 構建npm包項目的方案,這套方案也適用於其他大型Monorepo項目。此外,還補充了前端工程下package.jsontsconfighusky等配置知識以及CI/CD相關常識。

一、認識package.json

一個package.json代表了一個項目,可以通過npm/yarn/pnpm命令初始化一個package.json:

pnpm init

然後自行完善package.json。下面我選舉了幾個著名npm包的package.json作為學習參考,並且重點介紹一些字段含義來幫我們理解項目。

1.比如 vueusenpm包的 packages/shared/package.json

{
  "name": "@vueuse/shared",
  "type": "module",
  "version": "14.0.0",
  "author": "Anthony Fu <https://github.com/antfu>",
  "license": "MIT",
  "funding": "https://github.com/sponsors/antfu",
  "homepage": "https://github.com/vueuse/vueuse/tree/main/packages/shared#readme",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/vueuse/vueuse.git",
    "directory": "packages/shared"
  },
  "bugs": {
    "url": "https://github.com/vueuse/vueuse/issues"
  },
  "keywords": [
    "vue",
    "vue-use",
  ],
  "sideEffects": false,
  "exports": {
    ".": "./dist/index.js",
    "./*": "./dist/*"
  },
  "main": "./dist/index.js",
  "module": "./dist/index.js",
  "unpkg": "./dist/index.iife.min.js",
  "jsdelivr": "./dist/index.iife.min.js",
  "types": "./dist/index.d.ts",
  "files": [
    "dist"
  ],
  "scripts": {
    "build": "tsdown",
    "prepack": "pnpm run build",
    "test:attw": "attw --pack --config-path ../../.attw.json ."
  },
  "peerDependencies": {
    "vue": "^3.5.0"
  }
}

2.比如ahookspackages/hooks/package.json

{
  "name": "ahooks",
  "version": "3.9.6",
  "description": "react hooks library",
  "keywords": [
    "ahooks",
    "umi hooks",
    "react hooks"
  ],
  "main": "./lib/index.js",
  "module": "./es/index.js",
  "types": "./lib/index.d.ts",
  "unpkg": "dist/ahooks.js",
  "sideEffects": false,
  "authors": {
    "name": "brickspert",
    "email": "brickspert.fjl@alipay.com"
  },
  "publishConfig": {
    "registry": "https://registry.npmjs.org/"
  },
  "repository": "https://github.com/alibaba/hooks",
  "homepage": "https://github.com/alibaba/hooks",
  "scripts": {
    "build": "gulp && webpack-cli",
    "test": "vitest run --color",
    "test:cov": "vitest run --color --coverage",
    "tsc": "tsc --noEmit"
  },
  "files": [
    "dist",
    "lib",
    "es",
    "metadata.json",
    "package.json",
    "README.md"
  ],
  "dependencies": {
  },
  "peerDependencies": {
    "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
    "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
  },
  "license": "MIT",
  "gitHead": "11f6ad571bd365c95ecb9409ca3050cbbfc9b34a"
}

3.element-plus的packages/element-plus/package.json

{
  "name": "element-plus",
  "version": "0.0.0-dev.1",
  "description": "A Component Library for Vue 3",
  "keywords": [
    "element-plus",
  ],
  "homepage": "https://element-plus.org/",
  "bugs": {
    "url": "https://github.com/element-plus/element-plus/issues"
  },
  "license": "MIT",
  "main": "lib/index.js",
  "module": "es/index.mjs",
  "types": "es/index.d.ts",
  "exports": {
    ".": {
      "types": "./es/index.d.ts",
      "import": "./es/index.mjs",
      "require": "./lib/index.js"
    },
    "./global": {
      "types": "./global.d.ts"
    },
    "./es": {
      "types": "./es/index.d.ts",
      "import": "./es/index.mjs"
    },
    "./lib": {
      "types": "./lib/index.d.ts",
      "require": "./lib/index.js"
    },
    "./es/*.mjs": {
      "types": "./es/*.d.ts",
      "import": "./es/*.mjs"
    },
    "./es/*": {
      "types": [
        "./es/*.d.ts",
        "./es/*/index.d.ts"
      ],
      "import": "./es/*.mjs"
    },
    "./lib/*.js": {
      "types": "./lib/*.d.ts",
      "require": "./lib/*.js"
    },
    "./lib/*": {
      "types": [
        "./lib/*.d.ts",
        "./lib/*/index.d.ts"
      ],
      "require": "./lib/*.js"
    },
    "./*": "./*"
  },
  "unpkg": "dist/index.full.js",
  "jsdelivr": "dist/index.full.js",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/element-plus/element-plus.git"
  },
  "publishConfig": {
    "access": "public"
  },
  "style": "dist/index.css",
  "peerDependencies": {
    "vue": "^3.2.0"
  },
  "dependencies": {
    
  },
  "devDependencies": {
  },
  "web-types": "web-types.json",
  "browserslist": [
    "> 1%",
    "not ie 11",
    "not op_mini all"
  ]
}

4.比如 naive-uipackage.json (naive-ui項目不是monorepo)

{
  "name": "naive-ui",
  "version": "2.43.1",
  "packageManager": "pnpm@9.5.0",
  "description": "A Vue 3 Component Library. Fairly Complete, Theme Customizable, Uses TypeScript, Fast",
  "author": "07akioni",
  "license": "MIT",
  "homepage": "https://www.naiveui.com",
  "repository": {
    "type": "git",
    "url": "https://github.com/tusen-ai/naive-ui"
  },
  "keywords": [
    "naive-ui",
  ],
  "sideEffects": false,
  "main": "lib/index.js",
  "module": "es/index.mjs",
  "unpkg": "dist/index.js",
  "jsdelivr": "dist/index.js",
  "types": "es/index.d.ts",
  "files": [
    "README.md",
    "dist",
    "es",
    "generic",
    "lib",
    "volar.d.ts",
    "web-types.json"
  ],
  "scripts": {
    "start": "pnpm run dev",
    "dev": "pnpm run clean && pnpm run gen-version && pnpm run gen-volar-dts && NODE_ENV=development vite",
  },
  "web-types": "./web-types.json",
  "peerDependencies": {
    "vue": "^3.0.0"
  },
  "dependencies": {
    
  },
  "devDependencies": {
    
  },

}

文件目錄

1."type"字段

可以設置"type": "module" 或者"type": "commonjs"

含義:這個字段指定了Node.js應該如何處理.js文件的模塊系統。

作用

  • 當設置為"module"時,所有.js文件都會被當作ES模塊處理
  • 這意味着您可以使用import/export語法而不是require/module.exports
  • 如果沒有設置或者設置為"commonjs",則使用CommonJS模塊系統

但是你要注意:js/ts文件是"誰"運行的,如果是Node,自然遵循上述原則。但如果是webpack/vite/rollup/tsdown這類構建工具其實"type"這個字段對這類js/ts文件約束不到,因為此時不管是import還是require寫的,構建工具都應該能識別並轉換到target(target指配置打包cjs還是esm格式)

a. vueuse聲明瞭"type": "module" ,然後採用.js + "import"語法
b. 你還會發現很多項目也不愛寫type:"module",然後有兩種處理方式:

  • .mjs + "import"的語法,比如element-plus
  • .js + "require"的語法,比如ahooks

vueuse項目部分截圖
image.png
ahooks項目部分截圖
image.png

P.S. 項目下vitest.config.ts種都是採用import語法,不管你聲沒聲明"type": "module",這是因為Node v12 模塊系統就開始支持require和import兩種語法了。

2."module"字段

含義:當使用import ... from xx導入包時,構建工具(如webpack、rollup或vite等)用來識別ES模塊版本的入口文件。

作用

  • 當打包工具支持ES模塊時,會優先使用這個字段指定的文件作為入口
  • 有助於實現tree-shaking(搖樹優化),因為ES模塊是靜態分析的

在您的項目中"module": "./dist/index.mjs" 表示打包工具使用ESM導入時,從入口./dist/index.mjs這個文件導入。

3."main"字段

含義:當使用require()導入包時,Node.js會查找這個字段指定的文件。

作用

  • "main"字段指定了包的CommonJS入口點。
  • 這是傳統的包入口點定義方式。

"module"字段的區別

  • "main": CommonJS入口(用於Node.js的require)
  • "module": ES模塊入口(用於打包工具的ESM的import/export)

4."exports"字段(現代替代方案)

含義:這是Node.js 12+引入的現代包入口點定義方式,提供了更精細的控制。

在您的項目中,可以這樣配置

"exports": {
    ".": {
        "types": "./dist/index.d.ts",    // TypeScript類型定義
        "import": "./dist/index.js"     // ES模塊導入入口
    }
    "./*": "./dist/*"
  }

如果你同時提供了ESM和CommonJS產物導出,你可以這樣配置

"exports": {
    ".": {
      "types": "./es/index.d.ts",
      "import": "./es/index.mjs",
      "require": "./lib/index.js"
    }
}

解釋

  • ".": "./dist/index.js" - 當其他模塊通過 import ... from '@caikengren/uni-hooks'導入您的包時,Node.js將使用 index.js 文件作為入口點。
  • "./*": "./dist/*" - 這允許導入包的子路徑,例如 import useXXX from '@caikengren/uni-hooks/useXXX' 。這種模式允許訪問包內的特定文件或子模塊。
  • "types": "./dist/index.d.ts" - 告訴 TypeScript 編譯器在哪裏找到該包的類型聲明文件。

補充説明

  • exports應該比這比傳統的 main/module 字段提供了更強大的控制能力。 exports比較新,為了兼容性,一般也需要對應配置好main/module。
  • 控制包的訪問邊界。使用 exports 字段的一個重要好處是它可以保護包的內部文件。在沒有 exports 字段的情況下,包的所有文件都可以被導入。而使用 exports 字段後,只有明確定義的路徑才能被外部訪問,這為包作者提供了更好的封裝性。

5."files"字段

定義了npm上傳到倉庫時,需要上傳哪些文件(目錄),通常包含你打包的代碼文件、package.json、README還有其他運行時要用到的文件 。

例如:類似ahooks,它針對Node CJS、瀏覽器ESM和瀏覽器unpkg的形式打包了三份代碼,分別放在三個不同文件夾,這些文件夾都是要上傳的。所以files字段定義了dist、lib和es。
image.png

發佈配置

"private": true,
"publishConfig": {
  "tag": "1.1.0",
  "registry": "https://registry.npmjs.org/",
  "access": "public"
}

1."private"字段

1.private字段可以防止我們意外地將私有庫發佈到npm服務器。即當我們使用npm publish發佈時不會被當做一個“npm包”被髮布。 比如monorepo項目的根package.json文件(僅管理項目結構用,不是一個單獨的npm包)。

2.但即使"private": true,當我配置了publishConfig,那麼也是可以繼續發佈的。明確配置了publishConfig可以保證這個包是可以安全發佈的。

3.默認 "private"字段為 true

2."publishConfig"字段

1.當發佈包時這個配置會起作用,在這裏配置tag或倉庫地址。

2."registry":你團隊/公司的的npm倉庫地址。

3."tag":默認就是 latest(表示發佈的就是正式版)。有時候需要發佈beta版本(公開測試),那麼這個tag就起作用了。

4.假設我們的version: 1.1.1-beta.0,然後通過npm publish --tag beta發佈(注意:此時發佈命令多了一個--tag參數),如果不顯示指定--tag,那麼就會用到我們的publishConfig.tag作為這個tag值。

5.tag值的作用就是告訴倉庫這是個beta版本。當有人如下安裝時:

npm i your-package@latest

6.不會安裝最近發佈的beta版,而是安裝最近最新發布的latest版(發行版)。
如果需要安裝beta版,需要手動指定版本:

npm i your-package@1.1.1-beta.0

依賴配置

1."dependencies"

dependencies:聲明的是項目的生產環境中所必須的依賴包。打包時會把這些依賴的代碼打包進去(這些包往往是運行時起作用)。

"dependencies": {
   "react": "^18.0.2",
   "react-dom": "~18.0.2",
   "lodash": "4.17.21" 
}

這裏每一項配置都是一個鍵值對(key-value), key表示模塊名稱,value表示模塊的版本號。版本號遵循主版本號.次版本號.修訂號的格式規定:

  • 固定版本: "lodash": "4.17.21"表示安裝時只安裝這個指定的版本;
  • 波浪號: "react-dom": "~18.0.2"表示安裝18.0.x的最新版本(不低於18.0.2),也就是説安裝時不會改變主版本號和次版本號,修訂號會安裝最新的;
  • 插入號: "react": "^18.0.2"表示安裝18.x.x的最新版本(不低於18.0.2),也就是説安裝時不會改變主版本號,次版本號和修訂號會安裝最新的;

通常推薦"^18.0.2"這種,保證大版本的最新。

2."devDependencies"

devDependencies:聲明的是項目的開發環境、項目運行起來所必須的依賴包。打包時不會把這些依賴的代碼打包進去(這些包往往是編譯時起作用)。像typescriptvite還有一些vite plugin和type包。

"devDependencies": {
    "@types/node": "^20.10.0",
    "@vitejs/plugin-vue": "^6.0.1",
    "tsdown": "^0.15.12",
    "typescript": "^5.4.0",
    "vite": "^5.0.0",
  }

3."peerDependencies"

peerDependencies:這個專門用於npm包項目。我的npm包項目和安裝這個npm的項目可能依賴同一個模塊(另外一個npm包),那麼我的npm包打包時就不需要把這個模塊打包進去。比如:
我的npm包是依賴vue框架,所有安裝我這個npm包的項目都應該安裝了vue依賴,那麼就有
我的npm包package.json:

"name": "my-package",
"peerDependencies": {
    "vue": "^3.4.0"
}

a.當別人項目的vue的大版本 和 我的npm包相同

"dependencies": {
    "vue": "^3.4.1"
}

此時別人項目npm i my-package時可以安裝成功的。my-package和別人項目一起依賴vue@3.4.1

b.但是當別人項目的vue的大版本 和 我的npm包不同

"dependencies": {
    "vue": "^2.4.1"
}

此時別人項目npm i my-package時無法成功,會提示別人(報錯):

npm ERR! Could not resolve dependency: npm ERR! peer vue@"^3.4.0 from my-package@1.1.1

此時有一些比較安全可以繞過的方式:

npm i my-package --legacy-peer-deps

👉 適用於你明確知道自己在幹什麼,比如“這個庫雖然沒聲明支持 vue 2,但其實可以工作”。(這是有一定風險的)

參考

關於前端大管家 package.json,你知道多少?
PACKAGE.JSON
在NPM上發佈beta或alpha版

二、認識tsconfig配置和eslint config

tsconfig

tsconfig.json 是 TypeScript 項目的核心配置文件,用於控制 TypeScript 編譯器(tsc)的行為。它決定了哪些文件會被編譯、如何編譯、以及輸出結果的格式和位置。下面我將結合你的項目實際情況,詳細解釋常見的配置項及其作用:

1. compilerOptions

這是最重要的配置塊,決定了編譯器的具體行為。常見字段如下:

  • target:指定編譯後的 JavaScript 版本,比如 "ESNext""ES2020"。影響新語法的支持。
  • module:指定模塊系統,如 "ESNext""CommonJS"。你的項目採用 ESM,通常設置為 "ESNext"
  • lib:指定要包含在編譯中的庫,比如 ["DOM", "ESNext"],決定可用的全局類型。
  • declaration:是否生成類型聲明文件(.d.ts),通常庫項目會開啓。
  • outDir:編譯輸出目錄,比如 "dist"
  • baseUrl: 通常設置為根目錄".",它是ts中識別路徑的"起點"。比如import ... from 'src/...',表示從.路徑下找src目錄。
  • rootDir:源碼根目錄,決定哪些文件被編譯。
  • strict:開啓所有嚴格類型檢查選項,建議庫項目開啓。
  • esModuleInterop:允許默認導入 CommonJS 模塊,提升兼容性。
  • skipLibCheck:跳過庫文件的類型檢查,加快編譯速度。
2.paths 別名

TypeScript 僅在“編譯期”識別路徑別名,它不會影響運行時。要讓別名在運行時也可用,需要讓打包器(Vite/Rollup/tsdown)或 Node 的解析與之保持一致。必須先確保設置了baseUrl

常見配置:

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@caikengren/uni-hooks-shared": ["packages/uni-hooks-shared/index.ts"],
      "@caikengren/uni-hooks": ["packages/uni-hooks/index.ts"]
    }
  }
}

運行時配合(示例以 Vite 為例):

// vite.config.ts
import { defineConfig } from 'vite'
import path from 'node:path'

export default defineConfig({
  resolve: {
    alias: {
      '@caikengren/uni-hooks-shared': path.resolve(__dirname, 'packages/uni-hooks-shared/index.ts'),
      '@caikengren/uni-hooks': path.resolve(__dirname, 'packages/uni-hooks/index.ts'),
    },
  },
})

在庫項目中,更推薦通過“包名”引用,而不是源碼別名

// packages/hooks/package.json
{
  "name": "@caikengren/uni-hooks",
  "dependencies": {
    "@caikengren/uni-hooks-shared": "workspace:^"
  }
}

這樣在代碼裏直接:

import { foo } from '@caikengren/uni-hooks-shared'

避免了別名與打包器的雙重維護。

3. includeexclude
  • include:指定要編譯的文件或目錄(如 ["src"])。
  • exclude:指定不編譯的文件或目錄(如 ["node_modules", "dist"])。
4.extends

用於“繼承”一份基礎配置,統一 Monorepo 多包的 TypeScript 行為。

推薦在根目錄放置一份基準:

// tsconfig.json(根)
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "lib": ["ESNext", "DOM"],
    "strict": true,
    "declaration": true,
    "skipLibCheck": true
  }
}

子包繼承並按需覆蓋:

// packages/uni-hooks-shared/tsconfig.json
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "rootDir": "src",
    "outDir": "dist"
  },
  "include": ["src"]
}

可以多層繼承,但保持鏈路清晰,避免在各包內重複配置相同選項。

P.S. 如果只有根目錄有tsconfig.json,子包沒有tsconfig.json,子包會受到根目錄tsconfig.json的約束。
eslint config

關於js格式和規範上,傳統都是eslint + prettier,但配置時比較繁瑣,還要處理兩者的衝突。參考antfu大佬的譯文,可以使用@antfu/eslint-config(vueuse項目就使用了這個)來做eslint檢查。
@antfu/eslint-config是由 Vue 核心團隊成員、開源界大神 Anthony Fu 創建並維護的一套 ESLint 配置集合。它是目前社區中最流行的 ESLint 配置之一(GitHub Star 數非常高),其核心特點是“固執己見” (Opinionated) 且“開箱即用”。它採用了 ESLint 最新的 Flat Config (扁平化配置) 系統。

核心亮點:
  1. 一站式解決方案:它不僅僅是一個 ESLint 配置,它集成了 Prettier 的功能(通過 ESLint Stylistic),你不需要再安裝 Prettier。
  2. 自動檢測:它會自動檢測你的項目中是否使用了 Vue、React、TypeScript、UnoCSS 等,並自動啓用相關規則。
  3. 代碼風格

    • 無分號 (No Semicolons)
    • 單引號 (Single Quotes)
    • 尾隨逗號 (Trailing Commas)
  4. 多文件支持:除了 JS/TS,還支持 JSON、YAML、Markdown、HTML 等文件的 lint 和格式化。
  5. 導入排序:內置了 eslint-plugin-simple-import-sort,自動把 import 整理得乾乾淨淨。
安裝&配置
pnpm i -wD eslint @antfu/eslint-config

雖然這個包主打“零配置”,但實際開發中,我們通常需要根據團隊習慣微調一些規則。

@antfu/eslint-config 的默認導出是一個工廠函數,它接受任意數量的參數。

// eslint.config.js
import antfu from '@antfu/eslint-config'

export default antfu(
  {
    // ============================================================
    // 1. 全局功能配置 (Options)
    // ============================================================

    // 顯式啓用/禁用特定框架支持(默認會自動檢測,但顯式寫出更清晰)
    vue: true,
    typescript: true,

    // 格式化風格配置 (替代 Prettier)
    stylistic: {
      indent: 2, // 縮進 2 空格
      quotes: 'single', // 單引號
      jsx: true, // 支持 JSX
    },

    // 忽略文件 (相當於 .eslintignore)
    ignores: [
      'patches',
      'playground',
      'playgrounds',
      'docs',
      '**/types',
      '**/cache',
      '**/*.svg',
      '.cursor',
      '.trae',
      'scripts',
    ],
  },

  // ============================================================
  // 2. 具體規則覆蓋 (Overrides)
  // ============================================================

  // 一般性規則覆蓋
  {
    rules: {
      // 允許使用 console.log (默認是 warn 或 error)
      'no-console': 'off',
      // 允許使用未使用的變量 (通常用於解構時忽略某些屬性)
      'unused-imports/no-unused-vars': 'warn',
      // 允許使用 @ts-ignore(覆蓋 antfu 默認的禁用策略)
      'ts/ban-ts-comment': ['error', { 'ts-ignore': false }],
      // 如果你真的想要分號 (雖然 antfu 默認是無分號的)
      // 'style/semi': ['error', 'always'],
      'style/max-statements-per-line': 'off',

      'ts/ban-types': 'off',
      'node/no-callback-literal': 'off',
      'import/namespace': 'off',
      'import/default': 'off',
      'import/no-named-as-default': 'off',
      'import/no-named-as-default-member': 'off',
      'node/prefer-global/process': 'off',
      'ts/unified-signatures': 'off',
      'ts/no-unsafe-function-type': 'off',
      'ts/no-dynamic-delete': 'off',
    },
  },

  // 針對特定文件的規則覆蓋
  {
    files: ['**/*.vue'],
    rules: {
      // Vue 組件名必須由多個單詞組成?關掉它
      'vue/multi-word-component-names': 'off',
    },
  },

  {
    files: ['**/*.json'],
    rules: {
      // 允許 JSON 文件末尾有逗號 (某些工具不支持,可能需要關掉)
      'jsonc/comma-dangle': 'off',
    },
  },
)

配置lint script

{
    "sciprts": {
        "lint": "eslint --cache .",
        "lint:fix": "eslint --cache --fix .",
    }
}
  • --cache啓用緩存,緩存會把上次已通過的文件記住,下一次只對變更或受影響的文件重新檢查,提升速度。檢查的目標路徑是當前目錄.,具體包含哪些文件由 ESLint 配置決定。
  • 加上--fix 自動修復可自動修復的 ESLint 違規(如縮進、引號、分號等)。不能自動修復的規則會保留為錯誤或警告,需要手動處理。

pnpm-lock文件

1.pnpm-lock.yaml 用於鎖定整個 Workspace 的依賴樹,保證“可重複安裝”(同樣的依賴版本與拓撲)。
2.如果沒有這個lock文件,會發生什麼呢?
比如我的依賴:

packageX: ^1.0.0

當第三方包 packageX發佈了新版:1.1.0,那麼再次安裝(pnpm/yarn install)時都會安裝1.1.0新版本的庫,有時候可能會帶來問題(尤其生產環境不推薦這樣)。

3.關鍵點:

  • 單倉 Monorepo 只有“根鎖文件”,子包不生成獨立鎖;所有安裝動作最終寫回根鎖。
  • 鎖文件應提交到倉庫,CI/同事機器據此實現一致的安裝結果。
  • 發佈到 npm 不會攜帶鎖文件;庫消費者不受你的鎖文件影響。

4.常用命令:

# 嚴格模式(鎖與 package.json 不一致時失敗)
pnpm install --frozen-lockfile

# 僅更新鎖文件(不下載依賴)
pnpm install --lockfile-only

5.與 npm/yarn 的差異:

  • pnpm 採用“內容尋址存儲(Content-Addressable Store)”,遠程包先存入全局 .pnpm-store,再以符號鏈接掛到 node_modules,磁盤佔用更小。
  • Workspace 下通過 workspace: 協議把本地包也以符號鏈接方式關聯,升級內部版本時,由根鎖統一反映變更。

6.多人協作建議:

  • 不手改鎖文件;依賴升級統一用 pnpm up <pkg>@<range> 或在子包用 --filter 精準升級。
  • 合併衝突時,優先保留最新一次“成功安裝後”的鎖版本。

參考

【譯】antfu博客:為什麼我不用Prettier

三、認識monorepo和相關工具

monorepo 介紹

1.monorepo 是多個包在同一個項目中管理的方式,比較流行的一種管理方式。
軟件項目管理經歷了三個階段:

階段 名稱 管理方式 產生背景 / 特點 優勢 劣勢
1​. Monolith (單體巨石應用) 單倉庫管理所有項目代碼 項目初期,業務複雜度低,所有代碼集中在一個倉庫中。 結構簡單,初期管理方便。 1. 隨着業務增長,代碼量龐大,複雜度高。 2. 構建效率低下。
2. MultiRepo (多倉庫多模塊) 每個業務模塊獨立一個倉庫 為解耦巨石應用,將項目拆分為多個模塊,分別管理。 1. 模塊解耦,複雜度降低。 2. 各模塊可獨立開發、測試、發佈。 3. 構建效率提升。 1. 跨倉庫代碼共享困難。 2. 依賴管理複雜(底層模塊升級,依賴方需手動更新)。 3. 倉庫數量增多後,工程管理難度增加。 4. 構建耗時增加(需按順序構建多個倉庫)。
3.​ MonoRepo (單倉庫多模塊) 單一倉庫中管理多個項目或模塊 為解決MultiRepo在模塊數量增多後產生的管理問題。 1. 代碼共享便捷。 2. 共享依賴,減少包安裝和包一致性。 3. 共享工程配置,保證代碼風格和質量一致。 4. 便於構建工具優化(如增量構建、並行構建)。 1. 倉庫體積較大。 2. 需要工具支持來管理權限和優化構建性能。

2.monorepo項目目錄結構:

|-- node_modules
|-- packages
|   |-- packageA
|   |   |-- package.json
|   |-- packageB
|   |   |-- package.json
|   |-- packageC
|   |   |-- package.json
|-- eslint.config.js
|-- package.json

1個package.json代表一個項目,最外面的package.json代表根項目,裏面的package.json代表子包項目A、B和C等。
根項目存在的意義是:管理這些子包,比如共享根項目的依賴(npm包)子包間依賴自動link遞歸執行子包script命令等。

3.monorepo是一種管理規範,那麼pnpm workspace協議就是用來支撐起這種規範的技術協議。具體做法:

根目錄添加pnpm-workspace.yaml 配置文件:

packages:
  - 'packages/*'

這樣一來,/packages下的所有項目都會被 pnpm 識別為 workspace 的成員包

4.下面我們來看JS/TS項目領域,monorepo項目如何通過pnpm/changeset/turbo等工具解決下面三個問題:

  1. 共享代碼/依賴,依賴管理
  2. 版本更新、自動tag和發佈
  3. 並行/串行構建所有項目,處理構建順序

5.後續我們用這個monorepo項目結構進行講解。(記住這個結構,下面會多次用到

|-- node_modules
|-- packages
|   |-- hooks
|   |   |-- package.json ("name":"@caikengren/uni-hooks")
|   |-- shared
|   |   |-- package.json ("name":"@caikengren/uni-hooks-shared")
|   |-- use-request
|   |   |-- package.json  ("name":"@caikengren/uni-use-request")
|-- eslint.config.js
|-- package.json

image.png

pnpm workspace 管理依賴

問題1:共享代碼/依賴,依賴管理

這個問題可以拆成兩個子問題來看:

  1. 多個子包依賴外部第三方npm包,比如依賴vue/react,那麼這些這些包是不是可以共享(而不是每個子包項目都安裝一遍)?
  2. 子包之間存在依賴,比如子包A依賴本地的子包B、C,但B、C都還沒發佈怎麼依賴?能不能本地B、C鏈接到A的node_modules下呢?

1.pnpm i 安裝依賴

針對上面的第一個問題,通過把依賴安裝在根目錄,那麼子項目尋找依賴就會採用根目錄的依賴,從而實現共享依賴
--workspace參數
根目錄下安裝vue

pnpm i -w vue

-w相當於--workspace,表示在根目錄下安裝依賴。

比如我要安裝typescript

pnpm i -wD typescript

-D表示安裝devDependencies,-w-D可以合併為-wD

安裝完成後,只有根目錄package.json會有依賴聲明,如下:

"dependencies": {
    "vue": "^3.4.0"
  },
  "devDependencies": {
    "typescript": "^5.4.0",
  }

--filter參數
比如我有一個@caikengren/uni-hooks子包,依賴了另外兩個子包

  • @caikengren/uni-use-request
  • @caikengren/uni-hooks-shared
    那麼可以通過下面的方式,表示在@caikengren/uni-hooks這個子包中安裝另外兩個依賴。
pnpm i @caikengren/uni-use-request@workspace:^ --filter @caikengren/uni-hooks

pnpm i @caikengren/uni-hooks-shared@workspace:^ --filter @caikengren/uni-hooks

--filter xxx表示在xxx子包下安裝依賴,其中依賴的版本使用@workspace:^佔位。

安裝完成後 packages/uni-hooks目錄的package.json會出現:

"dependencies": {
    "@caikengren/uni-hooks-shared": "workspace:^",
    "@caikengren/uni-use-request": "workspace:^"
}

@workspace:^
@workspace:^ 是一種特殊的協議前綴,用於在依賴聲明中引用當前 workspace 中的本地包,並自動使用該包的版本號加上 ^ 語義化版本範圍。

也就是説,pnpm 會:

  1. 在當前 workspace 中查找名為 my-local-pkg 的本地包,不去遠程下載了;
  2. 獲取它在 package.json 中聲明的版本號(比如 1.2.3);
  3. 將依賴解析為 ^1.2.3,即允許安裝兼容的次要版本更新(遵循 semver 規範);
  4. 但前提是這個包必須存在於 workspace 中。如果不存在,構建會失敗。

2.pnpm自動link

前面,我們利用@workspace:^uni-hooks子包中安裝了另外兩個依賴:@caikengren/uni-hooks-shared@caikengren/uni-use-request,那麼這兩依賴實際是指向哪裏呢?

自動link原理
image.png

1.pnpm i -w B@workspace:^ C@workspace:^ --filter A
B,C被軟鏈接到A的node_modules

2.pnpm i -w Other@5.0.0
Other實際上是安裝到.pnpm store,.pnpm目錄相當於.pnpm store的映射(內容同步的),.pnpm目錄下的Other會被軟鏈接到根目錄的node_modules

拓展補充:ln是shell命令(ln -s相當於windows的快捷方式)。
ln -s(軟鏈接)的文件可讀寫(需要原文件允許軟鏈接讀寫),本質不是同一文件,而是創建了一個鏈接符號。
ln (硬鏈接)的文件可以讀寫,和原文件本質上是同一個文件,僅在不同地方展示了。

區分:

  • pnpm 的特性:安裝的遠程依賴會被放到.pnpm目錄,然後軟鏈接到node_modules,從而依賴共享,節省磁盤空間。
  • pnpm workspace的特性: 自動link。通過@workspace:^安裝的本地依賴,直接軟鏈接到node_modules,即自動link。

我們的項目中
uni-hooks子包依賴本地@caikengren/uni-hooks-shared@caikengren/uni-use-request ,安裝後你會發現node_modules下的依賴有箭頭符號,説明是“符號鏈接”(軟鏈接)
image.png

疑問 workspace:*workspace:^的區別?

1.@caikengren/packageA@workspace:* 打包後 (產物/發佈到 npm):

pnpm 會將其替換為當前 workspace 中該包的精確版本

{
  "dependencies": {
    "@caikengren/packageA": "1.0.0" 
  }
}

2.@caikengren/packageA@workspace:^ 打包後 (產物/發佈到 npm):

pnpm 會將其替換為以當前版本為基準的 (^) 範圍

{
  "dependencies": {
    "@caikengren/packageA": "^1.0.0"
  }
}

changeset 發佈

問題2:版本更新、changelog、自動tag和發佈

changesets 主要關心 monorepo 項目下子項目版本的更新、changelog 文件生成、包的發佈。一個 changeset 是個包含了在某個分支或者 commit 上改動信息的 md 文件,它會包含這樣一些信息:

  • 需要發佈的包
  • 包版本的更新層級(遵循 semver 規範)
  • CHANGELOG 信息

1.項目準備

安裝工具

pnpm i -wD @changesets/cli

安裝完成後,會在node_modules/.bin目錄(依賴包提供的命令會出現在這)看到changeset,意味着可以用npx來運行這個命令。
image.png

初始化

npx changeset init

會多出一個changeset的文件,如下
image.png

2.本地認證準備

假設你已經註冊了一個npm賬號(或私有倉庫賬號),然後通過terminal登錄,在終端輸入

npm adduser
# 或者(效果一樣)
npm login

3.發佈流程

假設你已經改過代碼並且提交了,準備發佈新版本。

1.創建一個新的 changeset 文件

npx changeset add

説明

  • 這個命令會交互式地讓你選擇要升級的包(在 monorepo 中)、版本類型(patch / minor / major)以及填寫變更描述。
  • 生成的文件會保存在 .changeset/ 目錄下,格式如 clever-horses-fix.md
  • 這些文件後續會被用來決定如何 bump 版本和生成 changelog。

第一個問題:選擇要改版的子包項目。「上下」鍵選擇子包,「空格」鍵表示選擇,「enter」確定最終選擇。
changeset add是要依據commit提交信息
image.png

進入第二個問題:選擇要遞增的版本級別(依次是major/minor/patch )。按「enter」進入待辦項目的下一個版本級別。(比如現在是major, 「enter」後進入minor)
image.png

進入第三個問題:總結
image.png

進入第四個問題:是否確認
image.png

最後,會多出一個changeset文件(md文件),描述了版本變更、changelog內容
image.png

2.根據 .changeset/ 中的所有 changeset 文件,自動 bump 相關包的版本,並更新依賴關係。

npx changeset version

説明

  • 它會讀取所有未處理的 changeset,計算每個包的新版本號。
  • 自動修改 package.json 中的版本字段。
  • 如果是 monorepo,還會更新內部依賴的版本(比如 pkg-a 依賴 pkg-b,且 pkg-b 升級了,這裏會自動更新引用)。
  • 同時會刪除已處理的 changeset 文件(或將其歸檔)。

image.png

可以看出子包下面都多了/修改了CHANGELOG.md文件,並且package.json的版本號發生了變化(1.0.0->1.1.0
image.png

image.png

3.將新版本的包發佈到npm倉庫

npx changeset publish
  • 説明

    • 它會執行 npm publish 對每個需要發佈的包。
    • 默認只發布那些版本號發生變化的包。
    • 需要你已經登錄到 npm(npm whoami)。
    • 此外,會給每個版本變動的包打tag.

先提交和push代碼

git add .
git commit -m 'chore(release): v1.1.0'
git push

再發布(企業級發佈,這一步通常屬於CI,在雲端流水線完成)

npx changeset publish

image.png

然後在npm倉庫 可以看到發佈的三個包
image.png

turborepo 構建

問題3:並行/串行構建所有項目,處理構建順序

1.介紹

為什麼需要 Turborepo

  • Monorepo 常見痛點:構建串行、慢;跨包依賴編排複雜;重複構建浪費時間。
  • yarn workspace 通常串行構建,整體耗時長且不可控。
  • pnpm workspace 能並行,但對複雜依賴拓撲的順序編排有限,且缺少內建的強緩存機制。
  • Turborepo面向這些問題,提供有序並行、增量與緩存的組合解法。

它解決了什麼問題

  • 構建編排:用任務依賴圖(DAG)明確“誰先誰後”,避免手工腳本膠水。
  • 並行執行:獨立包並行跑,依賴包按順序跑,縮短全量構建時間。
  • 增量構建:對輸入做哈希,只重建被改動影響的任務,減少無效工作。
  • 本地與遠程緩存:同樣輸入直接複用產物;接入遠程緩存後,CI 與開發者之間共享結果。

2.並行和構建編排

使用turbo初始化一個 Monorepo 的項目,有以下幾個 package:

  • apps/web,依賴 shared
  • apps/docs,依賴 shared
  • packages/shared,被 web 和 docs 依賴

目錄如下:
image.png

yarn 命令 只能串行運行任務: yarn workspaces run lint && yarn workspaces run build && yarn workspaces run test
image.png
但是,要使用 Turborepo 可以更快地完成相同的工作,您可以使用 turbo run lint build test
image.png
你會發現有些任務是可以並行執行的——lint和test任務可以並行。還有些是執行順序依賴的——web和docs 的構建依賴share構建完成。

配置如下:

{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "test": {
      "dependsOn": ["^build"]
    },
    "lint": {},
  }
}
  • tasks:聲明任務編排與產物輸出,key是turbo run XxxXxx
  • dependsOn:用 ^task 表示“先跑上游依賴的同名任務”,自動根據依賴圖排序。
  • outputs:聲明任務產物(如 dist/**),讓 Turborepo準確緩存和複用構建結果。

關於^task的理解是,不同包的任務編排:
當前 package 執行 build 任務之前,需要 先運行它依賴的包(dependencies / devDependencies / peerDependencies)build 任務。
當你配置了"dependsOn": ["^build"],那麼「web和docs 的構建依賴share構建完成後,再執行」,否則不會。

關於dependsOn另外一種情況,沒有^,表示同一個包中的任務編排。比如下面就表達了所有的test任務執行前,要先完成build命令。

{ 
    "tasks": { 
        "test": { "dependsOn": ["build"] } 
    }
}

進一步,你還可以限定到某個包的 任務編排。比如下面表達了 所有lint任務執行前,要先完成utils包的build任務。

{
  "tasks": {
    "lint": {
      "dependsOn": ["utils#build"] 
    }
  }
}

3.緩存和增量構建

第一次trubo run build後,會生成緩存存放在 node_modules/.cache/turbo/目錄下

第一次構建:還沒產物(沒有dist目錄),記錄子包項目文件的hash值。
image.png

第二次構建:子包文件的hash標識和之前的比較,如果沒變化,則跳過構建。
image.png

配置如下:

//turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "test": {
      "dependsOn": ["^build"]
    },
    "lint": {},
  }
}
P.S. tasks是新字段名,對應舊pipeline,含義一樣。

此外,還有cache配置和--filter命令參數:

cache:默認開啓;可對 dev 等任務關閉緩存,保證開發時的即時性。

{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "dev": {
      "cache": false
    },
  }
}

--filter:按包名或路徑過濾執行範圍。

turbo run <command> --filter=<package name>

構建(打包)工具

tsdown的主要功能

相關功能請查閲tsdown中文文檔,目前可以看到vueuse項目已經採用了tsdown來打包庫項目。

下面簡單介紹下tsdown功能:

1.零配置TypeScript支持
tsdown內置了對TypeScript的完整支持,無需額外的配置:

{
  "scripts": {
    "build": "tsdown"
  }
}

2.多格式輸出
支持同時生成多種模塊格式:

{
  "tsdown": {
    "format": ["esm", "cjs", "umd"],
    "entry": "src/index.ts",
    "outDir": "dist"
  }
}

並且自動為每個輸出格式生成對應的.d.ts聲明文件:

dist/
├── index.esm.js
├── index.esm.d.ts
├── index.cjs.js
├── index.cjs.d.ts
├── index.umd.js
└── index.umd.d.ts

3.Tree-shaking
極速Tree-shaking,自動移除未使用的代碼:

// src/utils.ts
export function used() { return 'used'; }
export function unused() { return 'unused'; }

// src/index.ts
export { used } from './utils';
// unused函數會被自動移除

4.外部依賴處理
配置外部依賴,避免將依賴打包進輸出文件:

{
  "tsdown": {
    "external": ["react", "vue"],
    "globals": {
      "react": "React",
      "vue": "Vue"
    }
  }
}

Rollup vs Vite vs tsdown

Rollup - 經典選擇

Rollup是最早專注於ESM打包的工具之一,以其優秀的Tree-shaking能力而聞名。

優勢:

  • 出色的Tree-shaking優化
  • 生成簡潔的ESM輸出
  • 豐富的插件生態系統
  • 適合庫開發

劣勢:

  • 配置相對複雜
  • 開發模式下的HMR支持有限
  • 構建速度相對較慢
Vite - 現代開發體驗

Vite基於原生ESM,提供了極快的開發服務器和優化的構建流程。

優勢:

  • 閃電般的冷啓動速度
  • 即時的熱模塊替換(HMR)
  • 開箱即用的TypeScript支持
  • 現代化的開發體驗

劣勢:

  • 主要用於應用開發
  • 對於純庫打包可能過於複雜
  • 依賴Node.js環境
tsdown - 專為ts庫項目

tsdown是一個新興的基於rolldown(基於rust)的打包工具,專門為TypeScript項目設計,結合了現代打包工具的優點。

性能對比

在實際項目中,三種工具的構建性能對比如下:

工具 冷啓動時間 HMR速度 輸出大小 配置複雜度
Rollup 中等
Vite 極快 中等
tsdown 極快 極低
選擇建議
  • 庫開發:推薦使用tsdown,零配置、多格式輸出、類型聲明自動生成
  • 應用開發:推薦使用Vite,優秀的開發體驗和構建性能
  • 傳統項目:如果已有Rollup配置且運行良好,可以繼續使用

參考

帶你瞭解更全面的 Monorepo - 優劣、踩坑、選型
從零到一使用 turborepo + pnpm 搭建企業級 Monorepo 項目
Changesets: 流行的 monorepo 場景發包工具

五、CI/CD

解釋:

  • CI (Continuous Integration 持續整合)
  • CD (Continuous Delivery/Deployment 持續交付/部署)

CI

CI的目標:

  • 自動運行測試:自動執行單元測試、集成測試等。例如:npm test
  • 代碼質量檢查:包括 ESLint、Prettier、TypeScript 類型檢查等。例如:npm run lintnpm run type-check
  • 構建驗證:確保包可以正確構建(如編譯 TypeScript 、打包等)。例如`npm run build

其中自動化測試代碼檢查構建驗證一般在什麼時候觸發?

  • push到主分支 (main, master)之前。必須的
  • push到特徵分支之前。可選
  • 提PR之前。可選

常用的CI平台 :

平台 特點
GitHub Actions 免費、與 GitHub 深度集成,YAML 配置,社區生態豐富
GitLab CI 內置於 GitLab,適合私有部署
Travis CI 曾經流行,現逐漸被 GitHub Actions 取代

CD

1.產物管理 (Artifact Management)
下載構建產物: 從 CI 流程(如 Jenkins workspace、GitHub Actions artifacts)中拉取打包好的 dist 或 build 目錄。給當前的發佈包打上 Tag 或版本號。(如果是docker,則會構建鏡像和推送到鏡像倉庫)

2.環境配置與注入 (Configuration Injection)

  • 構建時注入:如果是靜態站點,通常在 CI 階段就通過 DefinePlugin 或 import.meta.env 寫入了。
  • 運行時注入(CD 階段) 如果是 Docker 容器化部署,通常在 CD 階段通過 K8s ConfigMap 或環境變量將配置注入到容器中。

3.部署執行 (Deployment Execution)

a. 靜態資源部署 (SPA - Vue/React)

  • 上傳對象存儲: 將 HTML/CSS/JS 上傳到雲廠商的對象存儲(AWS S3, Aliyun OSS, Tencent COS)。
  • 更新CDN:為了防止用户在發佈過程中訪問到 404 文件,通常會先上傳帶 Hash 的靜態資源(JS/CSS),最後上傳入口文件(index.html)。

b.服務端應用部署 (SSR - Next.js/Nuxt/Node BFF)

  • 容器更新: 更新 Kubernetes (K8s) 的 Deployment 鏡像版本,執行滾動更新(Rolling Update)。
  • 服務重啓: 傳統服務器上使用 PM2 reload。

c.npm包這類工程產物

  • 上傳到npm倉庫。使用npx changeset publish或者npm publish完成上傳。

Git規範配置 - husky

git規範方案

該方案需要安裝以下依賴

  • husky
  • lint-staged
  • commitizen
  • cz-git
  • @commitlint/cli
  • @commitlint/config-conventional
pnpm i -wD husky lint-staged commitizen cz-git @commitlint/cli @commitlint/config-conventional

1.husky攔截Git hooks

Git Hooks:Git 原生就自帶一套“鈎子”機制。在 .git/hooks/ 目錄下,有一堆腳本(如 pre-commit.sample)。

  • 機制:當你執行特定的 Git 命令(如 commit、push、merge)時,Git 會自動去檢查這個目錄下有沒有對應的腳本文件。如果有,就執行它。
  • 痛點:.git 目錄是不會被提交到代碼倉庫的(它被 .gitignore 忽略)。這意味着,你在本地配置的鈎子腳本,你的同事拉取代碼後是看不到的,無法同步團隊規範。

Husky 的核心作用就是“篡改”Git 查找鈎子的路徑,並將其指向項目代碼中的位置(通常是根目錄下的 .husky/ 文件夾),這個文件夾是可以提交到 Git 倉庫共享給所有人的。

2.lint-staged 檢查代碼

每次提交代碼前,我們希望"提交的代碼"能通過eslint檢查。這裏有兩個關鍵詞:eslint檢查提交的代碼lint-staged就是用來做提交的代碼eslint檢查(而不是全部代碼檢查)。

3.commitlint校驗commit信息

所有的git項目,都應該去遵循一個git規範,這個規範之一就是git commit的message。你肯定見過很多這樣的message:chore(ci): xxxfeat(components): xxx。下面就介紹下常用的一種規範結構。

Commit Message 結構:

  • Header(頭部): 必須包含,是Commit Message的核心部分。

    • 類型 (type): 標記Commit的類別,例如 feat (新功能), fix (修復bug), docs (文檔), style (代碼風格) 等。
    • 範圍 (scope): 可選,用於説明Commit影響的範圍,如文件、組件或模塊。
    • 主題 (subject): 簡短地描述本次提交的內容,通常不超過50個字符,首字母小寫。
    • 格式: type(scope): subject
  • Body(描述): 可選,對Commit進行詳細描述,可以分成多行。

    • 解釋提交的動機、解決的問題等。
    • 每行建議不超過72個字符,以避免自動換行影響可讀性。
  • Footer(尾部): 可選,用於關聯Issue編號或標記重大性變更 (Breaking Changes)。

    • 關聯Issue: 使用如 Closes #123Fixes #123

@commitlint/cli 用來校驗 Git 提交信息是否符合約定的格式。配合 @commitlint/config-conventional 使用,可強制統一提交類型(如 featfixdocsrefactorchore 等)與消息結構,提升版本管理與自動化發佈的可靠性。

4.commitizen和cz-git 交互式提交

commitizen提供了cli(命令交互式)的方式來完成Commit Message填寫。
cz-git 是 Commitizen 的一個適配器(adapter),通常配合 commitizen 使用。基於cz-git可以更靈活的控制你的提交Message規範。

配置工具

1.初始化husky目錄

npx husky init

image.png

pre-commit是 Git 眾多鈎子中的一個,顧名思義,它在 Commit 之前 執行。也就是説這個文件裏的腳本,會在git commit真正執行前執行,避免不規範的提交。

2.commitizen 配置

  • Scripts: 添加 commit 命令來啓動交互式提交。
  • Config: 指定 commitizen 使用 cz-git 適配器。
    修改package.json:
{
  "scripts": {
    "prepare": "husky", //?
    "commit": "git-cz" //使用git-cz 來做提交(多個git命令的組合)
  },
  "config": { //commitizen工具的配置
    "commitizen": {
      "path": "node_modules/cz-git"
    }
  }
}
請根據你項目中實際安裝的 eslint/prettier 情況調整 lint-staged 裏的命令)

2.lint-staged配置

  • Lint-staged: 配置暫存區文件的校驗規則。
    修改package.json:
{
  "scripts": {
    "prepare": "husky", 
    "commit": "git-cz" 
  },
  "lint-staged": { // lint-staged工具的配置
    "*.{js,ts,vue,jsx,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{json,md,css,scss}": [
      "prettier --write"
    ]
  }
}

添加husky鈎子,pre-commit: 提交前執行 lint-staged(代碼格式化/檢查)。

echo "npx lint-staged" > .husky/pre-commit

3. Commitlint & Cz-git配置

cz-git 的強大之處在於它可以直接讀取 commitlint 的配置,從而實現“一份配置,兩處生效”。

在根目錄新建文件 commitlint.config.js (如果是 type: module 項目則為 .mjs 或 .cjs):

// commitlint.config.js
/** @type {import('cz-git').UserConfig} */
export default {
  // 繼承的規則
  extends: ['@commitlint/config-conventional'],
  // 自定義規則
  rules: {
    // type 類型定義,表示 git 提交的 type 必須在以下類型範圍內
    'type-enum': [
      2,
      'always',
      [
        'feat', // 新功能 feature
        'fix', // 修復 bug
        'docs', // 文檔註釋
        'style', // 代碼格式(不影響代碼運行的變動)
        'refactor', // 重構(既不增加新功能,也不是修復bug)
        'perf', // 性能優化
        'test', // 增加測試
        'chore', // 構建過程或輔助工具的變動
        'revert', // 回退
        'build' // 打包
      ]
    ],
    // subject 大小寫不做校驗
    'subject-case': [0]
  },
  // cz-git 的交互配置(可選,用於定製交互界面)
  prompt: {
    useEmoji: true, // 是否使用 emoji
    // 可以在這裏自定義提示語,例如中文化
    messages: {
      type: '選擇你要提交的類型 :',
      scope: '選擇一個提交範圍(可選):',
      customScope: '請輸入自定義的提交範圍 :',
      subject: '填寫簡短精煉的變更描述 :\n',
      body: '填寫更加詳細的變更描述(可選)。使用 "|" 換行 :\n',
      breaking: '列舉非兼容性重大的變更(可選)。使用 "|" 換行 :\n',
      footerPrefixsSelect: '選擇關聯issue前綴(可選):',
      customFooterPrefix: '輸入自定義issue前綴 :',
      footer: '列舉關聯issue (可選) 例如: #31, #I3244 :\n',
      confirmCommit: '是否提交或修改commit ?'
    },
    // 設置 scope 範圍(根據項目模塊調整)
    scopes: ['components', 'hooks', 'utils', 'styles', 'deps']
  }
}

添加 Husky鈎子,提交時校驗 commit message 格式。

echo "npx --no-install commitlint --edit \$1" > .husky/commit-msg

結果演示

現在有6個修改
image.png

#添加到暫存區
git add .![image.png](http://openwrite.cn/uploads/212/56587/b87d66aa-4bc8-4230-81d1-bef11eaaeeee.png)
# 執行commit script(替代git commit)
pnpm commit

image.png

六、實戰monorepo形式的npm項目

關於這一塊,可以參考我的一個項目 @caikengren/uni-hooks,後續我會根據這個項目展開寫幾篇博客和大家一起交流學習。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.