news 2026/10/3 17:03:27

Monorepo 工程化面试题:如何设计 Web + Node.js 中间层 + Shared + Scripts 的 Monorepo?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Monorepo 工程化面试题:如何设计 Web + Node.js 中间层 + Shared + Scripts 的 Monorepo?

一、面试题:为什么要用 Monorepo 管理 Web、Node.js 中间层、Shared 和 Scripts?

核心思路(一句话)

Monorepo 的核心不是“代码放一个仓库”,而是把多个应用和共享包放进同一个依赖图,通过统一依赖管理、类型共享、构建编排、影响分析和缓存实现全站工程化。

解决方案架构图

Monorepo │ ┌──────────────┼──────────────┐ │ │ │ apps packages tools │ │ │ ┌────┴────┐ ┌────┴─────┐ │ │ │ │ │ │ web server shared config scripts │ │ │ │ │ ├── types │ │ ├── utils │ │ └── schemas │ │ │ └──────────────┐ │ │ └──────────────┬─────────┘ │ pnpm workspace │ ┌─────────┴─────────┐ │ │ 类型共享 依赖图 │ │ TypeScript affected analysis │ 增量构建 + 缓存

典型目录:

repo/ ├── apps/ │ ├── web/ │ │ └── package.json │ └── server/ │ └── package.json │ ├── packages/ │ ├── shared/ │ │ ├── src/ │ │ └── package.json │ │ │ ├── browser/ │ │ └── package.json │ │ │ ├── node/ │ │ └── package.json │ │ │ └── config/ │ └── package.json │ ├── scripts/ │ └── ... │ ├── pnpm-workspace.yaml ├── package.json └── turbo.json

二、面试题:Monorepo 和“单仓库多目录”有什么区别?

核心思路(一句话)

真正的 Monorepo 是“一个仓库 + 多个独立 Package + 明确依赖图 + 统一工程能力”,不是简单把多个项目放在一个 Git 仓库。

结构化理解

层次普通单仓库Monorepo
Git一个仓库一个仓库
项目多目录多个独立 Package
依赖容易混乱Package 间显式依赖
类型共享手工复制/路径引用Workspace Package
构建通常全量根据依赖图增量构建
缓存项目自己处理Turborepo/Nx 等统一处理
影响分析人工判断根据依赖图计算
CI容易全部执行affected + cache

主要矛盾

不是: “代码有没有放到一个仓库?” 而是: “多个项目之间的依赖关系能不能被机器准确理解和调度?”

这才是 Monorepo 工程化的核心。


三、面试题:pnpm Workspace 在 Monorepo 中解决什么问题?

核心思路(一句话)

pnpm Workspace 解决的是“多个 Package 如何统一管理依赖以及相互引用”的问题,而不是负责完整的增量构建系统。

流程图

pnpm-workspace.yaml ↓ 识别 workspace packages ↓ 每个目录拥有独立 package.json ↓ package A │ │ workspace: ↓ package B ↓ pnpm 建立 workspace 依赖关系 ↓ 形成项目依赖图

例如:

# pnpm-workspace.yamlpackages:-"apps/*"-"packages/*"

Web:

{"name":"@company/web","dependencies":{"@company/shared":"workspace:*"}}

Server:

{"name":"@company/server","dependencies":{"@company/shared":"workspace:*"}}

注意

workspace:*的作用主要是:

告诉包管理器: 这个依赖必须来自当前 Workspace 中的 Package。

它不是:

自动解决 ESM/CommonJS 自动进行 TypeScript 编译 自动进行浏览器兼容 自动完成增量构建

这些属于其他层次。


四、面试题:Web 和 Node.js 共享一个 Package,为什么容易出现 ESM/CommonJS 和 Node 原生模块问题?

核心思路(一句话)

真正的问题不是“共享代码”,而是同一个 Package 被两个运行环境消费:浏览器和 Node.js 对模块格式、运行时 API、构建方式的要求不同。

问题模型

@company/shared │ ┌────────┴────────┐ │ │ Browser Node.js │ │ ESM / Browser ESM / CJS │ │ 无 fs / path 等 可以使用 fs/path

