Knockout.js与Primefaces整合日志 3

昨天发了版,最近空闲一点,继续更新。

客户端的架子搭好了,现在来搞服务器端。

在服务器端我们使用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的做法会使系统很容易受到脚本注入攻击。

下一节我们将尝试解决这一问题。

你可能感兴趣的:(JavaScript,JSF)