lerna + yarn workspace 使用總結

語言: CN / TW / HK

theme: cyanosis

前言

本文是筆者在學習和應用 lerna + yarn workspace 多包工程化管理模式的過程中,記錄的一些使用和問題彙總,作為筆記和分享於大家閱讀。

lerna 管理方式屬於 Monorepo 模式,這有別於傳統的 Multirepo 單倉庫應用模式,下面我們先來了解一下兩者的區別。

模式對比

Multirepo

傳統的專案開發模式,比如 create-react-appvue-cli 等框架模板腳手架生成的專案。 - 優點: 1. 各模組管理自由度高; 2. 各模組倉庫體積一般不會太大;

  • 缺點:
    1. 倉庫分散不好找,分支管理混亂;
    2. 版本更新頻繁,公共模組版本發生變化,需要對所有模組進行依賴更新;

### Monorepo - 優點: 1. 統一的規範、構建工具; 2. 方便版本管理和依賴,模組之間的引用除錯都變得非常方便; 3. Multirepo 的缺點就是它的優點。

  • 缺點:
    1. 隨著應用擴充套件,倉庫體積將變大。

lerna + yarn workspace 應用場景

  1. 作為業務元件庫的工程環境搭建。 實現單個業務元件作為單獨的 npm 包 進行管理和釋出,無需將各個業務元件分開建立在多個 Git 倉庫中,且它們的技術棧、構建工具、規範等都可以保持一致。

  2. 作為日常業務專案工程管理。 比如有一個低程式碼業務需求,低程式碼核心工作區的互動都相同,不同的是業務場景(外層殼子和一些定製功能),低程式碼相關的模組都可以複用,我們只需在這個倉庫內不斷去擴充套件業務需求即可,達到核心程式碼的複用(當然,可能會想到將低程式碼核心作為線上包)。

Lerna

Lerna 是一個管理工具,用於管理包含多個軟體包(package)的 JavaScript 專案,是 Babel 自己用來維護自己的 Monorepo 並開源出的一個專案。

它可以: - 統一的一套規範、構建標準; - 對相互耦合較大、相互獨立的 JS/Git 庫進行管理; - 統一的工作流和 Code Sharing(程式碼共享)。

下面我們從以下幾個方面來熟悉 Lerna:

  • Lerna 管理模式;
  • Lerna 入門指引;
  • Lerna 管理命令;
  • Lerna 配置檔案;
  • Lerna 應用Demo;
  • Lerna 版本釋出;
  • Lerna 最佳實踐;
  • Lerna 注意事項。

Lerna 管理模式

lerna 管理專案可以使用兩種模式,預設固定模式,當使用 lerna init -i 命令初始化專案時,此時為獨立模式。(模式是用來管理多個 package 發包時的方式)

  1. 固定模式: 在發包時會檢測 packages 下涉及到變更的包,給這些變更的包使用同一版本,未發生變更的包不應用改版本,且不做釋出升級;釋出時可通過 lerna publish major(大) | minor(中) | patch (小)自定義版本。

  2. 獨立模式(常用的模式): 允許每個包有自己獨立的版本號,在 lerna publish 釋出時,需要為每個改動的庫指定版本號(逐個詢問需要升級的版本號)。此模式,lerna.json - version 欄位指定為 independent。

Lerna 入門指引

  1. 全域性安裝 Lerna: bash npm install --global lerna

  2. 初始化 git 程式碼倉庫: bash git init lerna-repo && cd lerna-repo

  3. 初始化 Lerna: bash lerna init // lerna info Creating package.json // lerna info Creating lerna.json // lerna info Creating packages directory // lerna success Initialized Lerna files

  4. 此時得到了這樣一個倉庫目錄結構: bash lerna-repo/ packages/ package.json lerna.json 其中 packages 中儲存著每個獨立的包模組。

  5. 安裝 lerna 到倉庫 node_modules 中: bash npm install

至此,我們就完成了一個 Lerna 工程的初始化工作,下面我們掌握一些操作命令來管理 Lerna。

