arthurccubeArthur's interests
arthurccube
read my profile
sign my guestbook

Visit arthurccube's Xanga Site!

Name: Arthur
Gender: Male


Message: message me


Member Since: 8/15/2006

SubscriptionsSites I Read
mythmok

Blogrings
Programmers Ring
previous - random - next


Posting Calendar

|<< oldest | newest >>|
view all weblog archives

Get Involved!

Suggest a link

Recommend to friend

Create a site


Wednesday, June 24, 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 下載




Sunday, April 26, 2009

Ruby on Rails 建立用戶系統及加入OpenID

Ruby on Rails 建立用戶系統及加入OpenID

每個Web Application 也為了建立一個安全方便的用戶系統或登入系統而遇上麻煩。Don't Repeat Yourself (DRY), 我們可使用RoR的Plugins解決煩惱!

說到用戶系統,不能不提 MySpacefacebook 等公司也非常投入的OpenID 。在RoR使用OpenId 是非常簡單的事。

Step 1: Signup 登記使用者

先安裝 acts_as_authenticated plugins, 它負責所有基本登入工作。在console 跳至<application_root>

ruby script/plugin install http://svn.techno-weenie.net/projects/plugins/acts_as_authenticated/ 

 

安裝後可以用 acts_as_authenticated 建立 "user" Model 和 "account" Controller.

script/generate authenticated user account

這樣便建立了相關的Model 和 Controller。現在設定 Database 。

rake db:migrate 

Database 設定完畢,讀者可以檢查使用的 Database 是否有一個新的 Table "users"。所有用戶資料將會放在這裡。

令人喜出望外的是,你已經擁有基本用戶系統的所有編碼,測試時間到了!

ruby script/server

ror_01

打開 "/account/signup" ,可看到 Signup頁面

打開 /account/signup ,可看到Signup頁面。測試建立一個使用者後,看看資料庫是否更新,完成第一步。

 

Step 2: 設定登入保護 

我們演示登入保護,只要在 Views 和 Controllers 做一點修改便可。

首先,在<application root>/app/views/account 新增 "show.rhtml",貼上以下編碼。

<%=link_to "log out", "/account/logout"%>

<p><%=current_user.login %> is logged in.</p>

因為acts_as_authenticated 已把基本設定寫妥,我們打開<application root>/app/controllers/account_controller.rb",在 "include AuthenticatedSystem"下一行插入:

  before_filter :login_required, :except => [:login, :signup]

before_filter 設定先決的 action ,這裡我們執行 acts_as_authenticatedlogin_required action。所以這 controller 的所有 actions 也有預設登入保護。

因為我們加入了 :except => [:login, :signup],那麼 loginsignup 兩個頁面也不會被登入保護,沒有用戶的人仕也可瀏覽這兩個頁面。

測試!在瀏覽器鍵入 http://localhost:3000/account/show

ror_02

show 被登入保護了

Ooops,被送到 "login" 頁面。因為"show" 被登入保護了。這時候可以登入剛才新增的使用者:

ror_03

登入後被送到 show

成功登入後便會自動送到"show"。

現在你的application 已有一個完整的登入系統了!

**如果你的 "account_controller.rb" 有其他 actions ,或者 "/views/account" 有其他頁面,也會有登入保護的了。

 

 

Step 3: OpenID

終於到精采部份了,就是為加入OpenID! 如果你不使用OpenID ,可以跳過這部份。但這樣做之前要留意,OpenID是一個十分成功的共用登入系統,MySpacefacebook 也參與其中。以下是OpenId 編碼

安裝 ruby-openid RubyGems

sudo gem install ruby-openid -y

安裝 restful_authentication

script/plugin install http://svn.techno-weenie.net/projects/plugins/restful_authentication

安裝 open_id_authentication

這個 Plugins 和Rails 2.x 的相容問題較多。筆者發現一個較好的版本,不過它放在 github 裡。要先到 http://github.com/rails/open_id_authentication/tree/master 下載。解壓後改名為open_id_authentication 文件夾,並把它移到<application root>/vendor/plugins 內。

