6 分钟阅读

FMPP 是一款用来处理 FreeMarker 模板的文本引擎工具。

FMPP 本身是用 Java 编写的,Node.js 也可以执行 jar 文件,这就给 Node.js 渲染 ftl 模板提供了可能。

模板渲染引擎有两个很重要的素材是模板和数据,FMPP 支持的数据类型非常丰富。

在实际使用过程中,发现 FMPP 在渲染 List 对象(即 JavaScript 中的 Array)时,如果 List 元素为对象,则输出的格式并不符合 JSON 规范,看下面的示例:

// 数据
const obj = [{'name':'HTML'}, {'name':'CSS'}];
// ftl 模板
${skills}

则输出的结果为:[{name=HTML}, {name=CSS}],这是一个字符串,并且格式并不符合 JSON 规范,我们希望输出的格式和数据本身保持一致。

当然,我们可以写函数转换成 JSON 格式(输出在 HTML 代码中),但处理过程比较繁琐,因为对象字段的值也可以是对象,比如:

// 数据
const skills = [{'name':{first:'HTML',last:'FREEMARKER'}}, {'name':'CSS'}];

输出结果为:[{name={last=FREEMARKER, first=HTML}}, {name=CSS}],处理起来就不方便了。

那会不会是 FreeMarker 本身的问题呢?我们可以使用下面的 Java 代码进行验证:

package com.test;

import freemarker.template.*;
import java.util.*;
import java.io.*;

public class FreeMarkerTest {

  public static void main(String[] args) throws Exception {

    Configuration cfg = new Configuration(Configuration.VERSION_2_3_29);
    cfg.setDirectoryForTemplateLoading(new File("ftls"));
    cfg.setDefaultEncoding("UTF-8");
    cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);

    Map root = new HashMap();
    Map obj1 = new HashMap();
    Map obj2 = new HashMap();

    obj1.put("name", "HTML");
    obj2.put("name", "CSS");

    List skills = new ArrayList();
    skills.add(obj1);
    skills.add(obj2);

    root.put("skills", skills);

    Template temp = cfg.getTemplate("test.ftl");

    Writer out = new OutputStreamWriter(System.out);

    temp.process(root, out);
  }
}

在项目中导入 freemarker.jar,并在 ftls 目录添加 test.ftl 文件,由于 freemarker 默认不能直接渲染 Hash 对象,需要在模板中定义一个方法,我们命名为 stringifytest.ftl 的内容如下:

<#function stringify obj>
  <#if !obj??>
    <#return 'undefined'>
  </#if>
    <#if obj?is_boolean || obj?is_number>
        <#return obj?string>
    </#if>
    <#if obj?is_string>
        <#return '"' + obj?js_string + '"'>
    </#if>
  <#if obj?is_enumerable>
    <#local str = '['>
    <#list obj as x>
      <#local str = str + stringify(x)>
      <#if x_has_next>
        <#local str = str + ','>
      </#if>
    </#list>
    <#return str + ']'>
  </#if>
  <#if obj?is_hash || obj?is_hash_ex>
    <#local str = '{'>
    <#local arr = [] >
    <#local keys = obj?keys>
    <#list keys as x>
      <#if x!='class' && obj[x]?? && !obj[x]?is_method>
        <#local arr = arr + [x]>
      </#if>
    </#list>
    <#list arr as x>
      <#local str = str + x + ':' + stringify(obj[x])>
      <#if x_has_next>
        <#local str = str + ','>
      </#if>
    </#list>
    <#return str + '}'>
  </#if>
  <#return ''>
</#function>

${stringify(skills)}

结果输出为:[{name:"HTML"},{name:"CSS"}],这个是我们想要的结果,所以原生的 freemarker 没有问题。

解决方案

FMPP 支持扩展数据类型,只要导入自定义的 JSON 解析包就可以了,具体步骤如下:

编写自定义解析库

这里我们直接使用 flexjson 这个库,下载后,将 flexjson jar 包导入项目,解析库代码如下:

package com.test.fmpp;

import flexjson.JSONDeserializer;
import flexjson.JSONSerializer;
import fmpp.Engine;
import fmpp.tdd.DataLoader;

import java.util.List;

public class JSONFactory implements DataLoader {

  @Override
  public Object load(Engine e, List args) throws Exception {
    return new JSON();
  }

  public static class JSON {
    public String stringify(Object object) {
      return new JSONSerializer().deepSerialize(object);
    }

    public Object parse(String jsonString) {
      return new JSONDeserializer().deserialize(jsonString);
    }
  }
}

将上述代码打成 jar 包,命名为 json.jar

flexjson.jarjson.jar 导入 FMPP

下载 FMPP 后,将上述的 flexjson.jarjson.jar 都拷贝到 FMPP 的 lib 目录下(注:放在 lib 目录中的 jar 包会自动导入到执行上下文中)。在调用 fmpp 命令时,除了要将数据传入外,还需要传入 JSON 解析方法,代码如下:

`data:{${fmppData},JSONUtil:com.test.fmpp.JSONFactory()}`

在 ftl 模板中使用新的解析方法

现在 ftl 模板就可以这么写了:

// ftl 模板
${JSONUtil.stringify(skills)}

结果输出为: [{'name':'HTML'}, {'name':'CSS'}],可以直接输出在 HTML 中作为合法的 JavaScript 代码。

目前该方法已经集成在 NEI 工具中,有兴趣的朋友可以自己研究。

留下评论