Lerna 管理命令

  1. lerna init 將一個倉庫初始化為 lerna 倉庫(預設固定模式)。支援引數: bash --independent/-i – 使用獨立的版本控制模式

  2. lerna create 「package」 建立一個 package 到專案工程的 packages 下。

  3. lerna add 「module」

  4. 為每個 package 都安裝指定依賴: bash lerna add react
  5. 為指定的 package 安裝特定依賴: bash lerna add react-dom packages/package1 // or lerna add react-dom --scope=package1
  6. 新增依賴到根目錄 node_modules 中: bash npm install typescript -D
  7. package 之間的相互依賴(會在 package/package.json 下新增該依賴): bash lerna add package2 --scope package1 // or lerna add package2 packages/package1

  8. lerna publish 用於 npm 包版本釋出,具體細節可看下文 「Lerna 版本釋出」。

  9. lerna bootstrap 用於將 packages 連結在一起(前提是相互依賴的庫),並安裝 package 下的依賴到 package/node_modules。

    注意,它不會安裝根目錄 package.json 的依賴,如果需要安裝根目錄依賴,請使用 npm install。

引數:
- --hoist:依賴提升,把每個 package 下的依賴包都提升到工程根目錄(刪除包下的 node_modules,將依賴安裝在根目錄,但依賴註冊不會在 package/package.json 內刪除,也不會在 root/package.json 內新增此依賴)

  1. lerna clean 刪除各個包下的 node_modules(不會刪除根目錄 node_modules)。

  2. lerna ls 列出當前 Lerna 倉庫中的所有公共軟體包(public packages)。

  3. lerna run 「script」

  4. 執行每個包下面的 script(如果某個包沒有此 script,將會報錯) bash lerna run test
  5. 執行某個包下面的 script bash lerna run test --scope package1

  6. lerna exec 「shell」 允許去執行 shell 指令碼 bash lerna exec webpack

  7. lerna changed 檢查自上次釋出以來哪些軟體包被修改過。

  8. lerna link 連結互相引用的庫,當 pakcage/package.json 內明確了 packages 下的包時,才會將相關包連結到 package/node_modules 中。

  9. lerna info 檢視 lerna 及執行環境的資訊。

Lerna 配置檔案

在 lerna.json 配置檔案內可以指定工作模式、packages 的位置以及一些命令的預設引數定義,如下示例: json { "version": "1.0.0", "npmClient": "yarn", "useWorkspaces": true, "packages": [ "packages/*" ], "command": { "bootstrap": { "npmClientArgs": [ "--no-package-lock" ] }, "version": {}, "publish": { "npmClient": "npm", "ignoreChanges": [ "**/*.md", "**/test/**" ], "message": "chore(release): publish", "registry": "https://registry.npmjs.org", "conventionalCommits": true } } }

  • version: 當前倉庫的版本,Independent mode 請設定為 independent;
  • packages: 指定包所在的目錄,支援指定多個目錄;
  • npmClient: 允許指定命令使用的client, 預設是 npm, 可以設定成 yarn;
  • useWorkspaces: 使用 yarn workspaces 管理 Monorepo;
  • command.bootstrap.npmClientArgs: 指定預設傳給 lerna bootstrap 命令的引數;

  • command.publish.ignoreChanges: 指定那些目錄或者檔案的變更不用觸發 package 版本的變更;

  • command.publish.message: 執行釋出版本更新時的生成的 commit message;
  • command.publish.registry: 指定釋出到的 registry url,比如可以釋出到指定私服,預設是 npmjs.org;
  • command.publish.conventionalCommits: lerna version 將生成 CHANGELOG.md files(如果設定了這個,lerna 管理模式將直接使用固定模式,version = independent 的配置將失效)。

Lerna 應用 Demo

