第 21 章 构建与制品
副题:复现——同一份源码,任何时候构建出同样的制品
第 20 章的发布事故(互相踩)之后,团队开始建发布流程——第一步,是构建。代码在"开发机"上能跑,不等于在"生产环境"能部署:本地跑得好好的(第 15 章联调的教训),到了服务器上为什么起不来?本章回答:开发环境到生产环境的转换——源码怎么变成"可部署的制品",以及为什么"构建一次、处处运行"是目标。
第五部分地图(第一站):21 构建(本章)→ 22 CI/CD → 23 部署——发布三段式:构建管"做出制品",CI 管"自动验证",部署管"放上环境"——三个问题,三章,一条从代码到用户的链路(第 20 章说"发布流程是第五部分的主场",现在开场)。
本章要建立的认知
- "在我机器上是好的"是复现性破产的症状——构建的目标:同一份源码,任何时候构建出同样的制品;
- 构建把源码变成不可变制品:制品是部署的单位、回滚的单位——"构建一次、处处运行";
- 依赖锁定(lockfile)是复现的基石:依赖不锁,构建就是碰运气。
21.1 在我机器上是好的
阶段 4 的发布流程建设,从一次"在我机器上是好的"开始。
新同事小李部署测试环境,服务起不来——报错信息他没见过。排查半天,第一句话:"我本地跑得好好的。"这句话小明在联调时听过(第 15 章),现在在部署时又听到了——"在我机器上是好的"是环境问题的标准开场白。这次的原因有三个,正好是构建要解决的三个问题:
排查的过程也走一遍(它是"环境问题定位"的实景):小李把报错贴给小明,小明第一反应不是看报错,是问三个问题——本地能跑吗?(能)测试环境 Node 版本多少?(14)本地呢?(18)——三个问题定位到第一个原因;修好后前端白屏(第二个原因);再修好后功能又不对(第三个原因,依赖版本差异导致的行为不同)——环境问题的排查顺序:先比对环境(版本/依赖/配置),再读代码(第 15 章说过的"环境问题不在旅程里,在环境分层里"——这里是执行);"在我机器上是好的"的心理学也补一笔(它为什么总在深夜出现):开发者对"我的机器"有完全的控制,对"服务器"没有——本地能跑是"我环境里的事实",线上不能跑是"另一个环境的事实"——两个事实之间的桥,就是构建。
构建体系的收益清单(它是"为什么要建"的答案,也是 21.8 对应表的底稿):可复现(同一源码任何时候构建出同样的制品——"在我机器上"不再重要);可回滚(制品有版本,部署能指回——第 18 章"先回滚"的制品侧);可审计(谁构建的、从哪个提交、何时——第 20 章账本思维在交付侧的延伸)——三笔收益,对应三个"没有构建"的痛点(环境事故/无法回滚/部署不可查)。
原因一:依赖版本不一样。本地是 Node 18,测试环境是 Node 14——第 15 章说过的"依赖版本漂移"("版本漂移是环境问题里最隐蔽的:它不报错、不提示,只是行为不一样"),这次它真的不报错——服务起不来报的错和版本无关,是某个 API 在 Node 14 里行为不同。修好之后,小明问:"下次怎么保证两边版本一样?"——答案是依赖锁定(21.5)。
原因二:前端没构建。测试环境直接放了前端源码——浏览器打开页面,白屏(第 8 章说过的白屏)。原因:源码不是浏览器能跑的东西——JSX(第 9 章 React 的语法)、TypeScript 类型、ES 新语法,浏览器都不认识;源码要经过构建变成浏览器能跑的产物(21.3)。
**原因三:依赖没锁。**同一个项目,小李本地的 node_modules(依赖目录)和小明本地的版本不一样——因为没有 lockfile(第 15 章说过的"锁版本"——"环境一致性的第二道保险"),npm install 每次都装"当时最新的"——两个人装了两次,装出两份不同的依赖。现在它成了第一道(21.5)。
三个原因,一个共同点:开发环境和生产环境的"转换"没有机制——本地怎么跑的,生产不知道。构建就是把这个"转换"变成机制:让"能跑"从一个环境的现象,变成一个可复制的产物。
三个原因的"伤害排序"也补一笔(它是"先解决哪个"的依据):依赖漂移(原因一)最隐蔽——不报错只行为不同(第 15 章原话),线上出了怪问题查半天;前端没构建(原因二)最直接——白屏一眼看到,但"为什么白屏"对新手不显然(第 8 章的白屏是性能、这里是没构建——两种白屏,第 15 章说过判别法);依赖没锁(原因三)最持久——今天修好了,明天新人再来一遍——三个原因对应三个修复:锁版本、建构建、进流程(21.5/21.2/21.7)。
构建体系的演进也交代一句(它是第 2 章循环的又一轮):手搓部署(阶段 4 起点:第 20 章说过"部署靠谁改完谁部署"——构建也是手搓的:本地跑一遍打包命令,把产物传上去)→ 构建脚本(把打包命令写成脚本,少敲几行)→ 构建体系(本章:依赖锁定+制品仓库+版本管理)→ 第 22 章的自动化——每一步都是"原方案无法承受"逼出来的:手搓在 8 人团队时崩了(每个人搓得不一样),脚本在"要回滚"时崩了(没有版本怎么回)——构建体系的每一步,都是第 2 章演进循环的实例。
21.2 构建:从源码到制品
构建(build)的定义:把源码(人写的、需要加工的东西)变成制品(机器能直接跑的东西)的过程。源码是"给人看的",制品是"给机器跑的"——中间隔着编译、打包、转换(第 8 章说过"网页是三层分工的产物",构建是这三层的"生产工序")。
"构建一次、处处运行"(构建的目标):同一个制品,在任何环境(测试/生产)跑出同样的行为——环境差异(依赖/版本/配置)在构建时被"锁死"进制品,运行时不再有"我的机器"和"你的机器"的差别——"在我机器上是好的"这句话,在构建体系建立后应该绝迹:本地跑的是源码,测试/生产跑的是同一个制品,两边环境不同但跑的东西一样。
"构建"的类比也立一个(它让"源码 vs 制品"的直觉更准):菜谱 vs 做好的菜——源码是菜谱(人读的、可以讨论可以改),制品是做好的菜(直接上桌、不能回锅改),构建是烹饪(同一个菜谱,同一个厨房,做出来的菜应该一样——烹饪环境不一样,菜就不一样,这正是"锁构建环境"的原因,21.5)——"构建一次、处处运行"= 同一道菜端上所有桌子。
构建与第 12 章分层的类比(它是"代码是概念的证据"在构建侧的延伸):第 12 章把代码按职责分层(表现/业务/数据),构建把交付按阶段分层(源码/制品/部署)——同一份源码可以产出多个制品(前端制品+后端制品),同一个制品可以部署到多个环境——"层"的思想从代码内部延伸到交付链路(第 1 章地图的"工程线"在这一站有了具体的形态)。
构建的三个验收标准(它把"构建好不好"变成可判断的):可复现(同一源码+同一锁+同一环境 → 同一制品,21.5/21.6 的幂等性);可回滚(每个制品有版本,随时指回——21.6);可追溯(生产跑的制品 ↔ 源码的哪个提交——源码到制品的链上每一步有记录,第 24 章"谁部署了什么"在这里有答案)——三个标准合起来:构建体系是"源码到生产"的可信通道(每一步都可验证、可回退、可追溯)。
制品的两个性质(它们决定构建体系怎么设计):
- 不可变性:制品一旦构建出来,不能改——要改就重新构建一个新版本——制品是"只读的快照":改制品 = 改源码再构建(第 16 章说过的"版本号解决更新",制品的版本号就是它的身份证);
- 可回滚性:因为制品不可变,部署可以随时回到上一个制品(第 18 章事故处置的"先回滚"——回滚的单位是制品:
回到版本 N-1而不是"改几行代码再上线")——制品是部署的单位、回滚的单位(部署会看到回滚怎么执行)。
制品与源码的"等价关系"(它是"可追溯"的数学基础,一句):制品 = 源码 + 锁 + 环境的确定性函数——同一输入,同一输出(21.6 的幂等性)——所以"生产跑的制品"可以反推出"它来自哪个源码提交"(元数据记录着)——制品是源码在交付链上的"投影":源码是真相,制品是投影,投影可验证(第 21.6 的元数据就是验证手段)。
前端和后端的构建,形态不同(它们要解决的环境差异不同,21.3/21.4 分别讲):前端构建把源码变成静态文件(浏览器直接下载运行);后端构建把源码变成可执行制品(服务器直接启动运行)——同一个"构建"词,两种产物形态。
21.3 前端构建:打包的艺术
前端构建要解决的问题:源码(JSX/TS/新语法)→ 浏览器能跑的静态文件。它把"源码 → 制品"拆成几步:
源码(JSX/TS/多个文件)
→ ① 依赖解析(import 关系图:这个文件用了哪些模块)
→ ② 编译(JSX → JS、TS → JS、新语法 → 兼容语法)
→ ③ 打包(合并成一个/几个文件,减少浏览器请求数——第 10 章的下载段优化)
→ ④ 产物(静态文件:app.a1b2c3.js、style.css…)
打包是第 8/16 章的构建侧兑现(它们讲的是"优化",这里讲的是"在构建时怎么做到"):
- 第 8 章的 CSS 拆分(2MB CSS → 首屏内联+按需拆分):拆分发生在构建时——构建工具按页面拆分 CSS,首屏的内联进 HTML(第 16 章说过的"首屏 CSS 内联"),其余按需加载——第 8 章的优化手段,是构建工具做的事;
- 第 16 章的版本号哈希(
app.a1b2c3.js,长 TTL+版本号):哈希(内容指纹)是构建时自动生成的——文件内容变了,哈希变,文件名变,浏览器/CDN 把它当新资源(第 16 章说"URL 就是缓存键"——构建决定了 URL 长什么样)——第 16 章说"版本号放 URL 里"是缓存策略,构建就是那个"放"的动作。
代码分割(打包的进阶,第 10 章"下载慢是内容大"的构建侧解):把应用拆成"首屏需要的"和"用到了才加载的"——路由懒加载(第 9 章前端框架的路由:访问 /orders 才下载订单页的代码)——"少下载"在构建侧的手段:不是把代码变小(那叫压缩),是把"不急着用的代码"从首屏包里去掉(第 8 章说"下载段的优化一半是 CDN 一半是少下载"——少下载的构建侧就是代码分割)。
压缩与 tree-shaking(两个词,一句定位):压缩(去掉空白/缩短变量名——体积小 30-50%);tree-shaking("摇树"——把没被用到的代码从包里摇掉——没人再调的路径就是代码里的"死代码",tree-shaking 就是构建时的死代码清理)——打包体积 = 代码量的压缩结果,两个机制让"下载段"从源码体积到实际体积缩小一大截。
sourcemap(开发体验的桥,一句):构建后代码被压缩混淆了,报错行号对着压缩后的文件没法看——sourcemap(源码与产物的映射文件)让浏览器报错时显示源码的行号——"调试产物"和"看源码"之间的桥(上线后的报错排障,第 25 章看错误日志时它有用)。
产物大小的预算(它是"下载段"验收的构建侧,第 16 章的衔接):首屏包体积要有预算(第 15 章摊预算时"下载约 1.5 秒"——构建产物大小直接决定下载时间:1MB JS ≈ 首屏多 1-2 秒,量级示意)——构建时看产物大小报告(构建工具会输出每个包的大小),超预算的包要拆(代码分割/按需加载)——第 16 章的预算思维在构建侧的执行:构建时量体积,和运行时量延迟一样,都是"拿预算逐段量"(第 16 章说过的)。
前端构建的产物形态:一个静态文件目录(HTML/CSS/JS/图片),可以原样放到任何静态服务器/CDN(第 11 章)——前端"构建一次"的产物,和运行环境无关(任何服务器都能托管静态文件),这也是为什么前端构建是"处处运行"最容易实现的。
产物的目录结构(它透露构建的"设计",一句):index.html(入口,引用带哈希的 JS/CSS)+ assets/(哈希命名的文件)+ 按需拆分的 chunk——看产物目录就能看出构建配置(有没有代码分割/有没有哈希/有没有内联)——产物是构建的"体检报告"(构建得好不好,看产物就知道:哈希文件名=版本策略在、chunk 文件=代码分割在、index.html 大小=内联策略在)。
产物与 CDN 的关系(第 11/16 章衔接):前端构建的产物,直接发布到 CDN(第 16 章说"静态资源走 CDN 短路"——产物就是那份被 CDN 缓存的资源)——构建是 CDN 的内容来源:源码变了 → 构建出新产物(新哈希文件名)→ 上传 CDN → 浏览器拉到新版本——第 16 章的"长 TTL+版本号"策略,依赖的就是构建的哈希文件名机制(构建、CDN、缓存策略,一条链)。
框架与构建工具(第 9 章的兑现):前端框架(第 9 章)的脚手架自带构建工具(React/Vue 的官方脚手架内建打包/编译/热更新)——"框架内建常见问题答案"(第 9 章)在构建侧又一次兑现:构建工具是框架生态的一部分,换框架=换构建工具链(第 9 章说"机制学会了换框架只是换 API 名"——构建同理,机制是"源码→制品",工具是细节)。
构建时的环境差异(前端也有,但要小):构建机上的 Node 版本影响产物(构建工具的版本差异可能产出不同代码)——所以构建要在"锁定的环境"里进行(21.5 的依赖锁定 + 21.6 的"构建一次":同一环境构建,产物才可复现)。
"构建一次"还有一层时间含义(它是 CI 的伏笔):同一份源码只构建一次——构建产物直接进入测试/生产,不在测试环境"再构建一次"(测试环境重新构建=构建环境不同=产物可能不同,复现破)——"构建一次、处处运行"的"一次",就是字面的一次(第 22 章 CI 会看到:流水线里构建只出现一次,后面全是"用制品")。
21.4 后端构建:可执行制品
后端构建要解决的问题:源码 → 服务器能直接启动的东西。案例是 Node.js(第 5 章选型),Node 的"构建"和编译型语言(Java/Go)不同——Node 的构建主要是"依赖就位 + 启动准备":
源码(JS + package.json)
→ ① 依赖安装(按 lockfile 装,版本精确锁定)
→ ② 产物打包(代码 + 依赖,打成可部署的制品)
→ ③ 启动校验(构建后能启动吗?启动一次验证——"能启动的制品"才算制品)
为什么后端构建要"锁依赖"(它比前端更敏感):后端代码运行在服务器上,依赖版本差异直接改变行为(21.1 的 Node 14 vs 18)——前端构建把依赖"打包进产物"(浏览器端没有"装依赖"这回事),后端构建要把依赖"锁定并装对"(服务器端的 node_modules 是运行的一部分)——后端的复现,一半靠 lockfile(装什么版本),一半靠构建环境(在什么环境装)。
编译型语言的后端构建(一句话对照,帮助理解 Node 的特殊性):Java/Go 这类语言,构建是真正的"编译"(源码 → 字节码/二进制),产物是单一可执行文件——比 Node 的"代码+依赖"更"制品"(一个文件,拷到哪都能跑);Node 的制品是"代码+node_modules 目录",对环境的依赖更多(Node 版本、系统库)——这也是容器(Docker)登场的背景之一(把"代码+依赖+环境"一起打包)。
构建的时长(它是构建体验的一个现实,一句):构建要花时间(前端打包几十秒到几分钟,后端装依赖+校验更久)——构建时长决定了"改完代码到能部署"的等待——所以构建要快(第 22 章 CI 会看到"构建慢是流水线的瓶颈"——增量构建/缓存依赖是常见手段)——构建时长是发布节奏的一部分(第 20 章说"发布节奏决定工作流",构建时长决定发布节奏)。
后端构建的"启动校验"(它是构建和测试的交界):构建完成后启动一次(连上测试数据库、跑一遍健康检查——第 15 章联调清单的最小版)——"构建成功"不等于"能启动"——构建的验收标准:产物能启动(第 18 章的测试在这里的构建侧形态:至少"能跑起来")。
构建与测试的分工(它决定构建时跑什么):构建管"能启动"(启动校验——能跑起来),测试管"行为对"(单元/集成/E2E——跑得对,第 18 章)——构建时至少跑"能启动",CI 里跑"行为对"(第 22 章:提交代码自动跑全量测试——构建的启动校验是 CI 的入口检查)——"能跑"和"跑对"是两个验收,分别在构建和 CI 完成(第 22 章会看到它们怎么串成流水线)。
健康检查接口(启动校验的形态,它是第 24/25 章的起点):后端制品暴露一个 /health 接口(返回"服务活着"+关键依赖状态:数据库连得上吗)——启动校验调它,第 24 章高可用的探活也调它,第 25 章监控的"服务存活"也看它——一个接口,三个用途(构建校验/高可用探活/监控指标)——第 15 章说"测试环境是生产的彩排",健康检查就是彩排的第一幕。
构建失败的处理(它是构建体系的行为准则,一句):构建失败是"红灯",不是"意外"——构建红了,修复它优先于继续开发(第 20 章"主干红了先修"的构建版)——"构建通过"是提交代码后的第一道自动关卡(第 22 章 CI 会把它变成流水线的第一步)——构建失败最常见的原因(依赖问题/环境问题/代码问题),排障顺序和第 15 章环境排查一样:先比对环境,再看代码。
配置的时机:构建时 vs 运行时(它是 21.7"配置不进制品"的预告):构建时注入(构建时把环境变量写进制品——缺点:每个环境一个制品,复现破产);运行时注入(制品里留"配置的坑",部署时填——环境变量/配置文件,第 23 章展开)——"处处运行"要求配置运行时注入:同一份制品,测试环境填测试配置,生产填生产配置——制品不含环境,环境由部署注入(21.7 的边界在这里有了执行细节)。
数据库迁移的发布时机(它是构建/发布流程里最容易漏的一步,第 6 章的兑现):第 6 章说"表结构要改,要迁移脚本"——迁移在什么时候跑?发布时(先跑迁移,再部署新制品——顺序反了:新代码连旧表结构,报错)——迁移是发布流程的一部分(部署会把它正式纳入发布单;本章先立认知:发布 = 迁移 + 制品,两个动作的顺序有讲究)。
回滚时迁移怎么办(它是"回滚"最容易被忽略的角落):代码可以指回旧制品,数据库已经迁移过了不能"指回"(表结构改了就改了)——迁移的"回滚"靠的是"向前兼容"(第 6 章说过"迁移要能回滚"——迁移脚本要设计成"新代码兼容旧结构过渡",或者迁移本身可逆)——制品可回滚,数据库只能前进:这是回滚体系的第一个不对称(部署会看到完整的回滚策略)。
21.5 依赖锁定:复现的基石
依赖锁定(lockfile):把"每个依赖的精确版本"锁进一个文件(package-lock.json 一类),安装时照单安装——"当时最新的"变成"锁定的版本"。第 15 章说它是"环境一致性的第二道保险"——现在它是复现的第一块基石:
"当时最新的"为什么是复现的敌人(它是 lockfile 存在的理由):npm install 的默认行为是"装当时最新的兼容版本"——今天的"最新"和下周的"最新"不一样——没锁的依赖,构建时间不同=依赖不同=制品不同(第 21.1 事故三:两个人装了两次装出两份不同的依赖)——lockfile 把"时间"从构建的变量里剔除:构建不再依赖"当时",只依赖"锁里"(第 21.2 说"构建是函数"——lockfile 让函数少一个输入变量)。
"不可复现"的三种来源(构建要消灭的,正好是 21.1 的三个原因):
- 依赖漂移:没锁版本,两次安装装出两份不同的依赖——lockfile 消灭它(照单安装,全世界装出一样的);
- 环境差异:构建机、开发机、测试机的 Node 版本/系统不同——锁定构建环境消灭它(这里先靠"规定构建机");
- 构建不纯净:构建过程依赖了"当时的状态"(某次构建改了文件、手工改了产物)——不可变制品消灭它(制品只读,改制品=重新构建)。
"锁构建环境"的执行(它是三来源里"环境差异"的解法,具体怎么做):专用构建机(一台固定的机器/容器跑构建——不是"谁的电脑都能构建");构建环境版本化(构建机的 Node 版本、系统版本固定并记录——第 20 章决策记录的习惯:环境也记录);构建脚本化(构建命令写进脚本/配置文件,不靠记忆——第 21.1 事故修复的三件事之一)——"构建环境"和源码一样要版本管理(环境变了,复现就破——第 23 章容器是"环境也打包"的终极形态)。
lockfile 的使用纪律(它决定锁有没有用):依赖升级是显式动作(改 package.json 里的版本 → 重新生成 lockfile → 一起提交——升级是"故意"的,不是"碰巧"的);lockfile 要进仓库(和源码一起版本管理——第 20 章的提交纪律:lockfile 的变更要有 PR 有评审);CI 里校验 lockfile(第 22 章:构建时用 lockfile 安装,装出来的依赖和本地不一致就报错——"照单安装"是 CI 的第一条检查)。
依赖锁定与第 18 章测试的关系(它是"可复现"在测试侧的兑现):测试也要"照单"跑——本地测试用本地依赖、CI 测试用 lockfile 依赖,两边依赖不一致,测试结果就不可复现("我本地过了""CI 红了"——第 18 章测试的复现问题,根源还是依赖)——lockfile 是测试可复现的前提(第 18 章说"测试是承诺",承诺要可复现才有效——锁依赖让"测试通过"从环境事实变成代码事实)。
lockfile 的生成机制(它决定锁什么时候"更新"):lockfile 是安装时自动生成的(第一次 npm install 生成,之后照它装);升级依赖时重新生成(改 package.json 版本 → 重新 install → lockfile 更新 → 提交)——lockfile 的更新只来自"显式升级":日常安装不会改它(照单装),改它一定是因为有人故意升了版本——"锁"的语义:日常不动,动必有因。
事故的修复收尾(21.1 事故的完整闭环):修好之后,小明做了三件事——lockfile 提交进仓库(下次谁装都是同一份依赖);构建脚本写进项目(打包命令不再靠记忆);"部署清单"列了第一版(第 15 章联调清单的部署版:装依赖→构建→启动校验,发布单雏形)——事故的价值:把"这次怎么修的"变成"下次自动这么修"(第 18 章说"事故=漏掉的承诺",这里"承诺=构建体系")。
依赖升级的节奏(它是 lockfile 的配套):安全补丁及时升(第 19 章说"依赖是攻击面"——安全更新不能拖);大版本谨慎升(破坏性变更要测试兜底——第 18 章测试是升级的安全带);升级和功能开发分开(升级 PR 里只升级,不夹带功能——第 20 章 PR 要小的纪律,升级 PR 是它的典型场景)。
依赖树(lockfile 里装的是什么,一句):依赖不是平铺的,是树(A 依赖 B,B 依赖 C——node_modules 里套着 node_modules)——lockfile 锁的是整棵树的精确版本(不只直接依赖,还有间接依赖:"B 用的 C 是哪个版本"也锁住)——"照单安装"装的是整棵树:直接依赖锁住 + 间接依赖锁住,两次安装才真正一样(第 15 章说"依赖版本漂移"——漂移往往出在间接依赖上:直接依赖升级时,间接依赖被悄悄换掉)。
21.6 制品仓库与不可变性
构建产出了制品,制品要存放(制品仓库:放制品的地方,像代码仓库放代码一样)和管理(版本、保留策略)。
制品的版本(它是回滚的坐标):每个制品有一个唯一版本(构建时间+哈希,build-20260815-a1b2c3)——版本是制品的身份证:部署时记录"现在跑的是哪个版本",回滚时"回到哪个版本"——没有版本号的制品,是没法回滚的制品(第 23 章部署会看到回滚机制,这里先立"版本=坐标")。
制品的部署状态("哪个版本在跑"是部署体系的状态量,一句):"部署状态"要记录(当前测试环境是哪个制品、生产是哪个制品——第 24 章可观测性会看到它成为排障的起点)——"跑的是哪个版本"是部署问题的第一问(和第 15 章"先比对环境"同一个排查顺序:先知道跑的是什么,再查为什么不对)。
构建的幂等性(它是"复现"在构建本身的验证):同一份源码、同一个 lockfile、同一个构建环境,构建两次,产物应该一样(哈希文件名相同——内容指纹一致的直接检验)——构建是"函数":输入(源码+锁+环境)→ 输出(制品)——输入相同,输出必须相同(这正是"复现"的定义);输出不同(两次构建哈希不同),说明构建环境不纯净(21.5 的"构建不纯净")——构建的幂等性是可复现性的自检:不用等到部署出问题,构建时就能发现。
制品仓库的形态(一句定位,够用):制品仓库就是"放制品的地方"——对象存储/制品库服务(按版本存放制品,支持"指回某个版本")——它和代码仓库的分工:代码仓库放源码(人改的),制品仓库放制品(机器产的)——两个仓库,一条链(源码→构建→制品)。
制品与多实例(第 23/24 章的预告,一句):将来多台服务器(第 24 章高可用),所有实例部署同一个制品——"处处运行"在多实例时代就是"同一制品跑在 N 台机器上"——制品是部署的原子单位:一个制品,N 个实例,行为一致(第 23 章会看到多实例部署怎么做)。
制品不可变性的执行(它是"制品"和"目录"的区别):制品一旦发布,内容不可改——想改?重新构建一个新版本——"改制品"和"改源码"是两件事:改源码再构建是新制品,直接改制品是破坏复现(部署了"和源码对不上"的东西,第 21.5 的"构建不纯净")——不可变性的意义:部署的东西 = 构建过的东西 = 源码的准确产物(从源码到生产的整条链,每一步都可追溯——第 24 章可观测性的"谁部署了什么"在这里有了答案)。
制品的元数据("可追溯"的载体,一句):每个制品带着构建信息(源码提交号/构建时间/构建环境/依赖锁版本)——元数据让"生产跑的制品 ↔ 源码的哪个提交"一键可查(第 24 章排障时"这个 bug 是哪个版本引入的"——第 20 章 git blame 的制品版)——制品不只是代码,是"代码+出处"。
制品仓库的保留策略(一句):保留最近 N 个版本(回滚需要它们),更老的归档或删除——制品是存储成本(每个版本都占空间),保留策略是成本管理(第 4 章复杂度预算在制品上的应用)。
制品与第 26 章架构演进(一句预告):将来拆服务(第 26 章),每个服务有自己的制品和构建链(内容服务一个制品、订单服务一个制品)——本章的"构建一次、处处运行",在微服务时代变成"每个服务各自构建、各自部署"——构建体系的复杂度跟着架构长(第 2 章演进循环:先一个制品,再多制品)。
制品与安全(第 19 章的衔接,一句):制品仓库是供应链攻击的目标(第 19 章说"依赖也是攻击面"——攻击者可能污染制品仓库,让"官方制品"带后门)——制品的可信度:制品仓库的访问控制(谁能传制品)、制品校验(哈希校验——下载的制品和构建的一致吗)——第 19 章的"信任模型"在制品环节的形态:制品也要"验签"(第 10 章证书的签名思想,从网络到制品)。
制品与第 18 章回滚的衔接(它是"事故处置三动作"的制品侧):第 18 章说"事故处置第一步是回滚"——回滚在制品侧就是"把部署目标指回上一个制品"(不是重新部署源码,是指回制品)——制品体系让回滚从"重新上线"变成"指回版本"(秒级 vs 小时级)——第 23 章部署会看到完整的回滚机制。
21.7 构建与部署的边界
构建讲完了,立一条边界——构建管什么,部署管什么(它是避免"构建部署混在一起"的认知基础):
构建管:源码 → 制品(依赖、编译、打包、版本——本章的全部内容);部署管:制品 → 运行环境(放到哪台服务器、怎么启动、环境变量、流量切换——第 23 章的全部内容)——构建的产物是部署的输入:构建说"这个制品可以跑",部署说"这个环境跑它"——两个职责分开的意义:构建出问题查构建(复现问题),部署出问题查部署(环境问题)——第 15 章说"测试环境是生产的彩排",构建就是彩排的"剧本"(同一份制品,彩排和生产用同一个)。
构建与部署的交接(它是第 23 章发布单的输入,先列轮廓):部署前要确认三件事——制品版本(部署哪个制品);配置(这个环境的环境变量/域名);迁移(数据库迁移跑了吗,21.4 说过顺序)——这三项就是第 23 章发布单的第一版(第 15 章联调清单的部署版,从这里正式长大)。
构建与第 15 章彩排的完整闭环(它是"测试环境是生产的彩排"的构建侧收尾):同一份制品,先部署测试环境(彩排)→ 验证(第 15 章联调/第 18 章测试)→ 再部署生产(正式)——彩排的意义在构建体系下更清楚了:彩排和生产用的是同一个制品,彩排验证的就是生产将要跑的东西——"彩排"不再是"模拟",是"同一出戏的预演"(第 15 章说"测试环境是生产的彩排,不是本地的复制品"——现在有了制品的保证)。
部署方式的预告(第 23 章会展开,先混个脸熟):蓝绿部署(两套环境,切流量——新版本在旁边验证好再切)和金丝雀(先放 1% 流量试试,没问题再全量)——它们能成立的前提,就是本章的"不可变制品":制品是切换的单位(切到新制品/切回旧制品)——第 23 章的部署花样,都建立在制品的不可变性上。
构建与第 20 章评审的衔接(一句,把协作和交付串起来):构建配置也是代码(构建脚本/CI 配置进仓库、走 PR、被评审——第 20 章"代码和测试一起提交"的扩展:构建配置和源码一起提交)——构建体系的"轨道"和第 20 章代码的"轨道"是同一个(第 21 章说的"两个仓库一条链",链上走的是同一套协作流程)。
"构建一次、处处运行"的边界(诚实交代,防止口号化):制品相同,但环境配置不同(测试库 vs 生产库、域名不同)——配置不在制品里(21.4 说过的配置时机——配置由部署注入)——"处处运行"指代码行为一致,配置由部署时注入——制品不含配置,是"处处运行"能成立的前提(配置进制品=每个环境一个制品=复现破产)。
构建与本地开发的关系(最后一句,防止误解):构建体系建起来后,本地开发仍然是"跑源码"(热更新/调试需要源码态,第 9 章框架的开发模式)——"构建"只发生在交付链路上(本地开发用源码,测试/生产用制品——第 15 章环境分层的"本地是开发的自留地",在构建体系下依然成立)——"处处运行"是交付侧的承诺,不是开发侧的束缚(本地怎么方便怎么来,交付怎么一致怎么来)。
21.8 本章对应表
| 业务诉求 | 技术选择 | 为什么 | 代价/取舍 |
|---|---|---|---|
| 本地能跑线上不行 | 构建(源码 → 制品) | 环境差异在构建时锁进制品 | 构建系统要维护 |
| 前端源码浏览器不认 | 前端构建(编译/打包/哈希) | JSX/TS 要变成浏览器能跑的 | 打包体积要优化(8/16 章) |
| 后端依赖版本不一致 | 后端构建(锁依赖+启动校验) | 依赖差异直接改变行为 | 对环境依赖仍多(23 章容器) |
| 构建不可复现 | 依赖锁定(lockfile)+ 锁定构建环境 | 照单安装,两次构建一样 | 升级是显式动作(要流程) |
| 部署能回滚 | 不可变制品 + 版本号 | 制品只读,回滚=指回版本 | 制品要存(保留策略) |
| 构建和部署谁管什么 | 职责划分(构建出制品/部署放环境) | 出问题知道查哪边 | 配置注入归部署(23 章) |
每一行都在本章正文里有完整论证:事故在 21.1,构建定义在 21.2,前端在 21.3,后端在 21.4,依赖锁定在 21.5,制品在 21.6,边界在 21.7。先看"业务诉求"列——这一章的每一行,都是从"在我机器上是好的"那句话里长出来的。
本章对两类读者的收益(与前几章同款):前端读者——构建的打包/哈希/代码分割/懒加载,是前端"少下载"(第 8/16 章)的实现侧;后端读者——锁依赖/启动校验/迁移时机,是后端可部署性的基础;两类读者共同的收获:"复现"第一次成为机制——"在我机器上是好的"从口头禅变成历史(第 15 章环境问题、第 20 章发布事故,到这里有了制度的答案)。
阶段 4 的进度(五阶段故事线的登记):本章是第五部分第一站——团队 8 人、发布从"手搓"走向"流程"(第 20 章说"部署从手搓脚本到需要流程");本章建好了"流程的第一段"(构建),第 22 章自动化、第 23 章部署环境——第五部分的"发布三段式",本章是第一段。
阶段 4 的发布流程落地(第 20 章事故三的完整闭环):第 20 章说"发布没有轨道"——本章建了第一段轨道(构建);事故三的教训正在被流程消化:"谁改完谁部署" → "构建出制品、制品可回滚"——协作章的结尾问"谁来维护防线",本章回答"防线有了第一段轨道"(第 22-23 章把轨道铺完)。
本章与第 22 章的边界:本章管"构建怎么做对"(复现/锁定/制品);第 22 章管"构建怎么自动"(提交代码自动触发构建+测试——CI 是构建的"自动化形态",不是新的机制)——本章的"构建一次"是第 22 章流水线的第一个环节(CI 里第一步永远是构建:第 22 章会看到"构建-测试-检查-部署"的流水线,构建是它的地基)。
本章小结
构建速查(第五部分回指本章时翻回这里):
- 构建 = 源码 → 不可变制品——"构建一次、处处运行":环境差异在构建时锁进制品;
- 前端构建:依赖解析→编译→打包→哈希文件名(第 8 章 CSS 拆分、第 16 章版本号哈希的构建时机);
- 后端构建:锁依赖安装 + 启动校验——"能启动的制品"才算制品;
- 依赖锁定(lockfile):复现的基石——不可复现三来源(漂移/环境/不纯净)全被它或机制消灭;
- 制品不可变 + 版本号:回滚的单位是制品(第 18 章"先回滚"的制品侧);
- 构建与部署的边界:构建管"做出制品",部署管"放上环境"——配置不进制品(23 章注入)。 (六条速查对应本章结构:1 是总纲(复现),2-4 是"怎么构建"(前端/后端/锁定),5-6 是"制品与边界"——第 22 章 CI 回指本章时重点翻 1(复现)和 4(lockfile)。)
第五部分预告:21 构建(本章,做出制品)→ 22 CI/CD(自动验证:构建+测试自动跑)→ 23 部署(放上环境:服务器/容器/发布策略)——三段式合起来,就是"从代码提交到用户可见"的完整链路(第 1 章工程线的收尾段)。
- "在我机器上是好的"是复现性破产的症状——构建的目标:同一份源码,任何时候构建出同样的制品;
- 构建把源码变成不可变制品:部署的单位、回滚的单位——"构建一次、处处运行";
- 依赖锁定是复现的基石:依赖不锁,构建就是碰运气;
- 阶段 4 的发布流程第一步落地:下一步,把构建自动化——整条链路能不能自动。
下一章
构建体系建起来了:源码变成制品,制品可以部署。但新问题出现了:每次发布,构建要人跑、测试要人跑、检查要人做——人跑的构建,会忘、会跳、会偷懒(第 18 章说过"人力会累会跳")。
第 22 章,CI/CD:从代码提交到自动上线——把"人跑流程"变成"机器跑流程":提交代码 → 自动构建 → 自动测试 → 自动检查 → 自动部署。