CHARLIE SAYS

查理如是说
DATE 2026-08-24
THEME
SERIES / JHIPSTER / P-230 · JHipster 项目开发

JHipster 开发 15:改造 UI——不动生成代码的优雅方式

每个 JHipster 项目上线前都逃不过一个问题:“这界面太’JHipster 脸’了,能不能改成我们的品牌?“答案是能,而且不用跟生成器开战。这一篇讲我们团队把生成的界面改出品牌感的完整套路——核心原则只有一句:样式靠主题层,结构靠包装层,内容靠配置层,生成文件本身能不动就不动

为什么不动生成代码

先算账。改生成文件的成本不在今天,在下一次:

  1. jhipster upgrade 或实体再生成时,生成器会按模板重写文件,你的修改与模板冲突,merge 噩梦开始;
  2. Code review 无法区分”生成器升级带来的差异”与”同事的改动”,评审质量塌方;
  3. 生成文件里带着标记注释(模板片段的起止注释)与 .yo-rc.json 的记录,二者共同构成”再生成”的契约——改文件相当于单方面撕毁契约。

.yo-rc.json 记录了所有生成选项(前端框架、样式基座、认证方式……),.jhipster/Customer.json 记录每个实体的字段与配置。这两个文件是”声明”,生成代码是”投影”。改投影不改声明,投影迟早会被覆盖

