1、簡介
首先是關於Monorepo(一篇不錯的介紹Monorepo的文章),它是管理項目代碼的一種方式,主要手段是通過在一個項目倉庫中管理多個模塊/倉庫包。而Multirepo是傳統的倉庫管理方法,也是公司目前所用的方法,即所有的項目包都是獨立倉庫部署和管理。兩種方式對比如下:
兩種方式進行對比的話,千人千面。前者允許多元化發展(各項目可以有自己的構建工具、依賴管理策略、單元測試方法),后者希望集中管理,減少項目間的差異帶來的溝通成本。
雖然拆分子倉庫、拆分子 npm 包是進行項目隔離的天然方案,但當倉庫內容出現關聯時,沒有任何一種調試方式比源碼放在一起更高效。
在前端開發環境中,多 Git Repo,多 npm 則是這個理想的阻力,它們導致復用要關心版本號,調試需要 npm link。而這些是 MonoRepo 最大的優勢。
上圖中提到的利用相關工具就是今天的主角 Lerna ! Lerna是業界知名度最高的 Monorepo 管理工具,功能完整。而且目前很多大型開源項目都采用了這種方式,比如Babel、React、Meteor、Ember、Jest、Vue等等,當我們查看他們的源碼時,可以發現他們的目錄都是將主要內容放在packages目錄中,分多個模塊進行管理。
2、使用
2.1、全局安裝Lerna
npm install lerna -g
或者
yarn add lerna -D
安裝完成后,在控制台輸入lerna -v,只要可以顯示版本號,就意味着我們安裝成功了。控制台輸出如下圖:
2.2、初始化
初始化之前的步驟無非兩種,第一種是創建一個lerna-repo目錄,第二種是在git上創建一個項目lerna-repo並clone到本地,然后本地進入該目錄並執行
npx lerna init
一開始我們都是使用lerna的默認模式我們的目錄結構會變成如下:
顧名思義,lerna.json就是Lerna的配置文件,里面可以進行不同的業務或者不同的場景的配置話定義,我們可以查看初始化的lerna.json的文件,如下圖:
首先就是packages屬性,它的類型是一個數組,在這里可以配置可以發布的npm包的目錄,可以根據不同的場景進行定義發布的npm包所在的不同的文件夾。其次的屬性version就是當前lerna項目的版本號。
2.3、引入npm包/生成一個npm包
這里不得不介紹一下引入npm包的強大之處,它可以把你引入的包的git commit記錄完整的引入。我們一般都是先將要引入的包下載到本地,命令如下:
lerna import ../userlogin
如果是新項目,需要新生成一個npm包的話,可以使用如下命令lerna create <包名> [目錄]:
lerna create base packages/npm
ps:此處npm為原有目錄;
引入一個npm包其實還可以通過進入當前packages文件夾下,通過git clone的方式進行引入,這種優點是可以自由的在某一個項目中進行切換分支,如果是import的方式引入的npm包,目前我是沒找到自由切換單項目分支的方式,歡迎大家補充。
create時,按照lerna的提示輸入name、version等屬性就好了,如果為了省事可以一路回車走到最后,它就會以默認值的形式生成一個新的npm包。
無論是引入的npm包,還是生成的npm包,默認目錄都是在當前項目根目錄下的packages文件夾下,生成的目錄如下:
此處會有幾種常見的錯誤展示,第一種就是由於你是新建的lerna項目,沒有進行git commit,這時候進行lerna import會報錯如下:
Error: Command failed: git rev-parse HEAD lerna ERR! fatal: ambiguous argument 'HEAD': unknown revision or path not in the working tree. lerna ERR! Use '--' to separate paths from revisions, like this: lerna ERR! 'git <command> [<revision>...] -- [<file>...]' lerna ERR! lerna ERR! HEAD lerna ERR! lerna ERR! at makeError (/usr/local/lib/node_modules/lerna/node_modules/execa/index.js:174:9) lerna ERR! at Function.module.exports.sync (/usr/local/lib/node_modules/lerna/node_modules/execa/index.js:338:15) lerna ERR! at Object.execSync (/usr/local/lib/node_modules/lerna/node_modules/@lerna/child-process/index.js:22:16) lerna ERR! at ImportCommand.getCurrentSHA (/usr/local/lib/node_modules/lerna/node_modules/@lerna/import/index.js:129:34) lerna ERR! at ImportCommand.initialize (/usr/local/lib/node_modules/lerna/node_modules/@lerna/import/index.js:98:31) lerna ERR! at Promise.resolve.then (/usr/local/lib/node_modules/lerna/node_modules/@lerna/command/index.js:266:24) lerna ERR! at <anonymous> lerna ERR! lerna Command failed: git rev-parse HEAD lerna ERR! lerna fatal: ambiguous argument 'HEAD': unknown revision or path not in the working tree. lerna ERR! lerna Use '--' to separate paths from revisions, like this: lerna ERR! lerna 'git <command> [<revision>...] -- [<file>...]' lerna ERR! lerna lerna ERR! lerna HEAD
如果你沒有初始化git項目的話,會有如下報錯:
cli v3.8.0 lerna ERR! Error: Command failed: git log --format=%h lerna ERR! fatal: Not a git repository (or any of the parent directories): .git lerna ERR! lerna ERR! lerna ERR! at makeError (/usr/local/lib/node_modules/lerna/node_modules/execa/index.js:174:9) lerna ERR! at Function.module.exports.sync (/usr/local/lib/node_modules/lerna/node_modules/execa/index.js:338:15) lerna ERR! at Object.execSync (/usr/local/lib/node_modules/lerna/node_modules/@lerna/child-process/index.js:22:16) lerna ERR! at ImportCommand.externalExecSync (/usr/local/lib/node_modules/lerna/node_modules/@lerna/import/index.js:137:34) lerna ERR! at ImportCommand.initialize (/usr/local/lib/node_modules/lerna/node_modules/@lerna/import/index.js:82:25) lerna ERR! at Promise.resolve.then (/usr/local/lib/node_modules/lerna/node_modules/@lerna/command/index.js:266:24) lerna ERR! at <anonymous> lerna ERR! lerna Command failed: git log --format=%h lerna ERR! lerna fatal: Not a git repository (or any of the parent directories): .git lerna ERR! lerna
但是如果原項目是具有合並提交沖突的存儲庫時,import命令便無法嘗試應用所有提交。此時可以使用 --flatten命令標志來請求導入固定的歷史記錄,也就是將每次合並提交作為單獨的更改,引入合並。報錯如下:
lerna ERR! import Rolling back to previous HEAD (commit e232dec13e7a62943567257eff8573db2eb1a19e) lerna ERR! EIMPORT Failed to apply commit 2aafdfd. lerna ERR! EIMPORT Command failed: git am -3 --keep-non-patch lerna ERR! EIMPORT Can't find Husky, skipping applypatch-msg hook lerna ERR! EIMPORT You can reinstall it using 'npm install husky --save-dev' or delete this hook lerna ERR! EIMPORT error: Failed to merge in the changes. lerna ERR! EIMPORT hint: Use 'git am --show-current-patch' to see the failed patch lerna ERR! EIMPORT Applying: feat(component): add chromeDownload component lerna ERR! EIMPORT Using index info to reconstruct a base tree... lerna ERR! EIMPORT M packages/dt-react-component/.storybook/config.js lerna ERR! EIMPORT Falling back to patching base and 3-way merge... lerna ERR! EIMPORT Auto-merging packages/dt-react-component/.storybook/config.js lerna ERR! EIMPORT CONFLICT (content): Merge conflict in packages/dt-react-component/.storybook/config.js lerna ERR! EIMPORT Patch failed at 0001 feat(component): add chromeDownload component lerna ERR! EIMPORT When you have resolved this problem, run "git am --continue". lerna ERR! EIMPORT If you prefer to skip this patch, run "git am --skip" instead. lerna ERR! EIMPORT To restore the original branch and stop patching, run "git am --abort". lerna ERR! EIMPORT lerna ERR! EIMPORT lerna ERR! EIMPORT You may try again with --flatten to import flat history.
命令如下:
$ lerna import ../base --flatten
2.4、顯示當前列表
lerna list
2.5、 為項目包添加依賴
命令格式:
lerna add 包名 [--scope=特定的某個包]
例子:
lerna add component --scope=base
如上面例子代碼,執行成功這段代碼后,我們可以在base項目中引用component的包了,而且會自動把component包的版本號更新到base包的package.json中。此處需要注意,如果我們add的包不在我們的packages目錄下,它就會自動從npm的倉庫上進行下載,並且因為加了scope,所以只會給base包進行安裝依賴,如果不加scope這段代碼的話,會自動給packages目錄下所有的包更新安裝component依賴。如果原項目中在package.json中有當前npm包的配置,那么這行命令會將原配置刪除,並新增當前最新版本的npm包,保證其的版本實時性。
2.6、為每個包安裝依賴
lerna bootstrap --scope=特定的某個包
這行代碼功能和npm install或者yarn差不多,如果不加后綴scope,lerna會自動把當前工程下的所有包的依賴都安裝好。
請注意,Lerna在這有一個非常強大的功能,
2.7、刪除某個包的依賴
lerna clean --scope=特定的某個包
同安裝依賴,這行代碼就是刪除某個包的依賴,跟rm -rf node_modules功能一致,將某個項目的node_modules刪除,如果不加scope,就會默認刪除全部項目的依賴,如下圖,會出現提示:
2.8、運行項目中的script命令
lerna run 命令 --scope=特定的某個包
這里可以運行start等約定好的script命令,同理,如果沒有scope后綴,lerna會運行每個包的該script命令,截圖如下:
2.9、查看可以發布的包
lerna changed
該命令是查看當前可以發布的包,顯示自上次relase tag以來有修改的包,輸入后展示圖會與下圖類似:
如圖展示,當前有三個包可以進行發布,但是如果你發布完以后再執行該命令,此處就會展示No changed packages found。
2.10、發布某個項目
lerna publish --dist-tag=tag名
輸入該命令后,會展示如下圖:
在上圖中,控制台會讓用戶選擇要發布的版本的版本號,最后一個選項為自定義選項。--dist-tag=tag名為發布某一個分支的包,主要用於測試或者多版本並存的情況。發布模式如下圖:
2.11、查看diff
lerna diff
作用類似git diff,主要用於顯示自上次relase tag以來有修改的包的差異。
2.12、鏈接引用
lerna link
鏈接項目引用的庫。主要用於項目包建立軟鏈,類似 npm link。
2.13、exec
在每個包目錄下執行任何命令。
$ lerna exec -- <command> [..args] # runs the command in all packages $ lerna exec -- rm -rf ./node_modules $ lerna exec -- protractor conf.js $ lerna exec -- npm view \$LERNA_PACKAGE_NAME $ lerna exec -- node \$LERNA_ROOT_PATH/scripts/some-script.js
3、模式
Lerna提供了兩種管理項目的方式,一種是固定模式(Fixed mode),另一種是獨立模式(Independent mode),兩者區分如下:
固定模式下,packages文件夾下的所有包將會公用一個版本號version(這個version就是lerna.json中的version字段),會自動將所有的包綁定到一個版本號上面,當任意一個npm包發生了更新,整個公用版本號就會發生變化。
獨立模式下,每個npm包都具有一個獨立的版本號,在使用發布命令lerna publish命令時,可以為每個包單獨的進行發布操作,同時只更新當前包的版本號,不會影響其余包的版本。在此模式下,lerna.json的version字段改為independent即可。
lerna version 會檢測從上一個版本發布以來的變動,但有一些文件的提交,我們不希望觸發版本的變動,譬如 .md 文件的修改,並沒有實際引起 package 邏輯的變化,不應該觸發版本的變更。可以通過ignoreChanges配置排除,如下:
{ "packages": [ "packages/*" ], "command": { "bootstrap": { "hoist": true }, "version": { "conventionalCommits": true } }, "ignoreChanges": [ "**/*.md" ], "version": "0.0.1-alpha.1" }
效果如下: