Handlebars块级Helpers
1.Handlebars简单介绍:
Handlebars是JavaScript一个语义模板库,通过对view和data的分离来快速构建Web模板。它采用"Logic-less template"(无逻辑模版)的思路,在加载时被预编译,而不是到了客户端执行到代码时再去编译, 这样可以保证模板加载和运行的速度。Handlebars兼容Mustache,你可以在Handlebars中导入Mustache模板。
2.参考文章:
Handlebars官网:http://handlebarsjs.com/
Handlebars中文介绍(http://www.ghostchina.com/introducing-the-handlebars-js-templating-engine/ ):
Handlebars中文文档 - 块级helpers(译自官方版):https://segmentfault.com/a/1190000000347965
Handlebars.js 中文文档:http://keenwon.com/992.html
js模版引擎handlebars.js实用教程:http://www.cnblogs.com/iyangyuan/p/3471227.html
handlebars玩家指南:http://cnodejs.org/topic/56a2e8b1cd415452622eed2d
com.github.jknack.handlebars.Helper:http://www.programcreek.com/java-api-examples/index.php?api=com.github.jknack.handlebars.Helper
3.块级Helpers使用技巧:
Handlebars内置了with,
each
,if,
unless,
Helpers。log这5种基本的
if标签只能判断true或false,不能执行一些复杂的运算逻辑。
这些基本的Helpers并不能满足我们所有的需求,因此需要自定义一些辅助Helpers。
① 基本的
Helpers—each使用示例:orderList.hbs
的
<h1>订单列表</h1> <div class="record"> <table class="order> <thead> <tr class="centerTr" > <th >采购凭证号</th> <th >公司</th> <th >供应商编号</th> <th >项目交货日期</th> <th >产地</th> </tr> </thead> <tbody> {{#each _DATA_.orderList}} <tr> <td>{{purproofno}}</td> <td>{{company}}</td> <td>{{supplierno}}</td> <td>{{projectdate}}</td> <td>{{proplace}}</td> </tr> {{/each}} </tbody> </table> </div>
② 自定义Helpers使用示例:handlebars.coffee
Handlebars.registerHelper 'jsonToStr', (json, options) -> JSON.stringify(json) Handlebars.registerHelper 'add', (a,b, options) -> a + b Handlebars.registerHelper "formatPrice", (price, type, options) -> return if not price? if type is 1 formatedPrice = (price / 100) roundedPrice = parseInt(price / 100) else formatedPrice = (price / 100).toFixed(2) roundedPrice = parseInt(price / 100).toFixed(2) if `formatePrice == roundedPrice` then roundedPrice else formatedPrice Handlebars.registerHelper "formatDate", (date, type, options) -> return unless date switch type when "gmt" then moment(date).format("EEE MMM dd HH:mm:ss Z yyyy") when "day" then moment(date).format("YYYY-MM-DD") when "minute" then moment(date).format("YYYY-MM-DD HH:mm") else moment(date).format("YYYY-MM-DD HH:mm:ss") Handlebars.registerHelper "lt", (a, b, options) -> if a < b options.fn(this) else options.inverse(this) Handlebars.registerHelper "gt", (a, b, options) -> if a > b options.fn(this) else options.inverse(this) Handlebars.registerHelper 'of', (a, b, options) -> values = if _.isArray b then b else b.split(",") if _.contains(values, a.toString()) or _.contains values, a options.fn(this) else options.inverse(this) Handlebars.registerHelper 'length', (a, options) -> length = a.length Handlebars.registerHelper "isArray", (a, options) -> if _.isArray a options.fn(this) else options.inverse(this) Handlebars.registerHelper "between", (a, b, c, options) -> if a >= b and a <= c options.fn(this) else options.inverse(this) Handlebars.registerHelper "multiple", (a, b, c, options) -> if c isnt 0 then a * b / c else 0 Handlebars.registerHelper "contain", (a, b, options) -> return options.inverse @ if a is undefined or b is undefined array = if _.isArray a then a else a.toString().split(",") if _.contains a, b then options.fn @ else options.inverse @ Handlebars.registerHelper "match", (a, b, options) -> return options.inverse @ if a is undefined or b is undefined if new RegExp(a).exec(b) is null then options.inverse @ else options.fn @ Handlebars.registerHelper "isOdd", (a, b, options) -> if a % b == 1 options.fn(this) else options.inverse(this)
handlebars.coffee编译成handlebarsApp.js的结果:
Handlebars.registerHelper('jsonToStr', function(json, options) { return JSON.stringify(json); }); Handlebars.registerHelper('add', function(a, b, options) { return a + b; }); Handlebars.registerHelper("formatPrice", function(price, type, options) { var formatedPrice, roundedPrice; if (price == null) { return; } if (type === 1) { formatedPrice = price / 100; roundedPrice = parseInt(price / 100); } else { formatedPrice = (price / 100).toFixed(2); roundedPrice = parseInt(price / 100).toFixed(2); } if (formatedPrice == roundedPrice) { return roundedPrice; } else { return formatedPrice; } }); Handlebars.registerHelper("formatDate", function(date, type, options) { if (!date) { return; } switch (type) { case "gmt": return moment(date).format("EEE MMM dd HH:mm:ss Z yyyy"); case "day": return moment(date).format("YYYY-MM-DD"); case "minute": return moment(date).format("YYYY-MM-DD HH:mm"); default: return moment(date).format("YYYY-MM-DD HH:mm:ss"); } }); Handlebars.registerHelper("lt", function(a, b, options) { if (a < b) { return options.fn(this); } else { return options.inverse(this); } }); Handlebars.registerHelper("gt", function(a, b, options) { if (a > b) { return options.fn(this); } else { return options.inverse(this); } }); Handlebars.registerHelper('of', function(a, b, options) { var values; values = _.isArray(b) ? b : b.split(","); if (_.contains(values, a.toString()) || _.contains(values, a)) { return options.fn(this); } else { return options.inverse(this); } }); Handlebars.registerHelper('length', function(a, options) { var length; return length = a.length; }); Handlebars.registerHelper("isArray", function(a, options) { if (_.isArray(a)) { return options.fn(this); } else { return options.inverse(this); } }); Handlebars.registerHelper("between", function(a, b, c, options) { if (a >= b && a <= c) { return options.fn(this); } else { return options.inverse(this); } }); Handlebars.registerHelper("multiple", function(a, b, c, options) { if (c !== 0) { return a * b / c; } else { return 0; } }); Handlebars.registerHelper("contain", function(a, b, options) { var array; if (a === void 0 || b === void 0) { return options.inverse(this); } array = _.isArray(a) ? a : a.toString().split(","); if (_.contains(a, b)) { return options.fn(this); } else { return options.inverse(this); } }); Handlebars.registerHelper("match", function(a, b, options) { if (a === void 0 || b === void 0) { return options.inverse(this); } if (new RegExp(a).exec(b) === null) { return options.inverse(this); } else { return options.fn(this); } }); Handlebars.registerHelper("isOdd", function(a, b, options) { if (a % b === 1) { return options.fn(this); } else { return options.inverse(this); } });
③ Spring MVC框架中引入handlebars插件:
Maven配置:handlebars和handlebars-springmvc。
<dependency> <groupId>com.github.jknack</groupId> <artifactId>handlebars</artifactId> <version>4.0.5</version> </dependency> <dependency> <groupId>com.github.jknack</groupId> <artifactId>handlebars-springmvc</artifactId> <version>4.0.5</version> </dependency>
在Spring MVC配置文件servlet-context.xml中添加handlebars视图解析器配置:
<bean id="handlebarsViewResolver" class="com.github.jknack.handlebars.springmvc.HandlebarsViewResolver"> <property name="prefix" value="/page/" /> <property name="suffix" value=".html" /> <property name="contentType" value="text/html;charset=utf-8" /> <property name="failOnMissingFile" value="false" /> <property name="cache" value="false" /> </bean>
Controller中实现代码:
package com.ouc.handlebars.controller; import java.util.HashMap; import java.util.Map; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.servlet.ModelAndView; @Controller public class HandlebarsController { @RequestMapping(value = "/testHandlebars", method = RequestMethod.GET) public ModelAndView helloWorld() { Map<String, Object> map = new HashMap<String, Object>(); map.put("helloWorld", "Hello World!"); return new ModelAndView("helloWorld", map); } }
前端helloWorld.html文件:
<!DOCTYPE > <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>Hello World</title> <script type="text/javascript" src="js/jquery/jquery.min.js"></script> <script type="text/javascript" src="js/handlebars/handlebars-4.0.5.js"></script> </head> <body> <div> {{helloWorld}} </div> </body> </html>
④ 自定义Helpers后台实现:OucHandlebarHelpers.java
package ouc.handlebars.web.helpers; import com.github.jknack.handlebars.Helper; import com.github.jknack.handlebars.Options; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import ouc.handlebars.common.utils.MapBuilder; import ouc.handlebars.common.utils.NumberUtils; import ouc.handlebars.common.utils.Splitters; import ouc.handlebars.pampas.engine.handlebars.HandlebarsEngine; import org.joda.time.DateTime; import org.joda.time.Days; import org.joda.time.Hours; import org.joda.time.Minutes; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import java.io.IOException; import java.text.DecimalFormat; import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Map; @Component public class OucHandlebarHelpers { @Autowired private HandlebarsEngine handlebarEngine; @PostConstruct public void init() { handlebarEngine.registerHelper("isEmpty", new Helper<Object>() { @Override public CharSequence apply(Object obj, Options options) throws IOException { if (obj == null || obj.equals("")) { return options.fn(); } else { return options.inverse(); } } }); //此标签相当于 and 连接的if判断 handlebarEngine.registerHelper("multipleEquals", new Helper<Object>() { @Override public CharSequence apply(Object source, Options options) throws IOException { CharSequence ch=options.fn(); CharSequence ch2=options.inverse(); String obj1=String.valueOf(source); Object[] other=options.params; if(!Objects.equal(obj1,String.valueOf(other[0]))){ return ch2; } for (int i = 1; i < other.length; i+=2) { if(!Objects.equal(String.valueOf(other[i]),String.valueOf(other[i+1]))){ return ch2; } } return ch; } }); handlebarEngine.registerHelper("get", new Helper<Object>() { @Override public CharSequence apply(Object context, Options options) throws IOException { Object collections; Object param = options.param(0); if (context instanceof List || context instanceof Map) { collections = context; } else { collections = Splitters.COMMA.splitToList(String.valueOf(context)); } if (param == null) { return null; } if(collections instanceof List){ return ((List)collections).get(Integer.valueOf(param.toString())).toString(); } else{ return ((Map)collections).get(param).toString(); } } }); handlebarEngine.registerHelper("contain", new Helper<Object>() { @Override public CharSequence apply(Object context, Options options) throws IOException { Object collections; Object param = options.param(0); if (context instanceof Collection || context instanceof Map ) { collections = context; } else{ collections = Splitters.COMMA.splitToList(String.valueOf(context)); } if (param == null) { return options.fn(); } if(collections instanceof Collection){ if(((Collection)collections).contains(param)){ return options.fn(); } else{ return options.inverse(); } } else { Map map = (Map)collections; String check = options.param(1, "ALL"); if("key".equalsIgnoreCase(check)){ if(map.keySet().contains(param)){ return options.fn(); } else{ return options.inverse(); } } else if("value".equalsIgnoreCase(check)){ if(map.values().contains(param)){ return options.fn(); } else{ return options.inverse(); } } else{ if(map.keySet().contains(param) || map.values().contains(param)){ return options.fn(); } else{ return options.inverse(); } } } } }); handlebarEngine.registerHelper("containAny", new Helper<Object>() { @Override public CharSequence apply(Object context, Options options) throws IOException { Object collections; if (context instanceof Collection || context instanceof Map) { collections = context; } else { collections = Splitters.COMMA.splitToList(String.valueOf(context)); } if (options.params == null || options.params.length == 0) { return options.fn(); } if(collections instanceof Collection){ for (Object param : options.params) { for (String p : Splitters.COMMA.splitToList((String)param)) { if (((Collection)collections).contains(p)) { return options.fn(); } } } return options.inverse(); } else { Map map = (Map)collections; for (Object param : options.params) { for (String p : Splitters.COMMA.splitToList((String) param)) { if (map.keySet().contains(p) || map.values().contains(p)) { return options.fn(); } } } return options.inverse(); } } }); /** * * 传参 a, b, c * 然后a是一个"2015-08-12"或者''2015-08-12 12:00:00" * b的值可以是"dayStart",''now","dayEnd","2015-08-15",''2015-08-15 12:00:00"中的一个 * 如果b是dayStart, a就和当天的0点0分比较 * 如果b是now, a就和现在时间比较 * 如果b是dayEnd, a就和第二天的0点0分比较 * 如果b是一个date,a就和b比较 * 第三个参数: * 设置时间的比较级别:s:秒,m:分,h:小时,d:天 */ handlebarEngine.registerHelper("comDate", new Helper<Object>() { @Override public CharSequence apply(Object context, Options options) throws IOException { //获取参数 Object param2 = options.param(0); //默认按照秒比较时间 String comType = MoreObjects.firstNonNull((String) options.param(1), "s"); if(context == null || param2 == null){ return options.fn(); }else{ Date comTime1 = new DateTime(context).toDate();//Date)param1; String param = (String)param2; Date comTime2; if(Objects.equal(param , "dayStart")){ //compare1与当天的0点0分比较 comTime2 = DateTime.now().withTime(0 , 0, 0, 0).toDate(); }else if(Objects.equal(param , "now")){ //compare1就和现在时间比较 comTime2 = DateTime.now().toDate(); }else if(Objects.equal(param , "dayEnd")){ //compare1就和第二天的0点0分比较 comTime2 = DateTime.now().plusDays(1).withTime(0 , 0, 0, 0).toDate(); }else{ //compare1就和compare2比较 comTime2 = DateTime.parse(param).toDate(); } if(compareTime(comTime1 , comTime2 , comType)){ return options.inverse(); }else{ return options.fn(); } } } }); /** * 格式化金钱 第一个参数格式化到得金额,第二个是保留小数的位数,第三个是传值为空时返回 * 什么数据,E-empty string(默认),Z-zero * {{formatPrice 变量名 "W" 2 E}} */ handlebarEngine.registerHelper("formatPrice", new Helper<Number>() { Map<String, Integer> sdfMap = MapBuilder.<String, Integer>of().put( "W", 1000000, "Q", 100000, "B", 10000, "Y", 100 ).map(); @Override public CharSequence apply(Number price, Options options) throws IOException { String defaultValue = "E"; if (options.params != null && options.params.length ==3) { defaultValue = options.param(2).toString(); } if (price == null) return defValue(defaultValue); if (options.params == null || options.params.length == 0){ return NumberUtils.formatPrice(price); } else if( options.params.length == 1){ Object param1 = options.param(0); int divisor = sdfMap.get(param1.toString().toUpperCase()) == null?1:sdfMap.get(param1.toString().toUpperCase()); return getDecimalFormat(2).format(price.doubleValue()/divisor); } else if (options.params.length == 2 || options.params.length ==3 ){ Object param1 = options.param(0); Object param2 = options.param(1); int divisor = sdfMap.get(param1.toString().toUpperCase()) == null?1:sdfMap.get(param1.toString().toUpperCase()); return getDecimalFormat(Integer.valueOf(param2.toString())).format(price.doubleValue() / divisor); } return ""; } }); /** * Usage: <span>{{formatDecimal 10101.2 2}}</span> -> * <span> 10,101.20 </span> * 第一个参数是传入的数,第二是保留小数位 * 默认不保留小数位 */ handlebarEngine.registerHelper("formatDecimal", new Helper<Number>() { @Override public CharSequence apply(Number number, Options options) throws IOException { if (number == null) { return "0"; } DecimalFormat df; if (options.params == null || options.params.length == 0) { df = getDecimalFormat(0); } else { df = getDecimalFormat(Integer.valueOf(options.params[0].toString())); } return df.format(number.doubleValue()); } }); } private String defValue(String defaultValue) { if (defaultValue.toUpperCase().equals("E")) { return ""; } if (defaultValue.toUpperCase().equals("Z")) { return "0"; } return ""; } private DecimalFormat getDecimalFormat(Integer digits){ DecimalFormat decimalFormat = new DecimalFormat(); Integer fractionDigits = digits == null? 2:digits; decimalFormat.setMaximumFractionDigits(fractionDigits); decimalFormat.setMinimumFractionDigits(fractionDigits); return decimalFormat; } /** * 比较开始时间以及结束时间之间的大小(按照比较级别) * @param startTime 开始时间 * @param endTime 结束时间 * @param comType 比较级别 * @return Boolean * 返回是否大于(true:大于, false:小于) */ private Boolean compareTime(Date startTime , Date endTime, String comType){ Boolean result = false; if(Objects.equal(comType , "s")){ //按照秒比较 result = startTime.after(endTime); }else if(Objects.equal(comType , "m")){ //按照分钟比较 result = Minutes.minutesBetween(new DateTime(startTime), new DateTime(endTime)).getMinutes() > 0; }else if(Objects.equal(comType , "h")){ //按照小时比较 result = Hours.hoursBetween(new DateTime(startTime), new DateTime(endTime)).getHours() > 0; }else if(Objects.equal(comType , "d")){ //按照天比较 result = Days.daysBetween(new DateTime(startTime), new DateTime(endTime)).getDays() > 0; } return result; } }
⑤ HandlebarsEngine.java
package ouc.handlebars.pampas.engine.handlebars; import com.github.jknack.handlebars.Handlebars; import com.github.jknack.handlebars.HandlebarsException; import com.github.jknack.handlebars.Helper; import com.github.jknack.handlebars.Template; import com.github.jknack.handlebars.io.TemplateLoader; import com.google.common.base.Optional; import com.google.common.base.Strings; import com.google.common.base.Throwables; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.collect.Maps; import ouc.handlebars.pampas.common.UserNotLoginException; import ouc.handlebars.pampas.common.UserUtil; import ouc.handlebars.pampas.engine.RenderConstants; import ouc.handlebars.pampas.engine.Setting; import ouc.handlebars.pampas.engine.config.ConfigManager; import ouc.handlebars.pampas.engine.config.model.Component; import ouc.handlebars.pampas.engine.mapping.Invoker; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import javax.servlet.ServletContext; import java.io.FileNotFoundException; import java.util.Map; import java.util.concurrent.TimeUnit; /** * Author: Wu Ping */ @org.springframework.stereotype.Component @Slf4j public class HandlebarsEngine { private Handlebars handlebars; private Invoker invoker; private final LoadingCache<String, Optional<Template>> caches; private ConfigManager configManager; @Autowired public HandlebarsEngine(Invoker invoker, Setting setting, ConfigManager configManager, ServletContext servletContext) { this.invoker = invoker; TemplateLoader templateLoader = new GreatTemplateLoader(servletContext, "/views", ".hbs"); this.handlebars = new Handlebars(templateLoader); this.caches = initCache(!setting.isDevMode()); this.configManager = configManager; } private LoadingCache<String, Optional<Template>> initCache(boolean buildCache) { if (buildCache) { return CacheBuilder.newBuilder().expireAfterWrite(5, TimeUnit.MINUTES).build(new CacheLoader<String, Optional<Template>>() { @Override public Optional<Template> load(String path) throws Exception { Template t = null; try { t = handlebars.compile(path); } catch (Exception e) { log.error("failed to compile template(path={}), cause: {}",path, e.getMessage()); } return Optional.fromNullable(t); } }); } return null; } public <T> void registerHelper(String name, Helper<T> helper) { handlebars.registerHelper(name, helper); } public String execInline(String templateStr, Map<String, Object> params) { return execInline(templateStr, params, null); } public String execInline(String templateStr, Map<String, Object> params, String cacheKey) { try { if (params == null) { params = Maps.newHashMap(); } Template template; if (caches == null || cacheKey == null) { template = handlebars.compileInline(templateStr); } else { template = caches.getUnchecked("inline/" + cacheKey).orNull(); } if(template == null){ log.error("failed to exec handlebars' template:{}", templateStr); return ""; } return template.apply(params); } catch (Exception e) { log.error("exec handlebars' template failed: {},cause:{}", templateStr, Throwables.getStackTraceAsString(e)); return ""; } } @SuppressWarnings("unchecked") public String execPath(String path, Map<String, Object> params, boolean isComponent) throws FileNotFoundException { try { if (params == null) { params = Maps.newHashMap(); } Template template; if (isComponent) { if (caches == null) { template = handlebars.compile("component:" + path); } else { template = caches.getUnchecked("component:" + path).orNull(); } params.put(RenderConstants.COMPONENT_PATH, path); } else { if (caches == null) { template = handlebars.compile(path); } else { template = caches.getUnchecked(path).orNull(); } } params.put(RenderConstants.USER, UserUtil.getCurrentUser()); //user params.put(RenderConstants.HREF, configManager.getFrontConfig ().getCurrentHrefs(Setting.getCurrentHost())); if(template == null){ log.error("failed to exec handlebars' template:path={}", path); return ""; } return template.apply(params); } catch (Exception e) { Throwables.propagateIfInstanceOf(e, FileNotFoundException.class); if (e instanceof HandlebarsException) { Throwables.propagateIfInstanceOf(e.getCause(), UserNotLoginException.class); } log.error("failed to execute handlebars' template(path={}),cause:{} ", path, Throwables.getStackTraceAsString(e)); } return ""; } public String execComponent(final Component component, final Map<String, Object> context) { if (!Strings.isNullOrEmpty(component.getService())) { Object object = null; try { object = invoker.invoke(component.getService(), context); } catch (UserNotLoginException e) { log.error("user doesn't login."); if (context.get(RenderConstants.DESIGN_MODE) == null) { throw e; // 非 DESIGN_MODE 时未登录需要抛出 } } catch (Exception e) { log.error("error when invoke component, component: {}", component, e); context.put(RenderConstants.ERROR, e.getMessage()); } context.put(RenderConstants.DATA, object); } try { return execPath(component.getPath(), context, true); } catch (Exception e) { log.error("failed to execute handlebars' template(path={}),cause:{} ", component.getPath(), Throwables.getStackTraceAsString(e)); } return ""; } }
整个块级Helpers的介绍基本就到这里了,前面介绍handlebars的文章都不错。
李白准备在黄鹤楼上题诗的时候看了崔颢写的诗,他认为崔颢的诗可以称为是千古绝唱,觉着自己写不出可以超越崔颢的作品,所以慨叹:“眼前有景道不得,崔颢题诗在上头”。