Skip to content
签到签到

第 5 章:前端工程化、构建与架构

本章定位

本章从单仓库开发进入团队级工程体系,重点不是某个构建工具配置,而是理解包边界、依赖方向、source plane 和 artifact plane,以及插件式工程架构。

必须掌握清单

必须掌握

  • 能解释 monorepo 的包边界和依赖方向。

    相关答案: monorepo 只是统一管理多个包,真正的架构价值来自明确职责、公共 API 和单向依赖。应用层可以依赖领域与基础包,底层包不能反向导入具体应用,跨包访问应经过导出入口而不是内部路径。

    具体例子: apps/adminapps/web 都依赖 packages/runtime,runtime 只依赖 packages/types;如果 runtime 反向导入 admin 组件,就会形成循环和无法独立发布的耦合。

  • 能区分 source plane 与 artifact plane。

    相关答案: source plane 是开发工具直接读取 TypeScript 源码、测试和路径别名的视角;artifact plane 是消费者真正安装并加载编译产物、类型声明和 exports 的视角。源码测试通过不能证明发布包可用。

    具体例子: 单测通过 paths 找到 src/index.ts,但 npm 包的 exports 指向不存在的 dist/index.js,开发期正常、安装后却报错,因此发布前必须用打包产物做消费测试。

  • 能设计可复用的插件式扩展点。

    相关答案: 扩展点应定义稳定的上下文、注册契约、执行顺序、错误策略和卸载方式,让核心只依赖接口而不是具体插件。插件贡献应可枚举和撤销,避免通过修改全局对象形成隐式耦合。

    具体例子: 构建系统提供 registerPlugin(plugin): dispose,插件在 setup(context) 中注册转换器;禁用插件时调用 disposer,核心流水线无需增加业务插件专属的 if

  • 能定位构建缓存失效和依赖重复问题。

    相关答案: 构建缓存的命中依赖输入哈希,配置、环境变量、锁文件或未声明的生成文件都会改变结果;依赖重复常来自版本范围不一致、peerDependencies 配置错误或不同解析路径。定位时要比较依赖图和缓存输入。

    具体例子: 两个包分别安装不同 React 版本可能造成 hooks 使用两份实例;应从依赖树找到来源并统一版本。若 CI 每次重建,则检查任务是否把时间戳文件或无关环境变量纳入缓存键。

  • 能说明微前端和模块联邦各自的适用边界。

    相关答案: 微前端是按业务域拆分、独立开发和部署前端应用的架构目标;模块联邦是运行时加载远程模块和共享依赖的一种技术手段。团队与发布独立性不足时,引入它们通常只会增加治理成本。

    具体例子: 多个事业部需要独立发布各自后台模块时可采用微前端,模块联邦可动态加载页面;一个十人团队维护单一产品时,普通模块化 monorepo 往往更简单可靠。

原理讲解

source plane 是开发期从 TypeScript 源码解析的平面,artifact plane 是构建后从 lib 或 bundle 解析的平面。静态检查、类型推导和测试通常在 source plane 上运行,发布验证则要针对 artifact plane,避免源码可用但产物损坏。

monorepo 的关键不是把代码放在一起,而是明确包边界。公共类型、运行时核心和产品包应该分层,依赖方向只能向内,不能形成跨包循环。

插件式工程架构把扩展点定义为注册和 disposer。这样工具注册、构建插件和业务模块可以采用同一套生命周期,而不是在入口文件里堆叠分支。

代码示例或模板

json
{
  "scripts": {
    "typecheck": "tsc --noEmit",
    "build": "tsc -p tsconfig.build.json",
    "verify:artifact": "tsx scripts/verify-artifact.ts"
  }
}

这个模板说明 typecheck 和 build 属于不同检查面。发布前还要验证构建产物,而不是只验证源码。

AI Agent 概念对照

  • 插件化 Harness 对应工程化扩展机制。
  • Tool Registry 对应包注册。
  • Profile 和 bundle 对应可配置组合。
  • source/artifact plane 对应源码运行和发布运行。

面试追问

