Monorepo 項目管理方案:lerna + yarn workspace / pnpm

Monorepo 項目管理方案:lerna + yarn workspace / pnpm

前言

會寫這篇是因為這幾天開始了畢業設計的相關工作,然後跟著一個同學加入了一個做低代碼的項目。總之,該項目似乎是以 monorepo 的方式進行管理,而我都還不太了解這是個什麼東西,所以就藉著這篇來學習一下 monorepo 相關的知識點~

正文

什麼是 Monorepo?

monorepo 的全稱是 Monolithic Repository,是一種管理項目代碼的方式,顧名思義就是只有一個倉庫。

以往項目實踐中,對於不同模塊通常就是建多個各自的倉庫,然後各自做維護等等。但這樣的壞處就在於,對於維護不是特別方便,如果想去查看別的模塊的代碼或是邏輯,或是一個需求涉及到多個模塊的改動,那我們就必須去不同倉庫查找,修改,並且可能又要各自部署,過程中會出現很多的差錯,追根究底就是因為這種傳統管理代碼的方式過於分散,模塊之間的聯繫不夠集中。

於是就誕生了 monorepo 的代碼管理模式,就是在一個項目倉庫(repo)中管理多個模塊/包(package),不同於傳統的每個模塊建立一個 repo。

目前不少大型開源項目都採用 monorepo 的方式,像是 babel, react, vue 都是。monorepo 只需要搭建一套腳手架,就可以做到勾踐、測試、發布多個 package。

項目的第一級目錄的內容以腳手架為主,主要內容都會在 packages 目錄中,分多個 package 進行管理。大致結構會像這樣:

├── packages
|   ├── pkg1
|   |   ├── package.json
|   ├── pkg2
|   |   ├── package.json
├── package.json

此時會出現一個問題,雖然這樣該分成多個子 npm 包進行管理,但是當倉庫內容相關聯或是要進行復用時,調適開發就變得困難,可能存在依賴問題等等,也可能存在重複依賴安裝的問題,導致項目變得太大。所以,理想的開發環境應該只需要關心業務代碼,可以直接跨業務而不關心復用方式。

總的來說,monorepo 最主要的好處是 統一的工作流Code Sharing。比如我想看一個 package 的代碼、了解某段邏輯,不需要找它的 repo,直接就在當前 repo;當某個需求要修改多個 pacakge 時,不需要分別到各自的 repo 進行修改、測試、發版或者 npm link,直接在當前 repo 修改,統一測試、統一發版。只要搭建一套腳手架,就能管理(構建、測試、發布)多個 package。

Monorepo 項目管理方案:lerna + yarn workspace / pnpm

目前最常見的 monorepo 方案是 lernayarn 的 workspace 特性,接下來就來實踐一下這種 monorepo 工作流的方式。

Lerna

Lerna 是一個 npm 模塊管理工具,為項目提供了集中管理包的目錄模式,如統一的 repo 依賴安裝,package scripts 和發佈等特性。

安裝

先安裝一下,推薦全局安裝:

npm i -g lerna

// mac 可能要加 sudo 管理員權限
sudo npm i -g lerna

初始化項目

lerna init

Monorepo 項目管理方案:lerna + yarn workspace / pnpm

初始化完之後的項目目錄會長這樣:

|──lerna-test (項目根目錄)
|  |──packages
|  |  |── ...
|── lerna.json
|── package.json
  • lerna.json
{
  "packages": ["packages/*"],
  "version": "0.0.0"
}
  • package.json
{
  "name": "root",
  "private": true,
  "devDependencies": {
    "lerna": "^4.0.0"
  }
}

創建 npm 包

使用 lerna 我們通過 create 命令來增加相應的 package。

增加兩個包試試看:

lerna create @my-demo/cli
lerna create @my-demo/cli-shared-utils

Monorepo 項目管理方案:lerna + yarn workspace / pnpm

可以看到,在 lerna 的管理下,當我們創建兩個包時,lerna 在 packages 下幫我們了兩個單獨的文件夾,而每一個包都有各自的 package.json,各自維護。這就是所謂的 monorepo:只有一個倉庫,但是有多個各自獨立的包。

增加模塊依賴

lerna 中我們使用 lerna add 指令為模塊增加依賴。

  • 為所有模塊都增加依賴
lerna add chalk
  • 為指定模塊增加依賴