假设:

// shared/src/index.tsimportfsfrom"node:fs";exportfunctionreadConfig(){returnfs.readFileSync("./config.json","utf-8");}

然后:

// web/src/index.tsimport{readConfig}from"@company/shared";

问题:

Web ↓ shared ↓ fs ↓ Node.js 原生模块 ↓ 浏览器无法运行

关键认知

TypeScript 类型共享 ≠ 运行时代码共享。

这是这类题非常重要的区分。


五、面试题:如何设计一个既能给浏览器又能给 Node.js 使用的 Shared Package?

核心思路(一句话)

优先从架构上隔离运行时能力,再通过exports条件导出让不同环境获得正确入口;不要单纯依赖构建工具“兜底”。

推荐:

packages/ ├── shared/ │ ├── types/ │ ├── schemas/ │ └── pure-utils/ │ ├── browser/ │ └── browser-only-utils/ │ └── node/ └── node-only-utils/

架构:

Shared │ ┌────────────┼────────────┐ │ │ │ types pure-utils schemas │ │ │ └────── Browser + Node ───┘ Node-only │ fs / path / process Browser-only │ window / document

这比:

一个 shared 包里面什么都放 ↓ 靠 Webpack/Vite 排除 fs

更加稳定。


六、面试题:什么是package.json的exports条件导出?

核心思路(一句话)

exports可以根据消费者的解析条件,把同一个 Package 的不同入口暴露给不同运行环境。

例如:

{"name":"@company/utils","exports":{".":{"browser":"./dist/browser.js","node":"./dist/node.js","import":"./dist/index.mjs","require":"./dist/index.cjs","types":"./dist/index.d.ts","default":"./dist/index.mjs"}}}

逻辑:

消费者 ↓ 解析 @company/utils ↓ exports ↓ 判断条件 │ ├── browser → browser.js │ ├── node → node.js │ ├── import → index.mjs │ ├── require → index.cjs │ └── types → index.d.ts

但这里有一个非常重要的面试细节

exports条件由具体解析器/工具决定,不能简单理解成“浏览器自动选择 browser”。

例如:

Node.js Webpack Vite TypeScript Jest

它们的解析条件可能不同。因此面试时最好说:

exports提供条件导出能力,最终选择哪个条件由 Node.js、打包器、TypeScript 等消费者的模块解析规则决定。

这比简单说“浏览器选择 browser,Node 选择 node”准确得多。


七、面试题:如果工具函数同时被前端和 Node.js 使用,应该怎么设计?

核心思路(一句话)

把“纯逻辑”和“环境能力”分离:纯逻辑共享,Node/Browser API 分层,再通过入口或条件导出连接。

推荐:

@company/utils │ ├── pure │ ├── formatDate │ ├── debounce │ └── validate │ ├── browser │ └── storage │ └── node ├── file └── process

使用:

import{formatDate}from"@company/utils";

Node:

import{readJson}from"@company/utils/node";

Browser:

import{getStorage}from"@company/utils/browser";

这样依赖边界非常清楚。


八、面试题:如何防止 Node.js 的fs被打进浏览器 Bundle?

核心思路(一句话)

第一优先级是让浏览器依赖图根本不要触达fs;external或排除配置只是兜底,不是架构解决方案。

正确优先级

第一层:架构隔离 ↓ Browser 不依赖 Node-only Package ↓ 第二层:exports 条件导出 ↓ Browser 解析 browser 入口 ↓ 第三层:构建工具 external / alias / exclude ↓ 防止错误依赖继续进入 Bundle

例如 Vite/Rollup 场景:

// vite.config.tsimport{defineConfig}from"vite";exportdefaultdefineConfig({build:{rollupOptions:{// 这里只适用于明确知道某依赖不会由浏览器运行的情况。// 它不是解决 Node API 误进入浏览器依赖图的首选方案。external:["node:fs","node:path"]}}});

更好的方式

不要:

web ↓ shared ↓ node-utils ↓ fs

而应该:

web ↓ shared-browser ↓ 纯浏览器代码

以及:

server ↓ shared-node ↓ node-utils ↓ fs

