天天看點

Lerna 項目啟動(Monorepo 實踐)Lerna 項目啟動(Monorepo 實踐)前言正文結語其他資源

Lerna 項目啟動(Monorepo 實踐)

文章目錄

  • Lerna 項目啟動(Monorepo 實踐)
  • 前言
  • 正文
    • 1. 項目建構
      • 1.1 初始化根目錄
      • 1.2 目錄結構
      • 1.3 使用 yarn 作為依賴管理器
    • 2. 子項目建構
      • 2.1 建立子項目
      • 2.2 安裝依賴
      • 2.3 項目内容填充
      • 2.4 建立軟連接配接
      • 2.5 打包配置(babel、webpack 配置)
      • 2.6 typescript 開發配置
    • 3. 運作腳本配置
      • 3.1 根目錄指令配置
      • 3.2 各項目指令配置
    • 4. 打包 & 送出 & 釋出
      • 4.1 打包
      • 4.2 送出到遠端倉庫
      • 4.3 釋出
        • 4.3.1 踩坑記錄1:通路性設定
        • 4.3.2 踩坑記錄2:已釋出過标簽
      • 4.4 npm 檢視釋出結果
    • 5. 安裝 & 引用
  • 結語
  • 其他資源
    • 參考連接配接
    • 完整代碼示例

前言

今天帶大家認識一個平常比較少接觸到的概念:monorepo

雖然也是一個類似多項目管理的方式,但是又跟微服務不太相同,每個項目實際上是可以獨立運作、釋出的,隻不過是存在互相依賴的關系,并且作為一個項目的獨立子子產品,共同開發。

本篇要介紹的是一個叫做 lerna 的 monorepo 管理庫,類似的還有 nx、rushstack 等,不過這些都是後話,有興趣的都可以去嘗試看看。

本篇主要還是使用比較常見的 lerna + yarn workspaces 的建構方式,當然并不是說所有項目都必須要使用 monorepo 的管理方式,有人說這是一種趨勢,但不應該是一鍋端。在前途并不明朗,你都不知道為什麼要 monorepo 的情況下,還是繼續使用 multirepo 也并無不可,真正的等到需要的時候再進行遷移也還來得及。

下面我們馬上就帶大家走一遍使用 lerna 建構一個多項目倉庫的管理流程

正文

1. 項目建構

1.1 初始化根目錄

首先當然是要初始化一個項目

$ mkdir lerna_launch
$ cd lerna_launch
           

(真實的測試代碼我又把它遷移到 libs 目錄下,跟另一個 usage 項目區隔開來了,不過這不是重點哈)

建好目錄先别着急,全局裝一下 lerna 指令

$ yarn global add lerna
           

接下來就在項目的根目錄下使用 lerna 進行初始化

$ lerna init
lerna notice cli v4.0.0
lerna info Creating package.json
lerna info Creating lerna.json
lerna info Creating packages directory
lerna success Initialized Lerna files
           

1.2 目錄結構

網上也有很多資料,

lerna init

初始化過後的目錄結構如下

/lerna_launch
├── lerna.json
├── package.json
└── packages/
           

簡單來說就是兩個配置檔案:

  • 一個是作為多個項目的根項目的 npm 配置檔案

    package.json

  • 一個是 lerna 管理的配置檔案

    lerna.json

  • 最後一個是放置所有項目的目錄

    packages

    (這也就是為什麼 react、babel 等開源項目都沒有一個 src,隻看到了一個 packages 目錄),而這也不僅僅隻是 lerna 才是如此,使用 packages 目錄放置項目可以說是 monorepo 的管理器通用的規範,也是業内的推薦實踐标準

1.3 使用 yarn 作為依賴管理器

lerna 預設是直接使用 npm 作為依賴管理器的,但是說實話還是 yarn 比較出色一些,是以我們需要手動添加一些配置來啟用 yarn 作為預設的依賴管理器

  • /libs/package.json

首先是修改一下 package.json 的配置項

{
  "name": "root",
  "private": true,
  "workspaces": [
    "packages/*"
  ]
}
           

我們在 package.json 中間加上

