深入分析 JavaScript Monorepo 工具 (如 Lerna, Nx) 如何管理多包项目、共享配置和优化构建流程。

各位观众老爷,晚上好! 咳咳,今天咱们聊聊 JavaScript Monorepo 的那些事儿。说白了,就是如何用工具(比如 Lerna 和 Nx)来管理一大堆 JavaScript 项目,让它们像一个大家庭一样和谐相处,一起搞事情。

啥是 Monorepo?为啥要用它?

首先,咱们得搞明白 Monorepo 是个啥玩意儿。简单来说,就是把多个项目(或者说包)的代码都放在同一个代码仓库里。 这跟传统的每个项目一个仓库的模式(俗称 Polyrepo)不太一样。

为啥要用 Monorepo 呢? 主要有这么几个好处:

  • 代码共享更容易: 如果多个项目都需要用到同一个组件或者工具函数,Monorepo 可以让你直接引用,不用复制粘贴,避免代码冗余。
  • 依赖管理更方便: 所有的项目都在同一个仓库里,你可以更容易地管理它们之间的依赖关系,统一升级依赖版本。
  • 代码复用性更高: 将公共模块提取到共享包中,各项目可以共享使用,减少重复开发,提高代码复用率。
  • 原子提交更容易: 如果一个功能需要修改多个项目,你可以一次提交所有修改,保证功能的一致性。
  • 方便协作: 所有的项目都在同一个仓库里,团队成员可以更容易地了解整个项目的结构和各个项目之间的关系。

当然,Monorepo 也有一些缺点,比如:

  • 仓库体积更大: 所有的项目代码都在同一个仓库里,仓库体积肯定会更大,clone 和 checkout 的时间可能会更长。
  • 构建时间更长: 如果所有的项目都需要构建,构建时间可能会更长。
  • 权限管理更复杂: 需要更精细的权限管理,避免不同项目之间的代码互相干扰。
特性 Monorepo Polyrepo
代码共享 容易,直接引用 困难,需要发布到 npm 或者使用其他方式共享
依赖管理 方便,统一管理 复杂,需要手动管理每个项目的依赖
原子提交 容易,一次提交所有修改 困难,需要多次提交
代码复用 高,共享包可以被多个项目使用 低,重复开发的可能性较高
仓库体积
构建时间 可能更长 可能更短
权限管理 复杂 简单
协作方式 更容易了解整个项目结构和各个项目之间的关系 项目间隔离,需要更多沟通

Lerna:Monorepo 的老牌管家

Lerna 是一个老牌的 Monorepo 管理工具,它可以帮你管理多个包的发布、版本控制和依赖关系。

Lerna 的核心功能:

  • lerna bootstrap 安装所有项目的依赖,并且将项目之间的依赖关系链接起来。
  • lerna publish 发布所有有更新的项目到 npm。
  • lerna version 更新所有项目的版本号,并且生成 changelog。
  • lerna run 在所有项目中运行指定的 npm script。
  • lerna exec 在所有项目中执行指定的命令。

