由于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())
#define input #define hide #define inputDate #define inputDateSlot 。。。
使用的时候#@input('NAME', '姓名')就可以输出组件HTML,再用idea的实时模板把#@xx函数名录入进去,写的时候#@就可以调出组件名提示,也很方便。