JavaScript内核与高级编程之:`Monorepo`架构:`Lerna`和`Nx`在`JavaScript`项目中的实践。

各位观众老爷,晚上好!我是你们的老朋友,今天咱们聊聊JavaScript项目中的Monorepo架构,以及两位大咖:LernaNx。保证让你们听完之后,感觉自己立马就能搞一个宇宙级的Monorepo出来。

Monorepo:一个筐里装所有鸡蛋

啥叫Monorepo?简单来说,就是把多个项目、库、工具等等,全都放到同一个代码仓库里管理。 想象一下,你以前是每个项目一个仓库,现在把它们都塞到一个巨大的、豪华的仓库里。

为什么要用Monorepo?

你可能会问,这么做图个啥?好处可多了去了:

  • 代码复用更方便: 不同的项目之间共享代码,简直不要太容易。改个底层组件,所有项目都能受益,妈妈再也不用担心我到处复制粘贴了。
  • 依赖管理更简单: 统一管理依赖,避免版本冲突。再也不用为了解决依赖问题,头发都掉光了。
  • 原子性变更: 修改一个底层库,可以同时更新所有依赖它的项目。测试、发布一条龙服务,避免出现版本不一致的情况。
  • 协作更高效: 所有开发者都在同一个仓库里工作,更容易了解整个系统的架构,协作起来更加流畅。
  • 构建和测试更高效: 可以利用工具来分析代码依赖关系,只构建和测试受影响的部分,大大提高效率。

当然,Monorepo也不是完美的。它也有缺点:

  • 仓库体积大: 所有代码都在一起,仓库体积肯定不小。不过现在网络速度这么快,硬盘这么大,这都不是事儿。
  • 权限管理复杂: 需要更精细的权限管理,避免有人不小心改了别人的代码。
  • 构建和测试配置复杂: 需要花点心思配置构建和测试流程,才能充分发挥Monorepo的优势。

Lerna:Monorepo的开路先锋

Lerna是Monorepo领域的元老级人物,专门用来管理包含多个package的JavaScript仓库。它主要解决两个问题:

  1. 版本管理: 自动更新所有package的版本号,并发布到npm。
  2. 依赖管理: 自动安装和链接所有package的依赖。

Lerna的基本用法:

  1. 安装Lerna:

    npm install --global lerna
  2. 初始化Lerna仓库:

    lerna init

    这会在你的仓库里生成一个lerna.json文件和一个packages目录。lerna.json是Lerna的配置文件,packages目录用来存放所有的package。

  3. 创建package:

    packages目录下创建你的package,比如packages/component-apackages/component-b。每个package都应该有自己的package.json文件。

  4. 安装依赖:

    lerna bootstrap

    这会安装所有package的依赖,并创建符号链接,让它们可以互相引用。

  5. 发布package:

    lerna publish

    这会自动更新所有package的版本号,并发布到npm。

一个简单的Lerna Monorepo示例:

假设我们有一个Monorepo,包含两个package:component-acomponent-bcomponent-a依赖于component-b

  • 目录结构:

    my-monorepo/
    ├── lerna.json
    ├── package.json
    └── packages/
        ├── component-a/
        │   ├── index.js
        │   └── package.json
        └── component-b/
            ├── index.js
            └── package.json
  • lerna.json

    {
      "packages": [
        "packages/*"
      ],
      "version": "independent",
      "npmClient": "npm",
      "useWorkspaces": true
    }

    packages指定了package的路径,version指定了版本管理模式(independent表示每个package独立管理版本),npmClient指定了使用的包管理器,useWorkspaces 是否使用 workspaces ,使用之后依赖安装速度更快。

  • package.json (根目录):

    {
      "name": "my-monorepo",
      "private": true,
      "devDependencies": {
        "lerna": "^4.0.0"
      },
      "workspaces": [
        "packages/*"
      ]
    }

    private: true 表示这是一个私有仓库,不会被发布到npm。devDependencies 包含了Lerna的依赖。 workspaces 用于配置 workspaces 。

  • packages/component-a/package.json

    {
      "name": "@my-monorepo/component-a",
      "version": "1.0.0",
      "description": "Component A",
      "main": "index.js",
      "dependencies": {
        "@my-monorepo/component-b": "^1.0.0"
      }
    }
  • packages/component-b/package.json

    {
      "name": "@my-monorepo/component-b",
      "version": "1.0.0",
      "description": "Component B",
      "main": "index.js"
    }
  • packages/component-a/index.js

    import componentB from '@my-monorepo/component-b';
    
    function componentA() {
      console.log('Component A');
      componentB();
    }
    
    export default componentA;
  • packages/component-b/index.js

    function componentB() {
      console.log('Component B');
    }
    
    export default componentB;

