做跨平台项目做得久了,难免会碰到同一个局面:新业务要立一个 App,PM 说明天要看到能跑的东西,你打开上一代项目,心里清楚里面有三十多个历史补丁、两套半废弃的状态管理、还有一个没人敢删的旧构建脚本。复制它就等于把技术债打包继承。ReNative 模板系统解决的就是这个事,它把“起一个新项目”从复制目录变成了一次声明式的组合,你需要的是一套可版本化、可分发、可继承的工程骨架。这篇内容我会把 ReNative 模板系统从目录结构、声明字段、注册机制,到企业级场景下的多环境注入、私有源托管、流水线衔接,再到我踩过的坑,一次性拆开讲。适合两类人看:一是刚接触这套框架、想把官方示例跑通并改成自己骨架的开发者;二是团队里负责工程化基建、要给多条业务线统一交付标准的同学。文中涉及的具体参数和命令,凡是官方文档没有明说的部分,我都会标明这是基于常见实践的推断,你可以按自己项目的实际情况调整,不必照搬。
1. 模板系统到底在解决什么工程问题
在聊文件结构之前,先把“为什么要有模板”这件事说透。很多团队的做法是把模板等同于“初始化的脚手架”,跑一次就丢掉,后面全靠人肉维护。ReNative 的模板不是一次性脚本,它是一份持续生效的工程契约,理解这一点,后面的设计选择才站得住。
1.1 从复制粘贴脚手架到配置驱动
传统脚手架的问题在于“快照语义”:脚手架生成的那一刻是好的,之后业务代码和工程配置混在一起,模板就再也回不去了。第二年想做第三个 App,你只能从第一个 App 里再扒一次,扒的时候还得分清楚哪些是通用的、哪些是业务耦合的,这个分拣成本通常比重新写还高。ReNative 走的是另一条路:把工程骨架抽成独立的包,里面只放目录约定、平台配置基线、插件声明和依赖清单,业务代码全部通过后续的页面插件或业务模块挂进来。
这么设计的好处是可以被包管理器管理。模板有名字、有版本号、有依赖关系,能被安装、被升级、被锁定。你在项目里写下的模板版本号,等同于给“工程基线”打了一个提交点,半年后回来看,能精确还原当时用的是哪一版骨架。这一点在多人协作里价值极大,因为工程配置文件的冲突是最难 review 的,而版本号是最好 review 的。
代价也要说清楚:模板一旦成为依赖,就意味着它不能随便改。任何一次改动都要走发布流程,都要考虑存量项目会不会被影响。所以模板的粒度和边界必须提前想好,把稳定不变的东西放进去,把容易变的东西留给项目本地配置去覆盖。这个分层思路后面会反复出现,是整个系统的核心设计。
1.2 模板、插件、应用配置三者的职责边界
新手最容易混淆的就是这三者。我见过有人把业务页面写进模板,结果模板一升级,全公司的业务代码都被覆盖了;也见过有人把平台签名配置塞进模板,导致每次发布都要手动改回来。用一句话概括三者的分工:模板定义骨架,插件提供能力,应用配置决定取值。
模板负责的是“有哪些目录、哪些平台基线配置、默认需要哪些依赖和插件”。它关心的是结构,不关心具体值。插件负责的是“这个能力怎么接入各个平台”,比如一个推送能力,它要知道 iOS 怎么配置、Android 怎么配置、Web 端降级成什么。应用配置负责的是“在这个环境、这个 App 里,这些参数取什么值”,比如不同环境的接口地址、不同 App 的包名和应用名。
理解了这层,很多决策就变得理所当然了。为什么模板里不该写死接口地址?因为它属于应用配置。为什么模板里不该出现具体页面的业务逻辑?因为它属于插件或者业务代码。为什么模板需要声明插件依赖?因为骨架决定了这颗树需要挂哪些枝。我个人的经验是,在团队里把这三分法贴在文档第一页,能省掉后面百分之八十的争吵。
还有一个容易被忽略的点:模板是可以叠加的。基础模板提供最小可用骨架,业务模板继承它,再往上做行业定制。这种继承关系不是文件复制,而是配置的深度合并,所以顺序和优先级非常关键。下一节拆目录结构时,我会把合并优先级单独拿出来讲,因为这是实际项目里最容易出问题的地方。
2. 拆解一个模板的目录结构与声明字段
看一个东西最快的方式是看它的最小形态。模板目录本身并不神秘,本质上就是一棵普通目录树加一个描述文件。真正需要花时间理解的,是描述文件里每个字段在生成项目时会被如何处理,这决定了你能控制到什么粒度。
2.1 最小可用模板的目录构成
一个能跑通的最简模板,我一般会整理成这样的结构:
my-template/ ├── renative.template.json # 模板描述文件,核心 ├── package.json # 模板自身的依赖与元信息 ├── appConfigs/ │ └── base/ │ └── renative.json # 基线应用配置,会合并进生成的项目 ├── src/ │ └── index.js # 入口占位,业务侧会覆盖 └── assets/ └── icon.png # 各平台图标基线renative.template.json是整个模板的身份证,没有它,工具链就不会把这个目录识别成模板。appConfigs/base/renative.json是最容易被低估的文件,它承载的是“基线配置”,生成项目时会和用户自己的配置做深度合并,而不是简单覆盖。src和assets属于约定目录,工具链本身不强制内容,但你的构建脚本、插件、页面入口都会默认从这里找东西。
我建议模板里不要预置平台原生工程目录,也就是别把android/、ios/这些生成产物塞进去。原因很实在:不同机器的构建工具版本、签名证书、本地属性都不一样,一旦这些目录被模板带进版本库,后面每次构建都可能在本地产生 diff,团队里天天处理无关冲突。正确做法是让构建过程自己生成,模板只提供生成后的配置补丁,这条经验我后面在踩坑那节还会再提一次。
package.json在模板里的作用有两重:一是描述模板自身的元信息,方便发布到包仓库;二是声明模板运行时需要的工具依赖。这里有个小坑,模板的依赖和项目的依赖是两套东西,如果版本策略不一致,生成出来的项目可能出现依赖树悬空的情况,具体表现和排查方法我会放在第五节细讲。
2.2 声明文件里的关键字段与实际含义
描述文件里字段不算多,但每一个都影响生成结果,我用表格整理一下我实际会用到的那部分,并说明我为什么这么用:
| 字段 | 作用 | 我的使用建议 |
|---|---|---|
| 模板名称与版本 | 标识模板身份,供引用与锁定 | 名称带业务域前缀,版本严格语义化 |
| 模板描述 | 人读的说明,出现在交互式选择里 | 写清楚适用场景,别写“测试模板” |
| 模板依赖 | 生成项目时要装进项目的包 | 只放骨架必需项,业务依赖交给插件 |
| 模板插件 | 声明默认挂载的能力插件 | 按最小集给,多一个都是负担 |
| 模板配置 | 会合并进项目配置的片段 | 只放基线值,环境相关一律留空 |
| 模板标签 | 用于分类与筛选 | 按平台矩阵、行业、复杂度打标 |
这里最需要展开的是“模板配置”的合并行为。合并通常是递归的,对象逐层合并,数组多数情况下是覆盖而不是拼接。这意味着如果你在模板里给了一个默认的插件数组,项目里再写一个插件数组,很可能模板里的那一份就没了。我一般的做法是:模板里只提供对象型基线配置,数组型配置明确写在文档里,要求项目侧补全。这一点官方文档不一定写得很清楚,属于实践推断,你用之前最好自己验一遍,验的方法很简单,生成一个项目看合并后的配置文件长什么样。
另一个经常被忽视的是模板标签。它看起来只是分类用的元数据,但在模板多起来之后,标签直接决定了交互式选择界面的可用性。团队里模板超过十个,没有标签体系,选模板就跟抽盲盒一样。我的习惯是按“平台组合”和“业务复杂度”两个维度打标,前者让工具链能筛出多端模板,后者让人能一眼判断这是五分钟能用的还是需要配置半天的。
2.3 配置合并的优先级顺序
合并优先级这件事,值得单独占一小节,因为它决定了“在哪儿改配置才生效”。我实测下来,从高到低大致是这样的顺序:
- 项目本地显式写的配置,优先级最高,可以覆盖模板带来的任何基线值。
- 环境专属配置,比如按环境划分的配置目录里的内容,覆盖基线但不覆盖项目级显式配置。
- 模板带来的基线配置,作为兜底存在。
- 各平台的框架默认值,最底层。
这个顺序背后的逻辑是“越靠近具体项目,越应该有最终话语权”。模板是全局的、跨项目的,它的值必须能被单项目推翻,否则共享模板就变成了枷锁。环境配置高于基线,是因为同一份基线在不同环境下的取值本来就该不同,把它放在基线之上符合直觉。
实操中我踩过的坑是:有人把环境相关的东西写进了模板基线,然后在项目里怎么改都不生效,因为改的地方优先级不对。排查这类问题有个笨但有效的办法,就是把生成后的完整配置文件打印出来,对照上面这个顺序看目标字段最终落在哪一层。工具链一般有配置查看类的命令,没有的话就自己在构建脚本里打一行日志,成本很低,收益很高。
3. 五分钟做出第一个能跑的模板
前面讲的都是结构,这一节进入动作层面。我会按我自己实际的顺序走一遍:生成骨架、改声明、注册到本地、拉起验证。整个流程熟练之后确实能在五分钟内走完,但第一次做建议留出半小时,把每一步生成的结果都看一遍。
3.1 从官方模板生成骨架再改造
最快的起点不是从空目录手写,而是拿一个官方最小模板当底子,改造成自己的。流程大概是:先用命令行创建一个新项目,交互式选择最简模板;然后把这个项目里与工程骨架相关的部分抽出来,剔掉业务代码,整理成模板目录;最后补上描述文件。
这里有个判断标准值得说:什么该抽进模板,什么该留在业务项目。我的判断是“三条以上业务线都会用到的东西”才进模板。举个反例,鉴权流程看起来通用,但每个业务的登录方式、Token 策略、埋点要求都不一样,硬塞进模板的结果就是每个项目进来第一件事是先删掉模板里的鉴权代码。反例之外,像目录约定、代码规范配置、构建脚本封装、基础的主题变量这些,才是真正三条线都一样的,适合进模板。
抽取过程中我会刻意做两件事。第一件是把所有硬编码的字符串抽成占位符或配置项,包括应用名、包名、接口前缀、图标路径。第二件是删掉所有跟具体业务相关的资源文件,只留一份占位资源,避免模板包体积膨胀。这两件事看起来琐碎,但决定了这个模板半年后还能不能用。我见过一个模板里带着几十兆的演示图片,结果每次拉模板都要多等十几秒,团队里怨声载道。
改造完成之后,用描述文件把模板身份登记上去。名称建议带上组织或业务域前缀,避免和公共模板重名;版本从 1.0.0 起,严格遵守语义化版本,因为下游项目会按版本锁定。描述写清楚适用场景和平台覆盖范围,这行文案以后会被几十个人看到,值得多花两分钟。
3.2 本地注册与调试的正确姿势
模板写完了,怎么让工具链认识它?几种常见的来源形式:本地目录、包仓库里的包、代码仓库地址。开发阶段毫无疑问用本地目录,改一次立刻生效,不用打包发布,省时间。
本地注册的方式通常是在项目里添加模板来源,工具链会记录这个来源并在生成时解析。我一般会把本地路径写进一个单独的开发配置里,不提交到版本库,避免污染团队其他成员的配置。这一点很重要,本地绝对路径是最典型的“只在我机器上能跑”的来源。
调试阶段我关注三件事:
- 模板能不能被正确识别,也就是出现在模板列表里,名称和描述显示正常。
- 生成的项目里,配置文件是不是按预期合并的,重点看数组字段有没有被意外覆盖。
- 生成的项目能不能直接启动,先跑 Web 端最快,因为它对本地环境要求最低。
关于验证顺序,我强烈建议先 Web 后移动端。Web 端构建链路短、报错信息友好,能快速暴露依赖缺失、入口路径写错这类基础问题。移动端构建链路长,一个依赖冲突可能要等五分钟才报错,用它来做第一轮验证是浪费时间。等 Web 端跑通了,再上移动端,问题范围就小很多。
3.3 生成一个项目并逐项验证
生成项目的命令执行完之后,别急着写业务代码,先做一轮系统性验证。我自己的验证清单是这样的,你也可以直接拿去改:
| 验证项 | 检查内容 | 不通过的典型原因 |
|---|---|---|
| 目录结构 | 约定的目录是否齐全 | 模板目录被误删或命名不一致 |
| 配置文件 | 基线配置是否成功合并 | 描述文件里的配置字段拼写错误 |
| 依赖安装 | 依赖是否装全、有无警告 | 模板依赖与项目依赖版本冲突 |
| 入口启动 | Web 端能否正常渲染 | 入口文件路径与实际不符 |
| 平台构建 | 至少一个移动端能出包 | 平台工具链版本不匹配 |
| 资源引用 | 图标、字体是否可加载 | 资源路径写成了绝对路径 |
这份清单里,我踩得最多的是第四项和第五项。入口路径的问题是模板里写了相对路径,但生成后目录层级变了,相对路径就失效了。平台构建失败的原因通常和模板无关,是本地环境的问题,所以遇到这一类,先确认环境再怀疑模板。
还有一个细节:生成完之后,把项目里最终生效的完整配置导出来看一眼。这一步能发现大量隐性问题,比如某个基线值被更高优先级的空值覆盖了,或者某个插件声明被数组覆盖规则吃掉了。花两分钟看这一眼,比后面花两小时排查要划算得多。
4. 把模板做成企业级基建的关键改造
能跑通是及格线,能支撑多条业务线长期演进才算合格。这一节讲的是我在实际项目里做的几项改造,每项改造都对应一个真实存在的痛点,不是为了架构好看而架构。
4.1 多环境配置与参数注入方案
企业级项目和玩具项目的最大差别就是环境多。开发、测试、预发、生产,可能还有内部体验环境。如果模板里写死一套配置,每个环境都要手动改,出错是必然的。
我的做法是把配置按环境拆开,模板只提供基线,环境和差异全部下沉到项目侧。目录上大致是这样的结构:
appConfigs/ ├── base/ │ └── renative.json # 基线,来自模板 ├── develop/ │ └── renative.json # 开发环境覆盖 ├── staging/ │ └── renative.json # 预发覆盖 └── production/ └── renative.json # 生产覆盖环境目录里只写与基线不同的部分,相同的继承下来。这样做的直接好处是 diff 干净,review 的时候一眼能看出这个环境改了什么。间接好处是环境新增成本极低,加一个新环境就是加一个目录,写三五个字段的事。
敏感参数的注入要单独处理。签名证书、密钥这类东西绝对不能进配置文件和模板,我的做法是通过构建时的环境变量注入,配置里只留占位符引用。这里要注意一点,占位符的解析发生在构建期还是运行期,不同工具链行为不一样,需要实测确认。如果是运行期解析,那构建产物里可能还留着占位符字符串,得在启动时做兜底校验,否则线上会拿到一个字面量路径,排查起来很费劲。
参数注入还有一个实践细节:给所有可注入参数一个明确的默认值。默认值不是为了生产用,是为了让项目在缺少注入时能启动起来、能跑本地调试,而不是直接崩掉。这个习惯能省掉大量“新同学拉下代码跑不起来”的沟通成本。
4.2 私有模板源与版本策略
模板要共享,就得有地方存。小团队可以直接放在代码仓库里,用目录或子模块的方式引用。团队规模上去之后,我更推荐走私有包仓库,因为包仓库自带版本管理、权限控制、发布记录,这三点是代码仓库给不了的。
版本策略上,我的建议是模板版本和项目版本解耦,但要有明确的兼容承诺。具体来说,模板遵循语义化版本:补丁版本只改文档和注释,次版本加配置项且保持向后兼容,主版本才允许破坏性变更。项目侧锁定到次版本范围,既能拿到修复,又不会被破坏性变更打到。
升级节奏也需要约定。我的经验是模板的主版本升级必须配迁移说明,写明哪些字段被删了、哪些默认值变了、存量项目该怎么改。没有迁移说明的主版本升级,基本等于逼着下游项目放弃升级,模板就僵化了。迁移说明不用写得多正式,一个变更清单加几段示例配置就够,关键是别省略。
另外一个容易被忽略的点是模板的废弃策略。模板也会过时,会有一批模板没人用但还挂在列表里,新同学选模板的时候会被干扰。我一般会定期做一次盘点,把没人引用的模板归档,归档之前先在文档里标注废弃原因和推荐替代,保留半年再删。这个流程听起来麻烦,但比让十个模板自生自灭要好。
4.3 与构建流水线的衔接方式
模板最终要落到流水线里才能体现价值。我理解的衔接分两条链路:模板自身的发布链路,和项目侧的构建链路。
模板发布链路要做的事是:模板变更后自动跑一遍校验,包括文件结构检查、描述文件字段校验、以及最关键的“冒烟生成”——自动生成一个项目并跑一遍 Web 端构建。这一步能拦住绝大多数低级错误,比如描述文件写错导致模板不可用。我见过有团队模板直接手改手发布,结果某次改完所有新项目都起不来,回滚花了半小时,而这道冒烟检查的成本不到两分钟。
项目侧构建链路要做的事是:构建开始时确认模板版本符合预期,配置合并结果能被审计。做法通常是在构建脚本开头打一行日志,输出模板名称、版本、配置来源,这行日志在排查线上问题时价值极高,能立刻确认“这次构建到底用的是哪套骨架”。
还有一处值得做的是变更感知。模板更新后,存量项目不会自动跟着变,这是设计使然,但团队需要一个机制知道“我的项目落后了”。简单做法是定期跑一个脚本,扫描所有项目的模板版本,生成一份落后清单,按业务重要度排序,安排升级。复杂做法是接进流水线做强制检查,但我不建议一上来就强制,先让大家看到清单,形成习惯之后再收紧,阻力会小很多。
5. 踩坑实录与问题排查速查
这一节全是实打实踩出来的。模板系统的问题有个特点:报错信息往往指向业务代码,但根因在模板配置,所以排查方向很容易跑偏。下面这几个是我遇到频率最高的,附上我的定位思路。
5.1 依赖版本错位导致构建失败
最典型的表现是:模板单独看没问题,生成项目也没报错,但一构建就出现模块找不到或者重复实例化的错误。根因通常是模板声明了一套依赖版本,项目侧又装了另一套,包管理器在提升依赖的时候产生了两个版本共存。
定位方法是把依赖树打出来,搜关键包名,看有没有出现两个不同版本。有的话,先确认是谁引入的,通常能顺着依赖链找到模板声明的那一条。解决方式有两种:一是在项目侧显式锁定版本,把冲突压平;二是修改模板的版本范围,放宽或收紧。我倾向第二种,因为问题根源在模板,压平只是掩盖。
这里有个预防措施很有效:模板的依赖声明尽量用宽松范围,把具体版本锁定交给项目侧的锁文件。模板管“需要什么”,项目管“用哪一版”,职责清晰,冲突自然少。反过来,如果模板里写死精确版本,项目侧想升某个包就必须改模板,非常别扭。
注意:依赖冲突的报错信息经常具有误导性,指向的是一个完全无关的模块。遇到这类报错,第一反应应该是打依赖树,而不是去看报错指向的那个文件。
5.2 平台工程目录被覆盖的现场
这个坑我踩过两次,损失都不小。场景是这样的:模板里不小心带上了平台原生工程目录,生成项目的时候把本地的签名配置、本地属性文件覆盖掉了。表现是构建突然要求重新配置签名,或者本地调试的接口地址被重置。
根因是模板把“生成产物”当成了“源文件”。平台原生工程目录本质上是构建工具生成的中间产物,虽然有时候需要手动改,但它不适合作为模板内容分发。正确的处理方式是只分发配置补丁,让构建过程自己生成完整目录。
已经出了这个问题怎么救?如果项目在版本库里,直接回滚被覆盖的文件最快。如果没在版本库里,那就得靠本地备份或者重新配置。这也是我坚持“平台目录不进版本库、但关键本地配置要有备份”的原因。听起来矛盾,其实是分层的:生成产物不进版本库,但生成产物依赖的本地私密配置要单独备份。
提示:模板里凡是会被构建工具重新生成的目录,一律不要纳入。判断标准很简单,删掉它还能通过构建重新生成,那就是生成产物。
5.3 常见问题速查表
把上面这些和另外几类高频问题整理成一张表,出问题的时候按表扫一遍,能覆盖八成场景:
| 现象 | 高概率原因 | 处理动作 |
|---|---|---|
| 模板列表里看不到自定义模板 | 描述文件缺失或字段非法 | 校验描述文件结构与必填字段 |
| 生成后配置项不生效 | 优先级被更高层覆盖 | 导出完整配置逐层比对 |
| 数组配置丢失 | 数组合并策略是覆盖 | 项目侧完整声明数组 |
| 本地能跑、流水线失败 | 本地路径或环境变量未注入 | 检查构建脚本的变量来源 |
| 模板升级后项目报错 | 主版本破坏性变更 | 按迁移说明逐项调整 |
| 构建产物异常变大 | 模板带了演示资源或二进制 | 清理模板资源目录 |
| 跨系统构建失败 | 路径分隔符或大小写敏感差异 | 统一用路径拼接 API,检查文件名 |
这张表里我最想强调“本地能跑、流水线失败”这一条,因为它最难排查。本地有环境变量、有缓存、有全局安装的工具,流水线里什么都没有。我的习惯是每次改完模板,都想办法在一个干净环境里跑一次生成和构建,哪怕只是临时起个容器。这一步能提前暴露九成的环境依赖问题。
6. 关于模板粒度的一点个人取舍
写到这里,前面把结构、流程、改造、排查都过了一遍,最后我想聊一个没有标准答案的问题:模板到底该做多细。我试过两个极端,一个是极简模板只给目录结构和基础配置,另一个是重模板把代码规范、构建脚本、主题、甚至常用页面骨架全塞进去。
极简模板的好处是灵活,坏处是每条业务线都要自己搭一遍,半年后各条线的工程结构差异很大,想统一做点基建改造就得挨个适配。重模板反过来,起步统一,但升级成本高,任何一处变更都要考虑所有下游,而且容易把不该通用的东西固化下来。
我现在的位置在中间偏极简:模板只固化三样东西,目录约定、构建脚本封装、平台配置基线,其余全部交给插件和项目本地。判断某个东西要不要进模板,我会问自己一句,如果一年后有一个业务线想改掉它,改起来是改模板容易,还是改项目配置容易。答案通常是后者,那就别进模板。
还有一个实操小技巧值得分享。模板的改动我一般不直接改,而是先在一个真实项目里改,跑通两周,确认没问题再反哺回模板。这样能避免把试验性的东西直接推给所有下游。模板越是被依赖,变更就越要谨慎,这个节奏感需要时间培养,急不来。
另外建议给每个模板配一份极简的使用说明,不超过一页,写清楚适用场景、需要注入哪些参数、生成后第一步该做什么。这份说明的阅读量远超正式文档,因为大家都是在赶时间的时候才会去翻它。