微前端 Monorepo 在 Docker 中的构建方案

127 阅读4分钟

1. 天然的冲突

Docker 镜像构建的“线性逻辑”与 pnpm Workspace 的“网状依赖逻辑”之间存在天然的冲突

当 Docker 尝试“线性”地拷贝这个“网状”结构时,软链接往往会失效(指向了镜像外或者未被拷贝的路径),或者因为解包软链接导致原本共享的依赖被重复物理拷贝,造成镜像体积剧增。

1. Docker 的线性分层逻辑

Docker 镜像是基于联合文件系统(UnionFS) 的,它是一层叠一层的线性结构。每一层都是前一层的增量补丁。Docker 的缓存机制依赖于这一层的指令和对应的文件内容是否发生变化。

2. pnpm Workspace 的网状依赖逻辑

pnpm 的核心是内容寻址存储(CAS) 。在 Monorepo 中,它通过大量的软链接(Symlinks)和硬链接(Hard Links)建立起一个错综复杂的网状结构。

2. pnpm deploy「官方方案」

pnpm.io/docker

核心理念: 物理提取,按需隔离

1. 构建流

  • pnpm fetch: 仅基于 pnpm-lock.yaml 下载依赖到虚拟存储层。只要依赖清单未变,该层将实现极致的 Docker 缓存命中
  • pnpm install --offline: 拷贝源码后强制离线安装。完全跳过网络请求,利用本地缓存高速装配环境
  • pnpm deploy: 将指定子包从 Workspace 中“抽取”到独立目录,并将原有的内容寻址链接自动转换为真实的物理文件
# 1.【fetch 阶段】只需 lockfile,下载全局依赖到 pnpm 虚拟存储
# 这一层只要 lockfile 不变,就不会重新下载,极大加速构建
COPY pnpm-lock.yaml ./
RUN pnpm fetch

# 2.【安装阶段】拷贝源码并离线安装
COPY . .
# --offline 强制从 fetch 好的虚拟存储中读取,不再查网络
RUN pnpm install --offline

# 3.【deploy 阶段】将子包及其依赖“抽离”并物理化
# 这一步会自动把软链接替换为真实的物理文件,解决 ../node_modules 找不到的问题
RUN pnpm --filter=angular deploy /out/angular && \
    pnpm --filter=vue deploy /out/vue

# 4.【构建阶段】进入独立目录进行打包
WORKDIR /out/angular
RUN pnpm run build

WORKDIR /out/vue
RUN pnpm run build

# 5.【汇总阶段】将各处产物收集到一起
RUN mkdir -p /app/dist && \
    cp -r /out/angular/dist/* /app/dist/app-ng/ && \
    cp -r /out/vue/dist/* /app/dist/

2. 致命缺点

  • 孤岛效应deploy 后的目录呈绝对隔离状态,不会自动携带根目录的非依赖文件(如 tsconfig.base.json.eslintrc),导致基于相对路径的配置继承失效
  • 工具缺失:若构建脚本依赖根目录的共享工具(如 rimraf),必须在每个子包的 devDependencies 中显式声明,否则隔离后的环境将无法识别指令

3. Hoisted 「补丁方案」

核心理念: 结构模拟,路径桥接

1. 依赖平铺模式

pnpm install --node-linker=hoisted
  • 强制 pnpm 放弃网格化链接结构,将所有依赖(含各子包依赖)平铺安装至根目录 /app/node_modules
  • 彻底消除 Docker 镜像构建中常见的软链接解析失效问题,使文件系统回归经典的物理路径可达状态

2. 重建路径链路

由于依赖被“提升”到了根目录,而子包(Packages)的代码物理位置仍在深层目录,因此需要通过两个关键补丁来修复逻辑断层。

  1. 目录映射 Symbolic Link
RUN ln -s /app/node_modules /app/packages/node_modules

在子包层级人为创造一个指向根目录依赖的入口。完美修复源码中通过相对路径(如 @import '../node_modules/') 引用依赖时的解析报错

  1. 执行上下文 PATH Injection
ENV PATH=/app/node_modules/.bin:$PATH

将根目录的 .bin 可执行脚本库注入系统全局变量。确保在任意子包目录下执行 ng buildvite 时,系统能跨目录精准定位指令

4. 技术选型对比

维度官方方案 (Deploy)补丁方案 (Hoisted)
设计取向子包独立化:将子包视为独立微服务仓库整体化:保持 Monorepo 完整上下文
配置共享困难:需手动 cp 补齐父级配置文件原生支持:天然支持相对路径配置继承
安全性高:严控幽灵依赖,运行环境纯净中:依赖平铺可能引入隐式依赖风险
适用场景子包可完全独立部署、无外部配置依赖强耦合、重度共享配置的 Monorepo 项目