知序
返回
保存草稿
发布
H1
H2
H3
1
2
3
Excalidraw 图
AI 写作
AI 优化
目录
输入内容后显示目录
Markdown
# 11 - 实战:模板渲染 > 这篇解决什么问题:你在做服务端渲染(SSR),要把一份数据拼进 HTML——`<h1>商品列表</h1>`、下面一排商品列表。用 Java 拼字符串太痛、改样式就得重编译。你想把"渲染"这件事交给一段 JS 模板:Java 这边只管把数据准备好递过去,JS 那边出 HTML。但这里真正难的其实不是"拼字符串",而是**跨语言的数据该怎么桥接**——为什么 List 塞进去就报错、为什么非要套一层代理。 > 读完你会知道:为什么用 JS 做模板渲染、一个"传模板 + 传数据 → 返回 HTML"的统一调用契约、两段式迷你模板引擎的完整原理(`{{#each}}` 循环块 + `{{var}}` 插值)、`ProxyObject.fromMap` 与 `ProxyArray.fromList` 各自的职责,以及 `items` 这种数组为什么必须经代理包装才能被 JS 的 `.map/.join` 消费。 --- ## 1. 为什么把模板渲染丢给 JS 更划算 ### 1.1 三个理由 服务端渲染里,模板引擎的职责是把数据"拼进"HTML。把这部分逻辑放到 JS 侧,有三个实打实的理由: - **JS 模板生态成熟**:Handlebars、Mustache、EJS 等现成库摆在那,语法团队早就熟。 - **模板即数据**:模板字符串存库/配置,改版式不用发版 Java——和上一页规则引擎"规则即数据"是同一套哲学。 - **Java 侧保持纯净**:Java 只组装数据(`Map`/`List`),渲染那步完全交给 JS。 ### 1.2 统一调用契约 先盯住那个最重要的接口契约,后面所有代码都在为它服务——**传模板 + 传数据 → 返回 HTML**: ``` ┌─────────────┐ ProxyObject / ProxyArray ┌─────────────────────┐ │ Java 数据 │ ───────────────────────────────▶ │ JS 模板引擎 │ │ Map/List │ │ render(tpl, data) │ └─────────────┘ └──────────┬──────────┘ │ asString() ▼ ┌─────────────────────┐ │ 渲染后的 HTML 字符串 │ └─────────────────────┘ ``` 这个契约的美妙之处在于:**你用的是哪个模板引擎,Java 侧完全不用知道。** 自己实现的两行正则迷你引擎也好、生产里换成 Handlebars 也好,对 Java 都是同一个调用——"传字符串模板、传数据、接回 HTML 字符串"。这就是"接口隔离"换来的自由度。 下面这张图把"Java 给数据、JS 出 HTML"的流水线画出来:  *图:模板渲染:Java 给数据,JS 出 HTML* > 一句话:**模板渲染 = 一个固定契约「传模板 + 传数据 → 返回 HTML」,Java 与具体引擎彻底解耦。** --- ## 2. 真正难的不是字符串,是跨语言数据桥接 很多人以为值得讲的是"正则怎么写",其实模板渲染里最容易翻车的,恰恰是**数据怎么从 Java 安全地变成 JS 能用的形态**——这是跨语言互操作的老问题(08 篇的数据共享学习过)。先记住两条铁律,后面逐一拆: ``` Java 的 Map ──ProxyObject.fromMap──▶ JS 里的"对象"(能 .amount / .title) Java 的 List ──ProxyArray.fromList──▶ JS 里的"数组"(能 .map() / .join()) ``` **铁律一句话:JS 要当作"对象"用的 Java Map,用 `ProxyObject.fromMap`;JS 要当作"数组"用的 Java List,用 `ProxyArray.fromList`。** 桥接错位正是踩坑篇那条"ProxyArray 不能 push"的同族陷阱。 > 一句话:**渲染拼逻辑是表象,真正的功夫在把 Java `Map`/`List` 正确桥成 JS 的"对象/数组"。** --- ## 3. 一个零依赖的迷你模板引擎:逐行拆原理 本示例为了**零外部依赖**,自己实现了一个迷你的 JS 模板引擎(支持两种语法)。它只有两段正则,但正好让你看清"模板渲染的本质 = 数据注入 + 字符串替换"。 ### 3.1 迷你引擎的两种语法 ``` {{title}} → 变量插值(渲染成数据里 title 的值) {{#each items}}...{{this}}...{{/each}} → 循环块:对 items 的每个元素渲染一遍块体 ``` ### 3.2 运行结果(实测演示) ``` 渲染结果: <h1>商品列表</h1><ul><li>手机</li><li>电脑</li><li>平板</li></ul> ``` ### 3.3 定义 JS 渲染函数(两段正则) ```java context.eval("js", """ function renderTemplate(tpl, data) { // 1) {{#each items}}...{{/each}} 循环块(正则中的斜杠需转义书写) tpl = tpl.replace(/\\{\\{#each ([^}]+)\\}\\}([\\s\\S]*?)\\{\\{\\/each\\}\\}/g, (_, key, body) => data[key].map(item => body.replace(/\\{\\{this\\}\\}/g, item)).join('')); // 2) {{var}} 普通变量插值 return tpl.replace(/\\{\\{([^}]+)\\}\\}/g, (_, key) => data[key] ?? ''); } """); ``` 这里的退避符有个大坑:**JS 源码是写在一个 Java 文本块里的,正则里的 `\` 要写成 `\\` 转义**(比如 `/\\{\\{/` 实际传给引擎的是 `/{{/`——这是踩坑篇第 6 条)。所以读代码时,你要在心里"还原一层",`\\{` 就是 `\{`,`\\s` 就是 `\s`。 两段正则是这样分工的: - **第 1 步**:`{{#each key}}...{{/each}}` —— 匹配循环块,取出 `key` 和块体 `body`;对 `data[key]`(一个数组)逐元素把块体里的 `{{this}}` 替换成该元素,再 `join('')` 拼成一段。例如 `{{#each items}}<li>{{this}}</li>{{/each}}` 对 `["手机","电脑","平板"]` 渲染出 `<li>手机</li><li>电脑</li><li>平板</li>`。 - **第 2 步**:剩余的 `{{key}}` —— 普通插值,替换为 `data[key]`;`?? ''` 表示取不到就空串。例如 `{{title}}` → `商品列表`。 > 一句话:**迷你的本质是正则两连发——先拆循环块、再填普通插值,全过程就是数据注入 + 字符串替换。** --- ## 4. 完整调用链:从取函数到出 HTML ### 4.1 取出函数引用 ```java Value render = context.getBindings("js").getMember("renderTemplate"); ``` `function` 声明注册到了全局作用域,`getBindings` 把它取出来当作可调用的 `Value`。这里学过的"声明 vs 表达式"派上用场:用 `function 声明`,它才会挂到全局作用域(绑定)上,反手就能取。 ### 4.2 组装数据并注入(Proxy 桥接) ```java Map<String, Object> data = Map.of( "title", "商品列表", "items", List.of("手机", "电脑", "平板") ); context.getBindings("js").putMember("pageData", ProxyObject.fromMap(Map.of( "title", data.get("title"), "items", ProxyArray.fromList(List.of("手机", "电脑", "平板")) ))); ``` 这一块是重中之重,两个成员是两种桥法: - `title` 用普通字符串直接放进去——JS 拼字符串时直接拿来用。 - `items` **必须**用 `ProxyArray.fromList(...)` 包成数组——因为 JS 里要对它 `data[key].map(...)`,map 要求它是个可迭代数组。如果直接塞一个 Java `List`,JS 会以为"只是个普通对象",`.map` 就不存在,渲染直接崩。 ### 4.3 定义模板并调用渲染 ```java String tpl = "<h1>{{title}}</h1><ul>{{#each items}}<li>{{this}}</li>{{/each}}</ul>"; Value html = render.execute(tpl, context.getBindings("js").getMember("pageData")); System.out.println("渲染结果: " + html.asString()); ``` - 模板就是一段普通 Java 字符串,直接当参数传给 JS 渲染函数。 - `render.execute(tpl, pageData)`:调用 JS 函数,返回的还是 `Value` 包装的字符串,`asString()` 取回 Java 侧。 **渲染过程完整拆解:** ``` 模板: <h1>{{title}}</h1><ul>{{#each items}}<li>{{this}}</li>{{/each}}</ul> 第1步 each 块(items=[手机,电脑,平板]): {{#each items}}<li>{{this}}</li>{{/each}} → <li>手机</li><li>电脑</li><li>平板</li> 第2步 变量插值(title=商品列表): <h1>{{title}}</h1> → <h1>商品列表</h1> 结果: <h1>商品列表</h1><ul><li>手机</li><li>电脑</li><li>平板</li></ul> ``` > 一句话:**完整链路 = 取函数 → Proxy 注入数据 → 传模板执行 → `asString()` 拿 HTML,五步闭环。** --- ## 5. 两个关键 Q&A **Q:为什么 `items` 要 `ProxyArray.fromList`,而 `title` 直接放字符串?** JS 的 `data[key].map(...)` 要求 `data[key]` 是一个**可迭代的数组**。Java 的 `List` 只有经过 `ProxyArray.fromList` 包装,JS 才把它当成数组——才有 `.map`、`.join`。而 `title` 只是字符串,直接放进去 JS 拼就完事。**凡是"JS 要按数组操作"的 Java 集合,一律 Proxy 包装;只当值用的,扔进去即可。** **Q:示例的迷你引擎和 Handlebars 是什么关系?** 示例刻意不依赖外部 JS 库,用两行正则实现最小可用语法,目的是让你看清"模板渲染 = 数据注入 + 字符串替换"的本质。生产上把 `renderTemplate` 函数体换成加载 `handlebars.min.js` 后 `Handlebars.compile(tpl)(data)` 即可——**Java 侧调用方式一点不变**(还是传模板 + 传数据 → 拿 HTML)。这就是 1.2 那个契约的价值。 > 一句话:**代理包装决定数据"以什么身份"进入 JS;换引擎不改 Java 侧——因为接口契约被 `renderTemplate` 隔离了。** --- ## 6. 本篇小结 ```text ┌──────────────────────────────────────────────────────┐ │ 11 模板渲染 要带走的话 │ ├──────────────────────────────────────────────────────┤ │ 1. SSR 思路:Java 组数据 → JS 渲染 → 返 HTML │ │ 2. 固定契约:传模板 + 传数据 → 返回 HTML │ │ 3. Map 用 ProxyObject,List 用 ProxyArray │ │ 4. 迷你引擎两步走:each 循环块 + var 插值 │ │ 5. 换引擎不改 Java:函数体变了,调用契约不变 │ │ 6. 忘了:转义退避符在文本块里要写两遍 │ └──────────────────────────────────────────────────────┘ │ v 你已经能做出"Java 给数据、JS 出 HTML"的渲染流水线 ──→ 下一步:串起这些坑,看踩坑记录 ``` > 一句话收束:**渲染是表象,数据桥接是本——把 Map/List 正确桥成 JS 的对象/数组,一切模板都手到擒来。** ## 思考题(自测) 1. 如果模板要渲染嵌套循环(如"分类 → 分类下的商品列表"),这个迷你引擎的正则能处理吗?需要怎么扩展(提示:`{{#each}}` 要不要支持递归/标识符作用域)? 2. 模板引擎输出的 HTML 如果包含了用户输入(比如商品的 user-content),如何防 XSS?(提示:**转义是渲染层的义务,沙箱是执行层的隔离,两者是两回事,别混在一起**) 3. 换成 Handlebars 库时,加载 `require`/`load` 在沙箱下需要开什么权限?和示例的零依赖方案比,安全权衡如何?
预览
发布文章
添加摘要让读者更快了解文章内容
取消
确认发布
AI 写作助手
输入一个主题,AI 将为你生成一篇 Markdown 博客文章
AI 正在写作中...
取消
开始生成
AI 优化助手
描述你的修改需求,AI 将在原文基础上进行补充或微调
AI 正在优化中...
取消
开始优化
画布
从已有图里选
✕
图名
保存后生成 /images/excalidraw/<图名>.svg(只能用字母、数字、短横线、下划线、点)
取消
保存并插入