Wednesday, 24 June 2009

  • Ruby on Rails 的終極 Unobstrusive jQuery 方案

    在網上系站中,常被忽略而又是最重要的程式是什麼?答案是 Javascript!

    使用正當的話,它可以增加網站的效率和友善度,又可以大大減輕伺服器的負擔。而Javascript Frameworks 之中,最出名和實用的一定包括jQuery了。


    ror_01

    (jQuery 1.3 在出名的Javascript Frameworks 中可以稱得上是最快的。)


    而 jQuery 所包含的Plugins 有表格、圖片、瀏覽等,應有盡有 (http://docs.jquery.com/Plugins)。

    RoR 結合jQuery,將會帶來很大驚喜吧!

    使用jQuery,你的RoR Application 可享有所有 Unobtrusive JavaScript 帶來的優點,使編碼和Markup絕對分開,又可以要最快的速度建立所有用戶端的功能和介面效果。

    現在的 RoR + Prototype 方案,其中最大一個問題就是如何處理 ySlow 作者 Steve Souders 極度重視的 "Put CSS at top", "Put Javascript At bottom"問題。不少人正為這問題煩惱。

    以下文章將討論如何建立一個完全使用 jQuery ,不使用 Prototype 的方法

    Rails 組群亦提供了一些 jQuery 方案,可惜一般方案沒有完全利用 Unobstrusive Programming 的優點,使編碼胡亂放在頁面中間。

    這次我們會看看結合 content_for 和 jRails (http://ennerchi.com/projects/jrails),以建立最好的方案。

     


    第一步: 安裝 jQuery


    首先建立一個 Layouts。在 你的 controller 檔 (例如 /app/controllers/blogs_controller.rb) 的頂部插入編碼

    class BlogsController < ApplicationController  
       layout "application_layout"
    end


    然後,在/views/layouts 內新增 "application_layout.html.erb",並貼上編碼

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml"  xml:lang="en" lang="en">
    <head>
      
      <%=stylesheet_link_tag "http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.0/themes/cupertino/jquery-ui.css" %>
     
    </head>  
    <body>
     <%= yield %>
    </body>
    </html>
    <!-- include javascripts -->
    <%=javascript_include_tag("http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js") %>
    <%=javascript_include_tag("http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.1/jquery-ui.min.js") %>
    <!--//include javascripts //-->
    <!-- jquery and other functions-->
    <%javascript_tag do %>
    $(document).ready(function() {
        <%= yield :js_ready %>
    });
    <%end %>
    <!-- //jquery and other functions //-->


    以上編碼有五點要注意:

    1. stylesheet_link_tag Google CDN 包括了 jQuery UI 這jQuery UI 基本 的stylesheet
    2. <%= yield %> 將頁面主要內容放在原始檔中間位置。
    3. 底下兩行是 javascript_include_tag 編碼,把 jQuery 的原始檔,從 Google CDN 包括至網頁。這裡使用了有別於一般 include javascript 放在<head></head>中的方法。原因是"Put CSS at top", "Put Javscript At bottom", 使網頁以最高效率運作。
    4. 最後幾行 <%javascript_tag do %>...<%end%> 是輸出 Javascript 原始檔的地方。這幾行對於分開 Program 和 Markup 最為重要。還有,這裡利用了jQuery 出名的"$(document).ready()",使所有javascript 等候至 HTML 下載妥當後才執行。
    5. 注意編碼總共有兩處 "yield" 編碼。 使用這 'yield',使輸出可以放在不同的地方。我們利用了這特性,使 HTML 被放在原始檔的中間,而Javascript 被放到原始檔的底部。

    ror_03

    將javascript_include_tag 放在<head></head>, ySlow 立即指出問題。

    將javascript_include_tag 放在<head></head>, ySlow 立即指出問題。雖然還有Grade A的分數,但是加多幾個Javascript include,分數便會急降。最重要是,下載速度已經慢了很多。

    ror_02
    將javascript_include_tag 放在最底, ySlow 有 Grade A 的分數。

    Javascript 放在最底部使網頁加速。


    現在頁面可以使用 jQuery 了!


    第二步:使用jQuery 和 jQuery UI

    首先,我們要測試 jQuery UI 的介面效果。為此,我們先在頁面加上一排常用的Tabs

    在測試的頁面 (例如: /views/blogs/index.html.erb) 加入以下編碼:

    <div id="section_tabs">
      <ul>
        <li><%=link_to "Introduction", "#intro"%></li>
        <li><%=link_to "Contact Us", "#contact_us"%></li>
        <li><%=link_to "About Me", "#about_me"%></li>
      </ul>
      <div id="intro">
          Hello, it is nice to meet you.
      </div>
      <div id="contact_us">
          Email: arthurccube@nowhere.com
      </div>
      <div id="about_me">
          I am someone.
      </div>
    </div>
    <% content_for :js_ready do  %>
      jQuery('#section_tabs').tabs();
    <% end %>


    以上編碼建立了 Tabs DOM 結構,單單一句"jQuery('#section_tabs').tabs();", 便完成tabs 所需的所有 javascript 。jQuery UI 聰明地使用selector "id" (#section_tabs) 去找出了解相關 Tabs 的目標,並對相關 DOM 結構修改為Tabs 內容。

    而 "<% content_for :js_ready do  %>" 這段編碼,是對應在 application_layout 的 "<%= yield :js_ready %>",兩段編碼的結果,是令到相關的javascript 放到頁面的最底部。


    ror_04
    jQuery UI 成功建立了一個Tabs。

    ror_05

    這時我們再查看ySlow,"Put Javascript At Bottom" 仍然是完美的!

     

    第三步: Rails 去 Prototype 化和Unobstrusive 化


    Unobstrusive Javascript 的意思是 "把功能('行為層面')和網面的結構/內容和演示分開"(http://en.wikipedia.org/wiki/Unobtrusive_JavaScript)。

    可惜,RoR 因為使用了很多即用即寫的Prototype Helpers,使Javascript 和網頁變得難以分割

    令人高興的是,RoR的 Overriding 功能十分廣泛,我們可以輕易的把有問題的Helpers 修改。

    筆者選了最典型的問題Helper - observe_field 來做例子。

    首先,安裝 jRails Plugins ,使相關Helpers 使用jQuery:

    ./script/plugin install http://ennerchi.googlecode.com/svn/trunk/plugins/jrails


    在'<%= javascript_include_tag("http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.1/jquery-ui.min.js") %>' 之後插入編碼:


    <%= javascript_include_tag("jrails.js") %>

    這段編碼把 jRails 所寫的 Javascript 放到頁面。

    重啟Application,然後,在剛才的測試頁面 '/app/views/blogs/index.html.erb' 頂部插入編碼:

      <% form_tag 'search',
       {:id => 'form_search'} do %>
          Search: <%=text_field_tag "query"%>    
          <%= observe_field(:query,
            :url => { :controller => :blogs, :action => :search },
            :frequency => 0.5,
            :update => :intro,
            :with => :input)
          %>
      <% end  %>

    以上編碼的 observe_field 監察一個名為 'query' 的輸入(即是<%=text_field_tag "query"%> 的輸出)。等會,如果用者在瀏覽器修改這欄位的輸入,observe_field 便把資料送到在 blogs controller 的 search action。

    ror_06

    檢視編碼後可看到Javascript 被隨意放在HTML 編碼的中間。

    所以,為解決這 Unobstrusive 問題,要修改兩個檔案。

    首先在 /app/views/layouts/application_layout.html.erb 最底部插入編碼:

    <%= yield :rails_helpers %>

    這段編碼用來輸出我們將會修改的 Rails Helper 內容,因為我們將會修改的 observe_field 自己也有一個<script></script> tag, 這個 "yield :rails_helpers"不要放進任何"javascript_tag"內

    現在我們在 '/app/helpers/application_helper.rb' 插入編碼:

      # override the existing observe_field putting the javascripts at bottom
      def  observe_field(field_id, options = {})
        content_for :rails_helpers do
          super(field_id, options )
        end
         
      end

    以上編碼Override 了 RoR 原本的沒有 Unobstrusive 概念 的 'observe_field' Helper,使把輸出放到 :rails_helpers,即是整個網頁的最底部。

    現在刷新頁面,再檢視原始檔。所有javascript,包括observe_field的編碼也在最底部!

    ror_07

    所有javascript 也在最底部!

    現在,使observe_field 可以回傳結果,我們在 '/app/controllers/blogs_controller.rb'插入:

      def search
        render :text => "searching results from query <u><i>'#{params[:input]}'</i></u> @ #{Time.now}"
      end

    ror_08

    不用按鈕,在Search 輸入的字句也被送到伺服器中。

    把Javascript 放在最底的結果是,整個網頁的HTML 結構和相片可以用最快的速度下載到用戶端,即是最重要的'Put Javascript at bottom"!

    這個observe_field 只是一個Unobstrusive 化一個Rails Helper 的例子。讀者可用相同的放法修改所有相關的Helper,令Javascript 不存於頁面中間。

     

     

    第四步:安裝特別的 jQuery Plugins


    現在您的系統擁有Rails 和 jQuery。基本上可以安裝的工具很全面,筆者在此列出一個例子。

    我們將安裝一個非常好用的 simple auto_complete (http://github.com/grosser/simple_auto_complete/tree/master)

    首先,在 http://github.com/grosser/simple_auto_complete/tree/master 下載 simple_auto_complete plugins

    解壓後將文件夾放在/app/vendoer/plugins 內。

    在剛下載文件夾找到 '/example_js/javascripts',把 'jquery.autocomplete.js' 複製到 '/public/javascripts'。

    並把在 '/example_js/stylesheets' 的 'jquery.autocomplete.css' 複製到 '/public/stylesheets'。

    重啟Application。

    所有相關的Javascript 和 Stylesheets 已放到了適當位置。

    現在修改 '/app/views/layouts/application_layout.html.erb',使這些檔案包括到網頁輸出。

    在 '<head>...</head>' 之間任何位置插入編碼:

    <%=stylesheet_link_tag "jquery.autocomplete.css" %>


    在 '<%=javascript_include_tag("jrails.js")%>' 下面插入編碼:

    <%=javascript_include_tag("jquery.autocomplete.js") %>


    在'/app/controllers/blogs_controller.rb' 插入以下編碼:

        autocomplete_for :blog, :title do |items|
          items.map{|item| "#{item.id}: <b>#{item.title}</b>"}.join("\n")
        end


    註:以上編碼假設您有 blog.rb,其擁有欄名 title。讀者可改作其他 model 和欄名也有效。

    在測試的頁面 /views/blogs/index.html.erb 加入以下編碼:

    <% form_for :blog do |f|%>
       Autocomplete: <%= f.text_field :auto_user_name, :class => 'autocomplete', 'autocomplete_url'=>autocomplete_for_blog_title_blogs_path %>
     <%end %>
    <%content_for :js_ready do %>
       //autocomplete
       $('input.autocomplete').each(function(){
         var input = $(this);
         input.autocomplete(input.attr('autocomplete_url'));
       });
     <%end %>


    這段編碼首先建立關於 blog 的一張表格。然後,使用 jQuery (i.e. $) 的編碼監察相關輸入。

    留意輸入會被送到 'autocomplete_for_blog_title_blogs_path' 這路徑。

    所以我們要在 routes.rb 加入:

     map.resources :blogs, :collection => { :autocomplete_for_blog_title => :get}


    ror_09
    autocomplete 功能完成,輸入的字句會自動回傳提示。

     

    第五步:Forgery Token

    基於安全理由,網站可能啟動了 forgery token. 我們可以將所有 Ajax 也加上 forgery token ,使相關伺服器要求被接納。


    "/app/views/layouts/application.html.erb" 內的 "$(document).ready(function() {" 這句編碼下面加入:

    // All non-GET requests will add the authenticity token
    // if not already present in the data packet
    jQuery("body").bind("ajaxSend", function(elm, xhr, s) {
        if (s.type == "GET") return;
       
        if (s.data && s.data.match(new RegExp("\\b" + window._auth_token_name + "="))) return;
        if (s.data) {
          s.data = s.data + "&";
        } else {
          s.data = "";
          // if there was no data, jQuery didn't set the content-type
          xhr.setRequestHeader("Content-Type", s.contentType);
        }
        var auth_token = encodeURIComponent(window._auth_token_name);
       
          s.data = s.data || "";
        if (s.data.indexOf(auth_token) < 0 )  s.data += (s.data ? "&" : "") + auth_token + "=" +  encodeURIComponent(window._auth_token);
    });   

    總結

    雖然RoR 本身提供了很多有用的 Helpers,但是RoR 和 Prototype 密不可分的結構,使優化網頁變得複雜。

    這違反了Obstrusive 概念。

    筆者演示了怎樣把 Prototype 從 RoR 分開,而且將Javascript 放在原始檔的底部。

    之後,我們利用RoR 方便的 Overriding 特性,把一些Helpers 修改為輸出在原始檔最底部 (:rails_helpers)。

    最後,我們可以享受各個好用的RoR 或 jQuery Plugins ,亦容易的使程式 Obstrusive化。

     


    這篇文章的原始檔可在 http://github.com/arthurccube/rails_examples_jquery_unobstrusive/tree/master 下載



About this Entry

Who recommended?