// 為 @my-demo/cli-shared-utils 增加 semver 模块
lerna add semver --scope @my-demo/cli-shared-utils
  • 增加模塊內部之間的依賴
lerna add @my-demo/cli-shared-utils --scope @my-demo/cli

Monorepo 項目管理方案:lerna + yarn workspace / pnpm

發布

最後就是發布 npm 包啦~但說實話,最後這一步 publish 沒想到卻是最多坑的一步,網上關於 lerna 發包的過程也大多都不大對,給出的解決方案也大多都不統一也不對,所以在這邊統一針對我碰到的問題給出有效的解決方案!

lerna 發布的命令如下:

lerna publish

第一個碰到的問題如下:

lerna notice cli v4.0.0
lerna info current version 0.0.0
lerna ERR! ENOCOMMIT No commits in this repository. Please commit something before using version.

這個問題其實也比較好解決,報錯也沒那麼矇。這個原因是因為如果要發包到 npm 上,要求需要有一個 git 倉庫。

解決辦法也很簡單,就建一個 git 倉庫然後把代碼 push 上去就好了。做法也很簡單,package.json 有一個 repository 字段可以配置該包對應的遠程倉庫,要注意的是,我們必須為每一個 package 的 package.json 都要加上相應的配置,不需要都是同一個倉庫,但是都需要配置。下面給出我自己的配置作為參考:

// package.json

{
  ...

  "repository": {
    "type": "git",
    "url": "https://github.com/cclintris/lerna-test.git"
  }

  ...
}

接下來,理論上就可以直接 publish 了,但是如果你跟我一樣,在 lerna publish 之前就忘記建立好 git 倉庫或者你忘記 push 的話,這時候你如果直接 lerna publish 的話會再度失敗:

401 Unauthorized - GET https://registry.npmjs.org/-/npm/v1/user
lerna ERR! EWHOAMI Authentication error. Use `npm whoami` to troubleshoot.

這個報錯其實純粹是我在白目 hh。這個原因是因為還沒有登入 npm 帳號,不過這會引出另一個問題。

npm login

記得這邊必須用 npm 進行登入,因為 lerna 默認是 npm 作為發包 host 的,所以如果你用 yarn 登入的話他不會讓你發包。當然硬要用 yarn 也不是不行,但是則需要額外的配置,就是在 lerna.json 中有一個 npmClient 字段可以進行配置:

// lerna.json

{
  ...

  "npmClient": "npm",
  // or
  "npmClient": "yarn"

  ...
}

最後紀錄一個比較坑的問題,當你也用 npm 登入之後,可能還會遇到下面這個問題,導致 lerna publish 發包失敗:

lerna notice cli v4.0.0
lerna info current version 0.0.1
lerna info Looking for changed packages since v0.0.1
lerna success No changed packages to publish

這個 lerna 的控制台輸出並不是報錯,但是很明顯也不像是發布成功,到 npm 上也確實就會發現根本沒有發上去。這個原因跟 lerna 這個工具本身的發布機制有關。

追根究底,出現這個問題只有一種可能,就是在第一次執行 lerna publish 時失敗了,但是這個失敗並不是單純的失敗,其實是因為 lerna 在上次失敗的 publish 之後就為 packages 所有的代碼打上了一個 git tag,而在下次 publish 之前 lerna 就會去檢查該 git tag,發現沒有做任何 packages 相關的修改,於是 lerna 就認為沒有更新,沒有需要重新發包的必要。

有趣的是,雖然一行代碼都沒改,但是這時候你去看每個包各自的 package.json,會發現 version 字段被默認的累加了一個版本上去。對於這種情況的解決方式是執行以下命令:

lerna publish from-package

加上後面的 from-package 參數的意思就是,發布在最近 commit 中修改了 package.json 中的 version,且該 version 在 npm registry 中沒有發布過的包。這樣一來就能成功發布 npm 包了。

關於 lerna publish 還有很多參數,每個都做到不一樣的事情,且關於 lerna 也還有其他操作。這邊僅僅是列出一些最常見的操作,以及自己碰到的坑,具體情況還是根據自己做解決。

Monorepo 項目管理方案:lerna + yarn workspace / pnpm

依賴包管理

上述大概包含了 lerna 的整個生命週期了。不過當我們維護這個項目時,新拉下來倉庫的代碼後,需要為各個 package 安裝依賴包。

