IBM®
跳转到主要内容
    中国 [选择]    使用条款
 
 
Select a scope: Search for:    
    首页    产品    服务与解决方案     支持与下载    个性化服务    
跳转到主要内容

developerWorks 中国  >  Web development  >

向 Project Zero 应用程序中添加 Ruby 模板

用 RHTML 文件扩展 Ruby 支持来创建动态用户界面

developerWorks
文档选项

未显示需要 JavaScript 的文档选项

样例代码

英文原文

英文原文


级别: 中级

Dan Jemiolo (danjemiolo@us.ibm.com), 顾问软件工程师, IBM 

2008 年 2 月 14 日

Ruby 用户们,请注意!现在您能在创建 Project Zero 应用程序时完成 Groovy 和 PHP 用户可以完成的一切操作!在 前一篇文章 中,我们说明了如何扩展 Project Zero 以提供 Ruby 脚本语言的支持。我们编写的代码允许 Ruby 用户把脚本技巧转移到 Zero 平台并利用其独特编程模型。当然,脚本不是 Ruby 用来创建应用程序的惟一方法 —— 使用 Ruby on Rails 框架的程序员还将 Ruby 混合到类似于 JSP 和 PHP 的 HTML 模板中。对于创建动态用户界面,这些称为 RHTML 文件的模板非常有用,而本文将向您展示如何扩展 Ruby 支持来包括这些模板。阅读本文了解 Ruby 用户现在如何能在创建 Zero 应用程序时完成 Groovy 和 PHP 用户可以完成的一切操作!

开始之前

本文假设您已经下载了 Project Zero M2 或者已经完成了 入门教程 的学习,或者自己编写过简单的应用程序。您还应当阅读本文的前一篇文章 “向 Project Zero 应用程序中添加 Ruby 脚本”,了解 JSP、PHP 和 RHTML 等服务器端模板语言中的概念。

Project Zero 社区
请查阅 Project Zero 网站,了解 Project Zero 如何提供功能强大 — 但异常简单 — 的现代 Web 应用程序开发和执行平台。这个活跃的 社区 探讨项目开发、为开发人员提供帮助,而且非常乐意听取您的想法!

简介

在本文的前一篇文章 “向 Project Zero 应用程序中添加 Ruby 脚本” 中,您了解了如何扩展 Project Zero 以提供 Ruby 脚本语言支持,但是我们的解决方案并未涵盖 Ruby on Rails 开发人员使用 Ruby 创建应用程序可以采用的所有方法。Rails 框架提供一种将 Ruby 代码与 HTML 模板混用的方法,这是一种类似于 JSP 和 PHP 的想法。这些称为 RHTML 文件 的模板在创建动态用户界面时非常有用,并且避免了程序员必须使用 print 语句创建 HTML(同 J2EE servlet 一样)的需求。Zero 已经允许用户使用 Groovy 和 PHP 创建模板(分别为 .gt 和 .php 文件),那么为什么不提供 Ruby 模板支持呢?

创建 RHTML 处理器的策略

在决定如何实现 Zero 的 RHTML 处理器时,我们的主要目标是避免从头开始编写 RHTML 解析器。Ruby 模板是一项优秀的功能,但是不足之处在于它引入大量库并且编写会隐藏许多错误的编译器代码,要查找这些错误,需要大量用户专门花时间来测试。使用 Ruby 脚本,我们可以把所有的脚本执行工作委托给 JRuby;最好是让 JRuby 以类似的方式处理嵌入到 RHTML 文件中的代码,这样我们就不必编写自己的专用 Ruby 解析器。

当然,问题是 JRuby 无法执行 RHTML 文件,只能执行含有有效的 Ruby 语法的 Ruby 脚本。怎样才能让 JRuby 执行嵌入到 RHTML 文件中的 Ruby 代码而把并未修改的其余内容(普通 HTML)发送给客户端?而且如果我们设法提取并把 Ruby 代码传递给 JRuby,那么如何把代码输出与静态 HTML 整合起来?当考虑到分散在文件各处的所有 Ruby 代码片段全都要在同一个范围内(可以访问其他 Ruby 代码片段的变量)执行时,把代码与标记分离并将代码传递给 JRuby 显然是不够的。我们需要一种更好的方法。