"private": true

表示作為私有包釋出,因為根目錄其實并不應該被釋出;另一個是添加

"workspaces": ["packages/*"]

表示啟用了 yarn workspaces 的特性,這也是 yarn 支援 monorepo 的正确配置方式

  • /libs/lerna.json

第二部分則是修改 lerna 的配置項

{
  "packages": [
    "packages/*"
  ],
  "version": "0.0.0",
  "npmClient": "yarn",
  "useWorkspaces": true
}
           

packages 屬性和 version 屬性在初始化的時候都有了,加上

"npmClient": "yarn"

"useWorkspaces": true

就能夠與 yarn workspaces 連用了。

2. 子項目建構

有了 lerna 的殼之後,我們就可以先來填充一些内容啦

2.1 建立子項目

網上有的教程推薦自己 mkdir 然後 yarn init,也是可以,不過實際上 lerna 早就提供了相關的指令,用用看還是可以的,一些 npm 釋出需要的屬性都會給你配置好

$ lerna create pkg-1
$ lerna create pkg-2
           

使用

lerna create [packageName]

的方式,它就會自動給你在 packages 下建立你指定的項目名稱作為目錄名

Lerna 項目啟動(Monorepo 實踐)Lerna 項目啟動(Monorepo 實踐)前言正文結語其他資源

下面我們稍微修改一下 package.json 的包名,分别是 superfree-testpkg-1、superfree-testpkg-2

  • /libs/packages/pkg-1/package.json

{
  "name": "superfree-testpkg-1"
}
           
  • /libs/packages/pkg-2/package.json

{
  "name": "superfree-testpkg-2"
}
           

2.2 安裝依賴

接下來我們兩個項目都想要使用 babel 編譯 + webpack 打包的構模組化式,是以先來添加依賴。這裡不同的是我們可以直接在根目錄下(libs 目錄下)為不同項目添加依賴

如果使用 lerna 的指令可以這樣寫

$ lerna add @babel/core
$ lerna add webpack
# ...
           

由于 lerna 的 add 指令并不支援一次安裝多個依賴,但是實際上我們已經啟用了 yarn workspace 了,是以如果我們直接選擇用 yarn 指令安裝也是不沖突的

$ yarn workspaces add @babel/core @babel/preset-env @babel/preset-typescript webpack webpack-cli babel-loader -D
           

這邊注意到我們使用的是

yarn workspaces

指令,也就是會在前面配置過的每個 workspaces 比對的目錄下都執行相同的操作,如果我們隻想給其中一個項目添加依賴,我們可以使用下面指令

$ yarn workspace pkg-1 add jest -D
           

這裡就指定了 pkg-1 這個項目了

這裡使用 yarn 的重點在于它會将所有依賴全部安裝到根目錄下的 node_modules 當中,就不用去管什麼 --hoist 的共同依賴版本提升什麼鬼的,反正就是在根目錄就對了!

2.3 項目内容填充

搞了半天終于可以開始寫代碼了,我們先把 lib 下 lerna 預設建立的檔案删掉,我們再另外建立一個 src 目錄來放置源代碼,lib 則是後面我們使用 webpack 打包後的目标目錄

  • /libs/packages/pkg-1/src/index.ts

export const greetingPkg1 = (from: string = '@youxiantest/pkg-1') => {
  console.log(`[greetingPkg1] invoke greetingPkg1 from ${from}`);
};
           
  • /libs/packages/pkg-2/src/index.ts

import { greetingPkg1 } from 'superfree-testpkg-1';

export const greetingPkg2 = () => {
  greetingPkg1('@youxiantest/pkg-2');
  console.log('[greetingPkg2] invoke greetingPkg2 from @youxiantest/pkg-2');
};
           

兩個子項目的内容還是比較簡單,本篇的目标在于打包嘛,比較特别的點在于 pkg-2 依賴于 pkg-1。

2.4 建立軟連接配接

在原來的 multirepo 的模式下,我們就需要使用:pkg-1 下

yarn link

+ pkg-2 下

yarn link superfree-testpkg-1

的組合技來展現本地不發包的方式,這裡就展現出 lerna 強大了

