現(xiàn)代前端工程化-基于 Monorepo 的 lerna 模塊(從原理到實(shí)戰(zhàn))
本文你能學(xué)到什么?

看完本文后希望可以檢查一下圖中的內(nèi)容是否都掌握了,文中的例子最好實(shí)際操作一下,下面開始正文。
本文是前端工程化系列中的一篇,回不斷更新,下篇更新內(nèi)容可看文末的下期預(yù)告!宗旨:工程化的最終目的是讓業(yè)務(wù)開發(fā)可以 100% 聚焦在業(yè)務(wù)邏輯上
lerna是什么?有什么優(yōu)勢(shì)?
lerna 基礎(chǔ)概念
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是一個(gè)用來(lái)優(yōu)化托管在 git\npm 上的多 package 代碼庫(kù)的工作流的一個(gè)管理工具,可以讓你在主項(xiàng)目下管理多個(gè)子項(xiàng)目,從而解決了多個(gè)包互相依賴,且發(fā)布時(shí)需要手動(dòng)維護(hù)多個(gè)包的問(wèn)題。
關(guān)鍵詞:多倉(cāng)庫(kù)管理,多包管理,自動(dòng)管理包依賴
lerna 解決了哪些痛點(diǎn)
資源浪費(fèi)
通常情況下,一個(gè)公司的業(yè)務(wù)項(xiàng)目只有一個(gè)主干,多 git repo 的方式,這樣 node_module 會(huì)出現(xiàn)大量的冗余,比如它們可能都會(huì)安裝 React、React-dom 等包,浪費(fèi)了大量存儲(chǔ)空間。
調(diào)試繁瑣
很多公共的包通過(guò) npm 安裝,想要調(diào)試依賴的包時(shí),需要通過(guò) npm link 的方式進(jìn)行調(diào)試。
資源包升級(jí)問(wèn)題
一個(gè)項(xiàng)目依賴了多個(gè) npm 包,當(dāng)某一個(gè)子 npm 包代碼修改升級(jí)時(shí),都要對(duì)主干項(xiàng)目包進(jìn)行升級(jí)修改。(這個(gè)問(wèn)題感覺是最煩的,可能一個(gè)版本號(hào)就要去更新一下代碼并發(fā)布)
lerna的核心原理
monorepo 和 multrepo 對(duì)比
monorepo:是將所有的模塊統(tǒng)一的放在一個(gè)主干分支之中管理。multrepo:將項(xiàng)目分化為多個(gè)模塊,并針對(duì)每一個(gè)模塊單獨(dú)的開辟一個(gè) reporsitory來(lái)進(jìn)行管理。

lerna 軟鏈實(shí)現(xiàn)(如何動(dòng)態(tài)創(chuàng)建軟鏈)
未使用 lerna 之前,想要調(diào)試一個(gè)本地的 npm 模塊包,需要使用 npm link 來(lái)進(jìn)行調(diào)試,但是在 lerna 中可以直接進(jìn)行模塊的引入和調(diào)試,這種動(dòng)態(tài)創(chuàng)建軟鏈?zhǔn)侨绾螌?shí)現(xiàn)的?
軟鏈?zhǔn)鞘裁矗?span style="display: none;">
Node.js 中如何實(shí)現(xiàn)軟鏈
lerna 中也是通過(guò)這種方式來(lái)實(shí)現(xiàn)軟鏈的
fs.symlinkSync(target,path,type)
fs.symlinkSync(target,path,type)
target <string> | <Buffer> | <URL> // 目標(biāo)文件
path <string> | <Buffer> | <URL> // 創(chuàng)建軟鏈對(duì)應(yīng)的地址
type <string>
它會(huì)創(chuàng)建名為 path 的鏈接,該鏈接指向 target。type 參數(shù)僅在 Windows 上可用,在其他平臺(tái)上則會(huì)被忽略。它可以被設(shè)置為 'dir'、 'file' 或 'junction'。如果未設(shè)置 type 參數(shù),則 Node.js 將會(huì)自動(dòng)檢測(cè) target 的類型并使用 'file' 或 'dir'。如果 target 不存在,則將會(huì)使用 'file'。Windows 上的連接點(diǎn)要求目標(biāo)路徑是絕對(duì)路徑。當(dāng)使用 'junction' 時(shí), target 參數(shù)將會(huì)自動(dòng)地標(biāo)準(zhǔn)化為絕對(duì)路徑。
基本使用
const res = fs.symlinkSync('./target/a.js','./b.js');