Lerna 的使用方法:

  1. 安装 Lerna:

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

    lerna init

    这会在你的项目根目录下生成 lerna.jsonpackages 目录。lerna.json 是 Lerna 的配置文件,packages 目录用来存放所有的项目。

  3. 创建项目:

    packages 目录下创建你的项目,每个项目都是一个独立的 npm 包。

    例如,我们创建两个项目:package-apackage-b

    mkdir packages/package-a
    cd packages/package-a
    npm init -y
    cd ../../
    mkdir packages/package-b
    cd packages/package-b
    npm init -y
    cd ../../
  4. 配置 lerna.json

    打开 lerna.json 文件,配置你的项目。

    {
      "packages": [
        "packages/*"
      ],
      "version": "independent",
      "npmClient": "npm",
      "useWorkspaces": true,
      "command": {
        "publish": {
          "ignoreChanges": [
            "ignored-file",
            "*.md"
          ]
        }
      }
    }
    • packages: 指定项目所在的目录,这里我们使用 packages/* 表示 packages 目录下所有的目录都是一个项目。
    • version: 指定版本控制模式,independent 表示每个项目独立控制版本号。 还有固定模式: fixed, 所有包使用同一个版本号。
    • npmClient: 指定使用的 npm 客户端,这里我们使用 npm
    • useWorkspaces: 是否使用 npm workspaces。
    • command.publish.ignoreChanges: 配置发布时忽略的变更文件。
  5. 安装依赖:

    lerna bootstrap

    这会安装所有项目的依赖,并且将项目之间的依赖关系链接起来。 如果 useWorkspaces 设置为 true, Lerna 会使用 npm/yarn/pnpm workspaces 功能,将依赖安装到项目根目录的 node_modules 目录下,减少依赖重复安装。

  6. 发布项目:

    lerna publish

    这会发布所有有更新的项目到 npm。 Lerna 会自动检测哪些项目有更新,并且提示你输入新的版本号。

Lerna 的一些高级用法:

  • 使用 scope: 可以使用 scope 来限制命令的执行范围。

    lerna run test --scope=@my-org/package-a

    这会在 @my-org/package-a 项目中运行 test 命令。

  • 使用 ignore: 可以使用 ignore 来忽略一些项目。

    {
      "packages": [
        "packages/*",
        "!packages/package-c"
      ],
      "version": "independent"
    }

    这会忽略 packages/package-c 项目。

Lerna 的优点:

  • 简单易用,容易上手。
  • 功能完善,满足 Monorepo 管理的基本需求。
  • 社区活跃,有大量的插件和工具可以使用。

Lerna 的缺点:

  • 性能较差,构建速度慢。
  • 对 TypeScript 的支持不够好。
  • 配置比较复杂。

Lerna 示例代码:

// packages/package-a/index.js
export function helloFromA() {
  console.log("Hello from package A!");
}

// packages/package-b/index.js
import { helloFromA } from '@my-org/package-a';

export function helloFromB() {
  console.log("Hello from package B!");
  helloFromA();
}

// packages/package-b/package.json
{
  "name": "@my-org/package-b",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo "Error: no test specified" && exit 1"
  },
  "dependencies": {
    "@my-org/package-a": "*" // 依赖本地的 package-a
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

Nx:Monorepo 的性能王者

Nx 是一个更现代化的 Monorepo 管理工具,它专注于提高构建速度和开发效率。

Nx 的核心功能:

  • 依赖图分析: Nx 可以分析项目之间的依赖关系,并且生成依赖图。
  • 增量构建: Nx 可以根据依赖图,只构建受影响的项目,大大提高构建速度。
  • 缓存: Nx 可以缓存构建结果,避免重复构建。
  • 代码生成: Nx 可以根据模板生成代码,提高开发效率。
  • 插件: Nx 有大量的插件可以使用,支持各种框架和工具。

Nx 的使用方法:

  1. 安装 Nx:

    npm install --global nx
  2. 创建 Nx 工作区:

    npx create-nx-workspace my-workspace

    这会创建一个新的 Nx 工作区。 Nx 会提示你选择一个预设的配置,例如 React, Angular 或者 Node.js。

  3. 创建项目:

    nx generate @nx/react:application my-app

    这会创建一个新的 React 项目。 你可以使用 nx generate 命令创建各种类型的项目,例如 library, component, service 等。

  4. 构建项目:

    nx build my-app

    这会构建 my-app 项目。 Nx 会自动分析项目之间的依赖关系,并且只构建受影响的项目。

  5. 运行项目:

    nx serve my-app

    这会运行 my-app 项目。

Nx 的一些高级用法:

  • 使用 affected: 可以使用 affected 命令来查看受影响的项目。

    nx affected:build

    这会构建所有受影响的项目。

  • 使用 task runner: Nx 使用 task runner 来执行构建、测试和 lint 等任务。 你可以自定义 task runner 的配置。

  • 使用 plugins: Nx 有大量的插件可以使用,支持各种框架和工具。 例如,可以使用 @nx/eslint 插件来配置 ESLint, 使用 @nx/jest 插件来配置 Jest。

Nx 的优点:

  • 性能优秀,构建速度快。
  • 对 TypeScript 的支持非常好。
  • 功能强大,可以满足各种复杂的 Monorepo 管理需求。
  • 社区活跃,有大量的插件和工具可以使用。

Nx 的缺点:

  • 学习曲线较陡峭,需要学习 Nx 的各种概念和配置。
  • 配置比较复杂。

Nx 示例代码:

// libs/my-lib/src/index.ts
export function helloFromLib() {
  console.log("Hello from my lib!");
}

// apps/my-app/src/app/app.tsx
import { helloFromLib } from '@my-workspace/my-lib';

function App() {
  return (
    <div>
      <h1>Hello World!</h1>
      <button onClick={helloFromLib}>Say Hello from Lib</button>
    </div>
  );
}

export default App;

// nx.json (部分)
{
  "npmScope": "my-workspace",
  "affected": {
    "defaultBase": "main"
  },
  "implicitDependencies": {
    "workspace.json": "*",
    "package.json": {
      "dependencies": "*",
      "devDependencies": "*"
    },
    "tsconfig.base.json": "*",
    "tslint.json": "*",
    ".eslintrc.json": "*",
    "nx.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",
        "{workspaceRoot}/babel.config.json",
        "{workspaceRoot}/.swcrc"
      ]
    }
  },
  "namedInputs": {
    "default": [
      "{projectRoot}/**/*",
      "sharedGlobals"
    ],
    "sharedGlobals": []
  },
  "generators": {
    "@nrwl/react": {
      "application": {
        "style": "css",
        "linter": "eslint",
        "unitTestRunner": "jest",
        "e2eTestRunner": "cypress"
      },
      "library": {
        "linter": "eslint",
        "unitTestRunner": "jest"
      },
      "component": {
        "style": "css"
      }
    }
  }
}

