Everything you should know about Monorepo:那些你須要知道的 Monorepo 技術點

這篇文章旨在記錄有關 Monorepo 的全部技術點,以及基於 typescript + yarn workspaces + lerna 進行多個 node package 開發管理的最佳實踐。node

NOTE: 本文針對 library 開發的場景進行描述,其中的實踐並不必定知足全部的 monorepo 的場景,文章也不會全面的介紹各類 monorepo 的應用場景下的方案,可是會在部分章節必要的時候作一些簡單的說明。react

npm 和 yarn 之間的區別

正如咱們所熟知的,npm 和 yarn 都是原生的 node packge 的管理工具,他們在包管理方面具備相互兼容的功能,好比,依賴管理(dependency management),包發佈(publish),安裝(install)等。git

npm 是隨着 node 的發佈一塊兒發佈的,屬於 node 官方維護的包管理工具。github

yarn 是 Facebook 爲了改善前期 npm 在大型項目中依賴安裝性能而獨立開發維護的包管理工具,在依賴管理方面,yarn 除了大幅提高了安裝性能在,在分佈發佈系統中依賴一致性方面也進行了保證(yarn.lock), 從而解決了分佈發佈中因爲依賴版本不一致而致使應用表現不一致的問題。web

npm5 以後,npm 慢慢補齊了在性能與依賴一致性方面的差距,因此若是僅僅是單獨的做爲項目的依賴管理來講,使用 yarn 或者 npm 都是能夠的。typescript

yarn 與 npm 的最大區別是,yarn 原生引入了 workspaces 概念,讓使用 yarn 進行依賴管理的項目具有了原生的 mono-repo 的能力,而 npm 原生至今也沒有對等的功能,而要在不適用 yarn 的狀況下實現 workspaces 的能力,咱們不得不借助 lerna 來實現。接下來的章節,咱們會具體介紹 mono-repo & yarn workspaces & lernaexpress

什麼是 Mono-repo

mono-repo 相對應的一個概念的 multi-repo. 這兩個概念實際上是同一個問題的不一樣解決方案。npm

在咱們進行多個項目開發的過程當中,總會存在一些能夠複用的邏輯,這時候,咱們會經過可複用邏輯的相關性,把若干可複用邏輯封裝到同一個 package 中,而後咱們可能還會指望根據 package 的相關性,將若干 package 聚合在一塊兒進行管理(版本控制,git / svn),在進行 package 聚合管理的時候,就出現了兩種不一樣的管理方案:json

  • multi-repo: 最開始的時候,不少開發者會爲每一個獨立的 package 建立一個 git 倉庫,多個 package 就會存在多個相互獨立的 git 倉庫,這就是所謂的 multi-repo.bootstrap

  • mono-repo(多包倉庫): multi-repo 最大的問題在於同一個開發者同時維護多個 package, 且這些 package 之間還存在相互依賴的時候,管理起來會至關的麻煩,不只須要檢出多個倉庫,並且須要在某個 package 更新以後,手動的去更新他的依賴方,並且不一樣 package 的相同依賴須要安裝多份,因此爲了解決上述問題,提升開發效率,mono-repo 誕生了,mono-repo 會在同一個 git 倉庫中管理一組具備相關性的 package, 不一樣 package 之間的共同依賴會被提高,並且 package 之間的相互依賴也會在其中一個 package 更新後自動更新其依賴(也能夠不自動升級,下面的章節進行介紹),併發布新的版本。

爲何 & 何時用 Monorepo

因此,從上一章節的介紹中,咱們能夠了解到使用 mono-repo 的硬核指標就是:就有相關性的一組 package 的管理,咱們能夠嘗試問本身如下幾個問題來肯定咱們是否須要使用 mono-repo:

  1. 正在維護的這一組 packages 具備相關性麼?好比:都是一些工具類的庫,或者是是一個系列工具的不一樣部件的封裝(react),或者是一個體繫系統下的不一樣插件(bable)

  2. 這些 packages 之間是否相互依賴?好比有一個 core package, 被多個其餘 package 所依賴,而且須要在 core 版本發生變化後,升級依賴方的版本

  3. 這些 packages 之間是否有比較多得共同依賴?(具備侷限性,適當參考)