這段代碼的意思是為 創(chuàng)建一個(gè)軟鏈接 b.js 指向了文件 ./targert/a.js,當(dāng) a.js 中的內(nèi)容發(fā)生變化時(shí),b.js 文件也會(huì)發(fā)生相同的改變。
在 Node.js 文檔中,fs.symlinkSync()lerna 的源碼中動(dòng)態(tài)鏈接也是通過(guò) symlinkSync 來(lái)實(shí)現(xiàn)的。源碼對(duì)應(yīng)地址:軟鏈實(shí)現(xiàn)源碼地址參考1
function createSymbolicLink(src, dest, type) {
log.silly("createSymbolicLink", [src, dest, type]);
return fs
.lstat(dest)
.then(() => fs.unlink(dest))
.catch(() => {
/* nothing exists at destination */
})
.then(() => fs.symlink(src, dest, type));
}
更多關(guān)于軟鏈的文章,我后面會(huì)單獨(dú)寫一篇文章介紹軟硬鏈接,這里知道
lerna 鏈接部分的實(shí)現(xiàn)就可以了。Node fs 官網(wǎng) 參考2
lerna 基本使用
lerna 環(huán)境配置
lerna 在使用之前需要全局安裝 lerna 工具。
npm install lerna -g
初始化一個(gè)lerna 項(xiàng)目
mkdir lerna-demo,在當(dāng)前目錄下創(chuàng)建文件夾lerna-demo,然后使用命令 lerna init執(zhí)行成功后,目錄下將會(huì)生成這樣的目錄結(jié)構(gòu)。,一個(gè) hello world級(jí)別的 lerna 項(xiàng)目就完成了。

- packages(目錄)
- lerna.json(配置文件)
- package.json(工程描述文件)
lerna 常用命令
介紹一些 lerna 常用的命令,常用命令這部分可以簡(jiǎn)單過(guò)一遍,當(dāng)作一個(gè)工具集收藏就行,需要的時(shí)候來(lái)找下,用著用著就熟練了,主要可以實(shí)操下下面的實(shí)戰(zhàn)小練習(xí),這個(gè)過(guò)程會(huì)遇到一些坑的。
初始化 lerna項(xiàng)目
lerna init
創(chuàng)建一個(gè)新的由 lerna管理的包。
lerna create <name>
安裝所有·依賴項(xiàng)并連接所有的交叉依賴
lerna bootstrap
增加模塊包到最外層的公共 node_modules中
lerna add axios
增加模塊包到 packages中指定項(xiàng)目 下面是將ui-web模塊增加到example-web項(xiàng)目中
lerna add ui-web --scope=example-web
在 packages中對(duì)應(yīng)包下的執(zhí)行任意命令 下面的命令,是對(duì)packages下的example-web項(xiàng)目執(zhí)行yarn start命令 ,比較常用,可以把它配置到最外層的package.json中。
lerna exec --scope example-web -- yarn start
如果命令中不增加 --scope example-web直接使用下面的命令,這會(huì)在 packages 下所有包執(zhí)行命令rm -rf ./node_modules
lerna exec -- rm -rf ./node_modules
顯示所有的安裝的包
lerna list // 等同于 lerna ls
這里再提一個(gè)命令也比較常用,可以通過(guò)json的方式查看 lerna 安裝了哪些包,json 中還包括包的路徑,有時(shí)候可以用于查找包是否生效。
lerna list --json
從所有包中刪除 node_modules目錄
lerna clean
??注意下
lerna clean不會(huì)刪除項(xiàng)目最外層的根node_modules
在當(dāng)前項(xiàng)目中發(fā)布包
lerna publish
這個(gè)命令可以結(jié)合 lerna.json 中的 "version": "independent" 配置一起使用,可以完成統(tǒng)一發(fā)布版本號(hào)和packages 下每個(gè)模版發(fā)布的效果,具體會(huì)在下面的實(shí)戰(zhàn)講解。
lerna publish永遠(yuǎn)不會(huì)發(fā)布標(biāo)記為private的包(package.json中的”private“: true)
以上命令基本夠日常開發(fā)使用了,如果需要更詳細(xì)內(nèi)命令內(nèi)容,可以查看下面的詳細(xì)文檔 lerna 命令詳細(xì)文檔參考3
lerna 應(yīng)用(適用場(chǎng)景)
從零搭建一個(gè) 平臺(tái)基礎(chǔ)組件庫(kù)項(xiàng)目
lerna 比較適合的場(chǎng)景:基礎(chǔ)框架,基礎(chǔ)工具類,ui-component 中會(huì)存在 h5 組件庫(kù),web 組件庫(kù),mobile 組件庫(kù),以及對(duì)應(yīng)的 doc 項(xiàng)目,三個(gè)項(xiàng)目通用的 common 代碼。為了方便多個(gè)項(xiàng)目的聯(lián)調(diào),以及分別打包,這里采用了lerna 的管理方式。
接下來(lái)會(huì)講解使用 leran 搭建 ui-component 基礎(chǔ)組件庫(kù)的過(guò)程。
1. 項(xiàng)目初始化
創(chuàng)建一個(gè)文件夾 ui-component ,
切換到目錄 ui-component目錄下。執(zhí)行 lerna init