如此一來 lerna 會自動為 packages 下的所有項目自動進行軟連接配接,一次搞定

2.5 打包配置(babel、webpack 配置)

下一步我們為兩個項目配置類似的打包環境配置

  • /libs/packages/pkg-1/babel.config.json

  • /libs/packages/pkg-2/babel.config.json

    相同
{
  "presets": ["@babel/preset-env", "@babel/preset-typescript"]
}
           
  • /libs/packages/pkg-1/webpack.config.js

  • /libs/packages/pkg-2/webpack.config.js

    相同,就是中間的 library 名字不同罷了
const path = require('path');

module.exports = {
  mode: 'production',
  entry: path.join(__dirname, 'src/index'),
  output: {
    filename: 'index.js',
    path: path.resolve(__dirname, 'lib'),
    library: {
      name: '__youxiantest_pkg_1',
      type: 'umd',
    },
    globalObject: 'this',
  },
  resolve: {
    extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx|ts|tsx)$/,
        exclude: /node_modules/,
        use: 'babel-loader',
      },
    ],
  },
};
           

本質上就是配了下 babel 的編譯(module.rules),ts 的擴充名(resolve.extensions),然後打包成 library(output.library)

不同的是對于 pkg-2 我們可以再多配置一個 externals,來避免将 pkg-1 的源代碼也打包到 pkg-2 裡面去

module.exports = {
  // ...
  externals: {
    'superfree-testpkg-1': 'superfree-testpkg-1',
  },
}
           

同時記得确認一下 webpack 配置的打包結果目錄和檔案名要與 package.json 的 main 屬性對上

2.6 typescript 開發配置

由于我們在開發中使用 typescirpt 了,是以我們需要稍微配置一下包映射,來避免項目間的互相引用出錯,畢竟實際上是要發包之後才能看到最終的打包結果嘛

  • /libs/tsconfig.build.json

首先我們可以先在 tsconfig.build.json 寫好一些真正并且可以共用的 ts 配置

{
  "compilerOptions": {
    "esModuleInterop": true,
    "jsx": "react",
    "module": "ES6",
    "sourceMap": true
  },
  "exclude": ["node_modules", "dist"]
}
           
  • /libs/tsconfig.json

接下來則是寫上真正的 ts 配置,附上多個項目的目錄映射

{
  "extends": "./tsconfig.build.json",
  "compilerOptions": {
    "baseUrl": "./packages",
    "paths": {
      "superfree-testpkg-1": ["pkg-1/src"],
      "superfree-testpkg-2": ["pkg-2/src"]
    },
    "moduleResolution": "Node"
  }
}
           

如此一來我們就能夠在開發的時候直接體驗到 ts 的類型提示,同時也是與真正打包後的類型一緻的

3. 運作腳本配置

把兩個項目的内容以及打包配置都填充好之後,下面我們加上一些運作腳本就準備打包然後釋出啦

3.1 根目錄指令配置

  • /libs/package.json

首先我們在根目錄下面的 package.json 加上一些指令

{  
    "scripts": {
        "bootstrap": "lerna bootstrap",
        "clean": "rm -rf node_modules/ && lerna clean -y",
        "dev": "lerna run --stream --sort dev",
        "build": "lerna run --stream --sort build"
    }
}
           
  • bootstrap 指令:安裝依賴的時候我們可以使用

    lerna bootstrap

    也可以使用

    yarn install

    其實都是等價的沒什麼差別
  • clean 指令:lerna 提供了

    lerna clean

    指令來清除 packages 下每個項目的 node_modules,但是反而沒清根目錄下面的hh,是以自己搞一個統一一下
  • dev 指令:開發的時候我們希望每個項目都能夠實時的建構并打包,不管是用 webpack-dev-server 還是 webpack --watch,然而這些對于 lerna 來說都是屏蔽的,我們使用

    lerna run dev

    來一次運作所有項目的 dev 指令,

    --stream

    表示串形輸出結果,

    --sort

    表示由 lerna 自動識别多個項目之間的依賴關系并進行拓撲排序,最後按序執行指令
  • build 指令:最後要發包之前就要進行打包,跟 dev 一樣,lerna 并不關心具體的打包手法,反正就是每個項目執行一樣的指令就對啦!