怎麼建立 Monorepo

從這一章節開始,咱們就須要動手來實踐,到底怎麼建立 mono-repo, 以及集成一些最佳實踐,讓咱們的項目更加標準與高效。

建立 mono-repo 業內其實有多種方案,鑑於筆者精力,本文只介紹其中比較流行且比較活躍的兩種方案 yarn workspaceslerna.

咱們先簡單的對比下這兩種方案:

  • 相同點

    • 均可以獨立的建立 mono-repo

    • packages 之間的相互依賴(如下以 local dependency 來講明)均可以使用 syslink 來連接到本地,也能夠是直接安裝已經發布到指定 registry 中的版本

    • packages 的共同依賴均可以提高到根目錄,避免重複安裝,lerna 默認狀況下不會提高,須要在 bootstrap 的時候顯示指定 --hoist 參數

  • 不一樣點

    • yarn workspaces 不具具有 local dependency 之間語義版本的自動管理,包的統一發布等,須要進入到各個包內進行手動版本更新或者發佈

    • lerna 默認使用 npm 做爲包管理工具,可是經過 npmClient 來改成 yarn, 可是若是僅僅是使用 yarn 做爲依賴管理工具,yarn 和 npm 區別不大

yarn2 對 workspaces 有了不少提高,也提供了對 local dependency 版本更新時的管理,workspaces 發佈等新功能,感興趣的能夠深刻研究,後續筆者實踐後,也會整理一些使用心得出來,官方文檔

從以上的對比中,咱們能夠發現,yarn workspaces 進行 mono-repo 的管理時,其實方案是不完備的,好比進行相似 react 或者 babel 這種有明確的版本管理的 library 類型的 mono-repo 管理只使用 yarn workspaces 是不夠的,可是對於一個 node 開發的小型 web 項目,該項目由 client 端,server 端以及一個本身開發的組件庫組成,對於這種類型的項目,沒有很強的版本管理負擔(僅組件庫須要),且各個子項目又具有必定的獨立性,爲了提升開發場景下的構建效率,僅使用 yarn workspaces 來建立 mono-repo 是很是輕量的選擇。

因此進行 library 的多包倉庫的管理時,推薦的方案是 yarn workspaces + lerna 組合,沒錯,這兩個方案是能夠完美結合的,自己 yarn 的 workspaces 就是一個偏底層的能力,而 lerna 自己也僅是利用 npm 或 yarn 提供的能力來工做的,因此咱們能夠切換 larna 底層的多包能力爲 yarn workspaces 而同時使用 leran 額外的命令來進行包的版本管理和發佈。

動手建立

完整的項目能夠參考 github.com/dancon/mono…

筆者假設各位看官已經安裝 node 以及全局安裝 yarn 1.x 或者 2.x ,若是沒有,能夠參考如下文檔自行安裝:

  • 在 github 建立一個遠程項目,遠程地址爲

  • 檢出遠程倉庫, 並進行項目初始化,這裏使用 monorepo 進行演示

# 檢出代碼庫
git clone <remote-repo url>

# 進入項目根目錄
cd monorepo

# 初始化爲 npm 項目
yarn init --yes
複製代碼

<remote-repo url> 替換爲你建立的 github 倉庫地址

至此項目只有一個 package.json 文件

{
    "private": true,  // 避免根項目被髮布出去
    "workspaces": [
        "packages/*"
    ]                 // 暫時填寫爲 packages/* 指定 workspace 位置爲 packages
}
複製代碼
# 確保在 monorepo 目錄下,建立 packages 目錄,做爲 yarn workspace 或者子項目
mkdir packages
複製代碼

至此,項目已是在 yarn workspaces 模式下工做。

  • 配置 lerna 使用 yarn workspace
