monorepo项目依赖解析

196 阅读6分钟

通过文章你能了解到monorepo项目的工作目录和hoisting,以及软链接内容。文章很简单,也是学习的别人的内容来的。我也懒得总结,最后用了AI的整体总结,看看就行。

项目搭建

项目目录结构:

  • 核心功能包:packages
  • 自测组件的工程:examples
  • 项目文档:docs
xxx/
├── packages/           # 核心包目录
│   ├── components/     # UI组件库
│   ├── theme-chalk/    # 主题样式
│   └── utils/          # 工具函数
├── examples/           # 示例应用
├── docs/              # 文档站点
├── package.json       # 根配置
├── pnpm-workspace.yaml # workspace配置
└── pnpm-lock.yaml     # 依赖锁定

依赖管理

根目录中将内部包声明,避免从外部下载安装包,根node_modules中会生成内部包的软链接。根目录下将内部所有用到的公共库进行安装,避免在各个包中重复安装,hoisting。

image.png

{
  "dependencies": {
    "@xxx/components": "workspace:^",    // 链接到 packages/components
    "@xxx/examples": "workspace:^",      // 链接到 examples
    "@xxx/theme-chalk": "workspace:^",   // 链接到 packages/theme-chalk
    "@xxx/utils": "workspace:^",         // 链接到 packages/utils
    "@highlightjs/vue-plugin": "^2.1.0", // 外部依赖
    "highlight.js": "^11.11.1"           // 外部依赖
  }
}

工作空间

workspace 就是“让 monorepo 里所有子包互相引用时,永远不用先 publish、也不用自己手动建软链”——pnpm/yarn/npm 帮你一次性把链路全部摆好。

