本課程講解Rails 視圖(View),內(nèi)容包括常用的輔助方法(Helper),如何使用表單(Form),AJAX在視圖中的應(yīng)用以及如何借助其他的模板引擎實(shí)現(xiàn)簡(jiǎn)潔的頁(yè)面方案。
視圖(View)即 MVC 中的 V,也是Rails使用者最先見到的部分。在完成業(yè)務(wù)邏輯前,合理的設(shè)計(jì)視圖是 MVC 開發(fā)中最先得到用戶認(rèn)可的部分。本課程結(jié)合商品頁(yè)面的開發(fā),講解Rails 中的視圖。
本課時(shí)講解Rails 視圖(View)中的布局文件,常見的輔助方法(Helper)以及如何使用局部模板。
本章開始,我們將進(jìn)入 Rails 的視圖(view) 的開發(fā)中。如果你對(duì) Rails 這個(gè) MVC 框架還有一些模糊的話,建議讀一讀這篇文章。
Rails 是一個(gè) RESTful 風(fēng)格的 MVC 框架。
我們把第一章使用 bootswatch 創(chuàng)建的項(xiàng)目 copy 過來。現(xiàn)在,我們進(jìn)入到 app/views 這個(gè)文件夾吧。
layouts 里放的是布局文件。如果我們網(wǎng)站只有一種布局,那么一個(gè) application.html.erb 就足夠了。我們也可以為每個(gè)資源創(chuàng)建一個(gè) layout,比如 app/views/layouts/products.html.erb。
我們刪掉多余的代碼,增加一個(gè) yield 的輔助方法(helper)。
<div class="container">
<%= yield %>
</div>
訪問我們的頁(yè)面,希望你會(huì)看到和我一樣的效果。如果沒有,沒關(guān)系,可以到 這里 clone 我們的代碼。
http://wiki.jikexueyuan.com/project/rails-practice/images/chapter_3/1.png" alt="" />
yield 方法可以讓 Rails 使用我們的模板(template) app/views/products/index.html.erb 填充了布局(layout)。
我們?cè)倏匆幌?app/views/layouts/application.html.erb 中的這一行:
<%= yield(:page_stylesheet) if content_for?(:page_stylesheet) %>
如果我們?cè)?app/views/products/index.html.erb 中使用 content_for 方法,可以在這個(gè)layout 的這個(gè)位置,顯示額外的內(nèi)容,比如,我們?cè)?index.html.erb 的最上面增加:
<%= content_for :page_stylesheet do %>
<!-- 這是 index.html.erb 里單獨(dú)使用的 -->
<% end %>
再刷新下頁(yè)面,我們?cè)谠创a里看到:
http://wiki.jikexueyuan.com/project/rails-practice/images/chapter_3/2.png" alt="" />
在實(shí)踐開發(fā)里,我們經(jīng)常這樣做:布局中加載的是所有頁(yè)面通用的內(nèi)容和 css,js,而到了具體頁(yè)面,就通過 content_for 這個(gè)輔助方法定義自己的內(nèi)容,在我們的 application.html.erb 里,你可以找到四個(gè) content_for,這樣給我們的代碼里增加了一些靈活,也不必把所有內(nèi)容都寫到一起。
content_for? 判斷我們是否定義了這個(gè)變量。
如果我們想更改一下布局,該如何做呢?實(shí)踐中,我們的確會(huì)遇到以下幾種情形:
app/views/layouts/admin.html.erb我們?cè)?admin 的 controller 里聲明它使用另一個(gè):
class AdminController < ApplicationController
layout "admin"
通常我們把 admin 放到 module 中,而為 admin 建立一個(gè)通用的 controller,讓所有 admin 的 controller 都繼承它,這樣,我們不用反復(fù)的去定義了:
class Admin::BaseController < ApplicationController
layout "admin"
end
class Admin::ProductsController < Admin::BaseController
end
class Admin::CommentsController < Admin::BaseController
end
這時(shí),我們要在 action 里去變更這個(gè)布局,比如,創(chuàng)建一個(gè) Product 的時(shí)候:
def new
@product = Product.new
render layout: "another_layout"
end
def edit
render layout: false
end
上一節(jié),我們已經(jīng)使用了幾個(gè)輔助方法,這里我們?cè)俳榻B幾個(gè) Layouts and Rendering in Rails 提到的 helper。
你會(huì)發(fā)現(xiàn)在頁(yè)面里最多的是 link_to 這個(gè)方法,它的參數(shù)也是蠻多的,我們來詳細(xì)講解。
我們把現(xiàn)在的 view 修改一下,把 首頁(yè) 的鏈接加上。
<%= link_to "網(wǎng)店演示", root_path, class: "navbar-brand" %>
一個(gè)稍復(fù)雜的例子:
<%= link_to "刪除", product, :method => :delete, :data => { :confirm => "點(diǎn)擊確定繼續(xù)" }, :class => 'btn btn-danger btn-xs' %>
我們可以改變 link_to 的默認(rèn)行為(GET),:method => :delete 將發(fā)送 delete 請(qǐng)求。:confirm 將會(huì)告訴瀏覽器阻止我們當(dāng)前的動(dòng)作,直到點(diǎn)擊 確定。實(shí)現(xiàn)上面兩個(gè)效果,需要引入 ujs, 在我們的 app/assets/javascripts/simplex.js[1] 中已經(jīng)為我們引入了:
//= require jquery
//= require jquery_ujs
注[1]:通常我們使用的是 application.js,但是在 1.3 中我們?cè)O(shè)計(jì)了新的主題,目前我使用的是 simplex。
寫到這里,我要推薦 http://api.rubyonrails.org/ 這里了,對(duì)于各種 Rails 本身的方法,我們可以通過查詢 api 文檔得到。如果是某個(gè) Gem 提供的方法,我們可以直接翻看它的 README 或者代碼。
一個(gè)較實(shí)用的工具 Dash,可以幫你管理每個(gè)版本 api 文檔,查詢起來也很方便。不過它是收費(fèi)的。
http://wiki.jikexueyuan.com/project/rails-practice/images/chapter_3/3.png" alt="" />
建議你使用 api 文檔查找一下這方法,你會(huì)看到這個(gè)代碼提示:
image_tag("icon")
# => <img alt="Icon" src="/assets/icon" />
image_tag("icon.png")
# => <img alt="Icon" src="/assets/icon.png" />
image_tag("icon.png", size: "16x10", alt: "Edit Entry")
# => <img src="/assets/icon.png" width="16" height="10" alt="Edit Entry" />
image_tag("/icons/icon.gif", size: "16")
# => <img src="/icons/icon.gif" width="16" height="16" alt="Icon" />
image_tag("/icons/icon.gif", height: '32', width: '32')
# => <img alt="Icon" height="32" src="/icons/icon.gif" width="32" />
image_tag("/icons/icon.gif", class: "menu_icon")
# => <img alt="Icon" class="menu_icon" src="/icons/icon.gif" />
我們的圖片是來自 app/assets/images 的,我放了一個(gè) logo.png 在里面,你會(huì)發(fā)現(xiàn)它的地址是:http://localhost:3000/assets/logo-be2e3e66a18126c4042f84cd4aae4cb3.png。Rails 使用 sprockets-rails 來管理 app/assets 中的文件,后面章節(jié)我們會(huì)詳細(xì)介紹。
這里,我們可以關(guān)閉 be2e3e66a18126c4042f84cd4aae4cb3這種形式:
config/environments/development.rb
config.assets.digest = false
重啟我們的服務(wù),地址變?yōu)?http://localhost:3000/assets/logo.png。
我們經(jīng)常在 head 里和頁(yè)面里,增加 rss 和 atom 訂閱連接,這時(shí),我們可以使用 auto_discovery_link_tag 這個(gè)輔助方法。
<head>
...
<%= auto_discovery_link_tag(:rss, {controller: "products", action: "index"}, {title: "RSS Feed"}) %>
<%= auto_discovery_link_tag(:atom, {controller: "products", action: "index"}, {title: "ATOM Feed"}) %>
...
</head>
我們也可以在頁(yè)面中增加這個(gè)連接,這在 web2.0 興起后的博客中很常見,方便我們把數(shù)據(jù)加入到訂閱中。
<%= link_to "rss", products_url(format: "rss") %>
<%= link_to "atom", products_url(format: "atom") %>
剩下的問題是, Rails 如何提供這個(gè)數(shù)據(jù),我并不想等到 controller 里再去講這個(gè)部分,讓我們現(xiàn)在開始了解下:
Rails 是會(huì)根據(jù)我們的請(qǐng)求類型,做出響應(yīng)。
如果我們請(qǐng)求的是一個(gè) http://localhost:3000/products.html,Rails 會(huì)給我們 html 的頁(yè)面,而如果我們請(qǐng)求的是 http://localhost:3000/products.rss,Rails 會(huì)自動(dòng)選擇 rss 的模板,渲染(render)后返回我們結(jié)果。http://localhost:3000/products.atom 也是一樣。 所以,我們?cè)?app/views/products/ 中增加兩個(gè)文件:index.rss.builder 和 index.atom.builder。
在 controller 里,如果我們想對(duì)結(jié)果做一些其他的操作,就需要增加這個(gè)代碼:
app/controllers/products_controller.rb
respond_to do |format|
format.html
format.rss { ... }
format.atom { ... }
end
在這個(gè)例子中,我們并不需要改變什么,所以不用添加它。
app/views/products/index.atom.builder
atom_feed do |feed|
feed.title "商品列表"
feed.updated @products.maximum(:updated_at)
@products.each do |product|
feed.entry product, published: product.updated_at do |entry|
entry.title product.name
entry.content product.description
entry.price product.price
end
end
end
app/views/products/index.rss.builder
xml.instruct! :xml, version: "1.0"
xml.rss version: "2.0" do
xml.channel do
xml.title "商品列表"
xml.description "這是商品列表"
xml.link products_url
@products.each do |product|
xml.item do
xml.title product.name
xml.description product.description
xml.price product.price
xml.link product_url(product)
xml.guid product_url(product)
end
end
end
end
再次訪問 http://localhost:3000/products.rss 和 http://localhost:3000/products.atom,你會(huì)發(fā)現(xiàn)我們得到了結(jié)果。
我們用到了 .builder 這個(gè)結(jié)尾的文件,它會(huì)告訴 Rails 使用 Builder::XmlMarkup 這個(gè)庫(kù)(lib)來解析文件。所以看 rss.builder,它是按照 xml 格式寫的。atom.builder 用到了另一個(gè)輔助方法 atom_feed,寫法雖然不同,但是生成的內(nèi)容也是 xml 格式的。
在 scaffold 創(chuàng)建的文件里,你會(huì)看到 index.json.jbuilder,它會(huì)使用 JBuilder 這個(gè)庫(kù)來解析并生成 json 的結(jié)果。這會(huì)在后面的章節(jié)講到,你可以在 這里 先了解一下。
Railscasts.com 是所有 Rails 學(xué)習(xí)者必看的網(wǎng)站,這個(gè) 視頻 一定會(huì)幫助你理解上面的內(nèi)容。
在此,向 Ryan 致敬。
<head>
<%= stylesheet_link_tag "simplex", :media => "all" %>
</head>
css 文件的引用通常放到頁(yè)面的 head 標(biāo)簽之間。這里我們引用的是 css 文件,我們也可以把它改為 .css.scss,這樣可以在里面寫一些 scss 語(yǔ)法,而不用更改我們的引用。我們?cè)?2.1.3 中已經(jīng)提到了 scss。
...
<%= javascript_include_tag "simplex" %>
<%= yield(:page_javascript) if content_for?(:page_javascript) %>
</body>
瀏覽器是自上而下解析節(jié)點(diǎn)元素(DOM)的,所以,請(qǐng)注意我們把 js 文件加載放到頁(yè)面最下面,以免因?yàn)槟硞€(gè) js 解析問題導(dǎo)致頁(yè)面始終無法顯示。在引用完 js 庫(kù)后,我們還可以根據(jù)需要,單獨(dú)放置頁(yè)面的 :page_javascript。
ActionView 還為我們提供了其他很多輔助方法,可以查看 這里。
DRY, Don't Repeat Yourself.不要重復(fù)自己。
為了讓我們節(jié)省更多的頁(yè)面重復(fù)代碼,我們還可以使用局部模板(partial)。打開我們的 app/views/products/index.html.erb:
<% @products.each do |product| %>
<%= render partial: "product", locals: { product: product } %>
<% end %>
這里我們使用了局部模板,partial 指定了使用哪個(gè)模板,locals 向模板里傳遞了一個(gè)變量。在 _product.html.erb 里,我們顯示具體 product 的信息。
不過這是一個(gè)老套的寫法,Rails 4 給了我們更酷的寫法:
<%= render @products %>
不過,如果需要傳遞更多的變量(locals),還是要用第一種方法,當(dāng)然,你完全可以把 each do 的代碼放到局部模板里。
我們也可以不傳遞變量到局部模板里,它可以找到 @products,看一下 new.html.erb 和 edit.html.erb:
<%= render :partial => 'form' %>
也可以直接寫:
<%= render 'form' %>
如果我們?cè)陧?yè)面加載路徑中,放置了多個(gè)同名的局部模板,它會(huì)顯示離它最近的那個(gè)。我們可以把公用較多的模板,放到一個(gè)專屬的文件夾里,比如 shared,引用的時(shí)候:
<%= render partial: "shared/product", locals: { product: product } %>
注:當(dāng)使用 locals 傳遞參數(shù)時(shí),一定要聲明 partial。
我為大家在 shared 中放置了一個(gè)同樣的 _product.html.erb,大家可以在 index.html.erb 中調(diào)用看看。
說一個(gè)實(shí)踐中經(jīng)常用到的局部模板。
我建立了一個(gè)新的文件夾 application,這里會(huì)放布局文件使用的局部模板,我放了一個(gè) _flash.html.erb,這是 flash 通知??纯次覀兊?products_controller.rb,我們?cè)诓僮鞒晒髸?huì)有提示信息,它在我們的頁(yè)面上還沒有顯示。我修改了一下 application.html.erb:
<div class="container">
<%= render "flash" %>
<%= yield %>
...
為了讓 flash 符合 bootstrap 的格式,我做了代碼調(diào)整,大家可以參考代碼。flash 是 session 的應(yīng)用,通常在 controller 的 action 間傳遞信息,讀取成功后自動(dòng)清空。如果一個(gè) flash 沒有在合適得地方讀出來,那么它將被保存到讀出為止,這會(huì)造成本不該顯示它的地方卻顯示它,所以我把 flash 放到了 layout 中,使得所有頁(yè)面都會(huì)引用它,保證它產(chǎn)生后立刻顯示,并在顯示后自動(dòng)清空。