现在,你可以使用lerna bootstrap来安装依赖,然后使用lerna publish来发布package。

Lerna的进阶用法:

  • lerna run 在所有package中运行指定的npm脚本。例如,lerna run test会在所有package中运行npm test
  • lerna exec 在所有package中执行指定的命令。例如,lerna exec -- rm -rf node_modules会删除所有package的node_modules目录。
  • lerna changed 列出自上次发布以来发生更改的package。
  • lerna diff 显示自上次发布以来每个package的更改。

Nx:Monorepo的瑞士军刀

Nx是新一代的Monorepo工具,它不仅仅是一个版本管理工具,更是一个强大的构建和测试工具。Nx的核心思想是计算缓存依赖分析

  • 计算缓存: Nx会缓存每次构建和测试的结果,如果代码没有发生变化,Nx会直接使用缓存,避免重复构建和测试。
  • 依赖分析: Nx会分析代码的依赖关系,只构建和测试受影响的部分。

Nx的基本用法:

  1. 安装Nx:

    npm install --global nx
  2. 创建Nx workspace:

    npx create-nx-workspace@latest my-nx-monorepo --preset=npm --package-manager=npm

    这会创建一个新的Nx workspace,并选择npm作为包管理器。

  3. 创建application或library:

    nx generate @nx/react:application my-react-app
    nx generate @nx/js:library my-js-lib

    这会创建一个新的React application和一个JavaScript library。

  4. 构建和测试:

    nx build my-react-app
    nx test my-js-lib

    这会构建React application和测试JavaScript library。

一个简单的Nx Monorepo示例:

假设我们有一个Nx Monorepo,包含一个React application和一个JavaScript library。React application依赖于JavaScript library。

  • 目录结构:

    my-nx-monorepo/
    ├── apps/
    │   └── my-react-app/
    │       ├── src/
    │       │   └── app/
    │       │       └── app.tsx
    │       └── project.json
    ├── libs/
    │   └── my-js-lib/
    │       ├── src/
    │       │   └── index.ts
    │       └── project.json
    ├── nx.json
    ├── package.json
    └── tsconfig.base.json
  • nx.json

    {
      "npmScope": "my-nx-monorepo",
      "affected": {
        "defaultBase": "main"
      },
      "implicitDependencies": {
        "package.json": {
          "dependencies": "*",
          "devDependencies": "*"
        },
        ".eslintrc.json": "*"
      },
      "tasksRunnerOptions": {
        "default": {
          "runner": "@nrwl/nx-cloud",
          "options": {
            "cacheableOperations": [
              "build",
              "lint",
              "test",
              "e2e"
            ],
            "accessToken": "YOUR_NX_CLOUD_ACCESS_TOKEN"
          }
        }
      },
      "targetDefaults": {
        "build": {
          "dependsOn": [
            "^build"
          ],
          "inputs": [
            "default",
            "{projectRoot}/.eslintrc.json",
            "{projectRoot}/tsconfig.json",
            "{projectRoot}/tslint.json"
          ],
          "outputs": [
            "{projectRoot}/dist"
          ]
        }
      },
      "namedInputs": {
        "default": [
          "{{implicitDependencies}}",
          "{projectRoot}/**/*",
          "!{projectRoot}/**/?(*.)+(spec|test).ts?(x)?(.snap)",
          "{projectRoot}/.babelrc",
          "{projectRoot}/.swcrc"
        ],
        "production": [
          "default"
        ]
      }
    }

    npmScope指定了npm scope,affected指定了受影响的代码的默认分支,tasksRunnerOptions指定了任务运行器,targetDefaults指定了目标的默认配置,namedInputs指定了命名的输入。

  • package.json (根目录):

    {
      "name": "my-nx-monorepo",
      "version": "0.0.0",
      "license": "MIT",
      "scripts": {
        "start": "nx serve",
        "build": "nx build",
        "test": "nx test"
      },
      "private": true,
      "devDependencies": {
        "@nrwl/cli": "14.0.2",
        "@nrwl/eslint-plugin-nx": "14.0.2",
        "@nrwl/jest": "14.0.2",
        "@nrwl/linter": "14.0.2",
        "@nrwl/react": "14.0.2",
        "@nrwl/node": "14.0.2",
        "@nrwl/web": "14.0.2",
        "@nrwl/workspace": "14.0.2",
        "@testing-library/react": "13.0.0",
        "@types/jest": "27.4.1",
        "@types/node": "16.11.7",
        "@types/react": "18.0.0",
        "@types/react-dom": "18.0.0",
        "@typescript-eslint/eslint-plugin": "~5.24.0",
        "@typescript-eslint/parser": "~5.24.0",
        "eslint": "~8.15.0",
        "eslint-config-prettier": "8.1.0",
        "eslint-plugin-import": "2.26.0",
        "eslint-plugin-jsx-a11y": "6.6.1",
        "eslint-plugin-react": "7.30.0",
        "eslint-plugin-react-hooks": "4.6.0",
        "jest": "27.5.1",
        "prettier": "^2.6.2",
        "ts-jest": "27.1.4",
        "ts-node": "10.9.1",
        "typescript": "~4.7.2"
      },
      "dependencies": {
        "react": "18.2.0",
        "react-dom": "18.2.0"
      }
    }
  • apps/my-react-app/project.json

    {
      "name": "my-react-app",
      "$schema": "../../node_modules/nx/schemas/project-schema.json",
      "sourceRoot": "apps/my-react-app/src",
      "projectType": "application",
      "targets": {
        "build": {
          "executor": "@nrwl/web:webpack",
          "outputs": [
            "{options.outputPath}"
          ],
          "defaultConfiguration": "production",
          "options": {
            "compiler": "babel",
            "outputPath": "dist/apps/my-react-app",
            "index": "apps/my-react-app/src/index.html",
            "baseHref": "/",
            "main": "apps/my-react-app/src/main.tsx",
            "polyfills": "apps/my-react-app/src/polyfills.ts",
            "tsConfig": "apps/my-react-app/tsconfig.app.json",
            "assets": [
              "apps/my-react-app/src/favicon.ico",
              "apps/my-react-app/src/assets"
            ],
            "styles": [],
            "scripts": [],
            "webpackConfig": "apps/my-react-app/webpack.config.js"
          },
          "configurations": {
            "production": {
              "fileReplacements": [
                {
                  "replace": "apps/my-react-app/src/environments/environment.ts",
                  "with": "apps/my-react-app/src/environments/environment.prod.ts"
                }
              ],
              "optimization": true,
              "outputHashing": "all",
              "sourceMap": false,
              "extractCss": true,
              "namedChunks": false,
              "extractLicenses": true,
              "vendorChunk": false,
              "budgets": [
                {
                  "type": "initial",
                  "maximumWarning": "500kb",
                  "maximumError": "1mb"
                },
                {
                  "type": "anyComponentStyle",
                  "maximumWarning": "2kb",
                  "maximumError": "4kb"
                }
              ]
            }
          }
        },
        "serve": {
          "executor": "@nrwl/web:dev-server",
          "defaultConfiguration": "development",
          "options": {
            "buildTarget": "my-react-app:build",
            "hmr": true
          },
          "configurations": {
            "development": {
              "buildTarget": "my-react-app:build:development"
            },
            "production": {
              "buildTarget": "my-react-app:build:production",
              "hmr": false
            }
          }
        },
        "lint": {
          "executor": "@nrwl/linter:eslint",
          "outputs": [
            "{options.outputFile}"
          ],
          "options": {
            "lintFilePatterns": [
              "apps/my-react-app/**/*.{ts,tsx,js,jsx}"
            ]
          }
        },
        "test": {
          "executor": "@nrwl/jest:jest",
          "outputs": [
            "coverage/apps/my-react-app"
          ],
          "options": {
            "jestConfig": "apps/my-react-app/jest.config.ts",
            "passWithNoTests": true
          }
        }
      },
      "tags": []
    }
  • libs/my-js-lib/project.json

    {
      "name": "my-js-lib",
      "$schema": "../../node_modules/nx/schemas/project-schema.json",
      "sourceRoot": "libs/my-js-lib/src",
      "projectType": "library",
      "targets": {
        "build": {
          "executor": "@nrwl/js:tsc",
          "outputs": [
            "{options.outputPath}"
          ],
          "options": {
            "outputPath": "dist/libs/my-js-lib",
            "tsConfig": "libs/my-js-lib/tsconfig.lib.json",
            "packageJson": "libs/my-js-lib/package.json",
            "main": "libs/my-js-lib/src/index.ts",
            "assets": [
              "libs/my-js-lib/*.md"
            ]
          }
        },
        "lint": {
          "executor": "@nrwl/linter:eslint",
          "outputs": [
            "{options.outputFile}"
          ],
          "options": {
            "lintFilePatterns": [
              "libs/my-js-lib/**/*.ts"
            ]
          }
        },
        "test": {
          "executor": "@nrwl/jest:jest",
          "outputs": [
            "coverage/libs/my-js-lib"
          ],
          "options": {
            "jestConfig": "libs/my-js-lib/jest.config.ts",
            "passWithNoTests": true
          }
        }
      },
      "tags": []
    }
  • apps/my-react-app/src/app/app.tsx

    import React from 'react';
    import { myJsLib } from '@my-nx-monorepo/my-js-lib';
    
    function App() {
      return (
        <div>
          <h1>My React App</h1>
          <p>{myJsLib()}</p>
        </div>
      );
    }
    
    export default App;
  • libs/my-js-lib/src/index.ts

    export function myJsLib(): string {
      return 'Hello from my-js-lib!';
    }