九、面试题:Shared Package 为什么需要同时生成 ESM、CommonJS 和 TypeScript 类型声明?

核心思路(一句话)

ESM/CommonJS解决运行时模块兼容,.d.ts解决TypeScript类型消费,它们解决的是三个不同问题。

典型产物:

dist/ ├── index.mjs ├── index.cjs └── index.d.ts

对应:

ESM消费者 ↓ index.mjs CommonJS消费者 ↓ index.cjs TypeScript ↓ index.d.ts

package.json

{"name":"@company/shared","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.cjs"}}}

但要注意

如果整个项目:

Node.js ↓ 全部使用 ESM

那么没有必要为了“看起来完整”强行生成 CommonJS。

是否需要双格式,要看:

消费者是谁? ↓ Node.js版本? ↓ 构建工具? ↓ 是否存在CommonJS消费者?

十、面试题:如何实现一个完整的 Shared Package?

下面给一个比较适合面试讲解的最小完整方案。

目录

packages/shared/ ├── src/ │ ├── index.ts │ └── types.ts ├── tsconfig.json ├── package.json └── tsup.config.ts

src/types.ts

// 这个文件只保存类型定义。// 类型在 TypeScript 编译后不会产生 JavaScript 运行时代码,// 因此非常适合被 Web 和 Node.js 双方共享。exportinterfaceUser{id:string;name:string;}

src/index.ts

// export type 明确表示这是纯类型导出。// TypeScript 编译后不会把 User 作为运行时代码打进 Bundle。exporttype{User};// 这是纯 JavaScript 逻辑。// 没有使用 fs、path、process、window、document 等环境相关 API。// 因此浏览器和 Node.js 都可以安全使用。exportfunctionformatUser(user:User):string{return`${user.id}:${user.name}`;}

tsup.config.ts

import{defineConfig}from"tsup";exportdefaultdefineConfig({// 构建入口entry:["src/index.ts"],// 同时生成 ESM 和 CommonJS。// 实际项目是否需要两种格式,需要根据消费者决定。format:["esm","cjs"],// 生成 TypeScript 类型声明文件。dts:true,// 生产环境压缩。minify:true,// 清理旧的 dist。clean:true,// source map 方便调试。sourcemap:true});

package.json

{"name":"@company/shared","version":"1.0.0","type":"module","main":"./dist/index.js","module":"./dist/index.mjs","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.cjs"}},"scripts":{"build":"tsup"},"devDependencies":{"tsup":"^8.0.0","typescript":"^5.0.0"}}

实际项目中应根据具体构建工具生成的文件名校正exports路径,不能机械照抄。


十一、面试题:为什么“给 Shared 包配置 ESM + CommonJS 两套产物”仍然可能解决不了问题?

核心思路(一句话)

模块格式兼容和运行时环境兼容是两个问题。

例如:

shared ↓ index.mjs ↓ import fs from "node:fs"

即使:

ESM ✔ CommonJS ✔

浏览器仍然不能使用:

node:fs

所以:

模块格式 + 运行时环境

必须分别解决。

面试追问

“你生成了 ESM 和 CommonJS,是不是就能同时支持浏览器和 Node?”

正确回答:

不一定。ESM/CommonJS解决的是模块加载格式,而浏览器和Node.js的运行时能力不同。比如fs属于Node.js运行时能力,即使把代码编译成ESM,浏览器仍然无法运行。因此我会优先拆分browser/node入口,再结合exports条件导出,最后才使用构建工具的external等配置作为兜底。


十二、面试题:Monorepo 中如何实现 CI 增量构建?

核心思路(一句话)

先通过 Git Diff 找到变化,再沿 Monorepo 依赖图向上计算受影响的 Package,最后只执行受影响任务,并结合缓存跳过已经完成的任务。

完整流程图

Git Push ↓ git diff ↓ 找到变化文件 ↓ 映射到 Package ↓ 计算依赖图 ↓ 找到受影响 Package ↓ ┌─────────────────────┐ │ affected packages │ │ │ │ web │ │ server │ │ shared │ └─────────────────────┘ ↓ 依赖拓扑排序 ↓ 检查任务缓存 ┌────┴────┐ ↓ ↓ Cache Hit Cache Miss ↓ ↓ 复用结果 执行构建 └────┬────┘ ↓ 测试 ↓ 部署

