扩展#render指令,实现类似freemarker的nested指令功能

由于enjoy在设计的时候,直接就不支持nested指令,所以对于某些场景不太友好,希望还是可以支持下,因为有些方面的需求。

我的需求是这样的,在我们写非前后端分离的项目时,有很多form表单需要自己手动写,当然也会有相应的工具自动生成或者是ctrl+c/ctrl+v,但感觉这样不是很Nick, 所以想扩展一下指令,实现将表单进行html包裹,如下:

<div class="control-group">
  <label class="control-label required" for="typeId">理财类型</label>
  <div class="controls">
      <select id="typeId" class="w370 control not-arrow">
          <option value="" selected disabled>请选择</option>
      #for(t : TYPES)
          <option value="#(t.id)">#(t.name)</option>
      #end
      </select>
  </div>
</div>
<div class="control-group">
  <label class="control-label required" for="investId">理财渠道</label>
  <div class="controls">
      <select id="investId" class="w370 control not-arrow">
          <option value="" selected disabled>请选择</option>
      #for(t : INVESTS)
          <option value="#(t.id2)">#(t.name2)</option>
      #end
      </select>
  </div>
</div>


大家可以看到这里很多代码都是重复的,不同的地方是使用了两处数据,TYPES, INVESTS, 虽然他们的输入模式类似,但是在取key, value时,使用的是不同的值,这样就没办法使用统一模板了,当然在后台可以实现将数据转成统一的x.key, x.value数据这样输出,此方案也不太优雅,pass,现在希望使用模板定制输出, 如下:

#renderNested(template, label="理财类型", id="typeId")
      #for(t : TYPES)
          <option value="#(t.id)">#(t.name)</option>
      #end
#end
#renderNested(template, label="理财渠道", id="investId")
      #for(t : INVESTS)
          <option value="#(t.id2)">#(t.name2)</option>
      #end
#end

或者这样

#renderNested(template, label="理财渠道", id="investId")
      #investSelect()
#end

有的同学会说,这个简单啊,不用扩展,使用模板安全调用就可以了,但这里面还有一个问题就是,在同一个页面多次使用时,无法多次定义相同的函数名称,所以只能使用这种嵌套模式来实现

我自己扩展了#render指令来实现,这里面使用了io流来处理string,是否有更好的实现方案?请大家指点

@Directive("renderNested")
public class RenderNestedDirective extends com.jfinal.template.ext.directive.RenderDirective {

    private String parentFileName;
    private Map<String, SubStat> subStatCache = new SyncWriteMap<String, SubStat>(16, 0.5F);
    
    // 重点在这个标记
    private static final String NESTED_MARK = "<@nested>";

    @Override
    public void exec(Env env, Scope scope, Writer writer) {
        // 在 exprList.eval(scope) 之前创建,使赋值表达式在本作用域内进行
        scope = new Scope(scope);

        Object value = evalAssignExpressionAndGetFileName(scope);
        if (!(value instanceof String)) {
            throw new TemplateException("The parameter value of #renderNested directive must be String", location);
        }

        String subFileName = Include.getSubFileName((String) value, parentFileName);
        SubStat subStat = subStatCache.get(subFileName);
        if (subStat == null) {
            subStat = parseSubStat(env, subFileName);
            subStatCache.put(subFileName, subStat);
        } else if (env.isDevMode()) {
            // subStat.env.isSourceListModified() 逻辑可以支持 #render 子模板中的 #include 过来的子模板在 devMode 下在修改后可被重加载
            if (subStat.source.isModified() || subStat.env.isSourceListModified()) {
                subStat = parseSubStat(env, subFileName);
                subStatCache.put(subFileName, subStat);
            }
        }

        StringBuilder outer = render(subStat, subStat.env, scope);
        int index = outer.indexOf(NESTED_MARK);
        if (index > -1) {
            int endPos = index + NESTED_MARK.length();

            StringBuilder inner = render(stat, env, scope);
            if (Objects.nonNull(inner) && inner.length() > 0) {
                outer.replace(index, endPos, inner.toString());
            } else {
                outer.replace(index, endPos, "");
            }
        } else {
            throw new TemplateException("The #renderNested directive must contain \"" + NESTED_MARK + "\" mark", location);
        }
        write(writer, outer.toString());

        scope.getCtrl().setJumpNone();
    }

