锻造坊 - RJS 模板

作者:Reuven M. Lerner

在过去的几个月里,我在这篇文章中写了很多关于 JavaScript 的文章。这种语言内置于几乎所有现代 Web 浏览器中,现在已经发展成熟,并且是现代 Web 开发范例 Ajax 的核心。虽然长期以来,掌握 JavaScript 对于 Web 开发人员来说是一项可选技能,但现在它已成为一项必备技能,与 SQL、HTML、HTTP 和 CSS 一样重要。

JavaScript 复兴的原因之一是跨平台库的出现,这些库隐藏了长期困扰该语言的不兼容性。在很长一段时间里,用 JavaScript 编写的程序必须包含许多 if/then 语句,这些语句用于处理可能的跨平台不兼容性。

今天,我们可以通过使用库来避免在代码中出现这样的 if/then 语句,这些库为我们处理这些底层任务。Prototype 和 Dojo 是我在之前的专栏中介绍过的两个 JavaScript 库,它们之所以受欢迎,正是因为它们隐藏了许多这些细节。它们使 JavaScript 成为一种真正的跨平台语言,“平台”指的是 Web 浏览器,也指的是操作系统。

一些聪明的程序员为了使 JavaScript 标准化更加完整和轻松,更进一步。为什么不使用您的服务器端编程语言来为您生成 JavaScript 呢?也就是说,如果您正在使用 Ruby on Rails,也许您可以用 Ruby 编写命令,并将它们翻译成 JavaScript。这样做将允许您在所有模板中使用大致相同的代码,而无需在模板的不同部分切换语法。

这听起来可能是一个奇怪的想法,但我想得越多,我就越喜欢它。RJS(Ruby JavaScript 的缩写)模板是这种思想的一种体现。如果您喜欢用 Java 创建 JavaScript,您可能需要看看 Google Web Toolkit,它现在以开源许可证提供,并在 Java 世界中赢得了众多粉丝。

本月,我们将了解 RJS 以及它如何使 Web 开发人员的生活更加轻松。虽然我不认为 JavaScript 会消失,或者 Web 开发人员可以完全忽略它,但像 RJS 这样的技术意味着它可能会变得像今天的机器代码一样——可用且位于金字塔的底部,但通常被高级程序员忽略。

Ajax 和 Rails

要创建一个名为 ajaxdemo 的新 Rails 项目,请输入

rails ajaxdemo

现在,让我们创建一个名为 showme 的简单控制器

script/generate controller showme

我们不会在这个系统中使用任何模型,但您仍然可能需要在 config/database.yml 中定义一行或多行。相反,让我们在我们的 showme 控制器中创建一个新视图,存储为 app/views/showme/index.rhtml,如列表 1 所示。

列表 1. index.rhtml

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
    <head>
        <title id="title">Sample HTML page</title>
        <%= javascript_include_tag 'prototype' %>

        <script type="text/javascript">
        function updateHeadline()
        {
                var headline = $('headline');
                var new_headline_text = $F('future-headline');
                Element.update(headline, new_headline_text) ;
        }
        </script>

    </head>

    <body>
        <h1 id="headline">Headline</h1>
        <p><input type="text" id="future-headline" /></p>
        <p><input type="button" onclick="updateHeadline();"
                   value="Update headline" /></p>
    </body>
</html>

正如您所看到的,index.rhtml 页面是一个相对标准的 HTML 页面,其中包含一些使用 Prototype 库的 JavaScript 代码。该页面由一个标题、一个文本字段和一个按钮组成。按下按钮会调用函数 updateHeadline。此函数获取 future-headline 文本字段的当前值,并将标题更改为反映其内容。

使用远程调用

到目前为止,我们还没有做任何特别的事情。但是,现在,我们将做一些更复杂的事情:通过 Ajax 调用将文本字段的内容发送到服务器。服务器的响应将是我们的标题,翻译成 Pig Latin。

进行此更改需要做两件事。首先,我们需要在我们的应用程序控制器中编写一个方法,该方法获取标题,将其转换为 Pig Latin,然后返回该文本。其次,我们需要修改我们的模板,以便它从服务器获取更新后的文本,而不是从本地 JavaScript 函数获取。

我们更新后的模板如列表 2 所示。我们做了一些更改,首先是我们的表单现在有一个与之关联的 id 属性,名为 theForm。该表单包含一个元素,一个名为 future_headline 的文本字段。请注意,我们需要使用 name 属性而不是 id 属性,以便表单元素将与我们的 Ajax 调用一起提交。另请注意,我们将名称更改为 Ruby 友好的 future_headline(带有下划线),而不是 CSS 友好的 future-headline(带有连字符)。

