本課時通過對商品的添加、編輯和刪除,講解視圖中如何使用 UJS,jQuery 和 JSON,實(shí)現(xiàn)無刷新情況下的頁面更新。
上一節(jié),我們講解了 Rails 中的視圖(View),我們再回顧一下這個視圖是如何產(chǎn)生的:我們向服務(wù)器發(fā)起一個請求,服務(wù)器返給我們結(jié)果,查看源代碼,它是一篇 HTML 的代碼。
我們每次請求一個地址,都會給我們完整的 HTML 結(jié)果,對于內(nèi)容較少的網(wǎng)頁,傳輸起來還是很快的,但是對于內(nèi)容多的網(wǎng)頁,大篇的結(jié)果自然會拖慢頁面顯示。
當(dāng)我們?yōu)g覽頁面的時候,并不期望總是刷新整個頁面,因?yàn)樗鼪]必要?,F(xiàn)在我們有 ajax 技術(shù),可以只加載和顯示部分頁面代碼。舉個簡單的例子:當(dāng)我們提交了一條評論,頁面上自動顯示出我們提交的評論內(nèi)容。我們點(diǎn)擊購買按鈕,頁面上就提示我們購物車?yán)镌黾恿艘粋€商品。而這些,都不必要刷新整個頁面。
ajax 是 Asynchronous Javascript And XML 的縮寫,含義是異步的 js 和 XML 交互技術(shù)。XML,可擴(kuò)展標(biāo)記語言,我們使用的 HTML 是基于其發(fā)展起來的。
下面我們看下 Rails 是如何把 ajax 技術(shù)應(yīng)用在視圖(View)中的。
我們在 Gemfile 中已經(jīng)使用了 gem 'jquery-rails' 這個 Gem,它可以讓我們在 application.js 中增加這兩行:
//= require jquery
//= require jquery_ujs
jQuery 是一個輕量級的 js 庫,可以方便的處理HTML,事件(Event),動態(tài)效果,為頁面提供 ajax 交互。jQuery 有很完善的文檔及演示代碼,以及大量的插件。
Rails 使用一種叫 ujs(Unobtrusive JavaScript)的技術(shù),將 js 應(yīng)用到 DOM 上。我們來看一個例子:
http://wiki.jikexueyuan.com/project/rails-practice/images/chapter_3/5.png" alt="" />
我們已經(jīng)給刪除連接增加了兩個屬性:
<%= link_to "刪除", product, :method => :delete, :data => { :confirm => "點(diǎn)擊確定繼續(xù)" } %>
來看看我們的 HTML:
<a data-confirm="點(diǎn)擊確定繼續(xù)" rel="nofollow" data-method="delete" href="/products/1">刪除</a>
輔助方法 link_to 使用了 :data => { :confirm => "點(diǎn)擊確定繼續(xù)" } 這個屬性,為我們添加了 data-confirm="點(diǎn)擊確定繼續(xù)" 這樣的 HTML 代碼,之后 ujs 將它處理成一個彈出框。
在刪除按鈕上,還有 :method => :delete 屬性,這為我們的連接上增加了 data-method="delete" 屬性,這樣,ujs 會把這個點(diǎn)擊動作,會發(fā)送一個 delete 請求刪除資源,這是符合 REST 要求的。
我們可以給 a 標(biāo)簽增加 data-disable-with 屬性,當(dāng)點(diǎn)擊它的時候,使它禁用,并提示文字信息。這樣可以防止用戶多次提交表單,或者重復(fù)的鏈接操作。
我們?yōu)樯唐繁韱沃械陌粹o,增加這個屬性:
<%= f.submit nil, :data => { :"disable-with" => "請稍等..." } %>
當(dāng)我們提交表單時,會有:
http://wiki.jikexueyuan.com/project/rails-practice/images/chapter_3/6.png" alt="" />
如果你還沒看清楚效果,頁面就已經(jīng)跳轉(zhuǎn)了,我們可以給 create 方法增加一個 sleep 10:
def create
sleep 10
@product = Product.new(product_params)
...
更多 ujs 支持的屬性,我們在 這里 看到。
ujs 給我們帶來的一些便利還不止這些,我們來點(diǎn)復(fù)雜的:在不刷新頁面的情形下,添加一個商品,并顯示在列表中。
我們現(xiàn)在的列表頁是這樣的:
http://wiki.jikexueyuan.com/project/rails-practice/images/chapter_3/7.png" alt="" />
現(xiàn)在點(diǎn)擊添加,我們會進(jìn)入到 http://localhost:3000/products/new,我們并不改變它,畢竟在某些 js 失效的情形下,點(diǎn)擊這個按鈕還是要跳轉(zhuǎn)到 new 頁面的。
我們希望給頁面增加一個表單,來輸入新商品的信息,在這之前,我們想更酷一點(diǎn),我們使用 modal 來顯示這個表單:
<%= link_to t('.new', :default => t("helpers.links.new")), new_product_path, :class => 'btn btn-primary', data: {toggle: "modal", target: "#productForm"} %>
ujs 允許我們在 link 上增加額外的屬性,當(dāng)我們再次點(diǎn)擊 添加 按鈕時:
http://wiki.jikexueyuan.com/project/rails-practice/images/chapter_3/8.png" alt="" />
當(dāng)然我做了其他一些修改,你可以在 這里 找到完整的代碼。
為了產(chǎn)生一個 ajax 的請求,我們在表單上增加一個參數(shù) remote: true:
<%= form_for @product, remote: true, :html => { :class => 'form-horizontal' } do |f| %>
這時,ujs 將會調(diào)用 jQuery.ajax() 提交表單,此時的請求是一個 text/javascript 請求,Rails 會返回給我們相應(yīng)的結(jié)果,在我們的 action 里,增加這樣的聲明:
respond_to do |format|
if @product.save
format.html {...}
format.js
else
format.html {...}
format.js
end
end
在保存(save)成功時,我們返回給視圖(view)一個 js 片段,它可以在瀏覽器端執(zhí)行。
我們創(chuàng)建一個新文件 app/views/products/create.js.erb,在這里,我們將新添加商品,顯示在上面的列表中。
$('#productsTable').prepend('<%= j render(@product) %>');
$('#productFormModal').modal('hide');
我們使用 .js.erb 的文件,方便我們在 js 文件里插入 erb 的語法。
我們將一行商品信息使用 prepend 方法,插入到 productsTable 的最上面,j 方法將我們的字符串轉(zhuǎn)換成 js 片段。
好了,你可以試一試效果了。
你可能也像我一樣做了一些測試,導(dǎo)致插入了很多測試數(shù)據(jù),為了繼續(xù)不刷新頁面就完成刪除操作,我們給 刪除 按鈕上也增加一個 ajax 調(diào)用。
我們先給每一行記錄,增加一個唯一的 ID 標(biāo)識,通常使用“名字 + id”的形式,我們還需要給刪除連接增加 remote: true 屬性,我們編輯 app/views/products/_product.html.erb:
<tr id="product_<%= product.id %>">
...
<%= link_to "刪除", product, :method => :delete, remote: true, :data => { :confirm => "點(diǎn)擊確定繼續(xù)" }, :class => 'btn btn-danger btn-xs' %>
我們再增加一個文件以返回 js 片段給瀏覽器執(zhí)行 app/views/products/destroy.js.erb:
$('#product_<%= @product.id %>').fadeOut();
你可以再試試看。
現(xiàn)在,我們看一下添加商品時的返回結(jié)果:
$('#productsTable').prepend('<tr id=\"product_14\">\n <td><a href=\"/products/14\">kkk<\/a><\/td>\n <td>jjj<\/td>\n <td class=\"text-right\">CN¥ 999.00<\/td>\n <td>2015年2月26日 星期四 23:57:55<\/td>\n <td>\n <a class=\"btn btn-primary btn-xs\" href=\"/products/14/edit\">編輯<\/a>\n <a data-confirm=\"點(diǎn)擊確定繼續(xù)\" class=\"btn btn-danger btn-xs\" data-remote=\"true\" rel=\"nofollow\" data-method=\"delete\" href=\"/products/14\">刪除<\/a>\n <\/td>\n<\/tr>\n');
$('#productFormModal').modal('hide');
這里面大部分代碼是不必要的 HTML代碼,如何讓我們的返回結(jié)果更簡潔呢?我們現(xiàn)在發(fā)送個是 text/javascript 請求,返回給我們的是 js 片段。下一節(jié)我們發(fā)送 'json' 請求,我們在瀏覽器端使用 js 處理返回的 json 數(shù)據(jù)。
為了和添加商品區(qū)分開,我們在修改商品時,使用 json 來處理數(shù)據(jù),而且也在一個 modal 中完成。
<%= link_to t('.edit', :default => t("helpers.links.edit")), edit_product_path(product), remote: true, data: { type: 'json' }, :class => 'btn btn-primary btn-xs editProductLink' %>
我們給編輯鏈接,增加了 remote: true, data: { type: 'json' },這時我們沒有打開modal,我們把 js 代碼寫在 coffeescript 中。
我們新建一個文件,app/assets/javascripts/products.coffee。這個文件我們只在商品頁面使用,所以不必把它放到 simplex.js 中,現(xiàn)在我們只在商品的 index.html.erb 中使用它,所以:
<%= content_for :page_javascript do %>
<%= javascript_include_tag "products" %>
...
當(dāng)我們點(diǎn)擊編輯按鈕時,我們期望幾件事:
modal 層,顯示編輯表單好,我們寫上這部分代碼:
jQuery ->
$(".editProductLink")
.on "ajax:success", (e, data, status, xhr) ->
$('#alert-content').hide() [1]
$('#editProductFormModal').modal('show') [2]
$('#editProductName').val(data['name']) [3]
$('#editProductDescription').val(data['description']) [3]
$('#editProductPrice').val(data['price']) [3]
$("#editProductForm").attr('action', '/products/'+data['id']) [4]
再來看看我們的編輯表單:
...
<%= form_tag "", method: :put, remote: true, data: { type: "json" }, id: "editProductForm", class: "form-horizontal" do %>
...
<%= text_field_tag "product[name]", "", :class => 'form-control', id: "editProductName", required: true %>
...
<%= text_field_tag "product[description]", "", :class => 'form-control', id: "editProductDescription" %>
...
<%= text_field_tag "product[price]", "", :class => 'form-control', id: "editProductPrice" %>
...
我們讓表單提交的地址,可以根據(jù)選擇的商品而改變,同時我們設(shè)定它的 type 為 json 格式。
我們?yōu)槊恳粋€輸入框,設(shè)定了 ID,這樣,我們用讀取的 json 信息,分別填入對應(yīng)的編輯框內(nèi)。
然后,我們改動一下 controller 中的方法:
def edit
respond_to do |format
format.html
format.json { render json: @product, status: :ok, location: @product } [1]
end
end
def update
respond_to do |format|
if @product.update(product_params)
format.html { redirect_to @product, notice: 'Product was successfully updated.' }
format.json [1]
else
format.html { render :edit }
format.json { render json: @product.errors.full_messages.join(', '), status: :error } [2]
end
end
end
當(dāng)我們需要考慮 update 方法會有成功和失敗兩種可能時,我們的 ajax 調(diào)用,就要這樣來寫了:
$("#editProductForm")
.on "ajax:success", (e, data, status, xhr) ->
$('#editProductFormModal').modal('hide') [1]
$('#product_'+data['id']+'_name').html( data['name'] ) [2]
$('#product_'+data['id']+'_description').html( data['description'] ) [2]
$('#product_'+data['id']+'_price').html( data['price'] ) [2]
.on "ajax:error", (e, xhr, status, error) ->
$('#alert-content').show() [3]
$('#alert-content #msg').html( xhr.responseText ) [4]
更多 controller 的介紹,后面章節(jié)還會有,這里我們要了解的是,我們頁面拿到的信息,不再是 js 片段,而是 json 格式的數(shù)據(jù)。
當(dāng)我們處理大量數(shù)據(jù)的時候,json 明顯要比 js 片段更節(jié)省傳輸空間,我們也可以把處理動作寫到獨(dú)立的 js 文件中,不過,json 格式返回給我們的,是 9.9,而我們頁面顯示的是格式化后的 CN¥ 9.90,如果我們想把處理好格式的數(shù)據(jù)返還回來,該如何處理呢?
我們可以使用 jbuilder 做這件事,我們新建一個 update.json.jbuilder:
json.id @product.id
json.name link_to @product.name, product_path(@product) [1]
json.description @product.description
json.price number_to_currency(@product.price) [2]
如何知道我們的確使用的是 json 數(shù)據(jù)呢?我們可以查看瀏覽器的控制臺,或者查看命令行的 log 輸出。
http://wiki.jikexueyuan.com/project/rails-practice/images/chapter_3/9.png" alt="" />
http://wiki.jikexueyuan.com/project/rails-practice/images/chapter_3/10.png" alt="" />
在 這里 可以找到我調(diào)試好的代碼。
在實(shí)踐開發(fā)中,我們會從服務(wù)端拿到很多的內(nèi)容,比如幾十條訂單信息,我們可以用上面的方法把它們顯示到頁面上,也可以使用 http://handlebarsjs.com/ 這種模板引擎,使頁面和邏輯更加的獨(dú)立,清晰。當(dāng)我們面對少量的內(nèi)容時,js 片段要比寫一大堆 coffeescript 來的更省事些。所以,我們在確定選用哪種方式處理,要看我們面對的是怎樣的問題。
最后附上兩個附表。
附表一,當(dāng)我們 render json:..., status: :ok, ... 時,status 和符號的對應(yīng),可以在這里找到,一般我們用 :ok, :create, :success, :error 就足夠了。
| Response Class | HTTP Status Code | Symbol |
|---|---|---|
| Informational | 100 | :continue |
| 101 | :switching_protocols | |
| 102 | :processing | |
| Success | 200 | :ok |
| 201 | :created | |
| 202 | :accepted | |
| 203 | :non_authoritative_information | |
| 204 | :no_content | |
| 205 | :reset_content | |
| 206 | :partial_content | |
| 207 | :multi_status | |
| 208 | :already_reported | |
| 226 | :im_used | |
| Redirection | 300 | :multiple_choices |
| 301 | :moved_permanently | |
| 302 | :found | |
| 303 | :see_other | |
| 304 | :not_modified | |
| 305 | :use_proxy | |
| 306 | :reserved | |
| 307 | :temporary_redirect | |
| 308 | :permanent_redirect | |
| Client Error | 400 | :bad_request |
| 401 | :unauthorized | |
| 402 | :payment_required | |
| 403 | :forbidden | |
| 404 | :not_found | |
| 405 | :method_not_allowed | |
| 406 | :not_acceptable | |
| 407 | :proxy_authentication_required | |
| 408 | :request_timeout | |
| 409 | :conflict | |
| 410 | :gone | |
| 411 | :length_required | |
| 412 | :precondition_failed | |
| 413 | :request_entity_too_large | |
| 414 | :request_uri_too_long | |
| 415 | :unsupported_media_type | |
| 416 | :requested_range_not_satisfiable | |
| 417 | :expectation_failed | |
| 422 | :unprocessable_entity | |
| 423 | :locked | |
| 424 | :failed_dependency | |
| 426 | :upgrade_required | |
| 428 | :precondition_required | |
| 429 | :too_many_requests | |
| 431 | :request_header_fields_too_large | |
| Server Error | 500 | :internal_server_error |
| 501 | :not_implemented | |
| 502 | :bad_gateway | |
| 503 | :service_unavailable | |
| 504 | :gateway_timeout | |
| 505 | :http_version_not_supported | |
| 506 | :variant_also_negotiates | |
| 507 | :insufficient_storage | |
| 508 | :loop_detected | |
| 510 | :not_extended | |
| 511 | :network_authentication_required |
附表二:ajax 的回調(diào)方法,我們使用了 :success 和 :error,當(dāng)然還有其他的一些,我們需要了解下。
| event name | extra parameters * | when |
|---|---|---|
ajax:before |
before the whole ajax business , aborts if stopped | |
ajax:beforeSend |
[event, xhr, settings] | before the request is sent, aborts if stopped |
ajax:send |
[xhr] | when the request is sent |
ajax:success |
[data, status, xhr] | after completion, if the HTTP response was a success |
ajax:error |
[xhr, status, error] | after completion, if the server returned an error ** |
ajax:complete |
[xhr, status] | after the request has been completed, no matter what outcome |
ajax:aborted:required |
[elements] | when there are blank required fields in a form, submits anyway if stopped |
ajax:aborted:file |
[elements] | if there are non-blank input:file fields in a form, aborts if stopped |