    private StringBuilder render(Stat stat, Env env, Scope scope) {
        try (CharWriter charWriter = new CharWriter(1024); FastStringWriter fsw = new FastStringWriter()) {
            charWriter.init(fsw);
            stat.exec(env, scope, charWriter); // subStat.stat.exec(subStat.env, scope, writer);
            return fsw.toStringBuilder();
        }
    }

    /**
     * 对 exprList 进行求值,并将第一个表达式的值作为模板名称返回, 开启 local assignment 保障 #render 指令参数表达式列表 中的赋值表达式在当前 scope 中进行,有利于模块化
     */
    private Object evalAssignExpressionAndGetFileName(Scope scope) {
        Ctrl ctrl = scope.getCtrl();
        try {
            ctrl.setLocalAssignment();
            return exprList.evalExprList(scope)[0];
        } finally {
            ctrl.setWisdomAssignment();
        }
    }

    private SubStat parseSubStat(Env env, String subFileName) {
        EngineConfig config = env.getEngineConfig();
        ISource subFileSource = config.getSourceFactory().getSource(config.getBaseTemplatePath(), subFileName,
                config.getEncoding());

        try {
            SubEnv subEnv = new SubEnv(env);
            StatList subStatList = new Parser(subEnv, subFileSource.getContent(), subFileName).parse();
            return new SubStat(subEnv, subStatList.getActualStat(), subFileSource);
        } catch (Exception e) {
            throw new ParseException(e.getMessage(), location, e);
        }
    }

    public boolean hasEnd() {
        return true;
    }
}

重点是使用一个占位标记,然后获取到body内容后,对占位标记进行替换输出,主要代码如下:

StringBuilder outer = render(subStat, subStat.env, scope);
int index = outer.indexOf(NESTED_MARK);
if (index > -1) {
    int endPos = index + NESTED_MARK.length();

    StringBuilder inner = render(stat, env, scope);
    if (Objects.nonNull(inner) && inner.length() > 0) {
	outer.replace(index, endPos, inner.toString());
    } else {
	outer.replace(index, endPos, "");
    }
} else {
    throw new TemplateException("The #renderNested directive must contain \"" + NESTED_MARK + "\" mark", location);
}
write(writer, outer.toString());
private StringBuilder render(Stat stat, Env env, Scope scope) {
    // 资源会自动关闭的
    try (CharWriter charWriter = new CharWriter(1024); FastStringWriter fsw = new FastStringWriter()) {
        charWriter.init(fsw);
        stat.exec(env, scope, charWriter); // subStat.stat.exec(subStat.env, scope, writer);
        return fsw.toStringBuilder();
    }
}

这里使用了两次IO,还有对string进行操作,是否会影响到性能还没进行大量测试,但目前需求是可以满足了。

在使用的时候可以这样定义一个模板, 命名为 test.jf

<div class="control-group">
  <label class="control-label required" for="#(id)">#(label)</label>
  <div class="controls">
      <select id="#(id)" name="#(name??id)" class="w370 control not-arrow">
          <option value="" selected disabled>请选择</option>
      <!-- 此处添加指令标记, 不添加会报错 -->
      <@nested>
      </select>
  </div>
</div>

页面使用直接这样

#renderNested("test.jf", label="理财类型", id="typeId")
      <option value="1">test</option>
#end

#renderNested("test.jf", label="理财类型", id="typeId")
      <!-- 自定义的指令也可以 -->
      #@myDirective()
#end

#renderNested("test.jf", label="理财类型", id="typeId")
      <!-- 没有内容也是可以的 -->
#end

当然我还有一个更极端的想法,好像不太行,就是类似这样子

直接使用指令, 将自定义模板的内容当作变量插入到内嵌的标志中,目前还没实现。

#renderNested("test.jf", label="理财类型", id="typeId", nested=@myDirective())


评论区

杜福忠

2022-09-24 19:25