真正的答案是把整个 RHTML 文件 —— 代码和标记 —— 转换为 Ruby 脚本,类似于 JSP 编译器把 JSP 文件转换为 J2EE servlet 类的方法。一旦模板成为有效的 Ruby 脚本,我们就可以把它传递给 JRuby 而无需使用解析器,甚至不需要知道它的来源。下面几部分将讨论 RHTML 语法的详细信息及 RHTML 处理器中重要的转换过程。

了解 RHTML 语法

RHTML 文件是在特殊标记 <% 和 %> 内嵌入 Ruby 代码的 HTML 文件。在将页面返回给客户机之前,这些特殊标记内的所有内容都先在服务器上进行处理,因此最终用户永远看不到原始的 Ruby 代码。Ruby 代码必须有一条或多条完整的 Ruby 语句,并且某一组中的语句 (<% ... %>) 会受前面几组中执行的语句的影响。将代码片段分成多个组将帮助程序员避免必须用 print 语句生成 HTML 的情况,同时允许创建完全分离的代码和标记。清单 1 显示了 RHTML 内容的示例:


清单 1. 带有嵌入语句的样例 RHTML 文件
                
    
    <html>
        
      <body>
        <p>
        Here is a list of numbers 1 - 10:
        <%
          (1..10).each do |n| 
            print n, ' '
          end
        %>
        </p>
      </body>
    
    </html>
      

在将 Ruby 代码嵌入到 HTML 中时有一种特殊情况需要考虑:如果开发人员只需要插入单个值或者一个简单表达式的结果,则可以将开始标记修改为 <%= ... %>。清单 2 展示了在不使用完整语句的情况下把简单值嵌入到页面中的示例。


清单 2. 带有嵌入表达式的样例 RHTML 文件
                
      
    <html>
        
      <body>
        <p>
        The sum of 3 and 4 is <%= 3 + 4 %>
        </p>
      </body>
    
    </html>
      

生成有效的 Ruby 脚本

要通过类似于清单 1 和清单 2 中所示的文件来生成 Ruby 脚本,需要创建 Ruby 语句来表示静态 HTML 以及嵌入代码。必须把所有非 Ruby 文本(例如 HTML 和 JavaScript)都转换为输出文本字符串的 print 语句。所有 Ruby 代码都必须按原样复制,并根据需要添加 print 语句(针对 HTML)。对于嵌入值 (<%= ... %>) 的特殊情况,需要创建 print 语句,将 Ruby 表达式输出到标记之间。清单 3 展示了为清单 2 中的文件生成的脚本的内容:


清单 3. 与 RHTML 文件等效的 Ruby 脚本
                
    
    print "<html><body><p>The sum of 3 and 4 is "
    print 3 + 4
    print "</p></body></html>"
      

正如您所见,脚本的概念十分简单 —— 大部分是 print 语句,相应地混入了其他 Ruby 语句。对清单 3 中的代码可以执行的一个优化过程是使用换行字符使生成的 HTML 与原始文件更相似 —— 这将使开发人员可以更轻松地调试 RHTML 模板。我们将在实现中包括此优化过程。

存储 Ruby 脚本

规划 RHTML 处理器时,需要考虑的最后一项决定是在生成脚本后怎样处理这些脚本。最简单的方式是把脚本写入磁盘,并使用可以轻易映射到原始 RHTML 文件的文件名。例如,如果原始文件被命名为 my-file.rhtml,则生成的脚本可以是 my-file.rhtml.rb(.rb 是 Ruby 脚本的常用扩展名)。我们的处理器随后可以使用以下算法来生成和执行脚本:

  1. 为表示 RHTML 文件的脚本创建文件名。
  2. 如果脚本存在并且 RHTML 文件最近未修改,则跳过生成过程。
  3. 否则,生成脚本并使用步骤 1 中创建的名称将其保存在磁盘中。
  4. 执行脚本。

步骤 2 有两个目的:它是一个优化过程,因为可以避免一次又一次地生成相同脚本;而且对于开发人员来说非常方便,因为他们对 RHTML 文件所做的更改将立即随下一个请求显示出来,这样做在开发时将节省大量时间。

实现 RHTML 处理器

