昨天发了版,最近空闲一点,继续更新。
客户端的架子搭好了,现在来搞服务器端。
在服务器端我们使用JSF2.1,Primefaces 4.0.x (elite版,现在为了支持IE7还卡在4.0.16,预计明年1月IE8的官方支持过期,我们就可以考虑放弃IE7了),OmniFaces。
首先,我们需要让JSF组件支持我们的data-binding, data-vm等自定义属性。由于在JSF中,组件类自身的内部状态和标签上的属性使用一样的API来储存和访问(通过组件类上getter/setter或getAttributes()方法)。渲染器不能无脑地把组件类上所有属性都渲染到最终的HTML标签上,而必须采用一套硬编码的passthru属性表,渲染器仅把它自己认可的passthru属性渲染出来。这就导致了渲染器不认识的自定义的属性不会出现在渲染结果中。JSF2.2提供了新的passthough属性命名空间来解决这个问题,但在JSF2.1中,需要我们自己动手来实现。
OmniFaces针对HTML5的新属性(比如input上的placeholder属性)提供了一个Html5RenderKit。但这个解决方案是在ResponseWriter的startElement方法中做手脚,这导致如果一个组件会渲染出多个标签时,属性有时会被重复渲染到这些标签上。对一些表单组件来说这个问题不算严重,一来表单组件通常都是单一标签,二来就算重复渲染了,这些属性放在非表单标签上是没有任何效果的。但我们的场景要求更加严格,重复渲染data-bind就会导致意外的绑定,引起意外的UI动作。因此,我们可以借鉴OmniFaces的思路,并进行一些改进。
整体思路是,采用一个自定义的RenderKitWrapper来创建一个自定义的ResonseWriter,在这个ResponseWriter的startElement方法中,仅仅记录当前组件的clientId。改为在writeAttribute方法中,当发现当前渲染的属性为id时,把id值与clientId值进行比较,如果相等的话,说明当前正在渲染的标签是当前组件的主要标签,我们就把data-bind属性渲染到这个标签上。由于JSF内部是依赖clientId来对组件进行局部渲染,并且在decode阶段,通常是根据http请求中的clientId来定位组件,因此依赖clientId找主要标签的方案是完全可行的。
代码如下,首先写个RenderKitWrapper的实现,它的主要工作就是创建我们自定义的CustomAttributeResponseWriter。
** 从下面的三段代码可以看出,这个解决方案连续使用了三次装饰者模式,这是JSF自身所提供的扩展机制。
public class CustomAttributeRenderKit extends RenderKitWrapper
{
private RenderKit wrapped;
/**
* Construct a new custom render kit around the given wrapped render kit.
*
* @param wrapped
* The wrapped render kit.
*/
public CustomAttributeRenderKit(RenderKit wrapped)
{
this.wrapped = wrapped;
}
/**
* Returns a new CustomAttributeResponseWriter which in turn wraps the default
* response writer.
*/
@Override
public ResponseWriter createResponseWriter(Writer writer, String contentTypeList, String characterEncoding)
{
return new CustomAttributeResponseWriter(super.createResponseWriter(writer, contentTypeList, characterEncoding));
}
@Override
public RenderKit getWrapped()
{
return wrapped;
}
}
CustomAttributeResponseWriter源码如下:
import java.io.IOException;
import java.io.Writer;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;
import javax.faces.context.ResponseWriter;
import javax.faces.context.ResponseWriterWrapper;
import org.omnifaces.util.Components;
class CustomAttributeResponseWriter extends ResponseWriterWrapper
{
private static final Set<String> CUSTOM_ATTRIBUTES = new HashSet<String>("data-bind", "data-vm");
private static final Set<String> IGNORED_TAGS = new HashSet<String>("option", "script", "style");
private String currentClientId = "";
private String currentTag = "";
private ResponseWriter wrapped;
public CustomAttributeResponseWriter(ResponseWriter wrapped)
{
this.wrapped = wrapped;
}
@Override
public ResponseWriter cloneWithWriter(Writer writer)
{
return new CustomAttributeResponseWriter(super.cloneWithWriter(writer));
}
/**
* 在startElement中保持当前组件的clientId。由于ResponseWriter不会跨线程使用,可以保持在对象属性中。
*/
@Override
public void startElement(String name, UIComponent component) throws IOException
{
this.currentTag = name;
super.startElement(name, component);
if (component == null)
{
component = Components.getCurrentComponent();
}
if (component != null)
{
this.currentClientId = component.getClientId();
}
}
@Override
public void writeAttribute(String name, Object value, String property) throws IOException
{
super.writeAttribute(name, value, property);
/* Knockout attributes should only bind to the main element with same client id as the component */
if (name != null && !IGNORED_TAGS.contains(this.currentTag.toLowerCase()) &&
"id".equals(name) && this.currentClientId != null && this.currentClientId.equals(value)) {
if (component == null) component = Components.getCurrentComponent();
if (component != null) {
writeCustomAttributesIfNecessary(component.getAttributes(), CUSTOM_ATTRIBUTES);
}
}
}
private void writeCustomAttributesIfNecessary(Map<String, Object> attributes, Collection<String> names) throws IOException
{
for (String name : names)
{
Object value = attributes.get(name);
if (value != null)
{
super.writeAttribute(name, value, null);
}
}
}
}
然后我们还需要一个创建这个RenderKit的RenderKitFactory
import javax.faces.context.FacesContext;
import javax.faces.render.RenderKit;
import javax.faces.render.RenderKitFactory;
import java.util.Iterator;
public class CustomAttributeRenderKitFactory extends RenderKitFactory
{
private RenderKitFactory wrapped;
public CustomAttributeRenderKitFactory(RenderKitFactory wrapped) {
this.wrapped = wrapped;
}
@Override
public void addRenderKit(String renderKitId, RenderKit renderKit) {
wrapped.addRenderKit(renderKitId, renderKit);
}
/**
* 如果传入的renderKitId等于{@link RenderKitFactory#HTML_BASIC_RENDER_KIT},返回一个封装了原renderKit的CustomAttributeRenderKit实例。
* 否则直接返回原来的renderKit
*/
@Override
public RenderKit getRenderKit(FacesContext context, String renderKitId) {
RenderKit renderKit = wrapped.getRenderKit(context, renderKitId);
return (HTML_BASIC_RENDER_KIT.equals(renderKitId)) ? new CustomAttributeRenderKit(renderKit) : renderKit;
}
@Override
public Iterator<String> getRenderKitIds() {
return wrapped.getRenderKitIds();
}
}
最后,在faces-config.xml中配置使用这个自定义的RenderKitFactory。值得再次提醒的是,JSF的render-kit-factory配置使用的是装饰者模式,框架在调用这里配置的RenderKitFactory的构造方法时,会传入系统原本的RenderKitFactory ( 通常就是HTML_BASIC_RENDERKIT_FACTORY ),我们自定义的RenderKitFactory可以在此基础上进行扩展。
<faces-config
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd"
version="2.0" >
<factory>
<render-kit-factory>net.danieldeng.faces.common.renderkits.CustomAttributeRenderKitFactory</render-kit-factory>
</factory>
</faces-config>
大功告成,现在可以在JSF组件上直接使用data-bind,data-vm属性了。
<h:panelGroup id="DemoDiv" display="block" data-vm="DemoVM">
<h:inputText id="username" value="#{myBean.username}" data-bind="inputText: username"/>
</h:panelGroup>
<scirpt>
app.createVM({
username: "#{myBean.username}"
}, "DemoVM").bind("#DemoDiv")
</script>
很容易发现,这里有个缺陷,knockout.js的常规开发方式是用Javascript模型来驱动视图,因此模型的初始化需要放在Javascript中。而JSF的常规做法是组件直接绑定后台ManagedBean属性。因此这里不得不同时在组件的value属性上以及Javascript的模型初始化代码中重复使用#{myBean.username}这个EL表达式。并且,在Javascript嵌入EL的做法会使系统很容易受到脚本注入攻击。
下一节我们将尝试解决这一问题。