
本文详解如何使用 Handlebars 的 @key 数据变量,无需预处理或 lookup 辅助,直接渲染三层嵌套对象(类别 → 应用 → 告警类型 → 告警数组)为结构化 HTML 表格,适用于邮件通知等动态内容场景。
本文详解如何使用 handlebars 的 `@key` 数据变量,无需预处理或 lookup 辅助,直接渲染三层嵌套对象(类别 → 应用 → 告警类型 → 告警数组)为结构化 html 表格,适用于邮件通知等动态内容场景。
在构建用户订阅类 HTML 邮件(如应用告警汇总)时,后端常以深度嵌套字典形式组织数据:顶层为业务类别(如 "Requests" 和 "Services"),中间层为动态应用名称(如 "Division Map Layers"),底层为告警类型键(如 "Upvotes"、"Tagged Users"),其值为告警对象数组。Handlebars 原生支持这种结构——关键在于善用 @key 数据变量,它能在 {{#each}} 迭代对象时自动捕获当前键名,从而避免冗余的数据扁平化或自定义 Helper。
核心模板逻辑:三层 {{#each}} + @key 驱动标题
Handlebars 不支持直接遍历对象键名(如 JavaScript 的 Object.keys()),但 {{#each obj}} 在作用于普通对象时,会隐式迭代其可枚举属性值;此时 @key 即为对应属性名。我们据此构建三级嵌套循环:
- 第一层:遍历根对象({{#each this}}),@key 为 "Requests" 或 "Services"
- 第二层:遍历每个类别下的应用对象({{#each this}}),@key 为 "Division Map Layers" 等应用名
- 第三层:遍历每个应用下的告警类型对象({{#each this}}),@key 为 "Upvotes"、"Messages" 等类型名
- 第四层(数组):对 this(即告警数组)再次 {{#each this}},直接访问 alert_message 和 createdAt
以下是完整、可运行的模板代码(含语义化 CSS):
<script id="Template" type="text/template">
<div class="mega-menu">
{{#each this}}
<div>
<!-- 类别标题 -->
<div class="main">{{@key}}</div>
<!-- 遍历该类别下的所有应用 -->
{{#each this}}
<div class="app-div">
<!-- 应用标题 -->
<div class="app-name">{{@key}}</div>
<!-- 遍历该应用下的所有告警类型 -->
{{#each this}}
<div class="alert-name">{{@key}}</div>
<div class="app-div">
<!-- 告警列表表格 -->
<table>
<thead>
<tr>
<th style="width: 600px">Message</th>
<th style="width: 100px">Date</th>
</tr>
</thead>
<tbody>
<!-- 遍历当前告警类型的每条记录 -->
{{#each this}}
<tr>
<td>{{alert_message}}</td>
<td>{{createdAt}}</td>
</tr>
{{/each}}
</tbody>
</table>
</div>
{{/each}}
</div>
{{/each}}
</div>
{{/each}}
</div>
</script>
<style>
.mega-menu { border: 1px solid aquamarine; padding: 8px; font-family: sans-serif; }
.main { font-size: 24px; font-weight: bold; color: red; margin-bottom: 16px; }
.app-div { margin-left: 20px; }
.app-name { font-size: 18px; font-weight: bold; color: blue; margin-bottom: 8px; }
.alert-name { font-size: 16px; font-weight: bold; color: green; margin-bottom: 4px; }
table { width: 100%; margin-bottom: 8px; }
table, th, td { border: 1px solid lightgrey; border-collapse: collapse; }
th, td { padding: 2px 4px; }
th { font-size: 12px; text-align: left; color: grey; }
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.7.7/handlebars.min.js"></script>
<script>
const template = Handlebars.compile(document.getElementById('Template').innerHTML);
// 示例数据(与问题中完全一致)
const data = {
"Requests": { /* ... */ },
"Services": { /* ... */ }
};
document.body.innerHTML = template(data);
</script>注意事项与最佳实践
- ✅ 无需重构数据:本方案完美适配原始嵌套字典结构,不强制要求转为数组或添加 name 字段,保持后端数据契约稳定。
- ⚠️ @key 仅在 {{#each}} 中有效:切勿在 {{#if}} 或普通表达式中尝试 {{@key}},它只在迭代上下文中存在。
- ? 避免命名冲突:若数据中某层对象本身含有 key 属性(如 { key: "foo", value: "bar" }),@key 仍指向字典键名,而非该属性值——这是预期行为,无需担心覆盖。
- ? 邮件兼容性增强:实际用于 HTML 邮件时,建议将 CSS 内联(使用工具如 Premailer),并为
添加 role="presentation" 和 cellspacing="0" 提升客户端兼容性。
- ? 调试技巧:在模板中临时插入 {{log @key}} 或 {{json this}}(需注册 log/json Helper)可快速验证当前层级数据结构。
通过 @key 驱动的嵌套迭代,Handlebars 将复杂字典转化为清晰、可维护的模板逻辑。它不仅解决了多级动态标题的渲染难题,更体现了声明式模板引擎“数据即结构”的设计哲学——让开发者专注描述 what to render,而非 how to traverse。