# 安裝 lerna, 注意,使用 yarn add -W 將 lerna 安裝到根目錄
yarn add -W -D lerna

# lerna 初始化, 使用 independent 模式
yarn lerna init --independent
複製代碼

yarn add 的時候,若是沒有指定 -W 參數會報錯

自行建立 .gitignore 文件

生成 lerna.json 配置文件,內容以下:

{
  "packages": [
    "packages/*"
  ],
  "version": "independent"
}
複製代碼

添加如下配置,讓 lerna 切換使用 yarn 進行依賴管理,而且使用 yarn workspaces

{
  // 此項配置爲可選,在部分 IDE(VS Code 支持)中會讀這個 json schema 進行配置項的智能提示
  "$schema": "http://json.schemastore.org/lerna",
  "packages": [
    "packages/*"
  ],
  "version": "independent",
  "npmClient": "yarn",       // 告知 lerna 使用 yarn 做爲包管理工具
  "useWorkspaces": true      // 使用 yarn workspaces
}
複製代碼

至此,已經支持使用 yarn workspaces + lerna 進行管理了。

  • 初始化 typescript
# 安裝依賴
yarn add -W -D typescript

# 在更目錄下初始化
yarn tsc --init
複製代碼

修改根目錄下的 tsconfig.json 爲以下內容

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "allowJs": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "strict": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true
  },
  // 排除一些不須要處理的文件,在後面的章節中會詳細介紹其中的一些文件以及文件夾
  "exclude": [
    "node_modules",
    "packages/**/node_modules",
    "packages/**/lib",
    "packages/**/test",
    "packages/**/*.test.ts",
    "packages/**/jest.config.js"
  ]
}
複製代碼
  • 建立 packages
# 接下來建立兩個 package 分別爲 core 和 pkg1
cd packages
mkdir core
mkdir pkg1
複製代碼

每一個 package 的目錄結構以下:

.
├── src
│   └── index.ts     # 源碼目錄
├── jest.config.js   # jest 配置 yarn jest --init
├── package.json     # yarn init --yes
└── tsconfig.json    # package 的 ts 配置
複製代碼

jest.config.js 的具體配置在下面的章節中重點說明

package.json 着重說明如下配置:

{
  "name": "@scope/core",
  "main": "lib/index.js",    
  "types": "lib/index.d.ts",
  "publishConfig": {             // 若是咱們的包名包含在一個特殊的 @scope 下,爲了能讓包正常發佈,必須添加該項配置
    "access": "public"
  },
  "devDependencies": {
    "@types/node": "^13.13.5"
  },
  "scripts": {
    "test": "jest",
    "build": "rm -rf lib && tsc"  // 添加構建腳本
  }
}
複製代碼

tsconfig.json 配置內容以下:

{
  "extends": "../../tsconfig.json",  // 繼承根目錄下的 tsconfig.json 配置
  "compilerOptions": {
    "rootDir": "src",
    "outDir": "lib"
  }
}
複製代碼

接下來重點說明 mono-repo 中項目依賴管理,涉及如下幾個主題:

  • 包的安裝

  • 公共包安裝

  • 爲全部 package 安裝包

  • 安裝本地依賴

項目結合 lerna + workspaces, 因此包的安裝方式有兩種:

# 經過 yarn workspace 安裝
yarn add -W <package-name>
yarn workspace <workspace-name> add <package-name> # <workspace-name> 爲對應包 package.json 中的 name

# 經過 lerna 安裝
lerna add <package-name> # 爲全部 packages/* 安裝依賴
lerna add <package-name> --scope <workspace-name> # 效果同 yarn workspace
複製代碼

NOTE:

yarn add -W 效果和 lerna add 不指定 --scope 是不等價的

yarn add -W 是安裝通用依賴,更新根目錄下的 package.json

lerna add 不指定 --scope 是爲全部的 package 安裝依賴,更新全部 packages/**/package.json

若是咱們須要爲本身的包添加一個本地依賴,好比爲 @scope/pkg1 添加 @scope/core 做爲依賴,咱們既可使用 yarn workspace 或者 lerna add --scope 來安裝

