Ruby on Rails 實戰聖經

使用 Rails 4.2 及 Ruby 2.3

電子書製作中,歡迎留下 E-mail,有消息將會通知您。若您有任何意見、鼓勵或勘誤,也歡迎來信給我,謝謝。

Ajax 應用程式

It’s not a bug - it’s an undocumented feature. - Unknown

什麼是 Ajax?

Ajax 是 Asynchronous JavaScript and XML的縮寫,是一種不需要重新整理頁面,透過 JavaScript 來與伺服器交換資料、更新網頁內容的技術。目的在於改善使用者的操作介面,提昇流暢度。它主要是透過瀏覽器提供的XMLHttpRequestObject來達成,不過因為要支援跨瀏覽器,大多數人們會選擇使用 JavaScript Library 來處理 Ajax,例如最流行的 JQuery。

如果你對 jQuery 和 Ajax 完全陌生的話,推薦 CodeSchool: Try jQueryCode School: jQuery AjaxUdacity: Intro to AJAX

雖然Ajax的縮寫中包括XML,但是實務上並不一定要用XML格式,事實上也已經很少人使用XML當作傳輸的格式了。總歸來說,依照Ajax使用的格式分類,有三種方式:

  • 向伺服器請求 HTML 片段,然後客戶端瀏覽器上的 JavaScript 再替換掉頁面上的元素
  • 向伺服器請求 JavaScript 程式腳本,然後客戶端瀏覽器執行它
  • 向伺服器請求 JSON 或 XML 資料格式,然後客戶端瀏覽器的 JavaScript 解析後再動作。

第一種方式非常簡單,但是限制是一次只能更新一小塊內容。

Rails 預設使用第二種方式,一方面程式撰寫較容易,另一方面也是貫徹 Rails 將 template 統一在 server-side rendering 的設計哲學。

第三種方式則將 JavaScript 程式都放在客戶端瀏覽器上,相較於第二種則多了解析 JSON 或 XML 的部份。以Web API的設計角度來看,與表現層無關的JSON格式是比較乾淨的,可以獲得比較好的重複使用性。如果團隊中有專門的前端工程師,他們會比較喜歡這種方式。

Unobtrusive JavaScript

Rails 使用一種叫做 Unobtrusive JavaScript(UJS) 的方式來掛載內建的 JavaScript 功能,也就是你在 app/assets/javascripts/application.js 裡面載入的 //= require jquery_ujs,這些功能包括

  • 讓超連結可以用 :method 參數支援非 GET 方法
  • 用超連結、按鈕和表單可以用 :remote => true 支援 Ajax
  • 超連結、按鈕和表單可以用 "data-confirm" 參數可以跳確認對話視窗
  • 送出按鈕可以用 data-disable-with 參數在送出表單時暫時關閉按鈕避免重複送出

什麼是Unobtrusive呢?用個範例來說吧,以下代碼會將超連結改成用表單DELETE送出,並且用一個提示視窗來作確認:

link_to 'Remove', event_path(1), :method => :delete, :data => { :confirm => "Sure?" }

Rails 3以前的版本,會輸出:

<a onclick="if (confirm('Sure?')) { var f = document.createElement('form'); f.style.display = 'none'; this.parentNode.appendChild(f); f.method = 'POST'; f.action = this.href;var m = document.createElement('input'); m.setAttribute('type', 'hidden'); m.setAttribute('name', '_method'); m.setAttribute('value', 'delete'); f.appendChild(m);f.submit(); };return false;" href="/events/1">Remove</a>

Rails 3之後,會輸出:

<a rel="nofollow" data-confirm="Sure?" data-method="delete" class="delete" href="/events/1">Remove</a>

Unobtrusive也就是將JavaScript程式與HTML完全分開,除了可以讓HTML碼乾淨之外,也可以支援更換不同的JavaScript Library,例如把Rails內建的jQuery換成Protytype.jsangular.js等等。

Layout中有輸出一段<%= csrf_meta_tag %>的作用就是搭配給UJS使用的,讓JavaScript可以拿到CSRF安全驗證碼,我們會在安全一章討論到什麼是CSRF

第一種方式:替換 HTML 片段

編輯 app/views/events/index.html.erb 最下方加入:

<%= link_to 'Hello!', welcome_say_hello_path, :id => "ajax-load"  %>

<div id="content">
</div>