有了上面的基礎使用的瞭解,下面我們通過一個簡單 Demo 熟悉一下 Lerna 管理 Packages 的流程方式。

  1. 建立 Lerna 工程: bash git init lerna-demo && cd lerna-demo && lerna init

  2. 建立兩個 package: bash lerna create lerna-module1 lerna create lerna-module2

  3. 在 package 中維護幾行測試程式碼: ```js // lerna-module1/lib/lerna-module1.js module.exports = lernaModule1; function lernaModule1() { console.log('lerna-module1'); }

// lerna-module2/lib/lerna-module2.js const lernaModule1 = require('lerna-module1'); module.exports = lernaModule2; function lernaModule2() { console.log('lerna-module2'); } lernaModule1(); lernaModule2(); ```

  1. 在 lerna-module2 下新增一個執行指令碼: json // lerna-module2/package.json "scripts": { "test": "node ./lib/lerna-module2.js" }

  2. 執行指令碼: bash lerna run test --scope lerna-module2 哎呀,此時會看到終端報錯資訊:

    Error: Cannot find module 'lerna-module1'

  3. 手動建立 package 之間的關聯: bash lerna add lerna-module1 --scope lerna-module2 // lerna info Adding lerna-module1 in 1 package

    此時可以在 lerna-module2 目錄下看到生成了 node_modules 資料夾,並且在裡面放置了和 lerna-module1 一模一樣的包(軟連結)。

  4. 再來執行一次命令: bash lerna run test --scope lerna-module2 終端輸出: lerna-module1 lerna-module2

好啦,我們第一個簡單 Lerna 應用編寫完成。接下來就是釋出工作。

Lerna 版本釋出

packages 下的包版本釋出需要使用 lerna publish,這個命令組合了這兩個命令:lerna versionnpm publish

其中 lerna version 針對 Lerna 的管理模式(固定模式和獨立模式),在表現上有所不同。

但主要工作還是在進行 npm publish 之前,去管理哪些包要進行釋出,以及釋出過程中生成的 Git commit、Git tag 的提交。

  1. 固定模式下的 lerna version:
  2. 找出從上一個版本釋出以來有過變更的 package
  3. 根據當前 lerna.json 中的版本生成新的版本號
  4. 更新涉及到變更 package 下的 package.json 版本號;
  5. 更新 lerna.json 檔案中的版本號;
  6. 將 version 更新、生成的 CHANGELOG.md 檔案帶來的變動提交為一次 commit
  7. 基於這次 commit 為所有涉及到更新的 package 打上各自的 tag
  8. 推送 commit 和 tags 到遠端倉庫。

  9. 獨立模式下的 lerna version:

  10. 找出從上一個版本釋出以來有過變更的 package
  11. 提示開發者為需要更新的 package 選擇(一組 Version Select)要釋出的版本號
  12. 更新到 package 下的 package.json version 版本號
  13. 如果 packages 下其他包有依賴這個包,那麼其他包的 package.json 此包版本也會更新;
  14. 將 version 更新、生成的 CHANGELOG.md 檔案帶來的變動提交為一次 commit
  15. 基於這次 commit 為所有涉及到更新的 package 打上各自的 tag
  16. 推送 commit 和 tags 到遠端倉庫。

這裡需要注意一下 lerna 查詢包變更的邏輯:

在當前分支,找到最近一次 tag,將當前 commit 和 tag 進行比較,看哪些 package 下的檔案發生了變更。

命令使用如下:
bash lerna publish lerna publish semver // semver bump [major | minor | patch | premajor | preminor | prepatch | prerelease] lerna publish from git lerna publish from-package

初次使用釋出時可能會遇到以下一些問題和注意事項: 1. 避免開發者自己去打 tag。 lerna 釋出時會自動生成 tag,並且查詢更新是基於 tag 來識別的,避免開發者手動打上 tag 後,影響 lerna 查詢變更,可能會造成一些變更包沒有按照預期釋出。

  1. 避免多條分支同時進行。 在多條分支同時進行時,可能會生成相同的版本號,從而發生版本衝突。解決辦法:分支開發者之前應提前約定好版本。

  2. lerna publish 中途釋出失敗,如何進行重發布。 有時候釋出可能會失敗(比如 npm 沒有登入、沒有使用 npmjs 映象源),再次執行 lerna publish 時,因為 tag 已經打上了,無法再查詢到更新,進行包的釋出。