现在,你可以使用nx build my-react-app来构建React application,使用nx test my-js-lib来测试JavaScript library。如果修改了my-js-lib的代码,再次构建my-react-app时,Nx会自动检测到依赖关系,并重新构建my-react-app

Nx的进阶用法:

  • Nx Console: 一个VS Code插件,可以方便地生成代码、运行命令和查看依赖关系。
  • Nx Cloud: 一个云服务,可以缓存构建和测试结果,并提供分布式构建和测试功能。
  • Nx Plugins: 可以扩展Nx的功能,支持更多的技术栈和工具。

Lerna vs Nx:选择困难症?

Lerna和Nx都是优秀的Monorepo工具,选择哪个取决于你的需求。

特性 Lerna Nx
核心功能 版本管理、依赖管理 构建、测试、依赖分析、计算缓存
学习曲线 简单易上手 功能强大,学习曲线稍高
适用场景 简单的Monorepo,只需要版本管理和依赖管理 复杂的Monorepo,需要高效的构建和测试流程,以及强大的依赖分析能力
生态系统 相对简单 丰富,支持各种技术栈和工具

总结:

Monorepo是一种强大的代码管理模式,可以提高代码复用率、简化依赖管理、提高协作效率。Lerna和Nx都是优秀的Monorepo工具,可以帮助你更好地管理Monorepo。选择哪个取决于你的需求。

希望今天的讲座能帮助大家更好地理解Monorepo架构,并选择合适的工具来管理你的JavaScript项目。 谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注