現代前端工程化-徹底搞懂基于 Monorepo 的 lerna 模塊(從原理到實戰(zhàn))
本文你能學到什么?

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

lerna 軟鏈實現(如何動態(tài)創(chuàng)建軟鏈)
未使用 lerna 之前,想要調試一個本地的 npm 模塊包,需要使用 npm link 來進行調試,但是在 lerna 中可以直接進行模塊的引入和調試,這種動態(tài)創(chuàng)建軟鏈是如何實現的?
軟鏈是什么?
Node.js 中如何實現軟鏈
lerna 中也是通過這種方式來實現軟鏈的
fs.symlinkSync(target,path,type)
fs.symlinkSync(target,path,type)
target <string> | <Buffer> | <URL> // 目標文件
path <string> | <Buffer> | <URL> // 創(chuàng)建軟鏈對應的地址
type <string>
它會創(chuàng)建名為 path 的鏈接,該鏈接指向 target。type 參數僅在 Windows 上可用,在其他平臺上則會被忽略。它可以被設置為 'dir'、 'file' 或 'junction'。如果未設置 type 參數,則 Node.js 將會自動檢測 target 的類型并使用 'file' 或 'dir'。如果 target 不存在,則將會使用 'file'。Windows 上的連接點要求目標路徑是絕對路徑。當使用 'junction' 時, target 參數將會自動地標準化為絕對路徑。
基本使用
const res = fs.symlinkSync('./target/a.js','./b.js');

這段代碼的意思是為 創(chuàng)建一個軟鏈接 b.js 指向了文件 ./targert/a.js,當 a.js 中的內容發(fā)生變化時,b.js 文件也會發(fā)生相同的改變。
在 Node.js 文檔中,fs.symlinkSync()lerna 的源碼中動態(tài)鏈接也是通過 symlinkSync 來實現的。源碼對應地址:軟鏈實現源碼地址參考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));
}
更多關于軟鏈的文章,我后面會單獨寫一篇文章介紹軟硬鏈接,這里知道
lerna 鏈接部分的實現就可以了。Node fs 官網 參考2
lerna 基本使用
lerna 環(huán)境配置
lerna 在使用之前需要全局安裝 lerna 工具。
npm install lerna -g
初始化一個lerna 項目
mkdir lerna-demo,在當前目錄下創(chuàng)建文件夾lerna-demo,然后使用命令 lerna init執(zhí)行成功后,目錄下將會生成這樣的目錄結構。,一個 hello world級別的 lerna 項目就完成了。

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