可以採用下面兩種釋出方式:
- 執行 lerna publish from-git。會將當前標籤中涉及的NPM包再發布一次。(不會再更新package.json,只是執行npm publish); - 執行 lerna publish from-package。會將當前所有本地包中的 package.json 和遠端 npm 比對,如果 npm 上不存在此包的最新版本,都執行一次 npm publish。

Lerna 最佳實踐

目前業界使用最多的方案是:lerna + yarm workspace 結合的 Monorepo 方案,兩者工作職責劃分不同: - yarn 處理依賴安裝工作(只想做好包管理工具); - lerna 處理髮布流程。

此處內容可以在下文檢視 yarn workspace 使用指南。

Leran 注意事項

  1. 釋出前,提交工作區的變更。 在釋出前,需要提交工作區的檔案變更,否則終端會收到下面報錯資訊:

    lerna ERR! EUNCOMMIT Working tree has uncommitted changes, please commit or remove the following changes before continuing:

  2. 釋出前,需使用 npmjs.org 映象。 在釋出前,如果 npm 設定的映象源為淘寶映象,需要切換回 npm 映象: bash npm config get registry npm config set registry https://registry.npmjs.org

  3. 如果要釋出一個 Scope 包: Scope 是指具有“組織”的包,比如 Babel 的相關包都是這一格式:@babel/xxx,在釋出一個具有 Scope 包時,需要確保 Organization(組織)已在 npm 上建立,私有包需要收費,公共包則為免費。