3.2 各項目指令配置

前面提到 dev 跟 build 都是依賴每個項目自己的開發/打包方式,是以我們需要進到每個項目再填充一下指令

  • /libs/packages/pkg-1/package.json

{
    "scripts": {
        "dev": "webpack --watch",
        "build": "webpack"
    },
}
           
  • /libs/packages/pkg-2/package.json

{
    "scripts": {
        "dev": "webpack --watch",
        "build": "webpack"
    },
}
           

4. 打包 & 送出 & 釋出

最後終于進入到打包釋出環節啦

4.1 打包

先運作一下打包指令

$ yarn build
           
Lerna 項目啟動(Monorepo 實踐)Lerna 項目啟動(Monorepo 實踐)前言正文結語其他資源

确定一下打包成果

Lerna 項目啟動(Monorepo 實踐)Lerna 項目啟動(Monorepo 實踐)前言正文結語其他資源

4.2 送出到遠端倉庫

由于 lerna 的釋出實際上是會依賴遠端 git 倉庫的 tag 的(當然你也可以配置成不要,但是跟着預設推薦的走嘛哈哈)

是以我們先去 github 上建立一個新的倉庫,然後将目前倉庫送出到遠端倉庫上(這裡就不教學啦自己搞)

Lerna 項目啟動(Monorepo 實踐)Lerna 項目啟動(Monorepo 實踐)前言正文結語其他資源
Lerna 項目啟動(Monorepo 實踐)Lerna 項目啟動(Monorepo 實踐)前言正文結語其他資源

4.3 釋出

最後的最後終于可以釋出了!

不要忘了确認一下 yarn 源位址,還有登入資訊

$ yrm ls

  npm ---- https://registry.npmjs.org/
  cnpm --- http://r.cnpmjs.org/
* taobao - https://registry.npm.taobao.org/
  nj ----- https://registry.nodejitsu.com/
  rednpm - http://registry.mirror.cqupt.edu.cn/
  npmMirror  https://skimdb.npmjs.com/registry/
  edunpm - http://registry.enpmjs.org/
  yarn --- https://registry.yarnpkg.com

$ yrm use npm   # 切換成官方源才能釋出到官方倉庫啦
$ yarn login
           

最後就是使用

lerna publish

指令釋出啦

$ lerna publish
           
Lerna 項目啟動(Monorepo 實踐)Lerna 項目啟動(Monorepo 實踐)前言正文結語其他資源

4.3.1 踩坑記錄1:通路性設定

這裡有個坑就是如果你的包名使用了組織作用域(如

@youxian/xxx

),那在第一次釋出的時候預設是需要加上一個參數的

$ yarn publish --access=public
           

否則會導緻釋出失敗

是以在使用 lerna 釋出的時候如果是上述情況推薦先在單個包下面單獨釋出一次設定可通路性

--access=public

後續再使用 lerna 統一管理

4.3.2 踩坑記錄2:已釋出過标簽

第二個坑是 lerna 釋出的時候會添加一個 tag,并往遠端倉庫推送,而如果釋出失敗的話但是 tag 已經推送上去了,這時候我們就可以使用

$ lerna publish from-git
           

來直接使用 git 上的版本,而不需要再次更新版本号然後再送出

4.4 npm 檢視釋出結果

最後我們就可以到 npm 上看看有沒有釋出成功啦

Lerna 項目啟動(Monorepo 實踐)Lerna 項目啟動(Monorepo 實踐)前言正文結語其他資源

5. 安裝 & 引用

最後我們可以再起一個小項目,然後安裝自己剛發上去的包來體驗一下啦!

$ mkdir usage
$ cd usage
$ yarn init -y
$ yarn add superfree-testpkg-2
$ yarn start
           

最終結果如下(addition 的輸出是我修改源代碼後 ->

yarn build

重新打包 ->

lerna publish

再釋出,第一次成功後後面繼續釋出就又更順手更快啦)