ror_04

現在共有三個Plugins: acts_as_authenticated, restful_authentication, open_id_authentication

建立新 Tables

open_id_authentication 要兩個Database Tables 來記錄OpenId 資料,執行以下指令來建立。

rake open_id_authentication:db:create

rake db:migrate

open <application root>/config/routes.rb; 確保以下編碼正在運作。

map.root :controller => 'account'


Views 修改

先修改"<application root>/app/views/account/login.rhtml" ,在 "<p><%= submit_tag 'Log in' %></p>" 上面插入:

<p> use OpenID ... </p>
<p> <%= text_field_tag "openid_url" %> </p>

再把"<application root>/app/views/account/show.rhtml" 改寫成:

<%=link_to "log out", "/account/logout"%>
<% if current_user.not_openid?%>
<p><%=current_user.login %> is logged in.</p>

<% else %>
<p><%=current_user.identity_url %> is logged in as an open id user.</p>
<% end %>

Models修改

Model 只有一個相關的 <application root>/app/models/user.rb 。但這 user.rb 檔案有幾處要修改。

1. 加入 def not_openid?

  def not_openid?
    identity_url.blank?
  end

2. 修改def password_required?

  def password_required?
    not_openid? && (crypted_password.blank? or not password.blank?)
  end

3. 加入":if => :not_openid?" 至幾個validations,讓openid 使用者避開該項核對。


validates_presence_of     :login, :email, :if => :not_openid?
validates_length_of       :login,    :within => 3..40, :if => :not_openid?
validates_length_of       :email,    :within => 3..100, :if => :not_openid?
 


Controllers 修改:

只須修改"<application root>/app/controllers/account_controller.rb"。這檔案也要修改幾處地方。

1. 修改 login,以加入OpenId 登入:

def login
    return unless request.post?
    if using_open_id?
      open_id_authentication(params[:openid_url])
    elsif params[:login]
      password_authentication(params[:login], params[:password])
    end
  end

3. 修改password_authentication (如有需要的話)

   def password_authentication(login, password)
      if self.current_user = User.authenticate(params[:login], params[:password])
        successful_login
      else
        failed_login("Invalid login or password")
      end
    end
 

4. 加入 open_id_authentication

    def open_id_authentication(openid_url)
      authenticate_with_open_id openid_url, :required => [:nickname, :email] do |result, identity_url, registration|
        if result.successful?
          @user = self.current_user = User.find_or_create_by_identity_url(identity_url)
          if @user.new_record?
            # registration is a hash containing the valid sreg keys given above
            # use this to map them to fields of your user model
            {'login=' => 'nickname', 'email=' => 'email'}.each do |attr, reg|
              current_user.send(attr, registration[reg]) unless registration[reg].blank?
            end
            if @user.valid?
              @user.save
              self.current_user = @user
              successful_login
            else
              failed_login "Authentication failed on this website."
            end
          else
            self.current_user = @user
            successful_login
          end
        else
          failed_login result.message
        end
      end
    end

這個 action 精明地完成OpenId 核對,並把一些項目從OpenId Server 取回(假設你的OpenId戶口有:nickname:email 這兩項資料) 。如果是application 的新用者,這些變數將自動用作建立新用者的設定,即是一個新用者也完全不須使用signup頁面。

5. 加入兩個跳頁用的action

private
    def successful_login
      redirect_back_or_default({:action => :show})
      flash[:notice] = "Logged in successfully"
    end

    def failed_login(message)
      redirect_to(:action => 'login')
      flash[:warning] = message
    end

6. 設定 Database 。

打開console ,跳到在 <application root>

ruby script/generate migration AlterUserIdentityUrl

打開<application root>/db/migration/<新的編號_alter_user_identity_url.rb>,改寫為:

class AlterUserIdentityUrl < ActiveRecord::Migration
def self.up
  add_column :users, :identity_url, :string
  end

  def self.down
  remove_column :users, :identity_url
  end