yarn workspace @scope/pkg1 add @scope/core@1.0.0

# 等價於

lerna add @scope/core@1.0.0 --scope @scope/pkg1
複製代碼

以上方式指定了與本地 @scope/core 相同或者兼容的版本,因此 @scope/core 實際上是經過 syslinks 的方式指向本地倉庫

若是咱們不指定版本,則 yarn 或者 lerna 會從 npm registry 中拉取在線包,這時候,咱們項目中引用的就是 node_modules 中的包

如下圖爲 local dependency,指定相同或者兼容的版本號

不指定版本

版本管理與發佈

yarn workspaces 並無提供統一的版本管理與發佈,若是不是 lerna, 咱們也能夠單獨使用 yarn version,yarn publish 來進行管理,可是相互之間的依賴都須要人工管理,低效而易錯。

使用 lerna 利用 commit convention 經過 git message 來自動的進行版本管理,而且在包版本變動後,自動更新依賴方的版本,而且能夠各個包自動生成 CHANGELOG.md, 爲了使用 lerna 的這些能力,咱們只需進行如下配置:

lerna.json

{
  "packages": [
    "packages/*"
  ],
  "version": "independent",
  "npmClient": "yarn",
  "useWorkspaces": true,
  "command": {
    "publish": {
      "conventionalCommits": true,                // 啓用 commit convention 自動進行版本管理
      "registry": "https://registry.npmjs.org/",  // 指定 registry
      "message": "chore: publish"                 // lerna 自動提交時的 commit message 前綴
    },
    "version": {
      "message": "chore: publish"
    }
  }
}
複製代碼

第一次發佈的時候,無需經過 commit message 來自動升級版本,這時候,咱們可使用 from-package

lerna publish from-package
複製代碼

根目錄下的 package.json 添加以下 scripts 命令

{
  "scripts": {
    "release": "lerna publish",       // 添加 release script
    "build": "lerna exec --stream yarn build",
    "test": "lerna exec -- yarn test --passWithNoTests",
    "lint": "eslint --ext js,jsx,ts,tsx packages --fix",
    "gen": "plop"
  }
}
複製代碼

而後,後續的發佈中經過 yarn 來執行

# 第一次發佈
yarn release from-package

# 以後的發佈
yarn release
複製代碼

關於 commit convention 能夠參考: Conventional Commits

mono-repo 中,關於 commit message 須要單獨說明的是,在每次提交的時候,最好爲每一個包的修改指定對應的 scope, 好比:

git commit -m 'feat(core): add a new feature'

咱們也能夠利用 commitlint 來規範咱們的提交信息,後面的章節會詳細介紹其使用。

yarn workspaces

官方文檔:yarn workspace | Yarn

接下來咱們着重介紹一些經常使用的命令:

  • yarn workspaces info 展現當前項目中全部 workspace 的依賴關係

執行結果以下:

yarn workspaces v1.21.1
{
  "@pandolajs-test/core": {
    "location": "packages/core",
    "workspaceDependencies": [],
    "mismatchedWorkspaceDependencies": []
  },
  "@pandolajs-test/pkg1": {
    "location": "packages/pkg1",
    "workspaceDependencies": [
      "@pandolajs-test/core"
    ],
    "mismatchedWorkspaceDependencies": []
  }
}
✨  Done in 0.04s.
複製代碼
  • yarn workspaces run <command> 在全部 workspace 中執行 <command>

lerna 中的等價命令:lerna run <command>

  • yarn workspace <workspace> <command> 在指定的 <workspace> 中執行 yarn 的命令

lerna

官方文檔:GitHub - lerna/lerna: A tool for managing JavaScript projects with multiple packages.

  • 經常使用的全局參數

    • --since
  • 經常使用的命令

    • lerna bootstrap

    • lerna add

      • --scope
    • lerna publish

      • from-package
    • lerna run

    • lerna exec

Monorepo 最佳實踐