Lerna 項目啟動(Monorepo 實踐)Lerna 項目啟動(Monorepo 實踐)前言正文結語其他資源

結語

本篇就到此為止啦,相信跟着作者一路走到這裡的小夥伴會發現,過程還是比較麻煩複雜的,一個環節出錯都會導緻卡很久,是以決定使用 monorepo 一定要慎重,不要為了用而用!供大家參考哈,有關于 lerna 的更好的實踐方式歡迎提出,大家一起讨論學習~

其他資源

參考連接配接

Title Link
Yarn Workspace 使用指南 https://blog.csdn.net/tianxintiandisheng/article/details/115329134
lerna+yarn workspace+monorepo項目的最佳實踐 https://xiaoguoping.blog.csdn.net/article/details/99702447?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-6.no_search_link&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-6.no_search_link
Lerna https://www.lernajs.cn/
lerna - Github https://github.com/lerna/lerna
包管理器 - peer dependency 的安裝 https://blog.csdn.net/anleng6817/article/details/101126789?utm_term=yarn%E5%AE%89%E8%A3%85peerdependencies&utm_medium=distribute.pc_aggpage_search_result.none-task-blog-2allsobaiduweb~default-1-101126789&spm=3001.4430
npm 安裝 yarn_npm與yarn對peerDependencies處理的差異 https://blog.csdn.net/weixin_39797324/article/details/110892595?utm_term=yarn%E5%AE%89%E8%A3%85peerdependencies&utm_medium=distribute.pc_aggpage_search_result.none-task-blog-2allsobaiduweb~default-0-110892595&spm=3001.4430
Why babel remove lerna? #12622 https://github.com/babel/babel/discussions/12622
使用Lerna、Yarn管理Monorepo項目 https://segmentfault.com/a/1190000039795228
Lerna --多包存儲管理工具(一) https://segmentfault.com/a/1190000023954051
lerna + workspaces使用手冊 https://segmentfault.com/a/1190000039077541
Monorepo——大型前端項目的代碼管理方式 https://segmentfault.com/a/1190000019309820?utm_source=tag-newest
Lerna的依賴管理及hoisting淺析 https://yrq110.me/post/tool/how-lerna-manage-package-dependencies/
Using NPM Modules in Namespaced Typescript project https://stackoverflow.com/questions/64242513/using-npm-modules-in-namespaced-typescript-project
Typescript + Webpack library generates “ReferenceError: self is not defined” https://stackoverflow.com/questions/64639839/typescript-webpack-library-generates-referenceerror-self-is-not-defined
@lerna/filter-options - npm https://www.npmjs.com/package/@lerna/filter-options
Yarn Workspaces: Organize Your Project’s Codebase Like A Pro https://www.smashingmagazine.com/2019/07/yarn-workspaces-organize-project-codebase-pro/#project-root-workspace
Workspaces - yarn https://classic.yarnpkg.com/en/docs/workspaces/
How to set up a TypeScript monorepo and make Go to definition work https://medium.com/@NiGhTTraX/how-to-set-up-a-typescript-monorepo-with-lerna-c6acda7d4559
lerna — JS package 管理工具 https://medium.com/lion-f2e/lerna-js-package-%E7%AE%A1%E7%90%86%E5%B7%A5%E5%85%B7-e9ed360d1143
lerna 使用指南 https://www.jianshu.com/p/db3ee301af47
釋出npm包時遇到的一些坑 https://www.jianshu.com/p/40f732d91a8c
多包依賴管理–Lerna https://cloud.tencent.com/developer/article/1731742
externals - webpack https://webpack.js.org/configuration/externals/
lerna publish 失敗 https://class.imooc.com/course/qadetail/291954
git遠端版本回退 https://www.cnblogs.com/zjdxr-up/p/10941898.html
為什麼現代前端工程越來越離不開 Monorepo? https://zhuanlan.zhihu.com/p/362228487
Rush Stack https://rushstack.io/
Intro to Nx https://nx.dev/latest/react/getting-started/intro

完整代碼示例

https://github.com/superfreeeee/Blog-code/tree/main/front_end/others/lerna_launch