我們在第 4 步 lerna add 時也發現了,為某個 package 安裝的包被放到了這個 package 目錄下的 node_modules 目錄下。這樣對於多個 package 都依賴的包,會被多個 package 安裝多次,並且每個 package 下都維護 node_modules ,也不清爽。

為此 lerna 也提供了一個 --hoist 參數來把每個 package 下的依賴包都提升到工程根目錄,來降低安裝以及管理的成本。

lerna bootstrap --hoist

為了省去每次都輸入 --hoist 參數的麻煩,可以在 lerna.json 配置:

{
  "packages": ["packages/*"],
  "command": {
    "bootstrap": {
      "hoist": true
    }
  },
  "version": "0.0.1-alpha.0"
}

配置好後,對於之前依賴包已經被安裝到各個 package 下的情況,我們只需要清理一下安裝的依賴即可:

lerna clean

Monorepo 項目管理方案:lerna + yarn workspace / pnpm

可以看到,現在各自的 package 下的 node_modules 只剩下單獨依賴的資源,其他共用的包都會被提升到根目錄下的 node_modules

最後再次執行 lerna bootstrap 即可看到 package 的共同依賴都被安裝到根目錄下的 node_modules 中了。

monorepo 實踐技術方案

上面介紹完了 lerna 一些基本的使用方式和概念。這邊接著
來學習一下 monorepo 實際推薦的技術方案,當然不可能就只用 lerna 啦~

目前最常見的 monorepo 解決方案是 Lerna 加上 yarnworkspaces 特性,基於 lernayarn workspace 的 monorepo 工作流。由於 yarnlerna 在功能上有較多的重疊,我們采用官方推薦的做法,用 yarn 來處理依賴問題,用 lerna 來處理發布問題。

lerna + yarn workspace

搭建環境

  • 普通項目:clone 下來後直接 yarn install 就好,完成所有依賴安裝

  • monorepo: 各個 package 之間存在依賴,如 A 依賴於 B,因此我們通常需要將 B link 到 A 的 node_modules 裏,一旦 package 很多的話,手動的管理這些 link 操作負擔很大,因此需要使用自動化的 link 操作

解決方式:

yarn install

對於 monorepo 來說,其實也就是執行 yarn install 而已。但是原理其實是因為,yarn install 本身就附帶使用到了 yarn workspace 的功能,會自動幫忙解決安裝還有 link 一堆麻煩的問題。

其實上面也有提到 lerna bootstrap 這個命令,但是因為 lerna 默認只支持 npm,但是 npm 又不支持 workspace,所以才會說推薦的方案是 lernayarn 聯用。

yarn install

// 等價於

lerna bootstrap --npm-client yarn --use-workspaces

清理環境

在依賴亂掉或者工程整個亂掉的情況下,通常我們有可能選擇清理依賴。

  • 普通項目: 直接刪除 node_modules 以及編譯後的產物

  • monorepo: 不僅需要刪除 root 的 node_modules 和編譯產物還需要刪除各個 package 裏的 node_modules 以及編譯產物

解決方式:

lerna clean // 清理所有的 node_modules

yarn workspaces run clean // 執行所有 package 的clean 操作(清理編譯產物)

依賴管理

  • 普通項目:通過 yarn addyarn remove 即可簡單姐解決依賴庫的安裝和刪除問題

  • monorepo: 一般分為三種場景

  1. 某個 package 安裝依賴:
yarn workspace [packageB] add [packageA]
// 將 packageA 作為 packageB 的依賴進行安裝
  1. 所有 的 package 安裝依:
yarn workspaces add lodash
// 給所有的 packages 安裝依賴
  1. root 安裝依賴:一般的公用的開發工具都是安裝在 root 裏,如 typescript。
yarn add -W -D typescript

刪除依賴的話就把 add 全部改成 remove 就好了。

項目構建

  • 普通項目:建立一個 build 的 npm script,使用 yarn build 即可完成項目構建

  • monorepo:區別於普通項目之處在於各個 package 之間存在相互依賴,如 packageB 只有在 packageA 構建完之後才能進行構建,否則就會出錯,諸如此類,這實際上要求我們以一種順序規則進行構建。

可惜的是,yarn workspace 目前還不支持這種按照特定順序進行構建的需求,好在 lerna 提供了一個可以按照拓墣排序進行順序執行各 package 下某 npm script 的一種方法(構建的話通常是 build)。

lerna 如何進行這樣的拓墣排序呢?在 lerna 中,我們不會明定所謂的拓墣排序,我們通過在 package 中引入其他 package 作為依賴來指定拓墣結構。