在釋出 Scope package 時,需要在 package.json 宣告 access publishjson { "name": "@feu/tools", "publishConfig": { "access": "publish" // 如果該模組需要釋出,對於 scope 模組,需要設定為 publish } }

  1. 釋出意外中斷,進行重發布: 如果釋出因為某些原因中斷了,未釋出成功,再次執行釋出,會得到如下提示:

    lerna success No changed packages to publish

但由於包並未成功釋出到 npmjs 上,這時可以執行以下命令進行重發布: bash lerna publish from-git // or lerna publish from-package

  1. independent 模式並未生效: 在 lerna.json 下指定了 version 為 independent,但是釋出時卻還是固定模式的流程,原因可能是 lerna.json 內配置了 conventionalCommitsjson "command": { "publish": { "conventionalCommits": true } }

可以將其配置移除得到解決。

  1. 固定模式如何自己指定版本: 當我們執行 lerna publish 時,lerna 會自定分配一個版本提供我們使用;但這個版本可能不是我們期望釋出的版本;如何自己控制釋出的版本呢,在釋出時我們可以傳遞配置: bash lerna publish major(大) | minor(中) | patch (小) lerna publish patch // 釋出小版本

yarn workspace

對於 Monorepo 的工程,使用最多的方式是 lerna 結合 yarn workspace 一起使用。

因為 yarn 在依賴管理上做的非常不錯,適合我們業務場景的依賴模組管理。

package 的釋出工作依舊交由 lerna publish 來運轉。

下面我們從以下幾個方面來熟悉 yarn workspace:

  • yarn workspace 管理工程;
  • yarn workspace 管理命令;
  • yarn workspace 入門實戰。

yarn workspace 管理工程

初始化工程的步驟和上面 lerna 的方式一樣,與 lerna 不同的是,需要做以下配置:

  1. lerna.json 中宣告使用 yarn workspace 進行依賴管理: json { ... "npmClient": "yarn", "useWorkspaces": true }

  2. root/package.json 下必需包含 workspaces 陣列,與 lerna.json 下的 packages 保持一致: json { "private": true, // 工作空間不需要釋出 ... "workspaces": ["packages/*"] }

yarn workspace 管理命令

yarn 管理命令大致分為兩類(容易混淆,這裡先提及一下): - 處理工程下指定的包模組時使用:yarn workspace; - 處理工程根目錄全域性或所有包模組時使用:yarn workspaces

  1. yarn install 代替 npm install + lerna bootstrap 安裝工程依賴。

它與 lerna bootstarp 不同的是: - yarn install 會將 package 下的依賴統一安裝到根目錄之下。這有利於提升依賴的安裝效率和不同 package 間的版本複用(有些包是需要私有依賴的,而私有依賴會被多個包安裝多次,而提升依賴可以解決這一問題)。 - yarn install 會自動幫助解決安裝(包括根目錄下的安裝)和 packages link 問題。

  1. yarn add 「module」
  2. 為每個 package 都安裝指定依賴: bash yarn workspaces add react
  3. 為指定的 package 安裝特定依賴: bash yarn workspace package1 add react react-dom --save

    注意,package1 一定是 packages/package1/package.json name 欄位,有時候 package 的目錄名和 name 欄位不一致,要以 name 為準。

  4. 新增依賴到根目錄 node_modules 中: bash cd 根目錄 yarn add @babel/core -D -W (-W 表示將依賴新增到 workspaces 根目錄)

  5. package 之間的相互依賴(會在 package/package.json 下新增該依賴): bash yarn workspace package1 add package2

    注意,當 package2 沒有釋出在 npmjs 上時,此時會報錯:package2 not found;解決辦法:顯示指定 package2 的版本: yarn workspace package1 add [email protected]^1.0.0

  6. 在工程根目錄下引入 packages/package 包: bash yarn add [email protected]^1.0.0 -W

  7. yarn remove「module」 和上面 yarn add 命令格式相同,只需將 add 替換為 remove 即可。

  8. yarn run 「script」

  9. 執行工程根目錄下 scriptbash yarn test
  10. 執行指定包模組下的 scriptbash yarn workspace package1 run test

    值得注意的是,命令雖然是在根目錄下執行,但在執行檔案中拿到的 process.cwd() 是 package 下的執行檔案所在路徑

  11. 執行所有 package 下的 script 命令: bash yarn workspaces run test

    注意,如果某個 package 下沒有對應 script,將會終止命令,並報錯。若 package 不具備 script,可以定義一個佔位 script,類似如下:

json "scripts": { "lint": "echo lint successful." }

  1. yarn workspaces info 檢視 workspace 依賴樹資訊。

yarn workspace 入門實戰

  1. 新建 yarn workspace 工程: bash git init yarn-demo && cd yarn-demo && yarn init -y && yarn add lerna -D && lerna init

  2. 配置 lerna.json 改用 yarn workspaces: json // lerna.json { "npmClient": "yarn", "useWorkspaces": true, "packages": [ "packages/*" ], "version": "independent" }

  3. 根目錄 package.json 必須包含一個 workspaces 陣列: json { "private": true, // 工作空間不需要釋出 "workspaces": ["packages/*"], ... }

  4. 新建兩個 package: bash cd packages && mkdir yarn-module1 && cd yarn-module1 && yarn init -y cd packages && mkdir yarn-module2 && cd yarn-module2 && yarn init -y

  5. 新增幾行測試程式碼: ```js // yarn-module1/index.js function yarnModule1() { console.log('yarn-module1'); }

module.exports = yarnModule1;

// yarn-module2/index.js const yarnModule1 = require('yarn-module1');

function yarnModule2() { console.log('yarn-module2'); }

yarnModule1(); yarnModule2();

module.exports = yarnModule2; ```

  1. 為 yarn-module2 添加個 script: json // yarn-module2/package.json "scripts": { "test": "node index.js" }

  2. 回到根目錄執行 script: bash yarn workspace yarn-module2 run test

不出意外,會得到如下錯誤:

Error: Cannot find module 'yarn-module1'

  1. 建立 package 之間的關係: bash yarn install 可以看到,根目錄下的 node_modules 中已經存在了 yarn-module1 和 yarn-module2 這兩個包,與 lerna 的區別在於沒有在各自的 package 下建立 node_modules,而是統一連結到根目錄。

但,yarn-module2 中依賴的 yarn-module1,應該將其新增到 package.json 中,最好的方式是採用: bash yarn workspace yarn-module2 add [email protected]^1.0.0

  1. 再來一次 script: ```bash yarn workspace yarn-module2 run test

輸出: yarn-module1 yarn-module2 ```

最後

感謝閱讀。

借鑑:
阿里巴巴業務中臺前端專欄 - 圖解 lerna publish