客户端的架子搭好了,现在来搞服务器端。
在服务器端我们使用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的做法会使系统很容易受到脚本注入攻击。
下一节我们将尝试解决这一问题。