November 22, 2006

[Rails 筆記] tinyint ?

MySQL table 中欄位屬性如果為 tinyint(1) , Rails 會自動視其為 boolean 值,這個問題害我傷了很久的腦筋,今天找到答案了:

By default, the MysqlAdapter will consider all columns of type tinyint(1) as boolean. If you wish to disable this emulation (which was the default behavior in versions 0.13.1 and earlier) you can add the following line to your environment.rb file:

ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans = false



November 6, 2006

ruby plugin for vim

一直都是用 vim 寫 Ruby ,一直也都是黑與白,直到今天才熊熊想到,啊,沒有 syntax highlight。雖然 syntax highlight 對我來說沒什麼太大的用途,可是多點色彩,看起來好像會比較專業點,問了一下估狗大神,找到下面這個網址。

Vim/Ruby Configuration Files

抓 gem 檔回來安裝,設定好 vimrc ,人生又回復色彩了。




November 1, 2006

網頁自動化測試

要作網頁的 Unit test 有幾個工具可以用,例如 Ruby on Rails 本身就有提供 Unit test framework 了,而 PHP 也有幾套可以用,例如 PHPUnit、SimpleTest 以及 Spike PHPCoverage 測 code coverage,除了 RoR 我有用過外,PHP 的那些工具我都沒用過,不過這並不是我這篇想講的重點,我這裡主要是想介紹一個最近在玩的玩具,Web Service 的自動測試工具 - Watir

Watir 全名是 Web Application Testing in Ruby ,沒錯!你看到 Ruby 了,他就是用 Ruby 開發的測試架構。Watir 有什麼好處呢?第一,他是用 Ruby 開發,寫 Test Case 也是用 Ruby,感覺就親切許多,第二,他是直接 launch IE,並透過操控 IE 來測試你的 Web Application,所以可以測出一些可能在瀏覽器上才會遇到的問題,第三,寫好 Test Case 後,一切都可以自動化執行,第四,不管你的 Web Application 是用什麼語言寫的,Watir 都可以支援,因為他是啟動 IE 來瀏覽。不過它也有些缺點,第一,目前只支援 Windows 平台,第二,目前只支援 IE 瀏覽器,第三,因為是靠 IE 操作,所以測試時間會較其他測試方法長,不過總是比人工測快許多。


...繼續閱讀

October 24, 2006

innerHTML 與 createElement 的差異

查了一下網路上有關於 innerHTML 與 createElement() performance 的比較,都是顯示 innerHTML 有較好的 performance ,這跟我以前的印象有蠻大的差距,我一直都以為 innerHTML 會較差,直接 createElement() 以及 appendChild() 會有比較好的 performance,不過既然測試出來的數據都是這樣,看來也應該如此。

innerHTML 的優點是容易實做,程式碼容易維護,再加上 performance 較好,那麼我真的不知道使用 createElement() 跟 appendChild() 的理由為何了。

感謝 midoli 糾正我上一篇文章,我一時不查,以原本的想法觀念,寫了一篇錯誤的文章出來,看來作學問還是要秉持著嚴謹的態度。


October 16, 2006

[AJAX Pattern] Amazon A9 Search

上次在 Amazon 的 A9 Search 看到一個很有趣的 AJAX 應用,看了就很想自己實作看看,實際上也不困難,如果瞭解 AJAX 的原理,Javascript event 處理等就能寫出來了,所以在這裡介紹給大家看看。

[Amazon A9 Search]
傳統的搜尋引擎如 Google 或 Yahoo 等,在將搜尋結果丟出來後,會以分頁來顯示這些結果,使用者想要找的結果如果不是在第一頁的話,就會一頁接著一頁向後找尋。這種作法也沒什麼不對,很直覺,但是我看了 A9 的用法後,我反而覺得 A9 的作法更為實際,更加直覺。A9 怎麼操作的? Scrollbar !

ss
A9 會先丟出前幾筆的結果於網頁上,例如排名前 20 筆,但是當妳使用 Scrollbar 往下拉動時,更多資料就會源源不絕的顯示出來,同時妳也會發現左邊的 Scrollbar 越來越小,代表資料量越來越多。用寫的來描述這行為實在很難,妳可以上 http://www.a9.com ,透過實際操作便可瞭解。為什麼說他更直覺呢?因為如果第一次沒看到你想要的資料時,同時妳又看到 scrollbar 在旁邊,直覺上的操作就是會去拉動 scrollbar ,如同妳在 google 的搜尋結果中,也會拉動瀏覽器視窗的 scrollbar 來看底下的資料,如果沒有再點下一頁,所以既然都在拉動 scrollbar 了,何不在同時也去抓新的資料,所以我說他的操作方式更加直覺。