思路很赞,我们之前有个模板项目,是使用全局添加的共享模板函数,一个公共的文件,里面是用#define 定义的模板函数组件。
#define input #define hide #define inputDate #define inputDateSlot 。。。
使用的时候#@input('NAME', '姓名')就可以输出组件HTML,再用idea的实时模板把#@xx函数名录入进去,写的时候#@就可以调出组件名提示,也很方便。

杜福忠

2022-09-24 19:31

key, value 也可以做成参数传入就没问题了。用get(key)取值,模板函数与模板函数也可以复用,进行一些约定默认值就可以方便很多

Leo.du

2022-09-24 20:51

@杜福忠 这个思路也可以,但是还有一个问题,参数不够灵活,使用render指令,可以做到参数可变化,比如说,还想添加readonly, disabled, 或者添加class, style等,这样的话可以做到灵活变化

JFinal

2022-09-24 23:26

你的实现思路是非常好的,但是代码量有点多了,我刚刚写了一个版本,你试用一下:
1: 定义一个 #template 指令,该指令至少需要一个参数传递模板名,其它参数可任意,例如可传递赋值表达式传值进去:#template("template.txt", title="enjoy")
public class TemplateDirective extends Directive {

@Override
public void setExprList(ExprList exprList) {
if (exprList.length() < 1) {
throw new ParseException("#template 指令至少需要 1 个参数", location);
}
super.setExprList(exprList);
}

@Override
public void exec(Env env, Scope scope, Writer writer) {
if (scope.getRootData() == null) {
scope.setRootData(new HashMap());
}

// 将指令内部的全部内容传递给 #slot 指令去使用
Object[] values = exprList.evalExprList(scope);
String templateFile = values[0].toString();
scope.getRootData().put("__slotStat__", stat);

// 指令自身直接渲染参数传递的模板就行
StringBuilder sb = Engine.use().getTemplate(templateFile).renderToStringBuilder(scope.getRootData());
write(writer, sb.toString());
}

public boolean hasEnd() {
return true;
}
}

2: 定义一个 #slot 插槽指令
public class SlotDirective extends Directive {

@Override
public void exec(Env env, Scope scope, Writer writer) {
Object slotStat = scope.getRootData().get("__slotStat__");
if (slotStat == null) {
throw new TemplateException("slotStat 未找到", location);
}

if (slotStat instanceof Stat) {
((Stat)slotStat).exec(env, scope, writer);
}
}
}


测试代码:
1: 模板文件 template.txt 的内容如下:
AAA
#slot()
ZZZ

2: 测试用的主模板 test.txt 内容如下:
#template("template.txt")
#for (x : [0..3]) #(x) #end
#end

3: java 测试代码如下
public class Test {
public static void main(String[] args) {
Engine.use().setToClassPathSourceFactory();
Engine.use().addDirective("template", TemplateDirective.class);
Engine.use().addDirective("slot", SlotDirective.class, true);
String ret = Engine.use().getTemplate("test.txt").renderToString(Kv.create());
System.out.println(ret);
}
}

JFinal

2022-09-24 23:26

@Leo.du @杜福忠 试用一下我这个设计,代码量一定要少,充分利用指令扩展的强大能力

happyboy

2022-09-28 11:31

你们是真懒,有时候适当的冗余,更方便代码的阅读

杜福忠

2022-09-28 18:52

@JFinal 现在已经不用写UI代码了,UI 用JSON文件配置的,约定了一套内部规则,请求打到JSON文件的路径上,JF再根据配置反序列为UI代码。把上手成本降到更低,计划去学校推广了,再下一步由在校学生成立的各个团队,去适配各种业务的管理级系统,再下一步SaaS软件超市,一步步降低软件开发成本使用成本企业成本

JFinal

2022-09-28 20:54

@杜福忠 我觉得你这个方向很有前景,业务逻辑才是应用软件的核心,UI 尽可能最低成本最高效率实现是大势所趋,UI 在未来都该组件化

JFinal

2022-09-28 20:55

@杜福忠 json 配置 UI 怎么玩的? 贴点代码出来看看

杜福忠

2022-09-29 09:43

