Magxe 开发日志 3 - 实现文章页

在上一篇开发日志写完后不久,做了几件微小的工作就完成了文章页的模板渲染,于是版本号终于能进入 0.0.1 Alpha 了,实际上关于 Handlebars Helper 的改变很简单,都是基础的 CURD,不过由于 Handlebars.Net 本身的一个 bug 导致了进度的缓慢。这里还是想吐槽一下.NET 的生态问题,.NET 技术栈好吗?当然好啦,Entity Framework、ASP.NET 可以对扛 Java 三大件,PostSharp、Newtonsoft.Json 也可以渗入程序的各个方面并且 API 友好,但是除了这些大型应用呢?遗憾的是个人需求无法找到能够满足需求的依赖,可能有多个不同方面的实现,但是无法汇聚为一个开箱即用的框架。

contentFor 块级 Helper

这个功能并不包含在原生 Handlebars.js 中,而只是 Node 后端开发为了增强模板复用而新增的 Helper,作为一个 Express 框架的扩展:express-handlebars。实现起来也非常简单,也就是新增两个关键字 “contengFor” 和 “block”,一个负责将元素压入堆栈,而另一个负责将堆栈中的模板弹出。
如果直接在源码中集成这个功能,是比较简单的,甚至还可以进一步优化由模板生成的 AST,但似乎 Handlebars.Net 的作者并不希望为其增加更多的功能,见 Any plan about adding support of ‘block’ and ‘contentFor’?

不过用注册 Helper 的方式来实现也并不复杂,只需要建立一个公共的 Dictionary 就可以啦。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
internal static class BlockAndContentForHelper
{
internal static readonly Dictionary<string, string> Blocks = new Dictionary<string, string>();
}

internal class ContentForHelper: HandlebarsBaseHelper {
public ContentForHelper() : base("contentFor", HelperType.HandlebarsBlockHelper) {}

public override void HandlebarsBlockHelper(TextWriter output, HelperOptions options, dynamic context, params object[] arguments) {
var key = arguments[0].CastTo < string > ();
var sb = new StringBuilder();
using(var sw = new StringWriter(sb)) {
options.Template(sw, null);
}
Blocks[key] = sb.ToString();
}
}

internal class BlockHelper : HandlebarsBaseHelper
{
public BlockHelper() : base("block", HelperType.HandlebarsHelper)
{}

public override void HandlebarsHelper(TextWriter output, dynamic context, params object[] arguments)
{
var key = arguments[0].CastTo<string>();
if (Blocks.ContainsKey(key))
{
output.WriteSafeString($"{Blocks[key]}\n");
Blocks[key] = string.Empty;
}
else
{
Blocks[key] = string.Empty;
}
}
}

这里我敏捷地用了一个静态属性来作为容器是非常有 bad smell 的,目前我无法得知在多并发环境下会导致什么样的结果,如果未来这里出现问题将考虑其他实现方式。

Variables 和 Helper 执行顺序混乱

这是 Handlebars.Net 中的一个 bug,在 Added ViewEngine / Layout support 有人为其增加了渲染 Layout 的功能,但是由于模板嵌套的情况过于复杂,并没有直接使新增的 VM 继承原有的,而是直接增加了一个 DynamicViewModel 类,但是在后期绑定中又没有对模板的内容做出判断,导致无法反射出 VM 中的却存在的内容,暂时的解决方案是每次绑定都要判断 VM 的类型,虽然不够优雅,但是要解决这个问题可能需要大面积地重构,也就先放一放。详见 Fixes error binding context in layout

让 Helper 基于接口调用

暂时还没有看完 express-handlebars 的源码,尚不清楚在 JS 里是如何解决复杂的模板嵌套问题的,而 Ghost 主题会在不同的模板中多次调用 @[PropertyName] 来请求一个数据,这就令 Helper 无法对当前渲染的上下文作出假设,而静态类型语言在运行时调用同名属性又非常不优雅,因此让所有 VM 去实现和 Helper 以及 View 相协定的接口,以避免脏代码的出现。