本文首發於 vivo互聯網技術 微信公衆號 mp.weixin.qq.com/s/NlOn7er0i…
做者:孔垂亮前端
對於維護過多個package的同窗來講,都會遇到一個選擇題,這些package是放在一個倉庫裏維護仍是放在多個倉庫裏單獨維護,本文經過一個示例講述瞭如何基於Lerna管理多個package,並和其它工具整合,打造高效、完美的工做流,最終造成一個最佳實踐node
背景webpack
最近在工做中接觸到一個項目,這個項目是維護一套 CLI,發到 npm 上供開發者使用。先看一張圖:git
項目倉庫中的根目錄上就三個子模塊的文件夾,分別對應三個 package,在熟悉了構建和發佈流程後,有點傻了。工做流程如圖中所示:github
使用webpack、babel和uglifyjs把 pkg-a 的 src 編譯到 distweb
使用webpack、babel和uglifyjs把 pkg-b 的 src 編譯到 distnpm
使用webpack、babel和uglifyjs把 pkg-main 的 src 編譯到 distjson
最後使用拷貝文件的方式,把pkg-main、pkg-a、pkg-b中編譯後的文件組裝到 pkg-npm 中,最終用於發佈到 npm 上去。bootstrap
痛點segmentfault
很差調試。由於最終的包是經過文件拷貝的方式組裝到一塊兒的,而且都是壓縮過的,沒法組建一個自上到下的調試流程(實際工做中只能加log,而後從新把包編譯組裝一遍看效果)
包的依賴關係不清晰。pkg-a、pkg-b索性沒有版本管理,更像是源碼級別的,但邏輯又比較獨立。pkg-main中的package.json最終會拷貝到 pkg-npm 中,但又依賴pkg-a、pkg-b中的某些包,因此要把pkg-a、pkg-b中的依賴合併到pkg-main中。pkg-main和pkg-npm的package.json耦合在一塊兒,致使一些原本是工程的開發依賴也會發布到 npm 上去,變成pkg-npm 的依賴包。
依賴的包冗餘。能夠看到,pkg-a、pkg-b、pkg-main要分別編譯,都依賴了babel、webpack等,要分別 cd 到各個目錄安裝依賴。
發佈須要手動修改版本號。 由於最終只發布了一個包,但實際邏輯要求這個包即要全局安裝又要本地安裝,業務沒有拆開,致使要安裝兩遍。耦合一塊兒,即使使用 npm link 也會致使調試困難,
發版沒有 CHANGELOG.md。 由於pkg-a、pkg-b都沒有真正管理版本,因此也沒有完善的CHANGELOG來記錄自上個版本發佈已來的變更。
整個項目像是一個沒有被管理起來的 Monorepo。那什麼又是 Monorepo 呢?
Monorepo 的全稱是 monolithic repository,即單體式倉庫,與之對應的是 Multirepo(multiple repository),這裏的「單」和「多」是指每一個倉庫中所管理的模塊數量。
Multirepo 是比較傳統的作法,即每個 package 都單獨用一個倉庫來進行管理。例如:Rollup, ...
Monorep 是把全部相關的 package 都放在一個倉庫裏進行管理,每一個 package 獨立發佈。例如:React, Angular, Babel, Jest, Umijs, Vue ...
一圖勝千言:
固然到底哪種管理方式更好,仁者見仁,智者見智。前者容許多元化發展(各項目能夠有本身的構建工具、依賴管理策略、單元測試方法),後者但願集中管理,減小項目間的差別帶來的溝通成本。
雖然拆分子倉庫、拆分子 npm 包是進行項目隔離的自然方案,但當倉庫內容出現關聯時,沒有任何一種調試方式比源碼放在一塊兒更高效。
結合咱們項目的實際場景和業務須要,自然的 MonoRepo ! 由於工程化的最終目的是讓業務開發能夠 100% 聚焦在業務邏輯上,那麼這不只僅是腳手架、框架須要從自動化、設計上解決的問題,這涉及到倉庫管理的設計。
一個理想的開發環境能夠抽象成這樣:
「只關心業務代碼,能夠直接跨業務複用而不關心複用方式,調試時全部代碼都在源碼中。」
在前端開發環境中,多 Git Repo,多 npm 則是這個理想的阻力,它們致使複用要關心版本號,調試須要 npm link。而這些是 MonoRepo 最大的優點。
上圖中提到的利用相關工具就是今天的主角 Lerna ! Lerna是業界知名度最高的 Monorepo 管理工具,功能完整。
A tool for managing JavaScript projects with multiple packages.
Lerna is a tool that optimizes the workflow around managing multi-package repositories with git and npm.
Lerna 是一個管理多個 npm 模塊的工具,是 Babel 本身用來維護本身的 Monorepo 並開源出的一個項目。優化維護多包的工做流,解決多個包互相依賴,且發佈須要手動維護多個包的問題。
Lerna 如今已經被不少著名的項目組織使用,如:Babel, React, Vue, Angular, Ember, Meteor, Jest 。
一個基本的 Lerna 管理的倉庫結構以下:
推薦全局安裝,由於會常常用到 lerna 命令
npm i -g lerna
複製代碼
1.初始化
lerna init
複製代碼
init 命令詳情 請參考 lerna init
其中 package.json & lerna.json 以下:
// package.json { "name": "root", "private": true, // 私有的,不會被髮布,是管理整個項目,與要發佈到npm的解耦 "devDependencies": { "lerna": "^3.15.0" } } // lerna.json { "packages": [ "packages/*" ], "version": "0.0.0" } 複製代碼
2.增長兩個 packages
lerna create @mo-demo/cli
lerna create @mo-demo/cli-shared-utils
複製代碼
create 命令詳情 請參考 lerna create
3.分別給相應的 package 增長依賴模塊
lerna add chalk // 爲全部 package 增長 chalk 模塊
lerna add semver --scope @mo-demo/cli-shared-utils // 爲 @mo-demo/cli-shared-utils 增長 semver 模塊
lerna add @mo-demo/cli-shared-utils --scope @mo-demo/cli // 增長內部模塊之間的依賴
複製代碼
add 命令詳情 請參考 lerna add
4.發佈
lerna publish
複製代碼
publish 命令詳情 請參考 lerna publish
以下是發佈的狀況,lerna會讓你選擇要發佈的版本號,我發了@0.0.1-alpha.0 的版本。
發佈 npm 包須要登錄 npm 帳號
5.安裝依賴包 & 清理依賴包
上述1-4步已經包含了 Lerna 整個生命週期的過程了,但當咱們維護這個項目時,新拉下來倉庫的代碼後,須要爲各個 package 安裝依賴包。
咱們在第4步 lerna add 時也發現了,爲某個 package 安裝的包被放到了這個 package 目錄下的 node_modules 目錄下。這樣對於多個 package 都依賴的包,會被多個 package 安裝屢次,而且每一個 package 下都維護 node_modules ,也不清爽。因而咱們使用 --hoist 來把每一個 package 下的依賴包都提高到工程根目錄,來下降安裝以及管理的成本
lerna bootstrap --hoist
複製代碼
bootstrap 命令詳情 請參考 lerna bootstrap
爲了省去每次都輸入 --hoist 參數的麻煩,能夠在 lerna.json 配置:
{ "packages": [ "packages/*" ], "command": { "bootstrap": { "hoist": true } }, "version": "0.0.1-alpha.0" } 複製代碼
配置好後,對於以前依賴包已經被安裝到各個 package 下的狀況,咱們只須要清理一下安裝的依賴便可:
lerna clean
複製代碼
而後執行 lerna bootstrap 便可看到 package 的依賴都被安裝到根目錄下的 node_modules 中了。
lerna不負責構建,測試等任務,它提出了一種集中管理package的目錄模式,提供了一套自動化管理程序,讓開發者沒必要再深耕到具體的組件裏維護內容,在項目根目錄就能夠全局掌控,基於 npm scripts,使用者能夠很好地完成組件構建,代碼格式化等操做。接下來咱們就來看看,若是基於 Lerna,並結合其它工具來搭建 Monorepo 項目的最佳實踐。
1.commitizen && cz-lerna-changelog
commitizen 是用來格式化 git commit message 的工具,它提供了一種問詢式的方式去獲取所需的提交信息。
cz-lerna-changelog 是專門爲 Lerna 項目量身定製的提交規範,在問詢的過程,會有相似影響哪些 package 的選擇。以下:
咱們使用 commitizen 和 cz-lerna-changelog 來規範提交,爲後面自動生成日誌做好準備。
由於這是整個工程的開發依賴,因此在根目錄安裝:
npm i -D commitizen
npm i -D cz-lerna-changelog
複製代碼
安裝完成後,在 package.json 中增長 config 字段,把 cz-lerna-changelog 配置給 commitizen。同時由於commitizen不是全局安全的,因此須要添加 scripts 腳原本執行 git-cz
{ "name": "root", "private": true, "scripts": { "c": "git-cz" }, "config": { "commitizen": { "path": "./node_modules/cz-lerna-changelog" } }, "devDependencies": { "commitizen": "^3.1.1", "cz-lerna-changelog": "^2.0.2", "lerna": "^3.15.0" } } 複製代碼
以後在常規的開發中就可使用 npm run c 來根據提示一步一步輸入,來完成代碼的提交。
2.commitlint && husky
上面咱們使用了 commitizen 來規範提交,但這個要靠開發自覺使用 npm run c 。萬一忘記了,或者直接使用 git commit 提交怎麼辦?答案就是在提交時對提交信息進行校驗,若是不符合要求就不讓提交,並提示。校驗的工做由 commitlint 來完成,校驗的時機則由 husky 來指定。husky 繼承了 Git 下全部的鉤子,在觸發鉤子的時候,husky 能夠阻止不合法的 commit,push 等等。
// 安裝 commitlint 以及要遵照的規範
npm i -D @commitlint/cli @commitlint/config-conventional
複製代碼
// 在工程根目錄爲 commitlint 增長配置文件 commitlint.config.js 爲commitlint 指定相應的規範 module.exports = { extends: ['@commitlint/config-conventional'] } 複製代碼
// 安裝 husky
npm i -D husky
複製代碼
// 在 package.json 中增長以下配置 "husky": { "hooks": { "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" } } 複製代碼
"commit-msg"是git提交時校驗提交信息的鉤子,當觸發時便會使用 commitlit 來校驗。安裝配置完成後,想經過 git commit 或者其它第三方工具提交時,只要提交信息不符合規範就沒法提交。從而約束開發者使用 npm run c 來提交。
3.standardjs && lint-staged
除了規範提交信息,代碼自己確定也少了靠規範來統一風格。
standardjs就是完整的一套 JavaScript 代碼規範,自帶 linter & 代碼自動修正。它無需配置,自動格式化代碼並修正,提早發現風格以及程序問題。
lint-staged staged 是 Git 裏的概念,表示暫存區,lint-staged 表示只檢查並矯正暫存區中的文件。一來提升校驗效率,二來能夠爲老的項目帶去巨大的方便。
// 安裝
npm i -D standard lint-staged
複製代碼
// package.json { "name": "root", "private": true, "scripts": { "c": "git-cz" }, "config": { "commitizen": { "path": "./node_modules/cz-lerna-changelog" } }, "husky": { "hooks": { "pre-commit": "lint-staged", "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" } }, "lint-staged": { "*.js": [ "standard --fix", "git add" ] }, "devDependencies": { "@commitlint/cli": "^8.1.0", "@commitlint/config-conventional": "^8.1.0", "commitizen": "^3.1.1", "cz-lerna-changelog": "^2.0.2", "husky": "^3.0.0", "lerna": "^3.15.0", "lint-staged": "^9.2.0", "standard": "^13.0.2" } } 複製代碼
安裝完成後,在 package.json 增長 lint-staged 配置,如上所示表示對暫存區中的 js 文件執行 standard --fix 校驗並自動修復。那何時去校驗呢,就又用到了上面安裝的 husky ,husky的配置中增長'pre-commit'的鉤子用來執行 lint-staged 的校驗操做,如上所示。
此時提交 js 文件時,便會自動修正並校驗錯誤。即保證了代碼風格統一,又能提升代碼質量。
有了以前的規範提交,自動生成日誌便水到渠成了。再詳細看下 lerna publish 時作了哪些事情:
1.調用 lerna version
找出從上一個版本發佈以來有過變動的 package
提示開發者肯定要發佈的版本號
將全部更新過的的 package 中的package.json的version字段更新
將依賴更新過的 package 的 包中的依賴版本號更新
更新 lerna.json 中的 version 字段
提交上述修改,並打一個 tag
推送到 git 倉庫
2.使用 npm publish 將新版本推送到 npm
CHANGELOG 很明顯是和 version 一一對應的,因此須要在 lerna version 中想辦法,查看 lerna version 命令的詳細說明後,會看到一個配置參數 --conventional-commits。沒錯,只要咱們按規範提交後,在 lerna version 的過程當中會便會自動生成當前這個版本的 CHANGELOG。爲了方便,不用每次輸入參數,能夠配置在 lerna.json中,以下:
{ "packages": [ "packages/*" ], "command": { "bootstrap": { "hoist": true }, "version": { "conventionalCommits": true } }, "ignoreChanges": [ "**/*.md" ], "version": "0.0.1-alpha.1" } 複製代碼
lerna version 會檢測從上一個版本發佈以來的變更,但有一些文件的提交,咱們不但願觸發版本的變更,譬如 .md 文件的修改,並無實際引發 package 邏輯的變化,不該該觸發版本的變動。能夠經過 ignoreChanges 配置排除。如上。
實際 lerna version 不多直接使用,由於它包含在 lerna publish 中了,直接使用 lerna publish就行了。
Lerna 在管理 package 的版本號上,提供了兩種模式供選擇 Fixed or Independent。默認是 Fixed,更多細節,以及 Lerna 的更多玩法,請參考官網文檔:
採用 Monorepo 結構的項目,各個 package 的結構最好保持統一。
根據目前的項目情況,設計以下:
各 package 入口統一爲 index.js
各 package 源碼入口統一爲 src/index.js
各 package 編譯入口統一爲 dist/index.js
各 package 統一使用 ES6 語法、使用 Babel 編譯、壓縮並輸出到 dist
各 package 發佈時只發布 dist 目錄,不發佈 src 目錄
各 package 注入 LOCAL_DEBUG 環境變量, 在index.js 中區分是調試仍是發佈環境,調試環境 ruquire(./src/index.js) 保證全部源碼可調試。發佈環境 ruquire(./dist/index.js) 保證全部源碼不被髮布。
由於 dist 是 Babel 編譯後的目錄,咱們在搜索時不但願搜索它的內容,因此在工程的設置中把 dist 目錄排除在搜索的範圍以外。
接下來,咱們按上面的規範,搭建 package 的結構。
首先安裝依賴
npm i -D @babel/cli @babel/core @babel/preset-env // 使用 Babel 必備 詳見官網用法 npm i -D @babel/node // 用於調試 由於用了 import&export 等 ES6 的語法 npm i -D babel-preset-minify // 用於壓縮代碼 複製代碼
因爲各 package 的結構統一,因此相似 Babel 這樣的工具,只在根目錄安裝就行了,不須要在各 package 中安裝,簡直是清爽的要死了。
增長 Babel 配置
// 根目錄新建 babel.config.js module.exports = function (api) { api.cache(true) const presets = [ [ '@babel/env', { targets: { node: '8.9' } } ] ] // 非本地調試模式才壓縮代碼,否則調試看不到實際變量名 if (!process.env['LOCAL_DEBUG']) { presets.push([ 'minify' ]) } const plugins = [] return { presets, plugins, ignore: ['node_modules'] } } 複製代碼
修改各 package 的代碼
// @mo-demo/cli/index.js if (process.env.LOCAL_DEBUG) { require('./src/index') // 若是是調試模式,加載src中的源碼 } else { require('./dist/index') // dist會發到npm } // @mo-demo/cli/src/index.js import { log } from '@mo-demo/cli-shared-utils' // 從 utils 模塊引入依賴並使用 log 函數 log('cli/index.js as cli entry exec!') // @mo-demo/cli/package.json { "main": "index.js", "files": [ "dist" // 發佈 dist ] } // @mo-demo/cli-shared-utils/index.js if (process.env.LOCAL_DEBUG) { module.exports = require('./src/index') // 若是是調試模式,加載src中的源碼 } else { module.exports = require('./dist/index') // dist會發到npm } // @mo-demo/cli-shared-utils/src/index.js const log = function (str) { console.log(str) } export { //導出 log 接口 log } // @mo-demo/cli-shared-utils/package.json { "main": "index.js", "files": [ "dist" ] } 複製代碼
修改發佈的腳本
npm run b 用來對各 pacakge 執行 babel 的編譯,從 src 目錄輸出出 dist 目錄,使用根目錄的配置文件 babel.config.js。
npm run p 用來取代 lerna publish,在 publish 前先執行 npm run b來編譯。
其它經常使用的 lerna 命令也添加到 scripts 中來,方便使用。
// 工程根目錄 package.json "scripts": { "c": "git-cz", "i": "lerna bootstrap", "u": "lerna clean", "p": "npm run b && lerna publish", "b": "lerna exec -- babel src -d dist --config-file ../../babel.config.js" } 複製代碼
調試
咱們使用vscode自帶的調試功能調試,也可使用 Node + Chrome 調試,看開發者習慣。
。
增長以下調試配置文件:
// .vscode/launch.json { // 使用 IntelliSense 瞭解相關屬性。 // 懸停以查看現有屬性的描述。 // 欲瞭解更多信息,請訪問: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "type": "node", "request": "launch", "name": "debug cli", "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/babel-node", "runtimeArgs": [ "${workspaceRoot}/packages/cli/src/index.js" ], "env": { "LOCAL_DEBUG": "true" }, "console": "integratedTerminal" } ] } 複製代碼
由於 src 的代碼是 ES6 的,因此要使用 babel-node去跑調試,@babel/node 已經在前面安裝過了。
**最棒的是,能夠直接使用單步調試,調到依賴的模塊中去,**如上圖,咱們要執行 @mo-demo/cli-shared-utils 模塊中的 log 方法,單步進入,會直接跳到 @mo-demo/cli-shared-utils src 源碼中去執行。以下圖
到這裏,基本上已經構建了基於 Lerna 管理 packages 的 Monorepo 項目的最佳實踐了,該有的功能都有:
完善的工做流
流暢的調試體驗
風格統一的編碼
一鍵式的發佈機制
完美的更新日誌
……
固然,Lerna 還有更多的功能等待着你去發掘,還有不少能夠結合 Lerna 一塊兒使用的工具。構建一套完善的倉庫管理機制,可能它的收益不是一些量化的指標能夠衡量出來的,也沒有直接的價值輸出,但它能在平常的工做中極大的提升工做效率,解放生產力,節省大量的人力成本。
更多內容敬請關注 vivo 互聯網技術 微信公衆號
注:轉載文章請先與微信號:labs2020 聯繫。