舉例來說,如果 package1 必須在 package2 build 之後才能被 build,那麼我們就要將 package1 作為依賴添加到 package2 的 package.json 中。至於選擇添加到 dependencies 還是 devDependencies 都可以,取決於具體需求場景,但兩者都是可以被 lerna 正確解析的拓墣結構。

最後當拓墣結構都配置好後,我們就通過 lerna 提供的一個 --sort 參數告訴 lerna 要以拓墣順序的方式執行每一個 package 的某腳本:

lerna run --sort build

發布

最後就是發布的動作。上述大部分都是依賴 yarn workspace 負責一些依賴包管理的工作,最後發布的時候,推薦用 lerna 進行發布。也很簡單:

lerna publish [param]

其實就是 publish 命令,至於後面要不要跟什麼參數,在這邊就暫時不列出,有碰到需要時再去根據實際狀況搜吧~

pnpm

上面介紹了一種實踐 monorepo 常見的技術方案,接下來這邊要介紹第二種,pnpm,也是這次畢業設計的項目中應用到的 monorepo 管理方式。

pnpm VS lerna + yarn workspace

在實際開始 pnpm 之前,當然要先說說為什麼要用它,不用最普遍的 lerna + yarn workspace 就好?下面簡單總結幾點,了解一下就行了:

  1. pnpmnpm, yarn 更快

  2. pnpm 比起其他主流的包管理器來說更輕,更小。畢竟有時候我們不見得需要那麼多功能

  3. lerna + yarn 各種配置有時過於複雜,而且就 monorepo 的實踐來說,pnpm 天生就有這種能力,而 lerna + yarn 只是借用了某些能力得以實現

建立 monorepo workspace

當然首先要先裝 pnpm

npm install -g pnpm

然後建立一個項目先:

pnpm init

在項目根目錄建立一個 pnpm 的 workspcae 配置文件 pnpm-workspace.yaml,表明著這是一個 monorepo:

# pnpm-workspace.yaml

packages:
  # all packages in subdirs of packages/ and components/
  - "packages/**"
  # exclude packages that are inside test directories
  - "!**/test/**"

依賴管理

在 monorepo 的項目中,通常每個模塊,或是說每個 package 都會有一些公用的依賴。

假如你都是 react 技術棧,每個項目都要安裝 react 是不是太過於繁瑣和占用時間、空間?

yarn + lerna 中的方案是配置自動擡提升(hoisting),上面也有提到,這種方案會存在依賴濫用的問題,因為提升到頂層後是沒有任何限制的,一個依賴可能加載到任何存在於頂層 node_modules 存在的依賴,而不受他是否真正依賴了這個包,這個包的版本是多少的影響。但在 pnpm 中,無論是否被提升,存在默認隔離的策略,這一切都是安全的。

所以建議去除每個子項目的共同依賴,比如 react ,lodash 等,然後統一放入頂層的 package.json 內:

安裝於頂層依賴的命令如下:

pnpm add -w lodash

// -D 表示安裝到 devDependencies
pnpm add -D -w typescript

配置啟動命令

假定我們有兩個 react 子項目,分別為 @mono/app1 和 @mono/app2 (這裏指的是所在子項目的 package.json 裏的 name 名字):

那麽在項目根目錄 package.json 配置項目啟動命令:

"scripts": {
    "dev:app1": "pnpm start --filter \"@mono/app1\"",
    "dev:app2": "pnpm start --filter \"@mono/app2\""
},

--filter 可以指定要作用的具體子 package,可以配合任何 pnpm 的命令一起使用。

其實 pnpm 也就是一個包管理工具,關於 pnpm 更多可以上他的官網去學習,上去以後就會發現感覺上這仍然是一個很新的生態跟工具。本人對於 pnpm 也還沒有特別熟悉,僅是紀錄一下目前所學。

結語

這篇涵蓋了關於 monorepo 的基本概念,以及兩種不同實踐 monorepo 的技術方案,lerna + yarn 以及 pnpm。之後對於這些相關的知識點如果還有深入學習都會再持續更新分享,關於這篇希望對本來完全不懂 monorepo 的人來說有點幫助,若有不完整或錯誤的地方也歡迎各位大佬多多指教~

上一篇:yarn 和 npm的问题 yarn安装vue 不是内部命令


下一篇:flink启动命令分析