|
如何在OperaMasks 3.0中自定义原生UI构件 —渲染器(下)
在现实开发中编写基于Ajax框架的构件时,我们不可避免地需要在页面中渲染出引入外部的JavaScript脚本文件与CSS样式表文件的代码。比如说,在页面中放一个<w:button>,就应该在响应流中渲染出以下代码: <script type="text/javascript" src="/ear1/_global/resource/ext/om/ajax.js" charset="UTF-8"></script> <script type="text/javascript" src="/ear1/_global/resource/ext/ext-base.js" charset="UTF-8"></script> <script type="text/javascript" src="/ear1/_global/resource/ext/ext-core.js" charset="UTF-8"></script> <script type="text/javascript" src="/ear1/_global/resource/ext/package/util.js" charset="UTF-8"></script> <script type="text/javascript" src="/ear1/_global/resource/ext/package/widget-core.js" charset="UTF-8"></script> <script type="text/javascript" src="/ear1/_global/resource/ext/om/ButtonPlugin.js" charset="UTF-8"></script> <script type="text/javascript" src="/ear1/_global/resource/ext/package/button.js" charset="UTF-8"></script> <link class="x-skin" rel="stylesheet" type="text/css" href="/ear1/_global/resource/ext/skin/default/yuiext/css/ext-all.css"/> <link class="x-skin" rel="stylesheet" type="text/css" href="/ear1/_global/resource/ext/skin/default/yuiext/css/ext-extra.css"/> 渲染这种引入页面资源的代码,需要考虑以下几个问题:
为此,渲染引入页面外部资源需要一种独特的机制来支撑。在AOM中,使用了一种称为“资源描述符”的机制来管理页面资源的引入。下面我们就来看一下,在AOM中如何开发一个需要引入外部资源的构件。 在本节的描述中,我们会使用一个新的示例:编写一个代码高亮构件。在互联网上有很多用JavaScript实现的代码高亮开源项目,我们可以选用其中任意一种来作为我们这个新构件的客户端实现。在这里,我使用在百度中用“syntax highlighter javascript”关键字搜索的第一个结果 http://alexgorbatchev.com/wiki/SyntaxHighlighter。 利用第三方框架封装构件,首要的事情是确认License,在项目网站中我们可以看到这个项目使用的是LGPL 3协议,我们可以自由免费地使用。然后,我们需要了解这个框架的用法,下载(http://alexgorbatchev.com/downloads/grab.php?name=sh)后,通过阅读文档(http://alexgorbatchev.com/wiki/SyntaxHighlighter#For_Users),我们可以总结出这个框架的基本用法:
明白了基本用法之后,现在我们就可以开始编写新的构件了。在这里,我们仍然使用前面文章所使用的AOM构件工程,双击打开http://org.operamasks.demo.customercomponents命名空间下的myComponent.taglib.xml文件,在tags分类下添加一个新的标签:标签名称为code,component-type为org.operamasks.demo.Code。先不要发布,我们可以看到在component-type的输入框下是dependJSPackages与dependCSSPackages两个配置框,下面,就来看看这两个配置框的含义与用法。 前面已经提过,在许多场景下,不同构件的引入资源之间存在依赖关系。虽然我们可以不理会这种依赖关系,在每个构件中都直接渲染出它要引入的所有资源,但对于大型的构件库,这无疑是非常繁琐的。 在AOM中,使用资源描述符配置文件来描述这种依赖关系,来简化构件引入外部资源的声明。下面,我们结合示例来看看如何在AOM构件工程中配置资源依赖项目。 首先,我们需要把外部资源复制到构件工程中。请注意,构件资源文件都应该存放在AOM构件工程的"META-INF/resource"目录中。因此我们先在构件工程的“META-INF/resource”目录下,新建一个highlighter目录,然后把下载的syntaxhighlighter文件解压后复制到该目录。 下面开始定义外部资源的依赖项,我们首先双击打开构件工程“资源描述符”项目。在左边的“目录”中,选择“JS”分类,按“添加”按钮加入一条依赖项,用来引入核心脚本,资源名为highlight_core(可以随便起)。在资源文件中选择META-INF/resource目录下的highlighter/scripts/shCore.js文件。 ![]() 接下来,再添加一条用于引入Java语言高亮脚本的依赖项。继续在JS分类下添加一条依赖项,资源名为highlight_java,在资源文件中选择META-INF/resource目录下的highlighter/scripts/shBrushJava.js文件。并且,在“目录”窗口中,在新增的“highlight_java"依赖项上按右键,在右键菜单中选择”添加依赖资源“,在弹出窗口中选择在上一步中创建的highlight_core依赖项。 ![]() 我们可以继续添加需要支持高亮的语言脚本,每个依赖项都应该选择相关的脚本文件,并依赖于”highlight_core“项目。 ![]() 同理,我们可以加入对应的CSS样式依赖项。其中,highlight_css_core依赖项指向文件”META-INF/resource/highlighter/styles/shCore.css“,其他项目指向同名的主题样式,并依赖于highlight_css_core项目。 ![]() 存盘后,我们就定义好了所需要的资源描述符。总结一下,在资源描述符中,我们可以定义若干条资源依赖项,每条依赖项可以引入一个资源文件,并依赖于一个或多个其他依赖项。在编写渲染器时,只需要按照依赖项名称来引入资源,AOM引擎就会自动按照依赖关系渲染出该构件所需要的所有外部资源。 关闭资源描述符编辑窗口,回到myComponent.taglib.xml的编辑窗口。在默认情况下,我们希望这个代码高亮构件对Java语言进行高亮,并使用eclipse高亮风格。因此在dependJSPackages中指定highlight_java,在dependCSSPackage中指定highlight_css_eclipse。 现在,我们可以点击“发布”按钮,生成相关的java类。 构件基类的编写方法在这里不再重复,可以参考《如何在OperaMasks 3.0中自定义原生UI构件 — 渲染器(上)》中的描述。代码如下:
@ComponentMeta(tagName="code",
family="org.operamasks.demo.Code",
rendererType="org.operamasks.demo.Code")
public abstract class UICodeBase extends UIComponentBase {
/**
* 语法高亮所使用的语言。缺省值为java。
* 可选取值为:java, css, xml, javascript。
*/
protected String lang;
/**
* 语法高亮的主题风格。缺省值为eclipse。
* 可选值为:eclipse, default, midnight。
*/
protected String theme;
/**
* 是否显示行号。缺省值为true。
*/
protected Boolean showLineNumber;
/**
* 高亮行号。缺省值为空。
* 多个高亮行之间用逗号分隔。例如“2,4,5”
*/
protected String highlightLines;
}
下面我们开始编写渲染器,双击打开自动创建的CodeAjaxRenderer,可以看到已经自动生成了基本依赖代码:
package org.operamasks.demo;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import org.operamasks.faces.render.common.AjaxRendererBase;
public class CodeAjaxRenderer extends AjaxRendererBase {
@Override
public String[] getDependedCSSPackages(FacesContext context, UIComponent component) {
return new String[]{"highlight_css_eclipse"};
}
@Override
public String[] getDependedJSPackages(FacesContext context, UIComponent component) {
return new String[]{"highlight_java"};
}
}
这里的getDependedCSSPackages与getDependedJSPackages方法继承自ResourceRenderer接口,渲染器覆盖这两个方法,返回一个字符串数组,通知引擎首次渲染该构件时需要引入的资源依赖项目。从前面的代码高亮脚本基本用法,我们知道根据应用开发者所设置的高亮语言与主题风格,我们可能需要引入不同的资源依赖项目,因此需要对注册依赖项目的代码进行修改,根据构件对象的lang属性与theme属性取值来确定所注册的依赖项目,修改后的代码如下:
package org.operamasks.demo;
import java.util.HashMap;
import java.util.Map;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import org.operamasks.faces.render.common.AjaxRendererBase;
public class CodeAjaxRenderer extends AjaxRendererBase {
private static final Map<String, String> SUPPORTED_LANGS = new HashMap<String, String>() {{
put("java", "highlight_java");
put("css", "highlight_css");
put("xml", "highlight_xml");
put("javascript", "highlight_javascript");
}};
private static final Map<String, String> SUPPORTED_THEMES = new HashMap<String, String>() {{
put("eclipse", "highlight_css_eclipse");
put("default", "highlight_css_default");
put("midnight", "highlight_css_midnight");
}};
@Override
public String[] getDependedCSSPackages(FacesContext context, UIComponent component) {
UICode code = (UICode) component;
String lang = getLang(code);
if (SUPPORTED_LANGS.containsKey(lang)) {
return new String[] {SUPPORTED_LANGS.get(lang)};
} else {
throw new IllegalStateException("不支持的高亮语言类型:" + lang);
}
}
@Override
public String[] getDependedJSPackages(FacesContext context, UIComponent component) {
UICode code = (UICode) component;
String theme = getTheme(code);
if (SUPPORTED_THEMES.containsKey(theme)) {
return new String[] {SUPPORTED_THEMES.get(theme)};
} else {
throw new IllegalStateException("不支持的高亮主题风格:" + theme);
}
}
private String getLang(UICode code) {
String lang = code.getLang();
if (lang == null) {
lang = "java";
}
return lang;
}
private String getTheme(UICode code) {
String theme = code.getTheme();
if (theme == null) {
theme = "eclipse";
}
return theme;
}
}
然后,我们继续在渲染器中加入以下方法,在响应流中渲染出所需要的代码:
private static final String HIGHLIGHTALL_FLAG_KEY = "org.operamasks.demo.highlightall";
@Override
public void encodeHtmlBegin(FacesContext context, UIComponent component)
throws IOException {
UICode code = (UICode) component;
重新打包发布构件工程后,就可以在测试应用中测试这个新构件:
<w:page title="Insert title here">
<my:code lang="java" highlightLines="2,3">
public class myClass {
private int i;
public int getI() {
return i;
}
}
</my:code>
<my:code lang="css" >
.p {
width: 10px;
}
</my:code>
</w:page>
运行页面就看到代码高亮的效果。在浏览器中查看HTML源码,我们可以发现正确渲染出来了如下资源代码: <script type="text/javascript" src="/ear1/_global/resource/highlighter/scripts/shCore.js" charset="UTF-8"></script> <script type="text/javascript" src="/ear1/_global/resource/highlighter/scripts/shBrushJava.js" charset="UTF-8"></script> <script type="text/javascript" src="/ear1/_global/resource/highlighter/scripts/shBrushCss.js" charset="UTF-8"></script> ... <link class="x-skin" rel="stylesheet" type="text/css" href="/ear1/_global/resource/highlighter/styles/shCore.css"/> <link class="x-skin" rel="stylesheet" type="text/css" href="/ear1/_global/resource/highlighter/styles/shThemeEclipse.css"/> 在开发中,还有一些场景需要我们在渲染过程中(encodeResourceBegin方法中)才能确定需要引入哪些资源。这时,我们可以调用ComponentResource上的API来加入依赖资源,例如:
public void encodeResourceBegin(FacesContext context,ResourceManager rm, UIComponent component)
throws IOException {
ComponentResource resource = ComponentResource.getResourceInstance(rm);
resource.addJSPackageDependency("highlight_java");
resource.addCSSPackageDependency("highlight_css_eclipse");
......
}
关于ComponentResource的详细介绍,可参考《如何在OperaMasks 3.0中自定义原生UI构件 — 渲染器(上)》。 (以上的渲染器写法存在一个小bug,由于SyntaxHighlighter项目的脚本本来就被设计为通过引入不同的css样式文件来改变主题,因此一个页面只能使用一种主题。如果在页面中存在多个<my:code>构件,而且分别指定了不同的theme属性的话,只有一种主题能生效且整个页面都使用同一主题。修正这个bug需要对该项目的css样式文件做小量修改,但与本文主题无关,在这里就不展开讨论了。) 渲染器除了渲染代码外,还担负着另一个重要职责,对客户端提交上来的请求信息进行解码,并同步到服务器端构件模型体系中。构件开发者可以覆盖渲染器的decode方法,编写处理请求内容的逻辑,这个方法将会在完成构件树创建之后被调用,也就是说,这时我们可以通过引擎或构件API——例如getParent()方法与getChildren()方法等——获取到构件之间的组织关系。在decode方法中,开发者可以随时通过FacesContext.getExternalContext().getRequestParameterMap()方法获取到请求参数表,其中包含了所有从客户端提交的信息。由于提交的内容必然是由上一次请求渲染出来的响应代码所决定的,因此一个渲染器应该负责处理请求参数中的哪些参数,也应该由其渲染出来的响应代码而定。通常来说,decode方法中需要处理以下的具体任务。 在《如何在OperaMasks 3.0中自定义原生UI构件 — 基本概念》中我们提到,对于允许用户输入的构件(构件类实现了EditableValueHolder接口的构件),value属性是一个特殊的属性,它是被设计用来在客户端供用户修改的。因此它的值会通过请求参数来提交,而构件的其他属性值通常不支持在客户端直接修改,是通过视图状态(ViewState)进行跨请求传递的。下面我们以AOM官方构件w:textField渲染器为例来简单说明应该如何对请求参数中的构件提交值进行解码(为了突出重点,对代码进行了简化和合并):
/**
* w:textField组件的AJAX渲染器
*/
public class AjaxTextFieldRenderer extends AbstractFieldRenderer {
...
@Override
public void decode(FacesContext context, UIComponent component) {
...
// save submitted value
简单总结,需要处理请求参数的渲染器中,必然存在首次渲染与decode方法的合作关系,分别负责渲染提交代码与解码。特别地,在编写一个渲染输入构件的渲染器时,需要考虑以下几点:
至于字符串类型的提交值是否合法,如何转换,构件值发生改变后如何调用ValueChangeListener,如何把构件值更新到绑定的ManagedBean属性中等问题,在这里不需要关心,AOM引擎会在后续的处理中自动进行。 对于动作构件(实现了ActionSource或ActionSource2接口的构件),渲染器上的decode方法则担负了确定发起提交的动作事件源的职责。在一个页面中往往存在了多个动作构件,例如说多个<w:button>,当用户按下了其中一个按钮发起提交,AOM框架是怎么知道用户按下的是哪一个,又是如何去调用对应的Action方法的呢?玄机就在渲染器的decode方法中。下面我们以简化的<w:button>渲染器代码来说明:
public class AjaxButtonRenderer extends AjaxRendererBase implements ToolBarItemRenderer {
...
@Override
public void encodeResourceBegin(FacesContext context, ResourceManager manager, UIComponent component) {
...
同样的,动作构件的渲染器中也体现了首次渲染与decode方法的合作关系。首先,在首次渲染的encodeResourceBegin方法(或encodeHtmlBegin方法,视具体情况而定)中渲染出构件触发提交的客户端事件脚本,通常我们会选用onclick事件。在渲染出来的脚本中,通常会先处理应用开发者在构件上定义的事件脚本。然后执行提交脚本,提交脚本中应该保证在提交的请求参数中包含提交构件的clientId作为键值,通常可以调用RendererUtils工具类的encodeAjaxSubmit方法来实现。 这样,在decode方法中就可以判断当前正在解码的构件的clientId是否在请求参数中,从而确定当前构件是否事件源。如是,则为当前构件新建一个动作事件实例,并加入构件的事件队列。至于加入事件队列之后,何时触发,如何调用ManagedBean上的绑定方法等问题,AOM引擎会自动处理,在这里不用关心。 所有评论
Nari Chiboo
2010-06-03
评论道:
弱弱的问一下:syntaxhighlighter文件在哪里下载?给个url,谢谢!
rory2008
2010-05-29
评论道:
例子代码中 getDependedCSSPackages 与getDependedJSPackages方法体反了
1 共1页
您还没有登录,请登录后发表评论
|
相关文章
|
<script type="text/javascript" src="js/shCore.js"></script>
<script type="text/javascript" src="css/shBrushJScript.js"></script>
<link href="css/shCore.css" _fcksavedurl="css/shCore.css" _fcksavedurl="css/shCore.css" rel="stylesheet" type="text/css" />
<link type="text/css" rel="Stylesheet" href="/styles/shThemeMidnight.css" _fcksavedurl="/styles/shThemeMidnight.css" _fcksavedurl="/styles/shThemeMidnight.css"/>
<script type="text/javascript">
SyntaxHighlighter.all();
</script>
<pre class="brush: js">
function foo()
{some boys ...
}
</pre>




if (!reqMap.containsKey(HIGHLIGHTALL_FLAG_KEY)) {
writer.startElement("script", component);
writer.writeText("SyntaxHighlighter.all();", null);
writer.endElement("script");
}
writer.startElement("pre", component);
StringBuilder sb = new StringBuilder();
Formatter fmt = new Formatter(sb);
fmt.format("brush: %s; gutter: %s; highlight: %s", getLang(code), getShowLineNumber(code), getHighlightLines(code));
writer.writeAttribute("class", sb.toString(), null);
writer.writeText(text, null);
writer.endElement("pre");
}
}
@Override
public boolean getEncodeHtmlChildren(UIComponent component) {
return true;
}
private boolean getShowLineNumber(UICode code) {
Boolean showLines = code.getShowLineNumber();
if (showLines == null) {
showLines = true;
}
return showLines;
}
private static final String HIGHLIGHTLINES_PATTERN = "\\d+\\s*(,\\s*\\d+\\s*)*";
private String getHighlightLines(UICode code) {
String lines = code.getHighlightLines();
if (lines == null) {
return "null";
} else if (lines.matches(HIGHLIGHTLINES_PATTERN)) {
return "[" + lines + "]";
} else {
throw new IllegalStateException("非法的hightlightLines属性格式:" + lines);
}
}
((EditableValueHolder) component).setSubmittedValue(newValue);
}
}
@Override
public void encodeHtmlBegin(FacesContext context, UIComponent component) throws IOException {
...
String clientId = component.getClientId(context);
...
out.startElement("input", component);
out.writeAttribute("id", clientId, "clientId");
out.writeAttribute("name", clientId, "clientId");
...
}
...
}
ActionEvent event = new ActionEvent(component);
PartialUpdateCandidates.addLinkedUpdate(component);
component.queueEvent(event);
}
}
...
}