使用 typescript

每一個 workspace 中的 tsconfig.json 繼承根目錄下的 tsconfig.json 提高通用配置。

  • 安裝 typescript
yarn add -W -D typescript
複製代碼
  • 初始化項目
yarn tsc --init
複製代碼

使用 jest 進行單測

官方文檔:Getting Started · Jest

每一個 workspace 中使用本身的 jest 配置

  • 安裝依賴
yarn add -W -D jest @types/jest ts-jest
複製代碼
  • 初始化配置
yarn jest --init
複製代碼
{
    "preset": "ts-jest",
}
複製代碼

使用 eslint & prettier 統一代碼風格

更多查看文檔:www.robertcooper.me/using-eslin…

  • 安裝 lint 依賴
yarn add -W -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-config-prettier eslint-config-standard eslint-config-standard-with-typescript eslint-plugin-import eslint-plugin-jest eslint-plugin-node eslint-plugin-prettier eslint-plugin-promise  eslint-plugin-standard
複製代碼
  • 添加以下配置

.eslintrc.json

module.exports = {
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaVersion: 2020,
    sourceType: 'module',
    tsconfigRootDir: __dirname,
    project: [
      './packages/**/tsconfig.json'
    ]
  },
  env: {
    node: true
  },
  plugins: [
    '@typescript-eslint',
    'jest',
    'prettier'
  ],
  extends: [
    'eslint:recommended',
    'standard-with-typescript',
    'prettier/@typescript-eslint',
    'plugin:jest/recommended',
    'plugin:prettier/recommended'
  ],
  rules: {
    '@typescript-eslint/explicit-function-return-type': 'off',
    '@typescript-eslint/triple-slash-reference': 'off',
    '@typescript-eslint/strict-boolean-expressions': 'off'
  }
}
複製代碼
  • vscode 支持, .vscode/setting.json
{
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  }
}
複製代碼
  • prettier 配置

.prettierrc.json

{
  "semi": false,
  "tabWidth": 2,
  "singleQuote": true,
  "trailingComma": "none"
}
複製代碼

可根據本身口味進行修改

  • git hook 配置 & lint-staged
yarn add -W -D husky lint-staged
複製代碼
  • 配置以下:

.lintstagedrc

{
  "packages/**/*.{js,ts,jsx,tsx}": [
    "eslint --fix"
  ]
}
複製代碼

.huskrc.json

{
  "hooks": {
    // commitlint 配置的 git hook
    "commit-msg": "commitlint -E HUSKY_GIT_PARAMS",
    // 注意這裏 --since HEAD 的做用 參考:https://github.com/lerna/lerna/tree/master/core/filter-options#--since-ref
    "pre-commit": "lerna exec --concurrency 1 --stream lint-staged --since HEAD"
  }
}
複製代碼

使用 commitlint 進行語義版本的自動化管理

官方文檔:commitlint - Lint commit messages

  • 安裝依賴
yarn add -W -D @commitlint/cli @commitlint/config-conventional @commitlint/config-lerna-scopes
複製代碼
  • 添加配置文件

.commitlintrc.json

{
  "extends": [
    "@commitlint/config-conventional",
    "@commitlint/config-lerna-scopes"
  ]
}
複製代碼

使用 plop 進行樣板代碼配置,加速開發效率

官方文檔:GitHub - plopjs/plop: Consistency Made Simple

具體參考代碼庫實現:GitHub - dancon/monorepo

  • 安裝依賴
yarn add -W -D plop
複製代碼
  • 配置文件   plopfile.js
module.exports = function(plop) {
    // ...
}
複製代碼
  • 經常使用方法

    • setGenerator

    • setHelper

  • 項目根目錄配置如下 script

{
    "scripts": {
        "gen": "plop"
    }
}
複製代碼

而後執行 yarn gen [template-name] 就能夠建立你配置的模板,或者不指定 [template-name] 會列出全部你配置的模板。更多使用方式能夠參考官方文檔。

References

相關文章
相關標籤/搜索