Ruby on Rails 的終極 Unobstrusive jQuery 方案
岳浩宕
2023-12-01
使用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 //-->
以上編碼有五點要注意:
stylesheet_link_tag 從 Google CDN 包括了 jQuery UI 這jQuery UI 基本 的stylesheet
<%= yield %> 將頁面主要內容放在原始檔中間位置。
底下兩行是 javascript_include_tag 編碼,把 jQuery 的原始檔,從 Google CDN 包括至網頁。這裡使用了有別於一般 include javascript 放在<head></head>中的方法。原因是"Put CSS at top", "Put Javscript At bottom", 使網頁以最高效率運作。
最後幾行 <%javascript_tag do %>...<%end%> 是輸出 Javascript 原始檔的地方。這幾行對於分開 Program 和 Markup 最為重要。還有,這裡利用了jQuery 出名的"$(document).ready()",使所有javascript 等候至 HTML 下載妥當後才執行。
注意編碼總共有兩處 "yield" 編碼。 使用這 'yield',使輸出可以放在不同的地方。我們利用了這特性,使 HTML 被放在原始檔的中間,而Javascript 被放到原始檔的底部。
將javascript_include_tag 放在<head></head>, ySlow 立即指出問題。
將javascript_include_tag 放在<head></head>, ySlow 立即指出問題。雖然還有Grade A的分數,但是加多幾個Javascript include,分數便會急降。最重要是,下載速度已經慢了很多。
將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 放到頁面的最底部。
jQuery UI 成功建立了一個Tabs。
這時我們再查看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。
檢視編碼後可看到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的編碼也在最底部!
所有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
不用按鈕,在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}
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化。