[怎麼做?]

假設我們想讓內容顯示在一 DIV 區塊中,藉由使用者拖拉 scrollbar 來動態增加顯示內容的話:

第一步、於 DIV 區塊上顯示 scrollbar
這很簡單,只需要透過簡單的 style 屬性設定即可,前提就是妳必須先設定 DIV 區塊高度,並設定屬性 overflow:auto ,例如:
<div id="content" style="width:490px;height:490px; overflow: auto">
....  妳想要顯示的內容。
</div>
當妳於 DIV 區塊中的內容超過 DIV 高度時,瀏覽器便會於右邊出現 scrollbar。

第二步、攔截使用者 scrolling 動作
檢而言之,攔截該 DIV 區塊的 onscroll 事件,例如:
// 注意:以下程式碼使用 prototype.js 語法
$('content').onscroll = checkScroll;
checkScoll 函式便是我們想要處理 scrolling event 的函式。

第三步、呼叫 AJAX,向伺服器要新資料,例如:
// 注意:以下程式碼使用 prototype.js 物件
var opts = new Object;
opts.parameters = 'page=' + page_num;
opts.onComplete = showResponse;
var ajax = new Ajax.Request (url, opts);
page_num ++;

第四步、取得 AJAX 回傳結果,也就是上述程式碼中的 showResponse 函式:
function showResponse(origReq) {
    $('content').innerHTML += origReq.responseText;
}
看,這樣就完成了!超簡單!所以來 re-factoring 一下。

[Refactoring]
我們將這樣的概念稍微把他物件化,讓它可以重複被使用,而且增加一些保護措施。
function showResponse(origReq) {
        var nav = $(this.nav.contentID);
        var last = nav.lastChild;
        while (last.nodeName != 'DIV') {
                last = last.previousSibling;
        }
        nav.removeChild (last);
        nav.innerHTML += origReq.responseText;
        nav.innerHTML += '<div style="background: #AAAAAA;">Fetching ... </div>';
        this.fetching = false;
}

function ScrollNav(contentID, url) {
        this.contentID = contentID;
        this.contentObj = $(contentID);
        this.contentObj.nav = this;  // 記錄目前物件,給 checkScroll 函式使用
        this.fetching = false;
        this.scrollTop = this.contentObj.scrollTop; // 記錄一開始 scrollTop
        this.scrollHeight = this.contentObj.scrollHeight; // 記錄一開始 scrollHeight
        this.url = url;

        this.idx = 0; // page number

        this.checkScroll = function() {
                if(this.nav.scrollTop == this.scrollTop)
                        return;
                if (this.fetching)
                        return;

                this.nav.scrollTop = this.scrollTop;
                if (this.scrollHeight == parseInt(this.style.height) + this.scrollTop) {
                        var opts = new Object;
                        opts.parameters = 'page=' + this.nav.idx;
                        opts.onComplete = showResponse.bind(this);

                        var ajax = new Ajax.Request(this.nav.url, opts);
                        this.fetching = true;
                        this.nav.idx ++;
                }
        }

        // 攔截 onscroll event
        this.contentObj.onscroll = this.checkScroll;
}

function init() {
        var nav = new ScrollNav('content', 'scroll_test.php');
        var nav = new ScrollNav('content2', 'scroll_test2.php');
}

這裡頭有個 ScrollNav 物件,就是將這些東西都包進去了,此外,於 checkScroll 函式中會檢查 event 是否被重複攔截,以及檢查目前 scrollbar 是否被拉動至最下方,唯有 scrollbar 被拉到最下面時,我們才向伺服器要求更多的資料,避免頻寬資源的浪費。scrollTop 跟 scrollHeight 都是內建的屬性,scrollTop 代表目前顯示的 view 距離最上方多少有 pixel,而 scrollHeight 則代表內容 view 的總長,同樣是 pixel,當妳資料越塞越多時,scrollTop 跟 scrollHeight 都會跟著增加,如果要判斷 scrollbar 是否為拉到最下方,最簡單的方法便是計算 scrollTop 和 DIV 區塊的高度總長是否等於 scrollHeight 即可。

至於在 showResponse() 函式中,我們會在每次抓到的新資料中都塞進個「Fetching ...」的字眼,代表目前正在抓取更多資料,這是一種小技巧,就像在大部分的 AJAX Pattern 中都可以看到的 Loading 字眼,妳也可以用個 gif 動畫來取代,只不過每次取得新資料時,必須將上一個「Fetching ...」移除。