@JFinal 目前是JSON配置的,后期可做成UI拖拽等功能生成JSON的工具,用JSON做中介,PC和移动端都可以使用同一套规则,小程序等都可以反序列UI出来。
PC端反序列为UI,使用JF的renderTemplate模板,代码也很简单,就是for if 这些,很偏业务


杜福忠

2022-09-29 10:13

@JFinal 目前接触到很多企业是没有把业务转换为数据库逻辑思维的。市面有很多低代码平台,但是都需要企业自己去搭建自己的业务逻辑流程。虽然低代码很简单了,但是没有设计思维也是懵逼的,只能去市面各种找符合自己的系统。业务逻辑企业自己是清楚的,如果我们的平台搭建成功了,那么这些企业用微信沟通就可以搭建自己的业务框架了。钉钉应用中心就有这个思维,但是里面都是企业入驻的开发者,成本很高的,基本一个应用年费都上千了。我组织信息学院学生在我们平台当开发者的话,就没有企业成本了,而且学校还有补贴,用低成本可以挤入BI软件市场

JFinal

2022-09-29 11:01

@杜福忠 业务封装非常深入,对 enjoy 的灵活运用趋向极致

海哥

2022-09-29 15:48

@杜福忠 👍👍👍

zeroabc

2022-09-30 11:57

@杜福忠 牛b,用json自定义输出ui非常灵活

前端同样是layui,这里贴一下我们项目目前的玩法

​### #set(modelTag = "xxx")### 默认由拦截器自动处理|页面全局参数|modelTag(数据库类名,小写;跟json请求提交有关)|
#set(pageTag = UUID19.uuid())### 默认由拦截器自动处理|页面全局参数|pageTag(页面标记,为空时自动用modelTag的值;跟页面dom命名有关,务必避免跟其它页面重复;)|

#include("/commons/template/layui_admin_listview.html"
, pageTitle="考勤详情查询"
, tableCols="[[
{field: 'studentCode', width: 145, title: '学工号'}
,{field: 'name', width: 120, title: '姓名', templet: '#LAY-listview-tpl-stuView-#(pageTag)'}
,{field: 'sexName', width: 58, title: '性别'}
,{field: 'studentTypeName', width: 86, title: '学生类型'}
,{field: 'orgPath', width: 200, title: '管理单位路径', style: 'direction:rtl;'}
,{field: 'dormPath', width: 250, title: '楼栋宿舍路径', style: 'direction:rtl;'}
,{field: 'stuAttendanceStatusName', title: '考勤状态'}
,{field: 'statisticsDateStr', title: '考勤日期'}
,{field: 'entryTimeStr', title: '最近进出时间'}
,{field: 'entryModeName', title: '进出方式'}
]]"
, listUrl="list"
, pageTagTransmit=true
, withFileForm=true
)

#define beforeMain()### 位置在页面主要内容前(可定义js/css等等)
###---------------------------------------------------------------------------------beforeMain--start↓



###---------------------------------------------------------------------------------beforeMain--end--↑
#end

#define searchBox()### 搜索栏表单内容
###---------------------------------------------------------------------------------searchBox--start↓

#@laySearchBoxItemStart("姓名" ,"layui-input-inline")

#@laySearchBoxItemEnd()

#@laySearchBoxItemStart("学工号" ,"layui-input-inline")

#@laySearchBoxItemEnd()

#@laySearchBoxItemStartWS("楼栋" ,"layui-input-inline", "width:28px;", "width:400px;")
输入可搜索 #for(house : DataHelper.getRoleHouses(request)) #(house.name) #end
#@laySearchBoxItemEnd()

#@laySearchBoxItemStartWS("管理单位" ,"layui-input-inline" ,"width:28px;" ,"width:420px;")
输入可搜索 #for(shuyuan : DataHelper.getRoleShuyuans(request)) #(shuyuan.name) #end
#@laySearchBoxItemEnd()









按条件导出Excel


###---------------------------------------------------------------------------------searchBox--end--↑
#end
...


然后还有一个form的基本模板,用法跟listview差不多。

Leo.du

2022-10-03 15:51

热门分享

扫码入社