十三、面试题:如果shared修改了,如何知道 Web 和 Server 都需要重新构建?

核心思路(一句话)

关键不是“看谁改了”,而是沿依赖图反向查找“谁依赖了它”。

假设:

web ──────┐ ↓ shared ↑ server ───┘

修改:

shared

影响:

shared ↓ web server

所以:

shared changed ↓ affected(shared) ↓ shared + web + server

十四、面试题:为什么不能只根据git diff判断构建哪些项目?

核心思路(一句话)

Git Diff只能告诉你“文件发生了变化”,依赖图才能告诉你“变化会影响谁”。

例如:

git diff ↓ packages/shared/src/types.ts

不能简单得到:

只构建 shared

因为:

web ───→ shared server ─→ shared

真正需要:

shared ↓ 反向依赖 ↓ web + server

因此:

Git Diff + Workspace Package Graph = Affected Analysis

十五、面试题:Turborepo / Nx 在这里解决什么问题?

核心思路(一句话)

pnpm负责Workspace依赖管理,Turborepo/Nx负责任务编排、依赖拓扑、Affected分析和缓存;三者职责不同。

工具职责

Monorepo │ ┌───────────┼────────────┐ ↓ ↓ ↓ pnpm Turborepo Nx │ │ │ 依赖管理 任务编排 任务编排 workspace 依赖执行 影响分析 安装依赖 并行执行 缓存 本地缓存 远程缓存

一个常见误区

不要说:

“pnpm负责Monorepo所有事情。”

更准确:

pnpm负责Workspace和依赖管理;Turborepo或Nx负责基于任务依赖图的构建编排、缓存和增量执行。


十六、面试题:什么是任务缓存?为什么能够把 CI 时间显著降低?

核心思路(一句话)

任务缓存的核心是:输入没有变化,输出就可以复用,不必重复执行构建。

流程

Package ↓ 源代码 + 配置 + 依赖 + 环境变量 ↓ 计算任务 Hash ↓ ┌─────────────┐ │ Cache Store │ └──────┬──────┘ │ ┌─────┴─────┐ ↓ ↓ Hash存在 Hash不存在 ↓ ↓ 恢复产物 执行任务 ↓ ↓ Cache Hit Cache Miss

例如:

shared build Hash = ABC123 第一次: ABC123不存在 → build → 保存dist 第二次: ABC123存在 → 直接恢复dist → 不再build

远程缓存

Developer A ↓ Build ↓ Remote Cache ↑ │ CI ↓ 发现相同 Hash ↓ 直接恢复构建结果

这就是为什么团队规模变大以后,远程缓存价值非常高。


十七、面试题:如果修改的是 Shared 类型,影响分析应该怎么做?

这是这道题最值得深入回答的地方。

核心思路(一句话)

类型变化虽然可能不产生运行时代码,但它可能改变消费者的类型检查结果,因此不能简单认为“只改类型就不需要构建”。

例如:

// sharedexportinterfaceUser{id:string;}

修改:

exportinterfaceUser{id:number;}

依赖:

shared ├── web └── server

那么:

shared type changed ↓ web type-check server type-check

至少应该触发:

typecheck lint test

至于:

production build

是否一定执行,要看你的 CI 策略和任务依赖。

更成熟的任务模型

shared:typecheck ↓ web:typecheck server:typecheck shared:build ↓ web:build server:build

可以把:

build test lint typecheck

设计成不同任务,而不是所有变化都执行完整 Pipeline。


十八、面试题:如何处理“类型共享”和“运行时代码共享”?

核心思路(一句话)

类型共享和运行时共享应该分离设计,这是 Monorepo 中非常重要的架构边界。

推荐:

packages/ │ ├── types/ │ └── 纯TypeScript类型 │ ├── schemas/ │ └── 运行时Schema │ ├── utils/ │ └── 纯JS逻辑 │ ├── browser/ │ └── 浏览器API │ └── node/ └── Node.js API