列表 2. index.rhtml(Ajax 版本)


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
    <head>
        <title id="title">Sample HTML page</title>
        <%= javascript_include_tag 'prototype' %>
    </head>

    <body>
        <h1 id="headline">Headline</h1>

        <form id="theForm">
        <p><input type="text" name="future_headline" /></p>
        </form>

    <p><%= submit_to_remote "submit-button",
                    "Pig Latin it!",
            :url => { :action => "piglatin_sentence" },
            :submit => "fakeForm",
            :update => "headline" %></p>
    </body>
</html>

我们还用对 submit_to_remote 助手的调用替换了我们的按钮

<p><%= submit_to_remote "submit-button",
                "Pig Latin it!",
        :url => { :action => "piglatin_sentence" },
        :submit => "fakeForm",
        :update => "headline" %></p>

上面的代码做了很多事情

  • 它创建了一个按钮,其 DOM ID 为 submit-button。

  • 按钮的标签为“Pig Latin it!”。

  • 当单击按钮时,它使用 Ajax 调用服务器上当前控制器中的 piglatin_sentence 操作。

  • ID 为 fakeForm 的表单的内容被提交。

  • 从 Ajax 调用返回的值用于更新 ID 为 headline 的 HTML 元素的内容。

我们剩下的要检查的是我们的控制器,如列表 3 所示。控制器不一定知道它是由后台 Ajax 进程调用的,也不知道它的内容将用于更新标题元素。相反,它只是像任何方法一样被调用,将单词转换为 Pig Latin。翻译后的句子作为纯文本文件返回给用户的浏览器。

列表 3. showme_controller.rb

class ShowmeController < ApplicationController

  def piglatin_sentence
    # Get the headline
    sentence = params[:future_headline]
    words = sentence.split

    sentence = ""

    # Go through each word, applying the secret 
    # Pig Latin algorithm
    words.each do |word|
      if word =~ /^[aeiou]/i
        word << "way"
      else
        first_letter = word.slice(0,1)
        rest = word.slice(1..-1)

        word = "#{rest}#{first_letter}ay"
      end

      sentence << word
      sentence << " "
    end

    render :text => sentence
  end

end

返回 JavaScript

现在,真正的魔力开始了。正如我们刚刚看到的,我们的控制器 (piglatin_sentence) 向其调用者返回一个纯文本文档。当然,我们可以自由地以我们喜欢的任何格式返回数据。一种可能的格式可能是 XML。事实上,术语 Ajax 应该代表 Asynchronous JavaScript and XML(异步 JavaScript 和 XML),因此 XML 成为返回值的一种常见格式也就不足为奇了。另一种越来越流行的格式是 JSON(JavaScript 对象表示法),它是 JavaScript 对象的文本版本,可以快速轻松地交换数据。

但是,在 Ruby on Rails 的世界中,控制器可能返回给用户浏览器的另一种数据类型是 JavaScript。这听起来可能不是那么聪明,但请考虑 Prototype 对它做了什么。如果使用 link_to_remote 或 submit_to_remote 调用控制器,并且 HTTP 响应的内容类型为 xml+javascript,则会评估 JavaScript。

这可能会节省时间——而不是返回应该用于标题的文本,然后使用 JavaScript 插入它。(没错,我们可以通过使用传递给 submit_to_remote 调用的 :update 参数来简洁地说出这一点。但代码仍然存在。)相反,我们可以简单地返回 JavaScript 代码,该代码使用 DOM 来修改文档。这将大大简化我们的代码。

列表 4. index.rhtml(针对 HTTP 响应中的 JavaScript 进行了更新)

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
    <head>
        <title id="title">Sample HTML page</title>
        <%= javascript_include_tag 'prototype' %>
    </head>

    <body>
        <form id="fakeForm">
            <h1 id="headline">Headline</h1>
        <p><input type="text" name="future_headline" /></p>
        </form>

    <p><%= submit_to_remote "submit-button",
                    "Pig Latin it!",
            :url => { :action => "piglatin_sentence" },
            :submit => "fakeForm" %></p>
    </body>
</html>

列表 5. showme_controller.rb(更新为返回 JavaScript)

class ShowmeController < ApplicationController

  def piglatin_sentence
    # Get the headline
    sentence = params[:future_headline]
    words = sentence.split

    sentence = ""

    # Go through each word, applying the 
    # secret Pig Latin algorithm
    words.each do |word|
      if word =~ /^[aeiou]/i
        word << "way"
      else
        first_letter = word.slice(0,1)
        rest = word.slice(1..-1)

        word = "#{rest}#{first_letter}ay"
      end

      sentence << word
      sentence << " "
    end

    output = "Element.update($('headline'), '#{sentence}');"
    render :text => output, :content_type => "text/javascript"
  end

end

