Pony Gets a Template Engine
摘要
Pony语言获得新模板引擎,深度实现上下文感知的HTML转义机制,为构建Phoenix-like Web框架铺平道路。
核心亮点
- 上下文感知HTML转义:状态机跟踪标签、属性名、属性值、CSS、JavaScript上下文
- URL属性安全过滤:过滤javascript:、vbscript:、data:等危险URI scheme
- 事件处理器保护:onclick等事件处理器自动进行JavaScript字符串转义
- TemplateSink接口:支持LiveView风格差量渲染,最小化网络传输
- 参考Go的html/template:借鉴成熟设计理念
深度分析
大多数模板引擎使用单一转义函数处理所有变量,将<转换为<,>转换为>,&转换为&。这种方法在变量位于文本内容时可以工作,但在其他上下文中存在安全问题:
<a href="{{ url }}">{{ label }}</a>
<button onclick="{{ handler }}">Click</button>
<div style="color: {{ color }}">{{ text }}</div>
四个变量,三个不同HTML上下文。{{ url }}在URL属性中,entity encoding无法阻止javascript:alert('xss')执行。{{ handler }}在JavaScript事件处理器中。{{ color }}在CSS上下文中。每个上下文需要不同的转义规则。
Pony的HtmlTemplate不回避这个问题。状态机逐字符处理模板字面量段,跟踪当前是否在标签中、属性名中、属性值中(哪种类型)、注释中、脚本块中、样式块中,还是纯文本中。当模板遍历器到达变量时,状态机知道当前上下文。正确的转义自动跟随:
- 文本内容和常规属性:entity encoding
- URL属性(href, src, action):scheme过滤 + percent-encoding
- 事件处理器(onclick等):JavaScript字符串转义
- style属性:CSS值转义
- 注释:--序列剥离
TemplateSink与差量渲染
TemplateSink是应对真实问题的通用渲染接口。传统方式是调用render()获取字符串。但Livery (LiveView风格框架) 需要更精细的控制。
Livery思路:服务器渲染HTML但不每次发送整个页面。它找出变化的最小差异并发送。对于差量渲染,渲染层需要分离静态模板内容和动态值。静态({{ }}标记之间的HTML结构)在渲染之间从不改变。动态(被替换的值)可能会改变。
接收器接收交替的literal和dynamic_value调用。严格交错:对于N个动态插入,正好N+1个literal调用,从literal开始和结束。控制流块将其渲染输出折叠为单个动态值。
Livery的RenderSink实现此接口。第一次渲染时,收集静态和动态内容,发送到客户端并缓存。每次后续渲染时,将每个新动态值与缓存的先前值比较。只有实际改变的槽的索引和值通过WebSocket发送。如果没有变化,什么都不发送。
生态进展
Pony Web开发栈正在逐步成形:
- Lori - 网络通信
- Stallion - HTTP服务器
- JSON - 标准库中的JSON支持
- Templates - 页面渲染
- Livery - LiveView风格交互式UI
目标是构建Phoenix-like Web框架。基于Pony的actor模型和引用能力,提供服务器渲染HTML和最小化有线传输的交互式UI体验。
核心洞察
"It figures out the context for you. The right escaping follows automatically."
上下文感知转义是成熟的设计模式。Go的html/template已经实现了多年。这是一个好想法,比大多数模板引擎做的要好。知道有一个好的解决方案存在,复制它不是愚蠢的行为。