为什么 Schema 很重要?

例如前后端共享:

interfaceCreateUserRequest{name:string;}

这个类型:

TypeScript编译后不存在

运行时无法验证:

HTTP 请求到底是不是合法数据?

可以使用:

Zod

设计:

shared schema │ ┌──────────┴──────────┐ ↓ ↓ Browser Node.js │ │ 类型推导 请求校验 │ │ └──────────┬──────────┘ ↓ 同一份协议定义

这比只共享.d.ts更完整。


十九、面试题:如果让你设计一个 AI 前端 + Node.js BFF 的 Monorepo,你会怎么设计?

核心思路(一句话)

按照“应用层—共享协议层—运行时能力层—工程工具层”分层,保证浏览器和 Node.js 的依赖边界清晰。

推荐架构

repo │ ├── apps │ ├── ai-web │ │ └── 浏览器应用 │ │ │ └── ai-server │ └── Node.js BFF │ ├── packages │ │ │ ├── contracts │ │ ├── types │ │ └── schemas │ │ │ ├── utils │ │ └── 纯逻辑 │ │ │ ├── browser │ │ └── 浏览器能力 │ │ │ ├── node │ │ └── Node.js能力 │ │ │ └── config │ └── ESLint / TypeScript等配置 │ └── scripts └── 构建、部署、代码生成

依赖方向:

contracts ↙ ↘ ai-web ai-server ↓ ↓ browser node \ / utils

严格避免:

ai-web ↓ ai-server ↓ node/fs

以及:

contracts ↓ fs / path

因为contracts应该尽可能成为跨运行环境的最底层共享协议层。


二十、这道题真正考什么?

主要矛盾

不是会不会写pnpm-workspace.yaml,而是能不能建立“多 Package + 多运行环境 + 依赖图 + 增量构建”的完整工程模型。

次要矛盾

① ESM / CommonJS ② exports 条件导出 ③ TypeScript 类型声明 ④ Browser / Node Runtime 隔离 ⑤ affected analysis ⑥ 本地/远程缓存 ⑦ CI 任务编排

把它们串起来:

Monorepo │ ┌─────────┴─────────┐ ↓ ↓ Package Runtime 依赖图 环境边界 │ │ ↓ ↓ affected analysis exports │ │ ↓ ↓ 增量构建/测试 ESM/CommonJS │ │ └─────────┬─────────┘ ↓ Cache ↓ CI

二十一、面试官继续追问:如果你只能选择一个方案,你怎么回答?

不要直接开始讲工具。

应该按照:

问题 ↓ 原因 ↓ 架构 ↓ 工具 ↓ 异常场景 ↓ 优化

回答。

例如:

我不会把 Monorepo 简单理解成一个 Git 仓库。这个场景真正的问题是 Web 和 Node.js 处于不同运行环境,共享 Package 同时涉及模块格式、运行时依赖和类型共享,所以我首先会划清依赖边界。

Web 和 Node.js 都依赖纯类型、Schema 和纯工具函数;Node-only 的fs、path等能力单独放到 Node Package,Browser-only 能力单独放到 Browser Package。对于确实需要同时支持不同模块消费者的 Package,再通过exports提供import、require、types等条件入口。

Package 管理使用 pnpm Workspace,任务编排使用 Turborepo 或 Nx。CI 不根据 Git Diff 简单决定构建范围,而是先定位变化 Package,再沿依赖图计算 affected Package,最后结合任务缓存,只执行受影响且没有缓存的任务。

所以这套方案解决的其实是四个问题:运行环境隔离、模块格式兼容、依赖影响分析、增量构建缓存。

这个回答已经覆盖了整道题的核心。


二十二、最终「满分答案」——能背下来 + 能展开讲 + 能应对追问

面试题

如何使用 Monorepo 管理 AI 前端、Node.js 中间层、共享类型和工具脚本?

核心思路(一句话)

Monorepo 的核心不是“一仓多项目”,而是利用 Package 依赖图解决共享、运行时隔离、条件导出、影响分析和增量构建。

架构图

