由于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函数名录入进去,写的时候#@就可以调出组件名提示,也很方便。