end

rake db:migrate 

完成!可以測試了。 當然,讀者要先登記一個OpenId (詳情請參閱 http://openid.net/ )

http://localhost:3000/account/login ,我用一個沒有在localhost登記的OpenId

ror_05

先輸入你的Open Id ,然後按Submit

登入後,會自動跳到OpenId Server,這時要登入OpenId一次。

ror_06

你會被送到OpenId Server,輸入你的OpenId password ,並按登入

成功後,你便會使用OpenId 登入localhost ,並跳至相關頁面。

ror_07

 用OpenId 跳至show頁面

總結

在建立一個使用者系統,acts_as_authenticated Plugins 帶給我們很大的方便。"script/generate authenticated user account" 已經把大部份編碼完成。

之後我們使用ruby-openid gem 和 open_id_authentication ,它們使存取OpenId 極之方便。甚至新的使用者也可直接登入系統!只要在 OpenId 登入一次後,那麼以後到任何一個你信任的 OpenId 網站也不用再次登入或登記了。

 

 


Sunday, March 22, 2009

Ruby on Rails Geotagging 方案(Ajax+GeoKit+Ym4r)

Ruby on Rails Geotagging 方案(Ajax+GeoKit+Ym4r)


GoogleMap 使 Mashup 達到了新的境界,很多網站也新加入了地圖系統。這次看看怎樣使用RoR 的Plugins和Ajax 功能,可以一兩個小時便建立一個Geotagging 系統。


Step 1: 建立Ajax 功能


首先,我們建立RoR 必要的Controller 和 Views,並加入Ajax 功能。

建立map folder和檔案

可以利用 RoR application 附帶的 script 得到基本的 Controller 和 Views。鍵入以下指令

ruby script/generate controller map



RoR 自動建立了相關檔案和Tests

設定layouts


一個頁面最重要是結構。RoR 提供的方法使用者可以輕易把結構分享給各個頁面,這便是 layouts 的利害之處,也是RoR 提倡的 DRY (Don't Repeat Yourself)。

打開 /app/controllers/ 內的 map_controller.rb ,鍵入以下編碼:


class MapController < ApplicationController
  layout "blogs"

end

意思是在這個 map controller 內的所有頁面也是使用"blogs"這個 layout。因此,我們在 /app/views/layouts 內要加入 'blogs.html.erb' 檔案:



新增 blogs.html.erb layout 檔案

在這檔案貼上以下編碼:


<html>
  <head>
    <%= javascript_include_tag :defaults %>
  </head>
<body>
<%=yield%>

</body>
</html>

很像普通的 html 吧!重點是以下兩句:

  1. <%=javascript_include_tag :defaults%> - 把預設的javascripts 放到這裡,這包括 RoR 非常喜歡的 prototype framework
  2. <%=yield%> - 把相關頁面的結果在這裡顯示。

在 index 頁面加入 form

現在處理主要的頁面了。在/app/views/map 內新增 index.html.erb



index 是預設頁面

'index.html.erb' 像我們平日使用 index.html 一樣,成為 map controller 的預設頁面。而 .erb 代表它是 ruby files.

把下面編碼貼在index.html.erb 內

  <% form_remote_tag :url => '/map/location_search' do -%>
    <label>Location:</label> <%= text_field_tag 'location' %></br>
    <div><%= submit_tag 'Go' %></div>
  <% end -%>

  <div id="results"></div>

這裡使用了幾個常見的helper methods

  1. form_remote_tag: 使這張form 變為Ajax form。重點是簡單的"remote",代表了Ajax的意思。而:url 定義伺服器的目標 action (/map/location_search)。
  2. text_field_tag: 文字輸入鍵,它的 id 和 name 值設定為'location'
  3. submit_tag: 這張form 的 submit 鍵,其值為'Go'
'<div id="results"></div>' 留待我們接收伺服器的結果。


是時間測試了!重啟server,在你的<application root> 鍵入

ruby script/server

這是在瀏覽器鍵入 http://localhost:3000/map 便可看到結果!



瀏覽器看到的結果




Ajax 效果

因為在form_remote_tag 定義了伺服器的目標為 '/map/location_search'。所以我們便要建立相關的編碼, 在 /app/views/map 內新增一個'location_search.rjs' 檔案




建立rjs 檔案

.rjs 檔案是RoR重要的Ajax 工具。在這檔案內把我們所要的javascripts 包裝得像普通的 ruby 編碼,而且簡單得不能再簡單。把以下編碼加入到location_search.rjs


page.replace_html 'results', "#{params[:location] } is requested at #{Time.now}"

page.replace_htm 把 id 為 'results' 的 innerHTML 物件修改。"#{...} something" 是 Ruby 的String 物件,#{}可以放入其他Variable。而RoR 會把所有html Request parameters 放到params變數內。

所以,這時在剛才的form 鍵入'Hong Kong',並按Go 鍵,便可看到以下結果:


Ajax 效果



Step 2: 安裝Geokit 找出地理位置



RubyGems 幫助我們省卻很多編碼,這次我們找到了很好用的地圖工具 geokit Rubygems (http://geokit.rubyforge.com)

安裝geokit

安裝相關的gems,先確保Rubygems 記錄了下載的網址:

gem sources -a http://gems.github.com

可以安裝 geokit gems了:

gem install andre-geokit

使用GoogleMaps API,必需要先登記一個 Key,可從下面網站登記:

http://code.google.com/intl/zh-TW/apis/maps/signup.html

現在我們用 'localhost' 測試,在 API 申請表上的 'My Web Site URL' 填上 'http://localhost' 便可。我們便得到一條像下面字句的 Key:

ABQIAAAA54q9My7rL_Hcn6q9UGHTUxT2yXp_ZAY8_ufC3CFXhHIE1NvwkxRroKVvZLA8rVtuMyXjGkh0c8tDeg
使用這 Key, 把以下編碼放到 <application root>/app/config/environment.rb 最底部份 ( 別放在 Rails::Initializer.run do |config| .... end 那個 loop 內!):


require 'geokit'

GeoKit::Geocoders::google = 'YOUR_KEY_HERE'

GeoKit::Geocoders::provider_order = [:google]


設定完成,可以使用Geokit 了!打開 <application root>/app/controllers/map_controller.rb,貼上以下編碼:

class MapController < ApplicationController
  layout "blogs"
 
  def location_search
    @loc = GeoKit::Geocoders::MultiGeocoder.geocode(params[:location] )

  end

end


然後,打開<application root>/app/views/location_search.rjs,貼上下面編碼:

page.replace_html 'results', "#{ } located <b>#{@loc.lat}, #{@loc.lng}</b>"


剛才在 Controller 的相關 action 中 (location_search),使用了Geokit 找出輸入地點的經緯度,把結果記錄在 @loc 這變數內。所以在 location_search.rjs 這個 View 可以直接使用這變數。這得益於RoR 的MVC 結構,把編碼變得清晰易明。


現在可以測試了!如果未打開Webrick,在 <application root> 輸入以下指令:

ruby script/server



輸入 Tokyo, 並按 Go 鍵,可以得出以上結果!



Step 3: 用 ym4r 在網頁顯示 Google Map 地圖

RoR projects 可以安裝Plugins 以增加功能,相似於在Firefox 安裝 Add-ons 一樣。Plugins 和 Rubygems 概括的分別在於前者是給某一個project 使用,後者是給整個ruby 系統使用。使用時分別不大。


安裝plugins

首先,我們需要 svn。如果沒有的話,便要先安裝了。請先到 http://subversion.tigris.org/getting.html 看看如何安裝。如果是 ubuntu 的話,鍵入以下指令便可:

apt-get install subversion

以上指令只須執行一次,然後便可安裝plugins 。在<application root folder> 輸入:

ruby script/plugin install svn://rubyforge.org/var/svn/ym4r/Plugins/GM/trunk/ym4r_gm
這時<application root folder>/vendor/plugins 會多了一個 ym4r folder。


ruby script/plugin 指令下載了整個 ym4r plugins

在開始使用 ym4r 前,先設定Google Map Api Key 給 ym4r。ym4r 提供了一個便捷的方法設定此 Key 給不同的環境。打開<application root folder>/config/gmaps_api_key.yml,貼上:

development:
  YOUR_KEY_HERE

test:
  YOUR_KEY_HERE

production:
  YOUR_KEY_HERE



把剛才為 geokit 登記的 Google Map API Key 貼在上面YOUR_KEY_HERE 位置。安裝完成!

修改 MVC

現在可以使用Plugins 的功能了。因為所有邏輯應放在 Controller 內,所以打開 Map Controller (<application root folder>/app/controllers/map_controller.rb),貼上下面的編碼。


class MapController < ApplicationController

  # default action of the controller
  def index
    @map = GMap.new('map_div')
    @map.control_init(:large_map => true, :map_type => true)
    @map.center_zoom_init([75.5, -42.56], 4)
    @map.overlay_init(GMarker.new([75.6, -42.467], :title=> "Hello World", :info_window => "I am Here"))
  end

  def location_search
    @loc = GeoKit::Geocoders::MultiGeocoder.geocode(params[:location])
  end

end


上面編碼新增了 index action,@map 在這裡定義。對於有使用Google Map 編碼的朋友,那些 @map functions 似曾相識。不錯,ym4r 主要是把Google Map Api 的功能 Railize!在 index 的頁面便可使用Google Map 這物件了。index 內的四行便建立了 GMap 物件並存於@map 中,加上了 control ,把 Map的中心設定,還加入了一個 Marker!是否十分易懂?

要使用 Google Map ,首先要把其 javascript library 放進頁面。RoR 的結構和 ym4r 令這一步驟十分輕鬆。

把以下編碼貼在 <application root folder>/app/views/layouts/blog.html.erb 內:


<html>
<head>
   <%= javascript_include_tag :defaults %>
   <%= GMap.header %>
   <%= @map.to_html %>
</head>
<body>
<%=yield%>
</body>
</html>

新增了 GMap.header @map.to_html 兩句,便把需要的 javascript 放到頁面內。

我們可以在 index 頁面使用 Google Map 了,把下面編碼貼在 <application root folder>/app/views/map/index.html.erb




  <% form_remote_tag :url => '/map/location_search' do -%>
    <label>Location:</label> <%= text_field_tag 'location' %></br>
    <div><%= submit_tag 'Go' %></div>
  <% end -%>

  <div id="results"></div>
 

  <%= @map.div(:width => 600, :height => 400) %>


以上編碼新增了 <%= @map.div(:width => 600, :height => 400) %> 一句,便把所要的 Map HTML 放到頁面,並設定了它的長和濶。

這時我們可以試試結果。不過,安裝 Plugin 之後,也要重啟 application 的。先停止現在執行的 application,並再次輸入:

ruby script/server


在瀏覽器可以看到

 
剛才設定的 Google Map 在這裡顯示出來!

成功,看到Google Map 了!

(如果你看到 undefined method 'relative_url_root' for ActionContoller::AbstractRequest:Class 這錯誤,請到這裡看看 http://railsforum.com/viewtopic.php?id=24839 。修改map.rb 後應該成功)


Step 4: Mashup: 用 geokit 和 ym4r Geotagging


geokit 和 ym4r 強勁吧!結合兩者會有什麼結果!?

先把 <application root folder>/app/controllers/map_controller.rb 的 location_search 換成以下編碼:

  def location_search
    @loc = GeoKit::Geocoders::MultiGeocoder.geocode(params[:location] )
    @map = Variable.new("map")
    @marker = GMarker.new([@loc.lat,@loc.lng], :title => params[:location], :info_window => "Marker for #{params[:location]}")

  end

@map = Variable.new("map") 中的@map 聯絡了剛才的 GMap 物件,即是我們剛看見在頁面上的Google Map 。再GMarker 便把Geokit 找到的 geocode 資料建立一個 Marker 物件。


我們最後要修改 Ajax 的結果了,在 <application root folder>/app/views/map/location_search.rjs 最底部份貼上:


page << @map.clear_overlays
page << @map.add_overlay(@marker)
page << @map.center_and_zoom_on_markers([@marker])


上面編碼先取消地圖上的Overlays ,包括所有存在的 Markers,然後加入新的一個 Marker,最後置中於新 Marker 的位置。


完成!


打入 Tokyo 並按Ok 鍵便會找到東京的位置!



總結

這樣的 geotagging mashup 主要由幾個步驟完成:

  1. 在 RoR 建立Ajax function,主要是設定 layout ,包括建立 ajax form 的頁面和 .rjs server side 檔案。
  2. 安裝 geokit 找出輸入的地理位置。
  3. 安裝 ym4r plugin ,設定和建立頁面上的 Google Map。
  4. Mashup 以上項目。
RoR 有很多有用的gems 和 plugins,可以發揮你的想像力,Mashup 各樣東西。你可否把 flickr 的相片加到 Google Map 內呢?


此文章的原始檔可在 http://github.com/arthurccube/rails_examples_ajax_geokit_ym4r/tree/master 下載


Thursday, February 19, 2009

Ruby on Rails 教程 - 安裝及建立Blog Application

Ruby on Rails 教程 - 安裝及建立Blog Application

 大家也聽過 Agile Development 的新寵 Ruby On Rails (R.o.R.) 吧。 R.o.R. 有能力使新用者在一兩小時內建立一個 Blog 網站,這不可不算是Agile 吧!這教程可以給你看看 R.o.R. 這種威力!

下載和安裝 R.o.R.

這裡試範在 Ubuntu Linux 安裝 R.o.R. (在其他系統上,可參考http://wiki.rubyonrails.org/rails/page/installation)。如果你的系統已經安裝了 R.o.R,可以略過這部份(可以在 Console 輸入 ruby -vrails -v 指令確保是否已安裝)。


Step 1 安裝 Ruby

Ruby 是出色的物件向語言,也就是這系統的核心。

sudo apt-get install ruby irb ri rdoc ruby1.8
-dev build-essential


Step 2 安裝 RubyGems

RubyGems 是Ruby 的外掛程式系統,它提供多項指令使 RoR 極之方便。

sudo apt-get install rubygems

然後把 RubyGems 加入 Path (e.g. 在 /root/.bash_profile 內)

PATH=$PATH:/var/lib/gems/1.8/bin/

export PATH


Step 3 安裝 Rails

Rails 不單止令 Ruby 變成方便使用,也是十分有系統的 Framework。因為安裝了 RubyGems,所以安裝Rails 也變得很方便。

gem install rails --include-dependencies



Step 4 安裝資料庫

RoR提供介面使用多種Database(如SQLite)。不過筆者最常用的是MySQL。

sudo apt-get install mysql-server --include-dependencies



Step 5 安裝Mongrel Cluster (Optional)

R.o.R. 已內置了 WEBrick 伺服器,但功能沒有 Mongrel 的利害,所以,如在production 使用,您可以嘗試Mongrel。

gem install mongrel --include-dependencies

(選取適合系統的版本)

 

成功安裝!Checkpoints

Ruby 版本:  ruby -v

Rails 版本: rails -v

mysql 版本: mysql --version

Gems 列表 (如完成第五步,可看到 mongrel 在表中): gem list

 

建立基本網站

有了 R.o.R. 的各項工具,建立一個網站會出乎意料的快捷。

網站結構

首先,用 rails 指令建立網站

 

rails blog_application

cd blog_application


一個名叫 'blog_application' 便會建立,它還擁有 R.o.R. 的結構,例如 MVC 模組。我們由淺入深,先看看今次我們接觸到的文件夾.


/app 絕大部份的編碼也在這裡,包今了MVC 模組

/log 所有記錄檔案

/config 所有主要設定

/db Database 相關設定

/script 一些經常使用的或預設的 script


設定 Database

為R.o.R. 系統安裝 mysql Adapter

gem install mysql

(** Ooops, 有些Mysql 在 Ubuntu  在執行這命令時遇上 'extconf.rb failed' 錯誤,那在執行gem install mysql  前便要下面指令

MySQL 5.0 用者:

sudo apt-get install libmysqlclient15-dev

MySQL 4.0 用者:

sudo apt-get install libmysqlclient12-dev

)

之後為你的 blog_application 設定Database 為 mysql。修改 config/database.yml,這檔案設定不同環境的database:

#development environment
development:
  adapter: mysql
  database: blog_dev
  username: ruby
  password: 123456
  host: localhost
  port: 3306


# test environment
test:
  adapter: mysql
  database: blog_test
  username: ruby
  password: 123456
  host: localhost
  port: 3306

# production environment
production:
  adapter: mysql
  database: blog
  username: ruby
  password: 123456
  host: localhost
  port: 3306


(database.yml 為不同環境設定不同的database 設定)

yml 很注要space 和 tab 鍵的分別,暫時不用tab 鍵。留意 R.o.R. 把所有 application 分為development, test, 和 production  環境。其名字表明它們的意義。我們暫時注意 development 的設定。

Database 名字的 convention  是 '<application name>_<environment>',而production 的Database 使用 '<application name>'。我們的development Database  因此名為 'blog_dev'

當然,最重要的是在 mysql 建立這些Database 和 user!

          在 blog_application 執行以下script 開啟你的application

ruby script/server

R.o.R. 預設的port 是3000,所以在瀏覽器鍵入 'http://localhost:3000' 便可看到你的application



如看不到這介面,表示你的設定有問題。這時可看看/logdevelopment.log (因為預設是development 環境) 有什麼資料。

Generate scaffold 的利害


要知道R.o.R. 的威力,不能不知道generate 這script,而它最出色的是 scaffold 項目。

只要一個指令,我們便可建立大部份的編碼!我們利用rails 送給我們的generate script 完成,在blog_application 內,執行

ruby script/generate scaffold blog


這script 幫我們建立一個完善的 MVC 建構,換句話說,即由database 相關的Model (M) 起,到網頁相關的Controller (C) 和 View (V) ,也由這一個指令建立起來!

還不止,它甚止建立相關的 unit tests, functional tests 。作為一個認真的application,這些tests 不能缺少,我們將來可看到各種tests 在R.o.R. 是怎樣連繫起來。

看看現時的結果吧,因為R.o.R. 的 Routing 像mod_rewrites 一樣把各個URL request 及其他函數 送到相應的Controller,又把結果送回。這一切主要在 /config/routes.rb 設定。但預設的設定已足夠這教程使用,我們甚至不用開啟這檔案。

所以可以立即打開瀏覽器,鍵入"http://localhost:3000/blogs" ,便可看到我們的 blog:


嘩!發生什麼事?這是 R.o.R. 的預設錯誤畫面。這裡可以看到相關錯誤訊息,所以這問題主要是

Mysql::Error: Table 'blog_dev.blogs' doesn't exist: SELECT * FROM `blogs` 
即是沒有 blogs 這table。

使用rake db:migrate 建立 tables

建立tables, 不要直接使用MySQL ,而應使用更靈活和簡單的方法 - migrate 檔案。使用這些檔案,當你轉換database 也不用修改任可migrate 檔案;而且R.o.R. 自動處理database 的version control 。

打開 /db/migrates/<datetime>_create_blog.rb (e.g. 20090124054827_create_blogs.rb)

class CreateBlogs < ActiveRecord::Migration
  def self.up
    create_table :blogs do |t|
      t.column "title",                     :string, :default => "", :null => false
      t.column "entry",                   :text
      t.timestamps
    end
  end

  def self.down
    drop_table :blogs
  end
end

然後我們使用 'rake'指令,rake 相當於ruby make 指令。是用來執行一連串有用的命令。這次我們執行出名的

rake db:migrate

rake 便會把 /db/migrates 這的檔案執行,包括我們剛存檔的命令,所以這 blogs table 會建立到相關database 內。

因為預設是development 環境,所以在blog_dev可找到blogs table 了!


修改 Controller 和 Views

剛才的頁面和以下用到的大部頁面也是由剛才的 script/generate scafold 指令產生出來的。我們按下 'new blog' 後看到一個新增 blog 頁面。


首先我們要修改這首面,給用者輸入相關的資料。因為這 'new' 頁面是一個屬於 'blogs' 的 View,所以可以在  /app/views/blogs/new.html.erb 找到:

<h1>New blog</h1>

<% form_for(@blog) do |f| %>
  <%= f.error_messages %>

  <p>
    <%= f.submit "Create" %>
  </p>
<% end %>

<%= link_to 'Back', blogs_path %>


在 <%= f.error_messages %> 下面插入:

<p>
    <label> Title </label> <br/> <%= f.text_field :title %>
  </p>
  <p>
    <label> Entry </label> <br/><%= f.text_area :entry %>
  </p>


重新載入剛才的 "http://localhost:3000/blogs/new"


輸入欄目後按 'create' 鍵,便成功建立一個新的Blog!

上面的'f.text_field :field_name'編碼可看出是R.o.R. 的推斷力。

首先'form_for(@blog) do |f| '設定一張form 給@blog這物件,名為 f

而這張 form 便自動找出 @blog 在 modeldatabase的各個field 名,所以我們可以輕易使用 ':title' 和 ':entry'。

如是者,我們要修改另外三個檔案 'edit', 'show', 'list'

1. 在 'edit.html.erb' 的 <%= f.error_messages %> 下面插入:

<p>
    <label> Title </label> <br/> <%= f.text_field :title %>
  </p>
  <p>
    <label> Entry </label> <br/><%= f.text_area :entry %>
  </p>

2. 在 'show.html.erb' 的第一行插入:


 <p>
    <label> Title </label> <br/> <%= @blog.title %>
  </p>
  <p>
    <label> Entry </label> <br/><%= @blog.entry %>
  </p>

3.  在 'list.html.erb' 的:

<td><%= link_to 'Show', blog %></td>

改為

<td><b><%=blog.title%></b> <%= link_to 'Show', blog %></td>


 


完成!


總結:


1. 我們首先安裝了 R.o.R. 和 MySQL 。然後用gem 指立一系列的 RubyGems。

2. 用 'rails blog_application' 建立了整個blog_application的結構。'ruby script/generate scaffold blog'在這時出場,瞬間便建立了所有的頁面和有用的檔案。

3. 用 'rake db:migrate' 建立database

4. 修改頁面內容。成功!

最後要補充R.o.R.的強勁能力其實和其 MVC 結構分不開。

例如,為什麼在 /views/blogs/new.html.rb (V)@blog怎會和 database 的 blogs table連繫上?

其實在 blog_controller (C) '@blog = Blog.new'這編碼生產出來的。 而 Blog (M) 便是database 中的blogs table 的應用物件。

因為這緊密的 MVC 合作和所用名字的推斷 (e.g. blogs, Blog, blog_controller, /views/blogs/*),R.o.R. 便可從中省卻我們大部分不必要的編碼,令編程得心應手。


... Mongrel!

之前安裝了Mongrel 的朋友,你可以不執行 WEBrick而用Mongrel 伺服器。

以後你不用執行


ruby script/server


而用下面指令代替:

mongrel_rails start


其他一切Mongrel和WEBrick沒有分別,分別只是Mongrel是功能上比較穩定和強大。