1. 什么时候不需要微前端?

查看深度解析

标准回答: 团队规模小、发布节奏一致、业务边界不稳定或一个应用即可满足权限与性能目标时,不需要微前端。它主要解决组织和独立交付问题,不是组件复用或代码量大的默认答案。

原理展开: 微前端引入运行时加载、路由、样式隔离、共享依赖、跨应用通信和故障治理成本,只有独立团队与独立部署收益足够大才值得承担。

工程示例: 十人团队维护单一后台,使用 monorepo 分包和模块边界即可;拆成五个远程应用反而让一次公共升级需要兼容五套运行时。

常见误区: 把“项目大”直接等同于微前端,或期待它自动解决低质量模块耦合。

继续追问: 将来可能拆分,今天如何留余地?

回答方向: 先按业务域划清路由、状态和公共 API,禁止跨域内部导入;用数据证明独立发布需求后再选择运行时方案。

2. 为什么 source 路径和构建产物路径不能混用?

查看深度解析

标准回答: source 路径面向开发期类型检查和源码工具,产物路径面向真实消费者。混用会绕过 package exports、编译转换和发布内容验证,出现本地可用但安装包缺文件、模块格式不匹配的问题。

原理展开: 两个平面的依赖解析、扩展名、条件导出和 side effects 可能不同,测试必须明确验证哪一个平面。

工程示例: workspace 测试通过别名导入 src/index.ts,发布包却把 exports 指到不存在的 dist/index.js;只有 pack 后安装测试才能发现。

常见误区: 为方便调试让消费者深度导入另一个包的 src,从而形成未声明公共 API。

继续追问: 如何同时保证开发体验和产物真实性?

回答方向: 开发测试走 source plane,发布前增加 build、pack、临时消费项目和 exports/types 检查,两类测试都保留。

3. 如何避免多个包复制同一份依赖?

查看深度解析

标准回答: 统一版本策略和锁文件,正确区分 dependencies 与 peerDependencies,并通过 workspace 约束、依赖树检查和 bundle 分析发现重复。运行时单例库应由宿主提供兼容版本,而不是每个插件私带一份。

原理展开: 包管理器去重只在版本范围和解析条件兼容时发生;打包器还可能把同一依赖分别内联进多个产物,因此安装树与 bundle 都要检查。

工程示例: React 组件库把 React 放 peerDependencies,应用提供唯一实例;工具包可正常声明小型纯函数依赖,由包管理器去重。

常见误区: 把所有依赖都 hoist 到根目录,导致包的 manifest 不再完整,发布后才发现缺依赖。

继续追问: 两个应用确实需要不同主版本怎么办?

回答方向: 允许隔离安装或独立运行时,不强行共享;明确跨边界只传可序列化数据,评估体积与兼容成本。

4. 构建缓存失效时,如何定位是配置、版本还是产物问题?

查看深度解析

标准回答: 先记录任务输入哈希和缓存键,比较命中与未命中的环境、配置、锁文件和源码差异;再判断是输入声明变化、工具版本变化,还是缓存产物缺失或不可复现。不要先删缓存掩盖证据。

原理展开: 正确缓存要求输入完整、输出确定和工具链一致。未声明环境变量会导致错误命中,时间戳和绝对路径会导致无效失效。

工程示例: CI 每次重建时导出 task graph,发现构建信息写入当前时间并被纳入输出哈希;移除非确定字段后复测冷热构建。

常见误区: 只看 package 版本,不看 lockfile、Node 版本、平台和生成器配置;或把远端缓存下载失败误判成哈希变化。

继续追问: 如何验证缓存产物可靠?

回答方向: 同一提交分别做冷构建和缓存恢复,比较产物哈希并运行消费测试;对平台相关产物把平台纳入缓存键。

实践任务

设计一个最小 monorepo 的包边界,包含 shared types、运行时核心和产品前端,并写明依赖方向、构建顺序和发布验证。

验收标准

  • 包之间没有循环依赖。
  • 源码类型检查和产物验证分别可执行。
  • 能解释为什么两者不能混用。

关联材料