普通视图

发现新文章,点击刷新页面。
昨天 — 2026年1月26日首页

Monorepo的实现原理

作者 不灵不灵
2026年1月26日 14:06

Monorepo的实现需要

  1. 基础层:包管理器的Workspace功能

  2. 管理层:Lerna等版本和发布管理

  3. 构建层:Turborepo/Nx等高性能构建工具

  4. 架构层:合理的项目结构和依赖管理策略

  5. 工程层:CI/CD、代码质量、团队协作流程


Monorepo的实现原理(底层机制详解)

Monorepo的本质

  • 链接机制:通过符号链接/硬链接连接本地包
  • 依赖提升:共享相同版本的依赖,减少冗余
  • 统一入口:通过根package.json管理公共配置
  • 原子操作:跨包的操作(构建、测试、发布)保持一致性
  • 依赖图感知:理解包之间的关系,优化执行顺序

一、最核心原理:符号链接(Symlink)

1. 基本概念

# 传统多仓库
project-a/
├── node_modules/
│   └── project-b/     # 从npm安装
project-b/             # 独立的仓库

# Monorepo实现后
monorepo/
├── packages/
│   ├── project-a/
│   │   └── node_modules/
│   │       └── project-b -> ../../project-b  # 符号链接!
│   └── project-b/

2. 符号链接如何工作

# 创建符号链接的底层实现
ln -s ../../packages/utils packages/ui/node_modules/@company/utils

# 结果:
# packages/ui/node_modules/@company/utils -> packages/utils

# 验证链接
ls -l packages/ui/node_modules/@company/
# lrwxr-xr-x  1 user  group  20 Jan 25 10:00 utils -> ../../../utils

二、依赖解析机制

1. Node.js的模块解析算法

// Node.js查找模块的顺序:
1. 当前目录的node_modules
2. 父目录的node_modules(递归向上)
3. 全局node_modules

// Monorepo利用这个机制:
monorepo/
├── node_modules/                    # 根级(提升的依赖)
│   ├── react/
│   └── lodash/
├── packages/
│   ├── ui/
│   │   └── node_modules/
│   │       ├── @company/utils -> ../../utils  # 符号链接
│   │       └── local-dep/          # 本地特有依赖
│   └── utils/

2. 包管理器的工作原理

// npm/yarn/pnpm在Monorepo中的解析流程
function resolvePackage(packageName, currentDir) {
  // 1. 检查是否是工作区包
  if (isWorkspacePackage(packageName)) {
    // 返回本地路径而不是从npm下载
    return getLocalPackagePath(packageName);
  }

  // 2. 正常解析npm包
  return resolveFromNpm(packageName);
}

三、工作区(Workspace)的实现

1. 配置识别

// package.json
{
  "workspaces": ["packages/*", "apps/*"]
}

// 包管理器读取后:
const workspaces = ["packages/*", "apps/*"];
const packages = glob(workspaces); // 展开所有匹配的包

2. 依赖图构建

// 构建包之间的依赖关系图
class DependencyGraph {
  constructor() {
    this.nodes = new Map(); // 包名 -> 包信息
    this.edges = new Map(); // 包名 -> 依赖的包列表
  }

  buildFromWorkspaces() {
    // 1. 扫描所有workspace包
    for (const pkg of this.findAllPackages()) {
      this.nodes.set(pkg.name, pkg);

      // 2. 解析依赖
      const deps = this.extractDependencies(pkg);
      this.edges.set(pkg.name, deps);
    }
  }

  // 关键:检测内部依赖
  extractDependencies(pkg) {
    const deps = [];

    for (const [depName, version] of Object.entries(pkg.dependencies)) {
      if (version.startsWith('workspace:') || this.isLocalPackage(depName)) {
        // 这是工作区内部的依赖
        deps.push({
          name: depName,
          type: 'internal',
          path: this.findLocalPath(depName)
        });
      } else {
        // 外部npm依赖
        deps.push({
          name: depName,
          type: 'external',
          version: version
        });
      }
    }

    return deps;
  }
}

四、依赖提升(Hoisting)

1. 如何实现依赖提升

class HoistingResolver {
  hoistDependencies(packages) {
    // 1. 收集所有依赖
    const allDeps = this.collectAllDependencies(packages);

    // 2. 找出可提升的依赖(版本一致)
    const hoistable = this.findHoistableDeps(allDeps);

    // 3. 在根目录安装
    this.installAtRoot(hoistable);

    // 4. 从子包中移除重复依赖
    this.removeDuplicatesFromChildren(packages, hoistable);
  }

  findHoistableDeps(allDeps) {
    const depVersions = new Map();

    // 统计每个依赖的版本
    for (const {name, version} of allDeps) {
      if (!depVersions.has(name)) {
        depVersions.set(name, new Set());
      }
      depVersions.get(name).add(version);
    }

    // 只有所有包使用相同版本的依赖才可提升
    const hoistable = [];
    for (const [name, versions] of depVersions) {
      if (versions.size === 1) {
        // 所有包使用相同版本
        hoistable.push({name, version: Array.from(versions)[0]});
      }
    }

    return hoistable;
  }
}