想看結果嗎?請看 Demo 。裡頭有兩個 DIV 區塊,左邊的 DIV 區塊所 request 的 PHP 檔執行較快,而右邊的每次都會 sleep 5 秒鐘,所以感覺起來比較慢,拉動兩個 scrollbar 妳便可以發覺兩者間的不同。

[其他問題?]
目前這個範例還有些存在性的問題:
  1. 別吃光 browser 的記憶體了。塞資料時要設立停止點,別一直狂塞,吃光妳瀏覽器的記憶體了。
  2. 不一定要拉動到最下方才開始抓取資料,可以在使用者的 scrollbar 接近最下方時,便向伺服器要求新資料,可以達到更順暢的操作。


[這可以應用在哪?]< br/> 我想這些概念,實作都是很簡單,重要的是妳有沒有創意,有沒有想到這些 idea,以及怎麼應用在妳的網頁中,這我想應該讓妳自己去想想看,不過我覺得可以應用在某些功能上,例如 blog 留言版,留言版並不是每個人都想看,如果留言眾多時,每次都將留言塞到使用者的瀏覽器上也不甚合理,如果透過類似的方法,先讓使用者看到最新的留言,如果想繼續看下去,只需要拉動 scrollbar 即可。應該還可以想出更多有趣的應用,不過還是大家自己想想看吧。






October 2, 2006

[Rails 筆記] Boolean pitfall

Ruby 沒有明確的 boolean 型別定義,不過通常以 nil 或 false 來代表 false,其他通通都是 true ,所以有個陷阱了,C 語言同樣也沒有 boolean 型別,但可以用 0 代表 false,非 0 代表 true,我們也都習以為常,不過在 Ruby 中,0 代表 true,例如:

a = 0
if (a)
puts ("true")
else
puts ("false")
end

會印什麼,true 啊。


September 28, 2006

[Rails 筆記] 在 session 中儲存物件

答案是利用 Ruby 的 Marshal 物件,如下例:

class AdminController < ApplicationController
def do_login
if request.post?
user = User.login(params[:uid], params[:pwd])
if (user)
session[:user] = Marshal.dump(user)
end
end
end
end

然後在 application.rb 中:

class ApplicationController < ActionController::Base
model :user
def setup_variables
if (session[:user])
@login_user=Marshal.load(session[:user])
end
end
end
這樣有什麼好處,好處是對於一些常用的物件,例如 Query DB 得到的 Model,我不用在每個 page 都再去 query database,將他記錄在 session 中即可,而且可以避免 db schema 更改後還要改 code 的窘境。




September 25, 2006

[Rails 筆記] ActiveRecord 轉換 JSON

酷耶,看到這都快興奮的跳到桌子上了,看看這個連結吧:Accessing ActiveRecord objects in javascript
雖然我看不太懂這段 code 的意思,不過測試的結果,可以 work 耶! 讚!
就只剩下 JSON 轉 ActiveRecord 了,改天再來研究看看。

(1) 安裝 json:
% gem install json

(2) Add a new file : /usr/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/json.rb , 裡頭包含以下程式碼:

require 'json/lexer'
require 'json/objects'
module ActiveRecord
  module Json # :nodoc:
    DEFAULT_CONVERSIONS = { Time => [:to_s, :db] }
    def to_json(conversions = {})
      conversions = DEFAULT_CONVERSIONS.merge(conversions)
      self.attributes.keys.inject({}) do |hsh, key|
        value = self.send(key)
        hsh.merge(key => conversions[value.class] ? value.send(*conversions[value.class]) : value.to_s)
      end.to_json
    end
  end
end


(3) 在 environment.rb 中加上: [Update]: 似乎不用加以下這段,加了反讓 fastcgi 掛掉,不加也能 work 啦

require "#{RAILS_ROOT}/lib/active_record/json"
ActiveRecord::Base.class_eval { include ActiveRecord::Json }


(4) 使用時,呼叫 to_json() 函式即可。

user = User.new()
user.to_json()




[Rails 筆記] 如何使用 Stored Procedure (MySQL)

儘管 Rails 的原始作者之一 David Heinemeier 對 Database 的 Stored Procedure 並不太感興趣,都有了 Active Record 了,何需 Stored Procedure,不過需求總是存在的,要改好像也不太難,這篇筆記都是參考自 - MySQL stored procedures with Ruby 以及 StoredProceduresInMySql。這各式兩種作法,一種是改 Rails 原本的程式碼,另外一種則是改自己的程式碼 ,各有優缺點。