Monorepo │ ┌───────────────────┼───────────────────┐ ↓ ↓ ↓ apps packages scripts │ │ ┌────┴────┐ ┌──────┼────────┐ ↓ ↓ ↓ ↓ ↓ web server types utils node/browser │ │ │ │ └────┬────┴───────┴──────┘ ↓ Workspace ↓ Package Graph ↓ Git Diff → Affected Analysis ↓ 依赖拓扑排序 ↓ Cache Hit / Miss ↙ ↘ 复用结果 执行任务

第一层:Package 划分

apps/web apps/server packages/types packages/utils packages/browser packages/node packages/config scripts/

不要让一个shared包同时包含浏览器代码和 Node.js 的fs、path。


第二层:模块和运行时兼容

exports │ ├── types → .d.ts ├── import → ESM ├── require → CommonJS ├── browser → Browser入口 └── node → Node入口

ESM/CommonJS解决模块格式;Browser/Node入口解决运行时环境。两者不能混为一谈。


第三层:依赖管理

pnpm Workspace ↓ workspace:* ↓ Package依赖图

pnpm负责:

Workspace 依赖安装 Package引用

Turborepo/Nx负责:

任务依赖 Affected 并行执行 缓存

第四层:CI 增量构建

Git Diff ↓ 变化 Package ↓ 依赖图反向追踪 ↓ Affected Package ↓ typecheck / test / build ↓ 检查缓存 ↓ Cache Hit → 直接复用 Cache Miss → 执行并缓存

例如:

shared 修改 ↓ shared ├──→ web └──→ server ↓ shared + web + server

最后一针见血

普通开发者回答的是“pnpm Workspace 怎么配”;高级前端回答的是“不同运行环境如何隔离”;真正的工程化回答是“如何建立 Package 依赖图,并基于依赖图完成条件导出、影响分析、增量构建和缓存”。

这道题真正考的不是 Monorepo API,而是你能不能把“代码组织 → 模块解析 → 运行时边界 → 依赖图 → CI 增量构建”串成一套完整工程体系。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 17:02:23

实测体验|为什么室内漏水检测师傅都愿意用富探F21

从事漏水检测行业多年,用过不少国内外各类漏水听漏仪器,接触很多同行师傅交流发现,大家挑选测漏设备,最看重三点:定位准、机器抗造耐用、售后不折腾。市面上仪器五花八门,价格差距巨大,很多新手…

作者头像 李华
网站建设 2026/10/3 17:01:46

2026年江西省职业院校技能大赛(中职组)人工智能应用技术样题

2026年江西省职业院校技能大赛(中职组)人工智能应用技术样题 文章目录2026年江西省职业院校技能大赛(中职组)人工智能应用技术样题模块一:人工智能环境搭建任务 1:人工智能环境搭建任务 2: 人工智能网络通信…

作者头像 李华
网站建设 2026/10/3 17:01:46

用 OpenWorkBuddy 无限画布制作 AI 短剧:从剧本、分镜到视频的工作流拆解

用 OpenWorkBuddy 无限画布制作 AI 短剧:从剧本、分镜到视频的工作流拆解 摘要**:AI 短剧制作常见的问题是素材分散、镜头关系不清、修改一个画面却要重跑整条长提示词。OpenWorkBuddy 将剧本、角色、场景、分镜、参考图、视频与音频放到无限画布中&…

作者头像 李华
网站建设 2026/10/3 16:57:14

深夜emo必备这5款匿名树洞,让你敢把所有秘密放心说出口!

成年人的世界,好像永远在硬撑。白天假装情绪稳定、懂事大方,把委屈和心事全部藏在心底;只有到了深夜,卸下所有伪装,积攒已久的内耗、遗憾、委屈、无人诉说的心事才会彻底翻涌。很多情绪,不想发给朋友、不想…

作者头像 李华
网站建设 2026/10/3 16:56:06

Linux运维:MariaDB知识点总结

MariaDB知识点总结MariaDB是MySQL的分支,CentOS7默认数据库,端口3306,核心包含进程、安装加固、配置文件、库表SQL操作、用户权限、密码故障处理、备份恢复、主从复制。1. 双进程模型 mysqld_safe:守护监控进程:启动、…

作者头像 李华