五、构建和任务执行原理

1. 任务依赖图

// 计算任务执行顺序
class TaskScheduler {
  scheduleTasks(packages, taskName) {
    // 1. 构建任务图
    const taskGraph = this.buildTaskGraph(packages, taskName);

    // 2. 拓扑排序(确保依赖先执行)
    const executionOrder = this.topologicalSort(taskGraph);

    // 3. 并行执行无依赖的任务
    return this.executeInParallel(executionOrder);
  }

  buildTaskGraph(packages, taskName) {
    const graph = new Map();

    for (const pkg of packages) {
      const dependencies = this.getPackageDeps(pkg);
      const dependentTasks = [];

      // 找出依赖此包的其他包
      for (const otherPkg of packages) {
        if (otherPkg.dependencies[pkg.name]) {
          dependentTasks.push(`${otherPkg.name}:${taskName}`);
        }
      }

      graph.set(`${pkg.name}:${taskName}`, {
        dependencies: dependencies.map(dep => `${dep}:${taskName}`),
        dependents: dependentTasks
      });
    }

    return graph;
  }
}

2. 增量构建的实现

class IncrementalBuilder {
  constructor(cacheDir = '.cache') {
    this.cacheDir = cacheDir;
  }

  shouldBuild(packagePath, task) {
    // 1. 计算输入哈希
    const inputHash = this.calculateInputHash(packagePath, task);

    // 2. 检查缓存
    const cacheKey = this.getCacheKey(packagePath, task);
    const cachedHash = this.readCache(cacheKey);

    // 3. 比较哈希
    if (cachedHash === inputHash) {
      // 缓存命中,跳过构建
      return false;
    }

    // 4. 需要重新构建
    this.writeCache(cacheKey, inputHash);
    return true;
  }

  calculateInputHash(packagePath, task) {
    // 包括:
    // - 源代码文件
    // - 依赖项版本
    // - 构建配置
    // - 环境变量
    const sources = glob(`${packagePath}/src/**/*`);
    const pkgJson = require(`${packagePath}/package.json`);

    return hash({
      sources: sources.map(f => hashFile(f)),
      deps: pkgJson.dependencies,
      config: this.getBuildConfig(task),
      env: process.env.NODE_ENV
    });
  }
}

六、跨包引用的编译时处理

1. TypeScript路径映射

// tsconfig.base.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@company/*": ["packages/*/src"]
    }
  }
}

// 编译时,TypeScript将:
// import { utils } from '@company/utils';
// 转换为:
// import { utils } from '../packages/utils/src';

2. Babel/Webpack模块解析插件

// 自定义模块解析器
class MonorepoResolver {
  apply(resolver) {
    resolver.plugin('module', (request, callback) => {
      if (request.request.startsWith('@company/')) {
        // 重写请求路径
        const newRequest = Object.assign({}, request, {
          path: this.resolveLocalPackage(request.request)
        });
        return resolver.doResolve('module', newRequest, callback);
      }
      callback();
    });
  }

  resolveLocalPackage(packageName) {
    // @company/utils -> packages/utils/src
    const pkg = packageName.replace('@company/', '');
    return path.join(__dirname, 'packages', pkg, 'src');
  }
}

七、版本管理和发布的原理

1. 版本依赖更新算法

class VersionManager {
  updateVersions(packages, versionMap) {
    for (const pkg of packages) {
      // 1. 更新自己的版本
      pkg.version = versionMap[pkg.name];

      // 2. 更新依赖中的内部包版本
      this.updateDependencies(pkg, versionMap);
    }
  }

  updateDependencies(pkg, versionMap) {
    const depTypes = ['dependencies', 'devDependencies', 'peerDependencies'];

    for (const depType of depTypes) {
      if (!pkg[depType]) continue;

      for (const [depName, currentVersion] of Object.entries(pkg[depType])) {
        if (versionMap[depName]) {
          // 这是内部依赖,需要更新版本
          pkg[depType][depName] = `^${versionMap[depName]}`;
        }
      }
    }
  }
}

2. 发布时的包过滤

function getChangedPackages(commitRange) {
  // 1. 获取变更的文件
  const changedFiles = git.getChangedFiles(commitRange);

  // 2. 映射文件到包
  const changedPackages = new Set();

  for (const file of changedFiles) {
    const pkg = findOwningPackage(file);
    if (pkg) {
      changedPackages.add(pkg);

      // 3. 递归添加依赖此包的包
      const dependents = findDependents(pkg);
      for (const dep of dependents) {
        changedPackages.add(dep);
      }
    }
  }

  return Array.from(changedPackages);
}
❌
❌