1.Monorepo
Monorepo 是管理項目代碼的一個方式,指在一個項目倉庫 (repo) 中管理多個模塊/包 (package),不同於常見的每個模塊建一個 repo。
目前有不少大型開源項目采用了這種方式,如 Babel:
How is the repo structured?
The Babel repo is managed as a monorepo that is composed of many npm packages.
還有 create-react-app, react-router 等。可以看到這些項目的第一級目錄的內容以腳手架為主,主要內容都在 packages
目錄中、分多個 package 進行管理。
├── packages | ├── pkg1 | | ├── package.json | ├── pkg2 | | ├── package.json ├── package.json
monorepo 最主要的好處是統一的工作流和Code Sharing:
比如我想看一個 pacakge 的代碼、了解某段邏輯,不需要找它的 repo,直接就在當前 repo;當某個需求要修改多個 pacakge 時,不需要分別到各自的 repo 進行修改、測試、發版或者 npm link
,直接在當前 repo 修改,統一測試、統一發版。只要搭建一套腳手架,就能管理(構建、測試、發布)多個 package。
不好的方面則主要是 repo 的體積較大:
特別是,因為各個 package 理論上都是獨立的,所以每個 package 都維護着自己的 dependencies,而很大的可能性,package 之間有不少相同的依賴,而這就可能使install
時出現重復安裝,使本來就很大的 node_modues
繼續膨脹(我稱這為「依賴爆炸」...)。
基於對以上的理解,我認為當項目到一定的復雜度,需要且可以划分模塊、但模塊間聯系緊密的,比較適合用 monorepo 組織代碼。
目前最常見的 monorepo 解決方案是 Lerna 和 yarn
的 workspaces
特性。其中,lerna 是一個獨立的包,其官網的介紹是:
a tool that optimizes the workflow around managing multi-package repositories with git and npm.
上面提到的 Babel, create-react-app 等都是用 lerna 進行管理的。在項目 repo 中以lerna.json
聲明 packages 后,lerna 為項目提供了統一的 repo 依賴安裝 (lerna bootstrap
),統一的執行 package scripts (lerna run
),統一的 npm 發版 (lerna publish
) 等特性。對於「依賴爆炸」的問題,lerna 在安裝依賴時提供了--hoist
選項,相同的依賴,會「提升」到 repo 根目錄下安裝,但lerna 直接以字符串對比 dependency 的版本號,完全相同才提升,semver 約定在這並不起作用。
而使用 yarn 作為包管理器的同學,可以在 package.json
中以 workspaces
字段聲明 packages,yarn 就會以 monorepo 的方式管理 packages。相比 lerna,yarn 突出的是對依賴的管理,包括 packages 的相互依賴、packages 對第三方的依賴,yarn 會以 semver 約定來分析 dependencies 的版本,安裝依賴時更快、占用體積更小;但欠缺了「統一工作流」方面的實現。
lerna 和 yarn-workspace 並不是只能選其一,大多 monorepo 即會使用 lerna 又會在 package.json
聲明 workspaces
。這樣的話,無論你的包管理器是 npm 還是 yarn,都能發揮 monorepo 的優勢;要是包管理是 yarn ,lerna 就會把依賴安裝交給 yarn 處理。
2.簡單示例:
首先全局安裝lerna:
npm i -g lerna
創建一個項目文件夾並生成.git文件:
git init lerna
初始化lerna:
lerna init
在生成的packages文件夾中添加兩個包:
mkdir module1、mkdir module2
在module1中創建package.json:
npm init -y
在module1中新建index.js文件:
require("module2")
在module1中修改package.json:
"dependencies": { "module2": "^1.0.0" }
為packages目錄下所有包安裝它們的依賴,為內部互相依賴的package建立symlink,對所有的package執行npm prepublish:
lerna bootstrap
使用lerna將公共的依賴下載到外部,非公共的依賴下載到包本身的配置("hoist": true),然后執行lerna bootstrap:
//lerna.json "packages": [ "packages/*" ], "command": { "bootstrap": { "hoist": true } },
上述的--hoist
選項設置為true時,相同的依賴,會「提升」到 repo 根目錄下安裝,但lerna 直接以字符串對比 dependency 的版本號,完全相同才提升,semver 約定在這並不起作用。為解決這個問題可以使用yarn的workspaces
字段,在 package.json
中以 workspaces
字段聲明 packages,yarn 就會以 monorepo 的方式管理 packages。相比 lerna,yarn 突出的是對依賴的管理,包括 packages 的相互依賴、packages 對第三方的依賴,yarn 會以 semver 約定來分析 dependencies 的版本,安裝依賴時更快、占用體積更小;
在項目根目錄下的package.json中添加:
{ "private": true, "workspaces": ["module1", "module2"] }
或:
{ "private": true, "workspaces": ["packages/*"] }
在項目根目錄下的lerna.json中添加以下配置,並注釋hoist選項:
{ "packages": [ "packages/*" ], "useWorkspaces": true, "npmClient": "yarn", // "command": { // "bootstrap": { // "hoist": true // } // }, }
lerna和yarn workspace的區別:
hoist: 提取公共的依賴到根目錄的node_moduels,可以自定義指定。其余依賴安裝的package/node_modeles中,可執行文件必須安裝在package/node_modeles。
workspaces: 所有依賴全部在跟目錄的node_moduels,除了可執行文件
3.Lerna 命令
lerna create <name> [loc]
創建一個包,name包名,loc 位置可選
Examples
# 根目錄的package.json "workspaces": [ "packages/*", "packages/@gp0320/*" ], # 創建一個包gpnote默認放在 workspaces[0]所指位置 lerna create gpnote # 創建一個包gpnote指定放在 packages/@gp0320文件夾下,注意必須在workspaces先寫入packages/@gp0320,看上面 lerna create gpnote packages/@gp0320
lerna add <package>[@version] [--dev] [--exact]
增加本地或者遠程package
做為當前項目packages
里面的依賴
--dev
devDependencies 替代dependencies
--exact
安裝准確版本,就是安裝的包版本前面不帶^
, Eg:"^2.20.0" ➜ "2.20.0"
Examples
# Adds the module1 package to the packages in the 'prefix-' prefixed folders lerna add module1 packages/prefix-* # Install module1 to module2 lerna add module1 --scope=module2 # Install module1 to module2 in devDependencies lerna add module1 --scope=module2 --dev # Install module1 in all modules except module1 lerna add module1 # Install babel-core in all modules lerna add babel-core
lerna bootstrap
默認是npm i,因為我們指定過yarn,so,run yarn install,會把所有包的依賴安裝到根node_modules
.
lerna list
列出所有的包,如果與你文夾里面的不符,進入那個包運行yarn init -y
解決
lerna import <path-to-external-repository>
導入本地已經存在的包
lerna run
lerna run < script > -- [..args] # 運行所有包里面的有這個script的命令 $ lerna run --scope my-component test
lerna exec
運行任意命令在每個包
$ lerna exec -- < command > [..args] # runs the command in all packages $ lerna exec -- rm -rf ./node_modules $ lerna exec -- protractor conf.js lerna exec --scope my-component -- ls -la
lerna link
項目包建立軟鏈,類似npm link
lerna clean
刪除所有包的node_modules目錄
lerna changed
列出下次發版lerna publish
要更新的包。
原理:
需要先git add,git commit 提交。
然后內部會運行git diff --name-only v版本號
,搜集改動的包,就是下次要發布的。並不是網上人說的所有包都是同一個版全發布。
➜ lerna-repo git:(master) ✗ lerna changed info cli using local version of lerna lerna notice cli v3.14.1 lerna info Looking for changed packages since v0.1.4 daybyday #只改過這一個 那下次publish將只上傳這一個 lerna success found 1 package ready to publish
lerna publish
會打tag,上傳git,上傳npm。
如果你的包名是帶scope的例如:"name": "@gp0320/gpwebpack",
那需要在packages.json添加
"publishConfig": { "access": "public" },
lerna publish lerna info current version 0.1.4 #這句意思是查找從v0.1.4到現在改動過的包 lerna info Looking for changed packages since v0.1.4 ? Select a new version (currently 0.1.4) Patch (0.1.5) Changes: - daybyday: 0.1.3 => 0.1.5 #只改動過一個 ... Successfully published: - daybyday@0.1.5 lerna success published 1 package
參考:https://segmentfault.com/a/1190000019350611
參考:https://segmentfault.com/a/1190000019309820?utm_source=tag-newest
參考:https://blog.csdn.net/kalinux/article/details/116462002
參考:https://zhuanlan.zhihu.com/p/350329753
實現Monorepo的4種方式:https://blog.csdn.net/dfsgwe1231/article/details/105996358
具體的使用方法移步 Lerna 官網:https://lerna.js.org
yarn 官網對 workspace
的詳細說明:Workspaces | Yarn