共享配置:让 Monorepo 更和谐

在 Monorepo 中,共享配置是非常重要的,它可以保证所有项目的代码风格、构建流程和测试环境一致。

常见的共享配置方式:

  • 使用 tsconfig.json 可以将 TypeScript 的配置放在根目录下的 tsconfig.json 文件中,然后在每个项目的 tsconfig.json 文件中继承根目录下的 tsconfig.json 文件。

    // tsconfig.json (根目录)
    {
      "compilerOptions": {
        "target": "es5",
        "module": "commonjs",
        "strict": true,
        "esModuleInterop": true,
        "skipLibCheck": true,
        "forceConsistentCasingInFileNames": true
      }
    }
    
    // packages/package-a/tsconfig.json
    {
      "extends": "../../tsconfig.json",
      "compilerOptions": {
        "rootDir": "src",
        "outDir": "dist"
      },
      "include": ["src"]
    }
  • 使用 ESLint: 可以将 ESLint 的配置放在根目录下的 .eslintrc.json 文件中,然后在每个项目的 .eslintrc.json 文件中继承根目录下的 .eslintrc.json 文件。

    // .eslintrc.json (根目录)
    {
      "env": {
        "browser": true,
        "es2021": true
      },
      "extends": [
        "eslint:recommended",
        "plugin:@typescript-eslint/recommended"
      ],
      "parser": "@typescript-eslint/parser",
      "parserOptions": {
        "ecmaVersion": 12,
        "sourceType": "module"
      },
      "plugins": [
        "@typescript-eslint"
      ],
      "rules": {
        "no-unused-vars": "off",
        "@typescript-eslint/no-unused-vars": ["warn"]
      }
    }
    
    // packages/package-a/.eslintrc.json
    {
      "extends": "../../.eslintrc.json",
      "rules": {
        "no-console": "warn"
      }
    }
  • 使用 Prettier: 可以将 Prettier 的配置放在根目录下的 .prettierrc.js 文件中,然后在每个项目中都使用相同的 Prettier 配置。

    // .prettierrc.js (根目录)
    module.exports = {
      semi: false,
      trailingComma: "all",
      singleQuote: true,
      printWidth: 120,
    };
  • 使用 husky 和 lint-staged: 可以使用 husky 和 lint-staged 来在提交代码之前自动运行 lint 和 format 命令,保证代码质量。

    // package.json (根目录)
    {
      "devDependencies": {
        "husky": "^7.0.0",
        "lint-staged": "^12.0.0"
      },
      "husky": {
        "hooks": {
          "pre-commit": "lint-staged"
        }
      },
      "lint-staged": {
        "*.{js,jsx,ts,tsx,json,md}": [
          "prettier --write",
          "eslint --fix"
        ]
      }
    }

优化构建流程:让 Monorepo 飞起来

在 Monorepo 中,构建流程的优化非常重要,它可以大大提高开发效率。

常见的优化构建流程的方式:

  • 使用增量构建: 只构建受影响的项目,避免重复构建。 Nx 默认支持增量构建。
  • 使用缓存: 缓存构建结果,避免重复构建。 Nx 默认支持缓存。
  • 并行构建: 并行构建多个项目,提高构建速度。 Lerna 和 Nx 都支持并行构建。
  • 使用 Webpack Module Federation: 使用 Webpack Module Federation 可以将 Monorepo 中的项目拆分成多个独立的模块,然后动态加载这些模块。 这样可以减少构建时间和部署时间。
  • 代码分割: 使用代码分割可以将 Monorepo 中的项目拆分成多个小的 bundle,然后按需加载这些 bundle。 这样可以提高应用的加载速度。

Lerna vs Nx:选哪个?

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

  • 如果你的项目比较简单,对性能要求不高,可以选择 Lerna。 Lerna 简单易用,容易上手,可以满足 Monorepo 管理的基本需求。
  • 如果你的项目比较复杂,对性能要求很高,可以选择 Nx。 Nx 性能优秀,功能强大,可以满足各种复杂的 Monorepo 管理需求。
特性 Lerna Nx
易用性 简单易用,容易上手 学习曲线较陡峭,需要学习 Nx 的各种概念和配置
性能 性能较差,构建速度慢 性能优秀,构建速度快
TypeScript 支持 对 TypeScript 的支持不够好 对 TypeScript 的支持非常好
功能 功能完善,满足 Monorepo 管理的基本需求 功能强大,可以满足各种复杂的 Monorepo 管理需求
配置 配置比较简单 配置比较复杂
社区 社区活跃,有大量的插件和工具可以使用 社区活跃,有大量的插件和工具可以使用

总而言之,Lerna 就像一位老管家,经验丰富,处理基本事务得心应手;Nx 则像一位年轻的 CEO,充满活力,擅长优化流程,提升效率。选择谁,取决于你家的规模和发展目标。

好了,今天的讲座就到这里,希望对大家有所帮助! 祝大家在 Monorepo 的世界里玩得开心!

发表回复

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