昨天发了版,最近空闲一点,继续更新。
客户端的架子搭好了,现在来搞服务器端。
在服务器端我们使用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的做法会使系统很容易受到脚本注入攻击。
下一节我们将尝试解决这一问题。
分享到:
相关推荐
knockout 教程 中文版 Knockout使用js代码达到双向绑定的目的,类似Silverlight/WPF里的绑定一样,我们主要就是利用相关的特性进行开发的,极大地减少了代码开发量。
Beginning with a vital overview of Knockout.js, including the MVVM design pattern, you will create a simple but powerful application capable of integrating with ASP.NET MVC as well as gain a thorough ...
MVC使用Knockout.JS实现的单页应用程序(SPA)1
MVC使用Knockout.JS实现的单页应用程序(SPA)2
Knockout.js.2014.12.pdf
基于 BOOTSTRAP 和 KNOCKOUT.JS 的 ASP.NET MVC 开发实战。 利用动态服务端Web内容和响应Web设计共同构建的网站,在任何分辨率、桌面或移动设备下都可以进行良好的显示。通过本书的实践应用,你将可以学习对ASP.NET ...
ASP.NET MVC 5 with Bootstrap and Knockout.js Oreilly 2015 (英文pdf+源码)
With this practical book, you’ll learn how by combining the ASP.NET MVC server-side language, the Bootstrap front-end framework, and Knockout.js—the JavaScript implementation of the Model-View-...
简单的knockout.js/websockets/node.js实时游戏 这是一个非常简单的实时游戏,使用 node.js、knockout.js 和 socket.io 创建。 ##目标 尝试比其他玩家更快地解决简单的数学方程。 每场比赛都有时间限制(默认...
一套完整的框架及源代码,含数据初始数据,支持vs2010、2013,sql2008,学习真实项目及easyui、ko、fluent开发的资源
Diving into Knockout.JS
前端项目-knockout.mapping,Object mapping plugin for Knockout
Knockout.JS is an emerging JavaScript presentation framework that promotes a solid solution design in combination with Jasmine, a well-known unit testing library. They both ensure rapid development ...
第3章Knockout.js介绍 安装Knockout.js 基本示例 何为MVVM? 创建ViewModel 小结 第4章数据库应用 Entity Framework介绍 Code First Database First 创建测试数据 小结 第二部分数据处理 第5章表的查询...
Knockout.js 入门书籍 Knockout 是轻量级客户端 MVC 首选框架,核心能力为客户端数据绑定
https://cdnjs.cloudflare.com/ajax/libs/knockout-validation/2.0.4/knockout.validation.js https://cdnjs.cloudflare.com/ajax/libs/knockout-validation/2.0.4/knockout.validation.min.js jsdelivr ...
With this practical book, you'll learn how by combining the ASP.NET MVC server-side language, the Bootstrap front-end framework, and Knockout.js - the JavaScript implementation of the Model-View-...
Knockout Mapping plugin v2.4.1.debug.js
KnockOut抠图插件,支持32、64位版系统(与photoshop位数无关) 目前测试使用win10(64位)、photoshop cs6 13.0(32位) 1.下载后放在photoshop安装目录中的Plug-ins中(解压后Plug-ins文件夹中应该会包含:KnockOut.8bf...