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的文章都不错。
李白准备在黄鹤楼上题诗的时候看了崔颢写的诗,他认为崔颢的诗可以称为是千古绝唱,觉着自己写不出可以超越崔颢的作品,所以慨叹:“眼前有景道不得,崔颢题诗在上头”。