现在我们有了实现 RHTML 处理器的策略,需要把它与原始 Ruby 处理器关联起来。同 前一篇 文章一样,您可以将代码逐步添加到样例项目中,也可以从 下载 部分中下载完整项目。如果要逐步添加代码,则需要从前一篇文章中使用的样例项目开始,这样常规的 Ruby 支持就已经就绪。

目前 Ruby 支持是由名为 JRubyInterpreter 的类处理的,该类将实现 Zero Core 的 Interpreter 接口并使用 JRuby 解析器来执行 HTTP 请求。处理 RHTML 文件将要求我们编写另一个 Interpreter 类,该类先生成 Ruby 脚本,然后再调用 JRuby。我们可以扩展 JRubyInterpreter 类来避免重写面向 JRuby 的代码。清单 4 显示了完成所需操作的 RHTMLInterpreter 类的实现。


清单 4. RHTMLInterpreter 类
                
    
    package zero.scripting.ruby;

    import java.io.File;

    import zero.core.context.GlobalContext;
    import zero.core.events.HandlerInfo;
    import zero.scripting.html.HTMLCompiler;

    public class RHTMLInterpreter extends JRubyInterpreter 
    {
        public void invoke(HandlerInfo handlerInfo) 
        {
            try 
            {
                File scriptFile = new File(handlerInfo.handler);
                HTMLCompiler compiler = new HTMLCompiler();
                
                //
                // convert RHTML file to a Ruby script file
                //
                File compiledFile = compiler.compile(scriptFile, ".rb");
                
                super.invoke(compiledFile);
            } 
            
            catch (Throwable error) 
            {
                GlobalContext.put("/request/status", 500);
                GlobalContext.put("/request/error", error);
            }
        }
    }
      

注意,RHTMLInterpreter.invoke() 方法将使用名为 HTMLCompiler 的类来处理把 RHTML 转换为 Ruby 的艰巨任务;它的真正职责是确保在把执行委托给父类之前执行转换任务。HTMLCompiler 类很大,但是上面使用的 compile() 方法是最重要的部分。清单 5 展示了 compile() 方法中的代码。您可以从 下载 部分获得其余代码。


清单 5. HTMLCompiler.compile() 方法
                
    
    public File compile(File htmlFile, String extension)
        throws IOException, HTMLCompilerException
    {
        File compiledFile = getCompiledFile(htmlFile, extension);
        
        //
        // make sure we don't re-compile files that haven't changed
        //
        if (compiledFile.exists() && 
            compiledFile.lastModified() > htmlFile.lastModified())
            return compiledFile;
        
        String html = getHTML(htmlFile);
        
        int current = 0;
        int codeStart = html.indexOf(_EMBEDDED_START_TOKEN, current);
        
        FileWriter writer = new FileWriter(compiledFile);
        
        //
        // look for the code fragments and add them to the script as 
        // regular code statements. all HTML gets converted into 
        // print statements
        //
        while (codeStart >= 0)
        {
            //
            // record all HTML preceding next code fragment
            //
            writePrintStatements(writer, html.substring(current, codeStart));
            
            int codeEnd = html.indexOf(_EMBEDDED_END_TOKEN, codeStart);
            
            if (codeEnd < 0)
                throw new HTMLCompilerException("No matching end token found.");
            
            int stmtStart = codeStart + _EMBEDDED_START_TOKEN.length();
            String code = html.substring(stmtStart, codeEnd);
            
            //
            // special case - for <%= expression %> fragments, we just 
            // want to insert the expression's value, not make it a 
            // separate statement
            //
            if (html.charAt(stmtStart) == _EMBEDDED_VALUE_TOKEN)
                writeValuePrintStatement(writer, code.substring(1));
            
            //
            // normal case - code recorded as it was written
            //
            else
            {
                writer.write(code);
                writer.write('\n');
            }
            
            current = codeEnd + _EMBEDDED_END_TOKEN.length();
            codeStart = html.indexOf(_EMBEDDED_START_TOKEN, current);
        }
        
        //
        // take care of HTML after last code fragment, if any
        //
        if (current < html.length())
            writePrintStatements(writer, html.substring(current));
        
        writer.flush();
        writer.close();
        
        return compiledFile;
    }
      