packages:
  - packages/**    # 包含所有packages下的子包
  - examples       # 包含examples目录

它具体干的三件事

  1. 把“本地包”变成“虚拟包” 你在 packages/a/package.json 里写 "name": "@foo/a" 只要 pnpm-workspace.yaml 里把 packages/* 纳入 workspace, 整个仓库就瞬间多出一个“虚拟包” @foo/a,版本号就是 workspace:*不需要发到 npm,就能被别的子包当正式包一样依赖。
  2. 自动建软链 / 硬链,保证“实时同步” pnpm 安装时会把 node_modules/@foo/a -> packages/a 链过去; 你一边改 packages/a 源码,另一边引用它的子包(或根 docs)立刻看到变化,不用重新 npm link
  3. 统一依赖树 + 一键批量操作
  • 整个仓库只保留一份 pnpm-lock.yaml,所有子包共用,不会重复安装相同依赖。
  • 一条命令就能批量跑脚本: pnpm -r run build 把每个子包的 build 脚本按拓扑顺序全跑完。
  • 一条命令就能批量发包: pnpm -r publish 自动计算版本、打 tag、推 npm。

node_modules下软链接包名怎么来的?

软链接名称是和工作空间中子工程中package.json中的name一致。

pnpm 的安装逻辑(简化版)

  1. 扫描 pnpm-workspace.yaml 里列出的目录,把每个目录下的 package.json 当成“本地包”注册表。
  2. 读取根 package.json(或任何子包)里声明的依赖: "@xxx/components": "workspace:*"
  3. 发现依赖名 @xxx/components 能在“本地包注册表”里匹配到,于是
  • 不在远程 npm 拉包;
  • 直接在根 node_modules 下建一个 符号链接(软链)目录,名字就是 @xxx
  • 再把 @xxx/components 指向仓库里的 packages/components

所以 node_modules/@xxx 这个目录名 = package.json 里的 scope (@xxx) node_modules/@xxx/components 这个目录名 = package.json 里的包名 (@xxx/components) 你看到的内容跟 packages/components 完全一致,是因为那只是个软链,磁盘上只有一份实体代码

AI总结

以下是AI总的项目内容

Monorepo 架构设计文档

📋 项目概述

本项目采用 pnpm workspace 构建的 Monorepo 架构,用于管理一个 Vue 组件库及其相关工具。通过统一的依赖管理和软链接机制,实现了高效的包间协作和开发体验。

🏗️ 项目结构

xxx/

├── packages/ # 核心包目录

│ ├── components/ # UI组件库

│ │ ├── src/

│ │ │ └── button.vue # 按钮组件

│ │ ├── index.js # 组件导出入口

│ │ └── components.js # 组件注册

│ ├── theme-chalk/ # 主题样式包

│ │ ├── index.less # 样式入口文件

│ │ └── package.json

│ ├── utils/ # 工具函数包

│ │ └── package.json

│ └── components.js # 组件统一导出

├── examples/ # 示例应用

│ ├── app.vue

│ ├── index.html

│ ├── index.js

│ └── package.json

├── docs/ # 文档站点

│ ├── .vitepress/

│ │ ├── config.mjs # VitePress 配置

│ │ └── theme/

│ │ └── index.js # 主题配置

│ ├── components/ # 文档组件

│ └── package.json

├── package.json # 根配置文件

├── pnpm-workspace.yaml # Workspace 配置

└── pnpm-lock.yaml # 依赖锁定文件

⚙️ Workspace 配置

pnpm-workspace.yaml

packages:
- packages/** # 包含所有 packages 下的子包
- examples # 包含 examples 目录

注意: docs 目录未包含在 workspace 中,但通过特殊的模块解析机制仍可使用依赖。

🔗 依赖关系与链接机制

1. 根目录依赖管理

根目录的 package.json 作为依赖管理中心:

{

  "dependencies": {

  "@xxx/components": "workspace:^", // 链接到 packages/components

  "@xxx/examples": "workspace:^", // 链接到 examples

  "@xxx/theme-chalk": "workspace:^", // 链接到 packages/theme-chalk

  "@xxx/utils": "workspace:^", // 链接到 packages/utils

  "@highlightjs/vue-plugin": "^2.1.0", // 外部依赖

  "highlight.js": "^11.11.1" // 外部依赖

  },

  "devDependencies": {

  "@vitejs/plugin-vue": "^6.0.1",

  "less": "^4.4.1",

  "less-loader": "^12.3.0",

  "vite": "^7.1.6",

  "vitepress": "^1.6.4"

  }

}

2. 软链接实现

pnpm 自动在根目录的 node_modules/@xxx/ 下创建软链接:

node_modules/@xxx/

├── components -> ../../packages/components

├── examples -> ../../examples

├── theme-chalk -> ../../packages/theme-chalk

└── utils -> ../../packages/utils

链接类型: 符号链接(软链接)

  • 使用 lrwxr-xr-x 权限
  • 指向相对路径 ../../packages/xxx
  • 实现本地包的直接引用

3. workspace 协议详解

"@xxx/components": "workspace:^"

协议作用:

  • workspace: 标识这是内部包
  • ^ 表示兼容版本范围
  • 自动解析到本地对应目录
  • 避免从 npm 下载

📦 各包功能说明

@xxx/components (UI组件库)

  • 位置:packages/components/
  • 功能: 提供可复用的 Vue 组件
  • 导出方式: 通过 index.js 统一导出所有组件
  • 组件注册: 支持 Vue 插件式安装
// packages/components/index.js

import * as components from './components';

export default {

  install(app) {

    Object.entries(components).forEach(([key, value]) => {

      app.component(key, value);

    });

  },

};

@xxx/theme-chalk (主题样式)

  • 位置: packages/theme-chalk/
  • 功能: 提供组件的样式文件
  • 文件: index.less 样式入口
  • 使用方式: 在应用中直接导入样式文件

@xxx/utils (工具函数)

  • 位置:packages/utils/
  • 功能:提供通用工具函数
  • 状态:目前为空包,预留扩展

@xxx/examples (示例应用)

  • 位置:examples/
  • 功能:展示组件使用示例
  • 配置:ES模块类型
  • 运行:通过 Vite 开发服务器

🔧 依赖使用场景

1. 在文档中使用

// docs/.vitepress/theme/index.js

import DefaultTheme from 'vitepress/theme';

import TestUI from '@xxx/components'; // 使用组件库

import '@xxx/theme-chalk/index.less'; // 使用样式

import hljsVuePlugin from '@highlightjs/vue-plugin';


export default {

  ...DefaultTheme,

  enhanceApp: async ({ app, router, siteData }) => {

    app.use(TestUI); // 注册组件库

    app.use(hljsVuePlugin);

  },

};

2. 在示例中使用

// examples/ 中可以直接使用

import { TButton } from '@xxx/components';

⚡ 模块解析机制

1. Node.js 模块解析路径

  1. 当前目录的 node_modules
  2. 父目录的 node_modules
  3. 根目录的 node_modules ← 关键

2. VitePress 特殊处理

  • 命令执行: pnpm docs:dev 在根目录执行
  • 工作目录: VitePress 进程的工作目录是根目录
  • 依赖访问: 能够访问根目录的所有依赖

3. 为什么 docs 可以使用依赖

虽然 docs 不在 workspace 配置中,但:

  • VitePress 命令在根目录执行
  • 模块解析从根目录开始
  • 能够找到根目录 node_modules 中的依赖

🚀 开发与构建

开发环境

# 安装依赖
pnpm install

# 启动文档站点
pnpm docs:dev

# 启动示例应用
pnpm dev

# 预览文档
pnpm docs:preview

构建流程

# 构建文档

pnpm docs:build

依赖管理

# 查看依赖树
pnpm list --depth=0

# 添加依赖到根目录
pnpm add <package-name>

# 添加依赖到特定包
pnpm add <package-name> --filter @xxx/components