lerna 會自動創(chuàng)建一個 packages 目錄夾,我們以后的項目都新建在這里面。同時還會在根目錄新建一個 lerna.json配置文件
{
"packages": [
"packages/*"
],
"version": "0.0.0" // 共用的版本,由lerna管理
}
注意``lerna
默認使用的是集中版本,所有的package共用一個version,如果需要packages下不同的模塊 使用不同的版本號,需要配置Independent模式。命令行介紹時有提到這里 在json` 中增加屬性配置
"version": "independent"
package.json 中有一點需要注意,他的 private 必須設置為 true ,因為 mono-repo 本身的這個 Git倉庫并不是一個項目,他是多個項目,所以一般不進行直接發(fā)布,發(fā)布的應該是 packages/ 下面的各個子項目。
子項目創(chuàng)建
現在 package 目錄下是空的,我們需要創(chuàng)建一下組件庫內部相關內容。使用 leran create 命令創(chuàng)建子 package 項目。
lerna create ui-common
lerna create ui-common會在 packages 中創(chuàng)建 ui-common 項目,另外創(chuàng)建兩個基于 TypeScript 的 react 項目 ui-web 和 example-web,
在 package 目錄下運行
npx create-react-app ui-web --typescript
npx create-react-app example-web --typescript
這里補充一個小插曲吧,初始化
typescript項目后如何進行配置,可以直接用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
然后在項目根目錄創(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)建完兩個項目后, ui-web 和 example-web 中同時出現 node_modules,二者會有很多重復部分,并且會占用大量的硬盤空間
lerna bootstrap
lerna 提供了可以將子項目的依賴包提升到最頂層的方式 ,我們可以執(zhí)行 lerna clean先刪除每個子項目的 node_modules , 然后執(zhí)行命令 lerna bootstrop --hoist。
lerna bootstrop --hoist 會將 packages 目錄下的公共模塊包抽離到最頂層,但是這種方式會有一個問題,不同版本號只會保留使用最多的版本,這種配置不太好,當項目中有些功能需要依賴老版本時,就會出現問題。
yarn workspaces
有沒有更優(yōu)雅的方式?再介紹一個命令 yarn workspaces ,可以解決前面說的當不同的項目依賴不同的版本號問題, yarn workspaces會檢查每個子項目里面依賴及其版本,如果版本不一致都會保留到自己的 node_modules 中,只有依賴版本號一致的時候才會提升到頂層。注意:這種需要在 lerna.json 中增加配置。
"npmClient": "yarn", // 指定 npmClent 為 yarn
"useWorkspaces": true // 將 useWorkspaces 設置為 true
并且在頂層的 package.json 中增加配置
// 頂層的 package.json
{
"workspaces":[
"packages/*"
]
}
增加了這個配置后 不再需要 lerna bootstrap 來安裝依賴了,可以直接使用 yarn install 進行依賴的安裝。注意:yarn install 無論在頂層運行還是在任意一個子項目運行效果都是可以。
啟動子項目
配置完成后,我們啟動 packages 目錄下的子項目 example-web,原有情況下我們可能需要頻繁切換到 example-web 文件夾,在這個目錄執(zhí)行 yarn start。
使用了 lerna 進行項目管理之后,可以在頂層的 package.json 文件中進行配置,在 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 就可以運行 example-web 項目了。
配置完成后嘗試一下,項目正常啟動。

example-web 模塊中 引用 ui-common 中的函數
我們在 ui-common中定義一個網絡請求公共函數,在 ui-web 和 example-web 項目中都會用到。在項目 example-web 中增加 ui-common 模塊依賴,執(zhí)行命令
lerna add ui-common --scope=example-web
執(zhí)行命令后,在 example-web 的 package.josn中會出現

ui-common 已經成功被 example-web 中引用,然后在 example-web 項目中引用 request 函數并使用,例子中只是簡單使用下 ui-common 中的函數。
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ā)布
項目結構已基本搭建完成,我們嘗試發(fā)布一下 ,使用命令
lerna publish
由于之前我們在 lerna.json 中配置了
{
"packages": [
"packages/*"
],
"version": "independent",// 不同模塊不同版本
"npmClient": "yarn",
"useWorkspaces": true
}
執(zhí)行命令后在會出現如下內容,針對 packages 中的每個模塊單獨選擇版本進行發(fā)布。
如果想要發(fā)布的模塊統(tǒng)一,使用相同的版本號,需要修改lerna.json ,將 "version": "independent", 改為固定版本號,修改后嘗試重新使用 lerna publish進行發(fā)布,
注意??:這里再次聲明一下,如果使用了
independent方式進行版本控制,在packages內部的包進行互相依賴時,每次發(fā)布之后記得修改下發(fā)布后的版本號,否則在本地調試時會出現剛發(fā)布的代碼不生效問題(這個問題本人親自遇到過,單獨說下)
框架類項目
公司組件庫項目
組件庫項目類似上面實戰(zhàn)的目錄結構,但是會在 packages 包下添加很多其他的模塊,比如 ui-h5 , example-h5 等
工具類項目
舉例一些開源項目。
babel使用的就是lerna進行管理facebook/jest使用的是lerna進行管理alibaba/rax使用的是lerna進行管理
lerna 弊端
和傳統(tǒng)的 git submodules 多倉庫方式對比,我覺得 lerna 優(yōu)勢很明顯的,個人認為唯一不足的是: 由于源碼在一起,倉庫變更非常常見,存儲空間也變得很大,甚至幾G,CI 測試運行時間也會變長,雖然如此也是可以接受的。
下期預告
本文主要講解了 lerna 的基本使用,并且用它搭建了一個基礎目錄結構(我會補充一些基礎的配置 eslint,prettier 等,本文不多寫之前有寫過),這種搭建我們沒有必要每次都配置一遍,嘗試一遍就好了,工程化的最終目的是讓業(yè)務開發(fā)可以 100% 聚焦在業(yè)務邏輯上,下一篇文章會講解 輪子 create-mono-repo cli 腳手架的完整實現過程,如何快速創(chuàng)建 mono-repo 項目
參考文章
[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
