1. 从一堆各自为政的 VBA 模板说起
手里攒了七八个 VBA 模板文档,每个都是不同时期、不同需求下攒出来的。有的是批量改格式的,有的是自动生成报表的,还有几个是处理特定数据清洗逻辑的。单独拿出来跑,每个都能用,但问题在于——它们之间毫无关联。改了一个模板里的公共函数,另外几个模板里的同名函数还是老样子;新增一个通用工具方法,得挨个文档复制粘贴一遍。时间一长,哪个版本是最新的、哪个函数在哪几个文档里重复定义过,自己都记不清了。
这种状态就是典型的"散沙"。每个模板单独看都挺能打,凑在一起就是一盘各自为政的游击队,没有统一指挥,没有统一补给,改一处漏三处。真正让我下决心动手改造的,是上个月的一次批量任务:客户临时要求在所有报表模板里统一增加一个数据校验环节。我打开第一个文档改完,打开第二个文档发现函数签名不一样,第三个文档里干脆没有这个函数,第四个文档里的实现逻辑又是另一套写法。那天下午我花了整整三个小时在做重复劳动,而且改完之后心里清楚——下次再有类似需求,这套流程还得重来一遍。
问题的根子不在于 VBA 本身不好用,而在于模板文档的组织方式出了问题。每个.xlsm文件都是一个封闭的容器,代码、工作表结构、命名区域、引用关系全部锁死在各自的容器里。容器之间没有共享层,没有继承关系,没有同步机制。你改了一个容器里的东西,其他容器完全不知情。
我需要的是一套母版-副本自动同步机制:有一个权威的母版文档,所有公共代码、公共结构、公共配置都放在母版里维护;各个副本文档从母版继承这些公共内容,同时保留自己特有的业务逻辑;当母版更新时,副本能够自动或半自动地同步变更。这样我只需要维护一个地方,所有副本都能受益。
WorkBuddy 在这个场景里扮演的角色,就是那个"总控台"。它本身是一个面向文档处理和自动化任务的工作台工具,支持对多个文档进行统一管理和批量操作。我把它当作调度层来用:由它来管理母版和副本之间的同步流程,触发同步动作,校验同步结果。VBA 负责文档内部的逻辑执行,WorkBuddy 负责文档之间的协调调度。两者配合,才能把这盘散沙真正捏成一个整体。
下面我把整个改造过程拆开来讲,包括母版怎么设计、副本怎么继承、同步机制怎么跑通、以及中间踩过的那些坑。如果你手里也有多个 VBA 模板文档需要统一管理,这套思路可以直接参考。
2. 母版文档的结构设计与公共层抽取
2.1 哪些内容应该放进母版
改造的第一步不是写代码,而是做分类。我把所有模板文档里的内容拆成三类:
第一类是公共代码。比如通用的数据校验函数、日期处理函数、字符串清洗函数、日志记录模块、错误处理模块。这些代码在多个模板里重复出现,逻辑基本一致,只是偶尔有细微差异。这类内容必须上收到母版。
第二类是公共结构。比如标准的工作表命名规范、固定的表头格式、统一的命名区域定义、公共的配置参数表。这些结构在每个副本里都应该保持一致,否则后续的批量操作会出问题。
第三类是业务特有逻辑。比如某个模板专门处理销售报表的特定计算,另一个模板专门处理库存数据的特定转换。这些逻辑只属于特定副本,不应该放进母版,但需要以某种方式与母版公共层对接。
分类做完之后,母版文档的结构就清晰了。我建了一个MasterTemplate.xlsm,里面包含:
- 一个
CommonCode标准模块,存放所有公共函数和过程 - 一个
Config工作表,存放公共配置参数 - 一个
Log模块,负责统一的日志记录 - 一个
ErrorHandler模块,负责统一的错误捕获和处理 - 若干标准工作表模板,定义好表头和命名区域
注意:母版文档本身不直接用于业务执行,它只作为"源"存在。所有实际业务操作都在副本文档里进行。
2.2 公共代码的抽取原则
抽取公共代码时,最容易犯的错误是"抽得太粗"或"抽得太细"。抽得太粗,把业务逻辑也塞进公共层,导致母版越来越臃肿,副本反而失去了灵活性;抽得太细,每个小函数都单独维护,同步成本反而更高。
我的原则是:只抽取那些在三个以上副本中重复出现、且逻辑完全一致或应该完全一致的代码。如果某个函数只在两个副本里出现,或者两个副本里的实现有本质差异,那就先不抽,留在各自副本里,等条件成熟再考虑。
具体操作上,我用了 VBA 的Export功能把每个模块导出为.bas文件,然后用文本对比工具逐个比对。比对结果用表格记录下来:
| 模块名称 | 出现副本数 | 逻辑一致性 | 处理方式 |
|---|---|---|---|
| DataValidate | 6 | 完全一致 | 上收母版 |
| DateUtils | 5 | 完全一致 | 上收母版 |
| StringClean | 4 | 基本一致 | 上收母版,参数化差异 |
| ReportCalc | 2 | 有差异 | 暂不上收 |
| InventoryLogic | 1 | 独有 | 留在副本 |
这个表格看起来简单,但实际比对过程花了我将近两个小时。有些函数的差异非常隐蔽,比如一个用了Trim另一个没用,一个处理了空值另一个没处理。这些细节如果不仔细看,上收之后就会出问题。
2.3 配置参数的外置化
公共代码上收之后,紧接着要处理的是配置参数。原来每个模板里都有一堆硬编码的参数,比如文件路径、工作表名称、列号映射、阈值设定。这些参数如果留在代码里,母版同步时就会把副本特有的配置覆盖掉。
我的做法是把所有配置参数外置到Config工作表里,代码通过命名区域读取配置。比如:
Function GetConfig(key As String) As Variant On Error Resume Next GetConfig = ThisWorkbook.Names("cfg_" & key).RefersToRange.Value If Err.Number <> 0 Then GetConfig = Empty Err.Clear End If On Error GoTo 0 End Function这样副本只需要维护自己的Config工作表,公共代码从命名区域读取参数,母版同步时不会碰配置表,副本的个性化配置就能保留下来。
实操心得:命名区域的命名规范建议统一加前缀,比如
cfg_开头表示配置项,rng_开头表示数据区域,tbl_开头表示表格。这样在代码里一眼就能看出这个名称是干什么的,也方便批量管理。
3. 副本继承机制:让每个副本知道自己"继承"了什么
3.1 副本文档的初始化流程
母版建好之后,副本不能简单地复制一份就完事。复制出来的副本和母版之间没有任何关联,母版更新了副本也不知道。我需要的是有继承关系的副本——副本知道自己的公共层来自哪个母版,母版更新时副本能够识别出哪些内容需要同步。
我的做法是在每个副本文档里增加一个SyncMeta工作表,记录以下信息:
- 母版文档的路径
- 上次同步的时间戳
- 上次同步时母版的版本号
- 公共模块的校验和(用于检测母版是否变更)
副本初始化时,通过一个InitFromMaster过程从母版拉取公共模块和公共结构,同时写入SyncMeta信息。这个过程只需要执行一次,后续通过同步机制来更新。
Sub InitFromMaster(masterPath As String) Dim masterWb As Workbook Set masterWb = Workbooks.Open(masterPath, ReadOnly:=True) ' 导入公共模块 ImportModule masterWb, "CommonCode" ImportModule masterWb, "Log" ImportModule masterWb, "ErrorHandler" ' 复制公共配置结构 CopyConfigStructure masterWb ' 写入同步元信息 WriteSyncMeta masterPath, GetMasterVersion(masterWb) masterWb.Close SaveChanges:=False End Sub3.2 公共模块的导入与替换策略
导入公共模块时,最大的风险是覆盖副本里已有的同名模块。如果副本里已经有一个CommonCode模块,直接导入会弹窗询问是否替换,批量操作时这个弹窗会卡住整个流程。
我的处理方式是:导入前先检查目标模块是否存在,如果存在则先删除再导入。但删除之前要做一次备份,把原模块导出到临时目录,以防万一。
Sub ImportModule(sourceWb As Workbook, moduleName As String) Dim targetVbProj As Object Dim sourceVbProj As Object Dim tempPath As String Set targetVbProj = ThisWorkbook.VBProject Set sourceVbProj = sourceWb.VBProject ' 备份已有模块 If ModuleExists(targetVbProj, moduleName) Then tempPath = Environ("TEMP") & "\" & moduleName & "_backup.bas" targetVbProj.VBComponents(moduleName).Export tempPath targetVbProj.VBComponents.Remove targetVbProj.VBComponents(moduleName) End If ' 从母版导出并导入 Dim masterTempPath As String masterTempPath = Environ("TEMP") & "\" & moduleName & "_master.bas" sourceVbProj.VBComponents(moduleName).Export masterTempPath targetVbProj.VBComponents.Import masterTempPath ' 清理临时文件 Kill tempPath Kill masterTempPath End Sub注意:操作 VBProject 需要在 Excel 信任中心里勾选"信任对 VBA 工程对象模型的访问",否则代码会报错。这个设置在每个副本文档所在的环境里都要确认一遍。
3.3 版本号与校验和的设计
为了让副本能够判断母版是否发生了变更,我在母版里维护了一个版本号,每次修改公共层时手动递增。同时计算公共模块的校验和,作为辅助判断依据。
版本号存在母版的Config工作表里,校验和通过遍历公共模块的代码行计算 MD5 得到。副本同步时先比对版本号,如果版本号不同则执行同步;如果版本号相同但校验和不同,说明母版被意外修改了,需要人工确认。
Function GetMasterVersion(wb As Workbook) As String GetMasterVersion = wb.Worksheets("Config").Range("B1").Value End Function Function GetModuleChecksum(wb As Workbook, moduleName As String) As String Dim codeLines As String Dim i As Long Dim vbComp As Object Set vbComp = wb.VBProject.VBComponents(moduleName) For i = 1 To vbComp.CodeModule.CountOfLines codeLines = codeLines & vbComp.CodeModule.Lines(i, 1) Next i GetModuleChecksum = MD5Hash(codeLines) End FunctionMD5 的实现我用了一个纯 VBA 的版本,避免依赖外部库。虽然 VBA 做哈希计算效率不高,但公共模块的代码量通常不大,计算一次也就几百毫秒,完全可以接受。
4. WorkBuddy 总控台的调度逻辑与同步流程
4.1 为什么需要 WorkBuddy 来做调度
VBA 本身可以完成文档内的所有操作,但跨文档的批量调度是它的弱项。VBA 要操作另一个文档,必须先把那个文档打开,操作完再关闭。如果副本有十几个,逐个打开关闭的效率很低,而且中间任何一个文档出错都会中断整个流程。
WorkBuddy 的优势在于它可以在文档层面之上做调度。它能够管理多个文档的打开、关闭、保存状态,能够按顺序或并行触发多个文档里的宏,能够收集每个文档的执行结果并汇总。我把同步流程拆成几个独立的步骤,每个步骤由 WorkBuddy 触发对应的 VBA 过程,步骤之间由 WorkBuddy 控制流转。
具体来说,WorkBuddy 负责:
- 扫描副本目录,识别所有需要同步的副本文档
- 逐个打开副本,触发副本里的
SyncFromMaster过程 - 收集每个副本的同步结果(成功/失败/跳过)
- 生成同步报告
- 关闭副本文档(根据配置决定是否保存)
VBA 负责:
- 读取
SyncMeta信息 - 比对母版版本号和校验和
- 执行公共模块的导入替换
- 更新
SyncMeta信息 - 返回同步结果
4.2 同步流程的完整链路
整个同步流程分为六个阶段,我用 WorkBuddy 的任务编排功能把它们串起来:
阶段一:母版状态检查。WorkBuddy 先打开母版文档,读取当前版本号和校验和,确认母版处于可同步状态。如果母版正在被编辑(有未保存的更改),则暂停同步并提示。
阶段二:副本清单扫描。WorkBuddy 扫描指定的副本目录,列出所有.xlsm文件,排除临时文件和备份文件,生成待同步清单。
阶段三:逐个副本同步。WorkBuddy 按清单顺序打开每个副本,调用副本里的SyncFromMaster过程。这个过程内部会判断是否需要同步,如果副本版本号已经是最新则跳过。
阶段四:同步结果收集。每个副本同步完成后返回一个结果码,WorkBuddy 收集这些结果码并记录到日志里。
阶段五:异常处理。如果某个副本同步失败,WorkBuddy 记录失败原因,继续处理下一个副本,不中断整个流程。所有副本处理完后,统一报告失败清单。
阶段六:报告生成。WorkBuddy 把所有副本的同步结果汇总成一张表格,包含副本名称、同步前版本、同步后版本、同步状态、耗时、备注信息。
这个流程跑通之后,我只需要在 WorkBuddy 里点一下"执行同步",剩下的全部自动完成。十几个副本的同步时间从原来手动操作的一个多小时压缩到三分钟左右。
4.3 同步过程中的冲突处理
同步过程中最头疼的是冲突:副本里的某个公共模块被本地修改过,和母版的最新版本产生了差异。直接覆盖会丢失副本的本地修改,不覆盖则同步不完整。
我的处理策略是三路比对:把母版版本、副本当前版本、副本上次同步时的版本(从备份里取)三者进行比对。
- 如果副本当前版本和上次同步版本一致,说明副本没有本地修改,直接用母版覆盖。
- 如果副本当前版本和上次同步版本不一致,但母版版本和上次同步版本一致,说明只有副本改了,母版没改,保留副本修改,跳过同步。
- 如果三者都不一致,说明双方都改了,标记为冲突,生成冲突报告,由人工决定如何处理。
Function ResolveConflict(masterCode As String, currentCode As String, lastSyncCode As String) As String Dim currentChanged As Boolean Dim masterChanged As Boolean currentChanged = (currentCode <> lastSyncCode) masterChanged = (masterCode <> lastSyncCode) If Not currentChanged And masterChanged Then ResolveConflict = "overwrite" ' 副本没改,母版改了,覆盖 ElseIf currentChanged And Not masterChanged Then ResolveConflict = "keep" ' 副本改了,母版没改,保留 ElseIf currentChanged And masterChanged Then ResolveConflict = "conflict" ' 双方都改了,标记冲突 Else ResolveConflict = "skip" ' 都没改,跳过 End If End Function实操心得:冲突处理不要试图自动合并代码。VBA 代码的自动合并风险极高,很容易产生语法错误或逻辑错误。标记冲突让人工处理虽然麻烦一点,但安全得多。我通常会把冲突双方的代码导出到两个文本文件里,用对比工具看完之后手动决定保留哪边。
5. 实测中踩过的坑与排查过程
5.1 VBProject 访问被拒绝的排查
第一次跑同步流程时,所有副本都报同一个错误:Programmatic access to Visual Basic Project is not trusted。这个错误的根源是 Excel 信任中心的安全设置。默认情况下,Excel 不允许通过代码访问 VBProject,必须手动开启。
排查过程是这样的:我先在单个副本里手动执行ImportModule,确认代码逻辑本身没问题。然后检查信任中心设置,发现"信任对 VBA 工程对象模型的访问"没有勾选。勾选之后单个副本同步成功。
但问题来了:我有十几个副本,每个副本所在的环境都需要这个设置。如果副本被分发到其他机器上,那个机器上的 Excel 也需要同样的设置。这就变成了一个部署问题。
我的解决方案是在 WorkBuddy 的同步流程里增加一个前置检查步骤:打开副本后先尝试访问ThisWorkbook.VBProject,如果报错则记录该副本为"环境未就绪",跳过同步并提示用户手动开启信任设置。这样至少不会因为一个副本的环境问题导致整个流程崩溃。
Function CheckVBProjectAccess() As Boolean On Error Resume Next Dim vbProj As Object Set vbProj = ThisWorkbook.VBProject CheckVBProjectAccess = (Err.Number = 0) Err.Clear On Error GoTo 0 End Function5.2 模块导入后事件丢失的问题
公共模块导入替换之后,我发现副本里的一些事件过程失效了。比如Workbook_Open事件不再触发,Worksheet_Change事件也没有响应。
排查后发现原因是:ThisWorkbook和各个工作表的代码模块属于文档级模块,它们和普通标准模块的导入机制不同。我在导入公共模块时,不小心把ThisWorkbook模块也纳入了替换范围,导致事件代码被覆盖。
修复方式是在导入逻辑里增加一个排除清单,明确哪些模块不参与同步:
Function IsProtectedModule(moduleName As String) As Boolean Dim protectedList As Variant protectedList = Array("ThisWorkbook", "Sheet1", "Sheet2", "Sheet3") Dim i As Long For i = LBound(protectedList) To UBound(protectedList) If moduleName = protectedList(i) Then IsProtectedModule = True Exit Function End If Next i IsProtectedModule = False End Function注意:文档级模块(ThisWorkbook、各个 Sheet 的代码模块)和窗体模块都不应该参与母版同步。母版同步只针对标准模块(.bas)和类模块(.cls)。这个边界一定要划清楚,否则会出大问题。
5.3 同步后引用丢失的排查链路
另一个比较隐蔽的问题是:同步完成后,副本里的一些引用关系丢失了。比如原来副本引用了Scripting.Runtime库,同步后这个引用还在,但代码里用到的某些对象却报"未定义"。
排查过程分三步:
第一步,检查引用列表。打开副本的 VBA 编辑器,查看"工具-引用",确认所有需要的引用都存在。结果发现引用确实都在。
第二步,检查代码里的声明。发现公共模块里用了CreateObject("Scripting.Dictionary")这种后期绑定方式,而副本里原来用的是早期绑定Dim d As New Scripting.Dictionary。同步后公共模块的后期绑定代码覆盖了副本的早期绑定代码,但副本里其他模块还在用早期绑定的类型声明,导致类型不匹配。
第三步,统一绑定策略。我把所有公共模块里的字典、集合等对象统一改为后期绑定,同时在副本的业务模块里也逐步改为后期绑定。虽然后期绑定的性能略低,但兼容性更好,不会因为引用版本差异出问题。
这个坑给我的教训是:母版同步不仅仅是代码文本的同步,还涉及到引用关系、绑定方式、类型声明等隐性依赖的同步。在设计母版时就要把这些因素考虑进去,尽量使用后期绑定和松耦合的写法。
5.4 大批量同步时的性能问题
副本数量增加到二十个以上时,同步流程开始变慢。每个副本打开、同步、保存、关闭的循环耗时越来越长,整体同步时间从三分钟涨到了十几分钟。
分析后发现瓶颈主要在两个方面:一是文档打开和关闭的开销,二是每次同步都重新计算校验和。
优化措施:
- 把校验和计算改为增量方式,只计算变更过的模块,而不是全量计算。
- 副本打开时禁用屏幕刷新和自动计算,同步完成后再恢复。
- 多个副本的同步改为分批处理,每批五个,批间加短暂延迟,避免 Excel 进程资源争抢。
Sub OptimizeSync() Application.ScreenUpdating = False Application.Calculation = xlCalculationManual Application.EnableEvents = False ' 执行同步逻辑 Application.EnableEvents = True Application.Calculation = xlCalculationAutomatic Application.ScreenUpdating = True End Sub优化之后,二十个副本的同步时间回落到四分钟左右,虽然比最初的三分钟略长,但稳定性明显提升,不会再出现中途卡死的情况。
6. 母版-副本模式的日常维护经验
6.1 母版变更的规范流程
母版是整套机制的源头,母版一改,所有副本都要跟着动。所以母版的变更必须走规范流程,不能随手改。
我的流程是:
- 在母版的开发副本上做修改,不直接改母版本身。
- 修改完成后,在开发副本上跑一遍完整的测试用例,确认公共代码没有破坏现有功能。
- 测试通过后,把开发副本的公共模块导出,替换到正式母版里。
- 递增正式母版的版本号。
- 触发一次全量同步,让所有副本更新到最新版本。
- 同步完成后,抽查两到三个副本,确认同步结果正常。
这个流程看起来繁琐,但能有效避免"改了一个函数,崩了十个副本"的惨剧。我吃过这个亏:有一次直接在母版上改了一个日期处理函数,没测试就同步了,结果那个函数的一个边界条件处理错了,导致所有副本的日期计算全部偏移一天。发现的时候已经过了两天,十几个副本的数据都受了影响。
6.2 副本个性化配置的管理
副本在继承母版公共层的同时,需要保留自己的个性化配置。这些配置包括:
- 业务参数(比如报表的统计口径、阈值设定)
- 数据源路径(不同副本可能连接不同的数据文件)
- 输出路径(不同副本的输出目录不同)
- 启用/禁用某些可选功能
这些配置全部放在副本的Config工作表里,通过命名区域读取。母版同步时不会碰Config工作表,所以副本的个性化配置能够完整保留。
但这里有一个隐患:如果母版新增了一个配置项,副本的Config表里没有这个项,代码读取时会返回空值,可能导致逻辑异常。我的处理方式是在公共代码里对所有配置读取做空值保护,同时提供一个CheckConfigCompleteness过程,在同步完成后检查副本的配置表是否包含所有必需的配置项,缺失的项用默认值补齐。
Sub CheckConfigCompleteness() Dim requiredKeys As Variant requiredKeys = Array("data_path", "output_path", "threshold", "enable_log") Dim i As Long For i = LBound(requiredKeys) To UBound(requiredKeys) If IsEmpty(GetConfig(requiredKeys(i))) Then ' 写入默认值 WriteConfig requiredKeys(i), GetDefaultValue(requiredKeys(i)) End If Next i End Sub6.3 同步日志的分析与利用
每次同步都会生成日志,记录每个副本的同步状态、耗时、版本变化、冲突情况。这些日志平时看起来没什么用,但出问题的时候是排查的重要依据。
我养成了一个习惯:每周花十分钟翻一下同步日志,看看有没有异常模式。比如某个副本频繁同步失败,可能是那个副本的环境有问题;某个副本的同步耗时明显高于其他副本,可能是那个副本的代码量特别大或者有性能问题;某个副本频繁出现冲突,可能是那个副本的维护者经常在本地改公共代码,需要沟通协调。
日志的格式我设计得比较简单,每行一条记录,用竖线分隔字段:
2025-01-15 10:23:45 | ReportTemplate_A | v2.3 -> v2.4 | success | 2.3s | no conflict 2025-01-15 10:23:48 | ReportTemplate_B | v2.3 -> v2.4 | success | 1.8s | no conflict 2025-01-15 10:23:51 | InventoryTemplate | v2.2 -> v2.4 | conflict | 3.1s | CommonCode conflict这种格式用文本工具就能快速筛选和统计,不需要额外的解析逻辑。
6.4 副本数量增长后的管理策略
副本从最初的五六个增长到二十多个之后,管理复杂度明显上升。每个副本都有自己的生命周期,有的在活跃使用,有的已经归档,有的处于测试阶段。如果所有副本都参与全量同步,既浪费时间又增加风险。
我的策略是给副本打标签,分为三类:
- 活跃副本:正在使用中,每次母版更新后必须同步。
- 归档副本:已经不再使用,但需要保留历史版本,不参与自动同步。
- 测试副本:用于验证新功能,同步策略可以灵活调整。
标签信息存在 WorkBuddy 的配置里,同步流程根据标签决定是否处理某个副本。这样既保证了活跃副本的及时更新,又避免了无意义的同步操作。
实操心得:副本的命名建议包含用途和版本信息,比如
Report_Sales_v2.xlsm、Report_Inventory_v3.xlsm。这样在同步日志和报告里一眼就能看出是哪个副本,不用去翻目录。命名规范一旦定下来,所有副本都要遵守,否则后期管理会乱。
7. 这套机制还能怎么扩展
母版-副本同步机制跑通之后,我发现它的适用场景远不止 VBA 模板管理。任何需要"统一维护、多处使用"的文档场景,都可以套用这套思路。
比如 Word 模板的批量管理:把公共的样式定义、页眉页脚、宏代码放在母版文档里,各个业务文档作为副本继承母版内容。同步逻辑和 Excel 版本基本一致,只需要把模块导入替换的部分适配 Word 的对象模型即可。
再比如多版本文档的发布管理:母版作为开发版,副本作为发布版。开发版更新后,通过同步流程把变更推送到发布版,同时保留发布版特有的配置和内容。这样开发和发布分离,既保证了公共内容的一致性,又保留了各发布版本的独立性。
WorkBuddy 在这个扩展场景里的角色可以进一步强化:除了触发同步,还可以做同步前的预检、同步后的验证、同步报告的自动分发。把更多的协调逻辑上收到 WorkBuddy 层,VBA 层只负责纯粹的文档内操作,职责边界会更清晰。
我现在正在尝试的一个方向是把同步流程和版本控制系统对接。母版的每次变更都生成一个变更记录,同步时把变更记录一并写入副本的SyncMeta表。这样副本不仅能知道自己当前是什么版本,还能知道自己经历了哪些变更,排查问题时追溯起来更方便。
这套机制的核心价值不在于技术有多复杂,而在于它把"散沙"变成了"有组织的体系"。母版是唯一的真相来源,副本是受控的派生体,同步是连接两者的通道。只要这个结构立住了,后续增加多少副本、增加多少公共内容,都不会再回到原来那种各自为政的混乱状态。