注意,compile() 方法包括算法的第 2 步所指定的优化过程。第一条 if 语句将确保在确实需要时只生成一个脚本。另外,如果查看 writePrintStatements() 方法的代码,您将看到它为原始文件中找到的每一行代码都添加了一个换行符,因此生成的 HTML 看上去完全相同。

更新 Zero 配置文件

代码已经就绪,但是还有一个步骤需要完成:像在它之前的 Ruby 解析器一样,我们的 RHTML 解析器必须在 Zero Core 中注册,这样它才能知道如何处理 .rhtml 文件的请求。把清单 6 中的代码段添加到应用程序的 /config/zero.config 文件中将使内容更加规范。


清单 6. 添加到应用程序的 /config/zero.config 文件中
                
    
    [/app/interpreters]
    .rhtml=zero.scripting.ruby.RHTMLInterpreter
      

测试 Ruby 模板

现在 RHTML 支持已经就绪,让我们通过向应用程序中添加 RHTML 文件并从 Web 浏览器调用来测试 RHTML 文件。为了演示 Ruby 和 RHTML 与 Zero 的完整集成,模板将使用 Zero 的 GlobalContext API 来读取请求数据并创建响应。清单 7 中的 RHTML 文件通过在 HTML 响应内序列化来显示全局上下文的全部内容。


清单 7. 用于测试代码的 RHTML 文件
                
    
    <%require "java"%>
    <%include_class "zero.core.context.GlobalContext"%>
    <%include_class "zero.core.context.SimpleFormatter"%>
    <html>
      <head>
      <title>Sample RHTML Page</title>
      </head>
      <body>
        <p>The current contents of the global context are:
          <pre>
            <%
              formatter = SimpleFormatter.new
              GlobalContext.dump formatter
              print formatter
            %>
          </pre>
        </p>
      </body>
    </html>
      

将清单 7 中的标记添加到应用程序的 /public 目录中名为 test-ruby-and-markup.rhtml 的文件中。然后应当用 zero run 命令启动 Zero 应用程序,并通过把您喜爱的 Web 浏览器指向 http://localhost:8080/test-ruby-and-markup.rhtml 来进行测试。结果应当是显示全局上下文内容的一个简单 HTML 页面。图 1 显示了页面显示在 Mozilla Firefox 中的屏幕快照。


图 1. Ruby 模板所创建的 HTML 页面的屏幕快照
图 1. Ruby 模板所创建的 HTML 页面的屏幕快照

结束语

能够使用 Ruby 实现 Zero 应用程序本来就不错,如果还能使用 Rails 的模板功能就更好了。使用我们在本文中编写的代码,Ruby 用户现在可以完成 Groovy 和 PHP 用户在创建 Zero 应用程序时完成的一切操作。最后,把含有嵌入代码的 HTML 转换为可执行脚本所使用的策略十分通用,并且需要的话,可使用这些策略向平台添加更多强大的模板功能或者新语言。






回页首


下载

描述名字大小下载方法
JRuby 样例集成代码和测试文件zero.scripting.zip8419 KBHTTP
关于下载方法的信息


参考资料

学习

获得产品和技术
  • 下载 Project Zero M2 并开始应用本文所涵盖的最佳实践。

  • 下载 JRuby 1.0 以构建和运行本文所包括的代码。


讨论


关于作者

Dan Jemiolo 是 IBM 位于美国北卡罗来纳州 Research Triangle Park 的 Project Zero 团队的一名咨询软件工程师。目前主要负责 Zero 平台的可重用组件及其服务目录的工作。他的工作包括 Apache Muse 2.0 的设计和开发,也曾参加过 OASIS Web 服务标准组织。三年前他从 Rensselaer Polytechnic Institute 获得了计算机科学的理学硕士学位,之后加入了 IBM。




对本文的评价










回页首


Java 和所有基于 Java 的商标是 Sun Microsystems, Inc. 在美国和/或其他国家或地区的商标。 其他公司、产品或服务的名称可能是其他公司的商标或服务标志。

IBM 公司保留在 developerWorks 网站上发表的内容的著作权。未经IBM公司或原始作者的书面明确许可,请勿转载。如果您希望转载,请通过 提交转载请求表单 联系我们的编辑团队。
    关于 IBM 隐私条约 联系 IBM 使用条款