先看改 Rails 的方法:
(1) 找到 Rails 中的 mysql_adapter.rb 檔,他通常位於:
/usr/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/connection_adapters/mysql_adapter.rb
(2) 修改底下這行:
ConnectionAdapters::MysqlAdapter.new( mysql, logger, [host, username, password, database, port, socket], config)
把它改成:
ConnectionAdapters::MysqlAdapter.new( mysql, logger, [host, username, password, database, port, socket, Mysql::CLIENT_MULTI_RESULTS], config)

(3) 新增加一個 function:

def select_sp(sql, name = nil)
 rows = select(sql, name = nil)
 while (@connection.more_results?())
  @connection.next_result()
 end
 return rows
end


(4) 使用上,如下例:

connection.select_sp ("CALL my_stored_proc();")


如果妳不想改 code,就不能用 Rails 的 adapter ,而是要自己 connect。


September 15, 2006

[Rails 筆記] Rails Localization

Rails app 怎麼做 localization ? 好問題! 已經有人 port UNIX 上的 gettext 到 ruby 上,並有文件介紹如何與 Rails app 整合。

[安裝]
確認你有安裝 gettext,在 Ubuntu 上請用以下命令安裝:

% apt-get install gettext

安裝 libgettext-ruby1.8:

% gem install gettext

或者

% apt-get install
libgettext-ruby1.8

[建立環境]
舉例來說,如果我的 Rails app 目錄位於 /var/rails/sandbox/ 路徑下,先在該目錄下建立 po 檔案夾,裡頭包含各個你要 localize 的語言子目錄,如下:

% ls -l po
drwxr-xr-x 2 root root 4096 2006-09-15 20:21 en
drwxr-xr-x 2 root root 4096 2006-09-15 21:21 ja
drwxr-xr-x 2 root root 4096 2006-09-15 20:21 zh_CN
drwxr-xr-x 2 root root 4096 2006-09-15 21:04 zh_TW

編寫 Rakefile,如下:
require(File.join(File.dirname(__FILE__), 'config', 'boot'))

require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'

require 'tasks/rails'
require 'gettext/utils'

desc "Update pot/po files"
task :updatepo do
MY_APP_TEXT_DOMAIN = "sandbox" #sandbox 是我的 Textdomain, 你應該改成你自己的
MY_APP_VERSION = "sandbox 0.0.9"
GetText.update_pofiles(MY_APP_TEXT_DOMAIN,
Dir.glob("{app,lib}/**/*.{rb,rhtml}"),
MY_APP_VERSION)
end

desc "Create mo-files"
task :makemo do
GetText.create_mofiles(true, "po", "locale")
end

[攥寫你的程式碼]
你想要做 localize 的地方可以利用 _() 或者 N_() 函式包裝起來,如下:

<%= _("This string should be localized") %>


<%= N_("This string should be localized") %>


設定 application.rb,如下:
require 'gettext/rails'
class ApplicationController < ActionController::Base
GetText.locale = "zh_TW"
init_gettext "sandbox"
在這裡我先暫時將 locale 強制設定成 zh_TW 做測試,事實上,你可以透過幾種方法來改變語言(如果不直接設定 GetText.locale= 的話):
  • QUERY_STRING 中的 lang 參數設定。
  • Cookie 中的 lang 參數設定。
  • HTTP_ACCEPT_LANGUAGE 中的值。
  • 你也可以透過 config/routes.rb 讓某路徑對應到特定語言中,例如 /login/zh_TW/hello/ 可以對應到 zh_TW 語言中。
ActionController::Routing::Routes.draw do |map|
map.connect ':controller/:lang/:action/:id'
end

[建立 po 跟 mo 檔]
回到你的 Rails Root 路徑下,執行以下命令產生 pot 檔。

% rake updatepo

執行完後你會在 po/ 目錄下找到一份 pot 檔,檔名是你取的 Textdomain,將這份 pot 檔案翻譯後並改名成附檔名為 .po 的檔案,並放到該語言的目錄下,例如你翻譯成中文後,可以放到 zh_TW 目錄下。

翻譯完後,執行:

% rake makemo

你會發現在你 Rails app ROOT 目錄下會多個檔案夾 - locale ,在裡頭你會發現 zh_TW/LC_MESSAGES/sandbox.mo ,這就是產生的 mo 檔,有用過 gettext 的人,相信對這些流程應該不陌生。
執行你的 Rails app 後,你就可以發現你的字串都被置換了。


[參考資源]