lerna 會(huì)自動(dòng)創(chuàng)建一個(gè) packages 目錄夾,我們以后的項(xiàng)目都新建在這里面。同時(shí)還會(huì)在根目錄新建一個(gè) lerna.json配置文件
{
"packages": [
"packages/*"
],
"version": "0.0.0" // 共用的版本,由lerna管理
}
注意``lerna
默認(rèn)使用的是集中版本,所有的package共用一個(gè)version,如果需要packages下不同的模塊 使用不同的版本號(hào),需要配置Independent模式。命令行介紹時(shí)有提到這里 在json` 中增加屬性配置
"version": "independent"
package.json 中有一點(diǎn)需要注意,他的 private 必須設(shè)置為 true ,因?yàn)?mono-repo 本身的這個(gè) Git倉(cāng)庫(kù)并不是一個(gè)項(xiàng)目,他是多個(gè)項(xiàng)目,所以一般不進(jìn)行直接發(fā)布,發(fā)布的應(yīng)該是 packages/ 下面的各個(gè)子項(xiàng)目。
子項(xiàng)目創(chuàng)建
現(xiàn)在 package 目錄下是空的,我們需要?jiǎng)?chuàng)建一下組件庫(kù)內(nèi)部相關(guān)內(nèi)容。使用 leran create 命令創(chuàng)建子 package 項(xiàng)目。
lerna create ui-common
lerna create ui-common會(huì)在 packages 中創(chuàng)建 ui-common 項(xiàng)目,另外創(chuàng)建兩個(gè)基于 TypeScript 的 react 項(xiàng)目 ui-web 和 example-web,
在 package 目錄下運(yùn)行
npx create-react-app ui-web --typescript
npx create-react-app example-web --typescript
這里補(bǔ)充一個(gè)小插曲吧,初始化
typescript項(xiàng)目后如何進(jìn)行配置,可以直接用typescript編寫組件? 安裝 typescript需要的模塊包
$ npm install --save typescript @types/node @types/react @types/react-dom @types/jest
$ # 或者
$ yarn add typescript @types/node @types/react @types/react-dom @types/jest
然后在項(xiàng)目根目錄創(chuàng)建 tsconfig.json 和 webpack.config.js 文件:
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"lib": ["dom","es2015"],
"jsx": "react",
"sourceMap": true,
"strict": true,
"noImplicitAny": true,
"baseUrl": "src",
"paths": {
"@/*": ["./*"],
},
"esModuleInterop": true,
"experimentalDecorators": true,
},
"include": [
"./src/**/*"
]
}
jsx選擇reactlib開啟dom和es2015include選擇我們創(chuàng)建的src目錄
var fs = require('fs')
var path = require('path')
var webpack = require('webpack')
const { CheckerPlugin } = require('awesome-typescript-loader');
var ROOT = path.resolve(__dirname);
var entry = './src/index.tsx';
const MODE = process.env.MODE;
const plugins = [];
const config = {
entry: entry,
output: {
path: ROOT + '/dist',
filename: '[name].bundle.js'
},
module: {
rules: [
{
test: /\.ts[x]?$/,
loader: [
'awesome-typescript-loader'
]
},
{
enforce: 'pre',
test: /\.ts[x]$/,
loader: 'source-map-loader'
}
]
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.json'],
alias: {
'@': ROOT + '/src'
}
},
}
if (MODE === 'production') {
config.plugins = [
new CheckerPlugin(),
...plugins
];
}
if (MODE === 'development') {
config.devtool = 'inline-source-map';
config.plugins = [
new CheckerPlugin(),
...plugins
];
}
module.exports = config;
創(chuàng)建完兩個(gè)項(xiàng)目后, ui-web 和 example-web 中同時(shí)出現(xiàn) node_modules,二者會(huì)有很多重復(fù)部分,并且會(huì)占用大量的硬盤空間
lerna bootstrap
lerna 提供了可以將子項(xiàng)目的依賴包提升到最頂層的方式 ,我們可以執(zhí)行 lerna clean先刪除每個(gè)子項(xiàng)目的 node_modules , 然后執(zhí)行命令 lerna bootstrop --hoist。
lerna bootstrop --hoist 會(huì)將 packages 目錄下的公共模塊包抽離到最頂層,但是這種方式會(huì)有一個(gè)問(wèn)題,不同版本號(hào)只會(huì)保留使用最多的版本,這種配置不太好,當(dāng)項(xiàng)目中有些功能需要依賴?yán)习姹緯r(shí),就會(huì)出現(xiàn)問(wèn)題。
yarn workspaces
有沒(méi)有更優(yōu)雅的方式?再介紹一個(gè)命令 yarn workspaces ,可以解決前面說(shuō)的當(dāng)不同的項(xiàng)目依賴不同的版本號(hào)問(wèn)題, yarn workspaces會(huì)檢查每個(gè)子項(xiàng)目里面依賴及其版本,如果版本不一致都會(huì)保留到自己的 node_modules 中,只有依賴版本號(hào)一致的時(shí)候才會(huì)提升到頂層。注意:這種需要在 lerna.json 中增加配置。
"npmClient": "yarn", // 指定 npmClent 為 yarn
"useWorkspaces": true // 將 useWorkspaces 設(shè)置為 true
并且在頂層的 package.json 中增加配置
// 頂層的 package.json
{
"workspaces":[
"packages/*"
]
}
增加了這個(gè)配置后 不再需要 lerna bootstrap 來(lái)安裝依賴了,可以直接使用 yarn install 進(jìn)行依賴的安裝。注意:yarn install 無(wú)論在頂層運(yùn)行還是在任意一個(gè)子項(xiàng)目運(yùn)行效果都是可以。
啟動(dòng)子項(xiàng)目
配置完成后,我們啟動(dòng) packages 目錄下的子項(xiàng)目 example-web,原有情況下我們可能需要頻繁切換到 example-web 文件夾,在這個(gè)目錄執(zhí)行 yarn start。
使用了 lerna 進(jìn)行項(xiàng)目管理之后,可以在頂層的 package.json 文件中進(jìn)行配置,在 scripts 中增加配置。
"scripts": {
"web": "lerna exec --scope example-web -- yarn start",
}
lerna exec --scope example-web 命令是在 example-web 包下執(zhí)行 yarn start。
并且在頂層 lerna.json 中增加配置
{
"npmClient": "true"
}
然后在頂層執(zhí)行 yarn web 就可以運(yùn)行 example-web 項(xiàng)目了。
配置完成后嘗試一下,項(xiàng)目正常啟動(dòng)。

example-web 模塊中 引用 ui-common 中的函數(shù)
我們?cè)?ui-common中定義一個(gè)網(wǎng)絡(luò)請(qǐng)求公共函數(shù),在 ui-web 和 example-web 項(xiàng)目中都會(huì)用到。在項(xiàng)目 example-web 中增加 ui-common 模塊依賴,執(zhí)行命令
lerna add ui-common --scope=example-web
執(zhí)行命令后,在 example-web 的 package.josn中會(huì)出現(xiàn)

ui-common 已經(jīng)成功被 example-web 中引用,然后在 example-web 項(xiàng)目中引用 request 函數(shù)并使用,例子中只是簡(jiǎn)單使用下 ui-common 中的函數(shù)。
import React from "react";
import request from "ui-common";
interface IProps {}
interface IState {
conents: Array<string>;
}
class CommentList extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
conents: ["我是列表第一條"],
};
}
componentDidMount() {
request({
url: "www.baidu.com",
method: "get",
});
}
render() {
return (
<>
<ul>
{this.state.conents.map((item, index) => {
return <li key={index}> {item} </li>;
})}
</ul>
</>
);
}
}
export default CommentList;
發(fā)布
項(xiàng)目結(jié)構(gòu)已基本搭建完成,我們嘗試發(fā)布一下 ,使用命令
lerna publish
由于之前我們?cè)?lerna.json 中配置了
{
"packages": [
"packages/*"
],
"version": "independent",// 不同模塊不同版本
"npmClient": "yarn",
"useWorkspaces": true
}
執(zhí)行命令后在會(huì)出現(xiàn)如下內(nèi)容,針對(duì) packages 中的每個(gè)模塊單獨(dú)選擇版本進(jìn)行發(fā)布。
如果想要發(fā)布的模塊統(tǒng)一,使用相同的版本號(hào),需要修改lerna.json ,將 "version": "independent", 改為固定版本號(hào),修改后嘗試重新使用 lerna publish進(jìn)行發(fā)布,
注意??:這里再次聲明一下,如果使用了
independent方式進(jìn)行版本控制,在packages內(nèi)部的包進(jìn)行互相依賴時(shí),每次發(fā)布之后記得修改下發(fā)布后的版本號(hào),否則在本地調(diào)試時(shí)會(huì)出現(xiàn)剛發(fā)布的代碼不生效問(wèn)題(這個(gè)問(wèn)題本人親自遇到過(guò),單獨(dú)說(shuō)下)
框架類項(xiàng)目
公司組件庫(kù)項(xiàng)目
組件庫(kù)項(xiàng)目類似上面實(shí)戰(zhàn)的目錄結(jié)構(gòu),但是會(huì)在 packages 包下添加很多其他的模塊,比如 ui-h5 , example-h5 等
工具類項(xiàng)目
舉例一些開源項(xiàng)目。
babel使用的就是lerna進(jìn)行管理facebook/jest使用的是lerna進(jìn)行管理alibaba/rax使用的是lerna進(jìn)行管理
lerna 弊端
和傳統(tǒng)的 git submodules 多倉(cāng)庫(kù)方式對(duì)比,我覺得 lerna 優(yōu)勢(shì)很明顯的,個(gè)人認(rèn)為唯一不足的是: 由于源碼在一起,倉(cāng)庫(kù)變更非常常見,存儲(chǔ)空間也變得很大,甚至幾G,CI 測(cè)試運(yùn)行時(shí)間也會(huì)變長(zhǎng),雖然如此也是可以接受的。
下期預(yù)告
本文主要講解了 lerna 的基本使用,并且用它搭建了一個(gè)基礎(chǔ)目錄結(jié)構(gòu)(我會(huì)補(bǔ)充一些基礎(chǔ)的配置 eslint,prettier 等,本文不多寫之前有寫過(guò)),這種搭建我們沒(méi)有必要每次都配置一遍,嘗試一遍就好了,工程化的最終目的是讓業(yè)務(wù)開發(fā)可以 100% 聚焦在業(yè)務(wù)邏輯上,下一篇文章會(huì)講解 輪子 create-mono-repo cli 腳手架的完整實(shí)現(xiàn)過(guò)程,如何快速創(chuàng)建 mono-repo 項(xiàng)目
導(dǎo)圖插入后不是很清晰,有需要的公眾號(hào)回復(fù) lerna 可獲取原圖。
參考文章
[1] https://github.com/lerna/lerna/tree/main/utils/create-symlink [2] http://nodejs.cn/api/fs.html#fs_fs_unlink_path_callback [3] http://www.febeacon.com/lerna-docs-zh-cn [4] https://juejin.cn/post/6844903885312622606 [5] https://github.com/dkypooh/front-end-develop-demo/tree/master/base/lerna [6] http://www.febeacon.com/lerna-docs-zh-cn/routes/commands/bootstrap.html [7] https://github.com/lerna/lerna/tree/main/utils/create-symlink