团队的红线写在贡献指南里:entities/*/ 下生成文件的 HTML 结构与 service 类签名、shared/ 下的组件实现、admin/ 全目录,禁止直接修改;例外仅限”再生成也必须保留的手写业务逻辑”,且要在文件头注释标明 // 手写区:不会被再生成覆盖的扩展点

主题与品牌:CSS 变量一层就够

生成的样式基座是 SCSS + 全局变量,主题化的正确位置在 content/scss/(或 src/main/webapp/content/)下的全局样式层。做法是定义品牌 design token:

// content/scss/_brand-variables.scss
:root {
  --brand-primary: #1a56db;
  --brand-primary-hover: #1746b5;
  --brand-danger: #d92d20;
  --brand-bg: #f7f8fa;
  --brand-sidebar-bg: #101828;
  --brand-sidebar-text: #eaecf0;
  --brand-radius: 8px;
  --brand-font-family: 'Inter', 'PingFang SC', sans-serif;
  --header-height: 56px;
}

再在全局层做一次组件级映射,把 UI 框架的变量指向品牌 token:

// content/scss/_theme-bridge.scss(桥接层)
:root {
  --bs-primary: var(--brand-primary);       // 基座组件变量 -> 品牌 token
  --bs-border-radius: var(--brand-radius);
}

三层结构:

文件职责谁维护
token 层_brand-variables.scss品牌色板/字体/圆角设计侧
桥接层_theme-bridge.scssUI 框架变量映射前端侧
覆盖层_overrides.scss少量结构样式微调前端侧,压缩到最小

换品牌 = 换一个 token 文件。我们给两个客户交付同一套系统时,差异就只有 30 行 SCSS 与一张 logo。

想要 Tailwind 也可以:构建里接入 Tailwind,新写的手写组件用原子类,生成组件继续走桥接层的 CSS 变量。注意 preflight 可能重置基座框架样式,按需禁用部分 preflight——两套并存的关键是”各管各的 selector”。

logo 与静态资产属于”零风险替换区”:content/images/ 下把 logo-jhipster.png 换成自家品牌图(文件名保留,引用处不动),favicon 与登录页背景同理;字体放 content/fonts/ 并在 token 层引用。这一节做完,系统从”JHipster 脸”变成”公司脸”就完成了六成——剩下四成在首页与布局,见后文。

覆盖生成组件:三种姿势

生成的 list/detail/update 组件迟早有满足不了的一天。三种可选方案,侵入性递增:

方案做法适用代价
包装组件新建自己的组件,内部嵌生成组件加外围信息、改布局外壳最低
同名替换路由指向自建组件,彻底不走生成的页面交互完全重构高,等于自己维护
微前端Module Federation 挂独立子应用大团队独立发布最高,基建成本大

包装组件(默认推荐):

@Component({
  selector: 'jhi-customer-page',
  template: `
    <app-page-header title="客户管理" [subtitle]="deptName()">
      <button jhiHasAnyAuthority="ROLE_ADMIN" (click)="export()">导出</button>
    </app-page-header>
    <jhi-customer />   <!-- 生成的列表组件原样嵌入 -->
  `,
})
export class CustomerPageComponent {
  deptName = input<string>('');
}

然后把路由从生成组件改指向包装组件。改的是路由配置(我们自己的文件),不是生成组件本身——这是绕开”不许改生成文件”红线的关键手法。

同名替换用于交互彻底重写的场景(比如把 CRUD 列表改成看板)。生成的四件套保留在原地(不删,保持可再生成),路由指向我们在 app-own/(自定义目录)下的实现,视觉与交互完全自主。代价是这块从此脱离生成器保护,字段变更要手动同步——所以每次启用前我们都会问一遍:真的到这个程度了吗?多数时候答案是包装就够了。

微前端只在大组织里有意义:三个团队各自拥有独立前端、独立发布节奏,host 壳应用通过 Module Federation 在运行时拼装。JHipster 生成器没有内置这个选项,属于完全自建的部分,别为了技术尝鲜上——通信契约、版本协商、样式隔离每个都是长期税。

首页改造:home 的正确打开方式

home/ 是生成器明确预期你会改的地方(它没有任何业务价值,纯展示)。直接改 home.component.html 不算破坏契约——home 不随实体再生成。我们的首页演进路径:

  1. 阶段一:改标题文案 + 放产品营销图(10 分钟);
  2. 阶段二:按角色显示快捷入口卡片(读 AccountService 的 authorities 信号,未登录显示登录引导);
  3. 阶段三:接入运营数据概览(几个 httpResource 指向聚合 API),变成工作台。

顺手把 layout/navbar/ 的菜单换成业务菜单——layout 目录与 home 一样属于”预期内可改”区域,改造安全性高。

navbar 改造的实操要点:

  1. 菜单项数组放 TS 里集中声明(menuItems),每一项绑定 authority,模板里用 @if + 权限信号控制可见——菜单与路由守卫的权限声明保持同一套常量(config/authority.constants.ts),别在两处写字符串;
  2. 多级菜单自己写下拉交互,或者引 UI 库的 menu 组件,生成的 navbar 只是单级;
  3. 移动端汉堡菜单在生成的 layout 里已有响应式骨架,改样式时保留断点类名,别把移动端适配改丢了——这是设计稿评审时最容易漏验的场景;
  4. 面包屑(breadcrumb)生成器没有提供,属于自建件,放 shared/breadcrumb/,读当前路由的 data.pageTitle 渲染即可与 i18n 打通。

i18n 文案:一个 key 都不要硬编码

JHipster 全站 i18n(第 27 篇展开),改文案的标准路径:

src/main/webapp/i18n/zh-cn/
├── global.json          // 全局:菜单、按钮、提示
├── home.json            // 首页
├── customer.json        // 实体相关:字段名、校验提示
└── error.json           // 错误码翻译

改”客户”为”业主”?改 customer.json 里的 myApp.customer.home.title 等若干 key 即可,组件模板里的 jhiTranslate 管道引用 key,一处改全站生效。后端错误提示同理:第 11 篇的 errorKey(order.alreadyShipped)在 error.json 里配中文,前端 alert 组件自动翻译。在模板里写中文字面量是 code review 的 block 级问题——文案散落的系统做不了多语言,也说不清”哪个文案是谁的”。

生成器标记的尊重

打开任何生成文件,会看到成对的模板标记注释(标识生成区块的起止)。三条团队纪律:

  1. 手写代码插入在标记区块之外,或放到独立文件;
  2. 删标记注释 = 声明”此文件脱离生成器”,需在 MR 里说明并登记清单;
  3. 每次升级后跑 git diff 复查登记清单里的文件是否被模板更新波及;
  4. 需要判断”这个文件能不能改”时,先查 .jhipster/*.json 里有没有它的来源记录——有来源的都有再生成的可能。

登记清单(docs/deviation-from-generator.md)是个看似土但救命的东西:升级 JHipster 9 → 10 时,它就是”哪些文件要手工对账”的工作量清单。清单用表格维护三列:文件路径、偏离类型(替换/包装/内嵌手写)、原因与负责人。我们升级 8 → 9 时清单上有 17 个文件,对照清单两小时完成对账;同期另一个没做登记的项目,光 diff 就看了三天。

小结

  • 不动生成文件的本质是保护”再生成”契约:.yo-rc.json.jhipster/*.json 是声明,代码是投影;
  • 主题三层法:token 层定品牌、桥接层做映射、覆盖层最小化,换客户换 token 即可;
  • 覆盖组件从包装开始,同名替换是例外,微前端是组织级决策;
  • home 与 layout 是预期内可改区;文案只走 i18n key,禁止字面量;
  • navbar 菜单集中声明、权限常量与路由守卫同源,移动端断点别改丢;
  • 偏离登记清单让下一次大版本升级从三天变两小时。

系列导航

← 软件工程 015:开发实践:领域驱动开发(DDD) 目录 开发工具 015:正则表达式:在线工具汇总 →
← 返回文章列表