要查看实际效果,请查看列表 4 和 5。列表 4 是我们的模板 index.rhtml 的更新版本,它与之前的版本基本相同,除了我们现在能够从对 submit_to_remote 的调用中删除 :update 参数

<p><%= submit_to_remote "submit-button",
                "Pig Latin it!",
        :url => { :action => "piglatin_sentence" },
        :submit => "fakeForm" %></p>

我们没有指示应该在客户端更改什么,而是转而在服务器端执行此操作

output = "Element.update($('headline'), '#{sentence}');"
render :text => output, :content_type => "text/javascript"

换句话说,我们告诉我们的控制器生成类型为 text/javascript 的响应,知道我们发送的任何内容都将由用户的浏览器评估。然后,我们发送一个响应,该响应使用 Element.update 将标题更改为我们翻译后的句子。果然,一旦我们安装了这个新版本的软件,标题就会继续被更改。

与此相关的力量是巨大的。例如,我们可以有条件地更新标题,根据我们服务器数据库中的禁用词典检查标题。我们可以跟踪最常用的词。我们可以限制用户每天更新标题的次数。

更妙的是,因为我们返回的是 JavaScript 程序,而不是单个 HTML 元素的内容,我们可以修改页面的多个部分,甚至加入一些 Scriptaculous 效果以获得良好的效果。返回 JavaScript 是 Prototype 提供的一个看似简单的功能,但它为巨大的可能性打开了大门。

RJS

现在,到了您一直等待的时刻——您可能会认为,尽管评估 JavaScript 响应的概念很强大,但在 Ruby 控制器中创建和维护 JavaScript 代码是很烦人的。我们在其中放入 SQL 就已经够糟糕了;一个文件中包含三种语言似乎有点过分了。

而且,这就是 RJS 模板的用武之地。基本思想是,我们假设响应将以 JavaScript 的形式出现,并且它将修改当前页面上的一个或多个元素。RJS 为我们提供了一种紧凑的语法来做出这些更改,因此我们可以创建非常小的文件来完成大量工作。

我们需要做的更改很小。首先,我们修改我们的 piglatin_sentence 方法,使其修改的不是 sentence(局部变量),而是 @sentence(实例变量)。我们还删除了对 render 的调用,因为我们不会直接渲染任何内容。

然后,我们创建一个名为 piglatin_sentence.rjs 的文件。这是一个视图,就像 .rhtml 文件一样,因此与它一起放在 views 目录中。但是,它由一行组成

page[:headline].replace_html @sentence

换句话说,我们应该获取当前页面,找到 ID 为 headline 的元素,并将其 HTML 内容替换为我们从该方法获得的 @sentence 的值。

列表 6. showme_controller.rb(更新为使用 RJS)

class ShowmeController < ApplicationController

  def piglatin_sentence
    # Get the headline
    sentence = params[:future_headline]
    words = sentence.split

    @sentence = ""

    # Go through each word, applying the
    # secret Pig Latin algorithm
    words.each do |word|
      if word =~ /^[aeiou]/i
        word << "way"
      else
        first_letter = word.slice(0,1)
        rest = word.slice(1..-1)

        word = "#{rest}#{first_letter}ay"
      end

      @sentence << word
      @sentence << " "
    end
  end

end

果然,这效果很好。通过少量代码,我们设法完成了很多工作。而且,和以前一样,我们可以添加 Scriptaculous 调用,更新页面上的多个元素,显示和隐藏 HTML 元素——基本上,任何我们可能想做的事情。

结论

当我第一次读到 RJS 模板时,我认为这是我听过的最奇怪的想法之一。那是因为在 Ruby 中编写 JavaScript 的概念既奇怪又没有必要。我现在理解了这种模板的力量和巧妙之处,并期待在我的许多 Ajax 驱动的网站上使用它。通过一些学习和对 Web 开发方式的一些改变,您可能会做出同样的发现。

资源

有很多关于学习 Ajax 的优秀印刷和在线资源。我最喜欢的网站之一是 ajaxian.com,作者在其中讨论和评论与 Ajax 相关的工具。

有两本好书可能会派上用场。Pragmatic Programmers 出版了 Dave Thomas 和 David Heinemeier-Hansson 获奖作品 Agile Development in Rails 的第二版,O'Reilly 出版了 Scott Raymond 的 Ajax on Rails。通过这两本书,您应该能够全面了解 Ruby on Rails 的工作原理,以及如何使用 JavaScript 和 RJS 创建有趣且动态的 Web 应用程序。

Reuven M. Lerner,一位长期从事 Web/数据库咨询的顾问,是伊利诺伊州埃文斯顿西北大学学习科学专业的博士候选人。他目前与妻子和三个孩子住在伊利诺伊州斯科基。您可以在 altneuland.lerner.co.il 阅读他的博客。

加载 Disqus 评论