<script>
$('#ajax-load').click( function(e){
  e.preventDefault();
  var url =  $(this).attr("href");
  $.ajax(url, {
    success: function(response) {
      $("#content").html(response);
    }
  });
</script>

如此點下超連結後,就會把回傳的HTML置入到<div id="content">裡面。

第二種方式:使用 JavaScript 腳本

編輯 app/views/events/index.html.erb,在迴圈中間加入

<%= link_to 'ajax show', event_path(event), :remote => true %>

在迴圈外插入一個<div id="event_area"></div>

編輯 app/controllers/events_controller.rb,在 show action 中加入

respond_to do |format|
  format.html
  format.js
end

新增 app/views/events/_event.html.erb,內容與 show.html.erb 相同

新增 app/views/events/show.js.erb,內容如下

$('#event_area').html("<%= escape_javascript(render :partial => 'event') %>")
             .css({ backgroundColor: '#ffff99' });

瀏覽 http://localhost:3000/events

escape_javascript()可以縮寫為j()

上述 :remote => true 背後的原理,如同以下的 jQuery 程式碼:

$("#ajaxscript").click(function(e){
  e.preventDefault();
  var url =  $(this).attr("href");
  $.ajax(url, {
    dataType: "script"
  })
})

你會發現到這段程式碼其實非常一般化,這也是為什麼 Rails 可以將它變成 :remote => true 的原因。

Ajax 按鈕

除了超連結 link_to之外,按鈕 button_to 加上:remote => true參數也會變成 Ajax。

button_to "Remove",event_path(@event)

除了已經看過的 :data => { :confirm => "Are you Sure?" }之外,disable_with可以避免使用者連續按下送出:

button_to "Remove", event_path(@event), :data => { :disable_with => 'Removing' }

Ajax 表單

除了超連結 link_to 加上 :remote 可以變成 Ajax 之外,表單 form_for 也可以加上:remote變成 Ajax。

form_for @event, :remote => true

第三種方式:使用 JSON 資料格式

JavaScript Object Notation(JSON)是一種源自JavaScript的資料格式,是目前Web應用程式之間的準標準資料交換格式,在Rails之中,每個物件都有to_json方法可以很方便的轉換資料格式。

<%= link_to 'ajax show', event_path(event), :remote => true, :data => { :type => :json }, :class => "ajax_update" %>

點擊ajax show就會送出Ajax request了,但接下來要怎麼撰寫處理JSON的程式呢?以下是一個範例:

<script>
$(document).ready(function() {
    $('.ajax_update').on("ajax:success", function(event, data) {
        var event_area = $('#event_area');
        event_area.html( data.name );
    });
});
</script>

使用 JSON 通常還會搭配 client-side template 機制,將回傳的資料搭配 template 後,再插入到 HTML 之中。

另一種變形的用法則是將HTML片段當做JSON的資料來傳遞。

另一種JSON的變形是JSONP(JSON with Padding),將JSON資料包在一個JavaScript function裡,這個做的用處是讓這個API可以跨網域被呼叫。要回傳JSONP格式,需要用render :js並多一個參數是:callback。例如以下的程式可以搭配 jQuery 的 jsonp 格式:

respond_to do |format|
  format.js { render :json => @user.to_json, :callback => params[:callback] }
end

Example code

事實上,Rails預設讓每個換頁都用上了Ajax技巧,這一招叫做Turbolinks,在預設的Gemfile中可以看到gem "turbolinks",以及Layout中的data-turbolinks-track

它的作用是讓每一個超連結都只用Ajax的方式將整個body內容替換掉,這樣換頁時就不需要重新載入head部份的標籤,包括JavaScriptCSS等等,目的是可以改善換頁時的速度。

也因為它沒有整頁重新載入,所以如果有放在application.js裡面的 jQuery ready 事件處理,會變成只有第一次載入頁面才執行到,換頁時就失效了,所以必須改成 Turbolinks 的 page:change 事件,也就是:

$(document).ready(function(){
  //...
}

都要改寫成

$(document).on("page:change", function(){ 
 //...
})

如果想要明顯體會它的效果,可以在app/assets/javascripts/application.js裡面加上

Turbolinks.enableProgressBar();

最後,因為它會影響JavaScriptEvent Bindings行為,所以在搭配一些JavaScript比較吃重的應用程式,例如使用JavaScript FrameworkBackboneAngularJSEmber時會移除不用,以免互相影響。

或是你也可以針對特定的超連結,加上 data-no-turbolink 的屬性來關閉 Turbolinks:

<div id="some-div" data-no-turbolink>
  <a href="/">Home (without Turbolinks)</a>
</div>

例如 Facebook Share 的 Javascript code 就被 turbolinks 影響,請參考 turbolinks + facebook share button

瀏覽器同源政策和 CORS

跨網域(cross-domain) 的 AJAX 會被 Same-origin policy 瀏覽器安全政策所限制:

解決的方式可以用 CORS 或JSON-P,這要看 server-side 伺服器端支援哪一種。CORS 是一個新的網路標準,而 JSON-P 一種向後相同的 hack 方式。

JSON-P

JSONP算是一種 workaround 解,在 jQuery 中可以用以下語法:

     $.ajax({
       url:,
       dataType: "jsonp",
       success: function(data) { 
               //... 
               clearTimeout(wikiRequestTimeout);
       }
     })

JSONP 缺點: 1. 只能送 HTTP GET 2. 沒有辦法 error handling,只能等 timeout,例如設定一個 timeout 顯示錯誤訊息,然後在上述成功時再clearTimeout

   var wikiRequestTimeout = setTimeout(function(){ 
     ....append error text
   }, 8000);

CORS

CORS 則是目前的新標準解決方案:

Rails 範例 Code: https://github.com/ihower/rails-exercise-ac5/pull/2

更多線上資源

》回到頁首