├── CNAME ├── _config.yml ├── .gitignore ├── images ├── chapter10 │ ├── mvc.png │ ├── folder-model.png │ ├── folder-view.png │ ├── folder-config.png │ └── folder-controller.png ├── chapter25 │ └── cart.png ├── chapter27 │ ├── fsm.png │ └── state-machine.png ├── chapter28 │ ├── paid.png │ ├── v2-v3.png │ ├── api-keys.png │ ├── braintree.png │ ├── dash-board.png │ ├── payment-form.png │ ├── post-error.png │ ├── show-product.png │ └── braintree-client.png ├── chapter01 │ ├── c9.io.png │ └── try_ruby.jpg ├── chapter11 │ ├── not-php.png │ ├── no-route-error.png │ ├── no-action-error.png │ ├── no-route-error2.png │ └── no-controller-error.png ├── chapter12 │ ├── bmi-1.png │ ├── bmi-2.png │ ├── bmi-3.png │ ├── bmi-4.png │ ├── puts-to-console.png │ ├── render-params-1.png │ ├── render-params-2.png │ ├── render-hello-world.png │ ├── controller-view-mapping.png │ ├── render-hello-world-with-view.png │ └── invalid-authenticity-token-error.png ├── chapter14 │ ├── layout.png │ ├── yield-title.png │ ├── render-icon-1.png │ ├── render-icon-2.png │ └── missing-template-error.png ├── chapter21 │ ├── views.png │ ├── index-html.png │ └── index-json.png ├── chapter29 │ ├── heroku.png │ └── heroku-pricing.png ├── chapter09 │ ├── paging-01.png │ ├── paging-02.png │ ├── paging-03.png │ └── rubygems.png ├── chapter02 │ └── welcome_page.png ├── chapter08 │ └── cake_maker.jpg ├── chapter13 │ ├── form-vs-mvc.png │ ├── vote-candidate-01.png │ ├── vote-candidate-02.png │ ├── vote-candidate-03.png │ ├── vote-candidate-04.png │ ├── vote-candidate-05.png │ ├── vote-candidate-06.png │ ├── vote-candidate-07.png │ ├── vote-candidate-08.png │ ├── vote-candidate-09.png │ ├── vote-candidate-10.png │ ├── vote-candidate-11.png │ ├── vote-candidate-12.png │ └── forbidden-attributes-error.png ├── chapter15 │ └── user-table.png ├── chapter19 │ ├── sendmail-1.png │ └── sendmail-2.png ├── chapter24 │ ├── user-list-1.png │ ├── inheritance-1.png │ └── inheritance-2.png ├── chapter03 │ └── pending_migration.png ├── chapter04 │ ├── post-scaffold-1.png │ ├── post-scaffold-2.png │ ├── post-scaffold-3.png │ ├── post-scaffold-4.png │ ├── user-scaffold-1.png │ ├── user-scaffold-2.png │ └── user-scaffold-3.png ├── chapter17 │ ├── user-store-model.png │ ├── user-store-tables.png │ ├── many-to-many-group.png │ ├── many-to-many-model.png │ ├── many-to-many-tables.png │ ├── user-store-product-model.png │ └── user-store-product-tables.png ├── chapter18 │ └── model-lifecycle.png └── chapter16 │ └── pending-migration-error.png ├── README.md ├── markdown ├── chapter22-testing-with-rspec-part-1.md ├── chapter20-background-job.md ├── chapter19-send-email.md ├── chapter10-mvc.md ├── chapter00-about.md ├── chapter01-ecosystem-and-introduction.md ├── chapter21-api-mode.md ├── chapter27-order.md ├── chapter03-command-line-tools.md ├── chapter09-using-gems.md ├── chapter26-shopping-cart-part-2.md ├── chapter28-payment.md ├── chapter24-organize-your-code.md ├── chapter18-model-validation-and-callback.md ├── chapter02-environment-setup.md ├── chapter08-ruby-basic-4.md ├── chapter04-your-first-rails-application.md ├── chapter16-model-migration.md ├── chapter12-controllers.md ├── chapter25-shopping-cart-part-1.md ├── chapter07-ruby-basic-3.md ├── chapter23-testing-with-rspec-part-2.md ├── chapter14-layout-render-and-view-helper.md └── chapter15-model-basic.md └── LICENSE /CNAME: -------------------------------------------------------------------------------- 1 | learnrails.kaochenlong.com 2 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | resources/ 2 | bin/ 3 | output/ 4 | -------------------------------------------------------------------------------- /images/chapter10/mvc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter10/mvc.png -------------------------------------------------------------------------------- /images/chapter25/cart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter25/cart.png -------------------------------------------------------------------------------- /images/chapter27/fsm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter27/fsm.png -------------------------------------------------------------------------------- /images/chapter28/paid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter28/paid.png -------------------------------------------------------------------------------- /images/chapter01/c9.io.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter01/c9.io.png -------------------------------------------------------------------------------- /images/chapter11/not-php.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter11/not-php.png -------------------------------------------------------------------------------- /images/chapter12/bmi-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter12/bmi-1.png -------------------------------------------------------------------------------- /images/chapter12/bmi-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter12/bmi-2.png -------------------------------------------------------------------------------- /images/chapter12/bmi-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter12/bmi-3.png -------------------------------------------------------------------------------- /images/chapter12/bmi-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter12/bmi-4.png -------------------------------------------------------------------------------- /images/chapter14/layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter14/layout.png -------------------------------------------------------------------------------- /images/chapter21/views.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter21/views.png -------------------------------------------------------------------------------- /images/chapter28/v2-v3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter28/v2-v3.png -------------------------------------------------------------------------------- /images/chapter29/heroku.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter29/heroku.png -------------------------------------------------------------------------------- /images/chapter01/try_ruby.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter01/try_ruby.jpg -------------------------------------------------------------------------------- /images/chapter09/paging-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter09/paging-01.png -------------------------------------------------------------------------------- /images/chapter09/paging-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter09/paging-02.png -------------------------------------------------------------------------------- /images/chapter09/paging-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter09/paging-03.png -------------------------------------------------------------------------------- /images/chapter09/rubygems.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter09/rubygems.png -------------------------------------------------------------------------------- /images/chapter28/api-keys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter28/api-keys.png -------------------------------------------------------------------------------- /images/chapter28/braintree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter28/braintree.png -------------------------------------------------------------------------------- /images/chapter02/welcome_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter02/welcome_page.png -------------------------------------------------------------------------------- /images/chapter08/cake_maker.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter08/cake_maker.jpg -------------------------------------------------------------------------------- /images/chapter10/folder-model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter10/folder-model.png -------------------------------------------------------------------------------- /images/chapter10/folder-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter10/folder-view.png -------------------------------------------------------------------------------- /images/chapter13/form-vs-mvc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter13/form-vs-mvc.png -------------------------------------------------------------------------------- /images/chapter14/yield-title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter14/yield-title.png -------------------------------------------------------------------------------- /images/chapter15/user-table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter15/user-table.png -------------------------------------------------------------------------------- /images/chapter19/sendmail-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter19/sendmail-1.png -------------------------------------------------------------------------------- /images/chapter19/sendmail-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter19/sendmail-2.png -------------------------------------------------------------------------------- /images/chapter21/index-html.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter21/index-html.png -------------------------------------------------------------------------------- /images/chapter21/index-json.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter21/index-json.png -------------------------------------------------------------------------------- /images/chapter24/user-list-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter24/user-list-1.png -------------------------------------------------------------------------------- /images/chapter28/dash-board.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter28/dash-board.png -------------------------------------------------------------------------------- /images/chapter28/payment-form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter28/payment-form.png -------------------------------------------------------------------------------- /images/chapter28/post-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter28/post-error.png -------------------------------------------------------------------------------- /images/chapter28/show-product.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter28/show-product.png -------------------------------------------------------------------------------- /images/chapter10/folder-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter10/folder-config.png -------------------------------------------------------------------------------- /images/chapter11/no-route-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter11/no-route-error.png -------------------------------------------------------------------------------- /images/chapter14/render-icon-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter14/render-icon-1.png -------------------------------------------------------------------------------- /images/chapter14/render-icon-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter14/render-icon-2.png -------------------------------------------------------------------------------- /images/chapter24/inheritance-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter24/inheritance-1.png -------------------------------------------------------------------------------- /images/chapter24/inheritance-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter24/inheritance-2.png -------------------------------------------------------------------------------- /images/chapter27/state-machine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter27/state-machine.png -------------------------------------------------------------------------------- /images/chapter29/heroku-pricing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter29/heroku-pricing.png -------------------------------------------------------------------------------- /images/chapter03/pending_migration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter03/pending_migration.png -------------------------------------------------------------------------------- /images/chapter04/post-scaffold-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter04/post-scaffold-1.png -------------------------------------------------------------------------------- /images/chapter04/post-scaffold-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter04/post-scaffold-2.png -------------------------------------------------------------------------------- /images/chapter04/post-scaffold-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter04/post-scaffold-3.png -------------------------------------------------------------------------------- /images/chapter04/post-scaffold-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter04/post-scaffold-4.png -------------------------------------------------------------------------------- /images/chapter04/user-scaffold-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter04/user-scaffold-1.png -------------------------------------------------------------------------------- /images/chapter04/user-scaffold-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter04/user-scaffold-2.png -------------------------------------------------------------------------------- /images/chapter04/user-scaffold-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter04/user-scaffold-3.png -------------------------------------------------------------------------------- /images/chapter10/folder-controller.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter10/folder-controller.png -------------------------------------------------------------------------------- /images/chapter11/no-action-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter11/no-action-error.png -------------------------------------------------------------------------------- /images/chapter11/no-route-error2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter11/no-route-error2.png -------------------------------------------------------------------------------- /images/chapter12/puts-to-console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter12/puts-to-console.png -------------------------------------------------------------------------------- /images/chapter12/render-params-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter12/render-params-1.png -------------------------------------------------------------------------------- /images/chapter12/render-params-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter12/render-params-2.png -------------------------------------------------------------------------------- /images/chapter13/vote-candidate-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter13/vote-candidate-01.png -------------------------------------------------------------------------------- /images/chapter13/vote-candidate-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter13/vote-candidate-02.png -------------------------------------------------------------------------------- /images/chapter13/vote-candidate-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter13/vote-candidate-03.png -------------------------------------------------------------------------------- /images/chapter13/vote-candidate-04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter13/vote-candidate-04.png -------------------------------------------------------------------------------- /images/chapter13/vote-candidate-05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter13/vote-candidate-05.png -------------------------------------------------------------------------------- /images/chapter13/vote-candidate-06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter13/vote-candidate-06.png -------------------------------------------------------------------------------- /images/chapter13/vote-candidate-07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter13/vote-candidate-07.png -------------------------------------------------------------------------------- /images/chapter13/vote-candidate-08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter13/vote-candidate-08.png -------------------------------------------------------------------------------- /images/chapter13/vote-candidate-09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter13/vote-candidate-09.png -------------------------------------------------------------------------------- /images/chapter13/vote-candidate-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter13/vote-candidate-10.png -------------------------------------------------------------------------------- /images/chapter13/vote-candidate-11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter13/vote-candidate-11.png -------------------------------------------------------------------------------- /images/chapter13/vote-candidate-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter13/vote-candidate-12.png -------------------------------------------------------------------------------- /images/chapter17/user-store-model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter17/user-store-model.png -------------------------------------------------------------------------------- /images/chapter17/user-store-tables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter17/user-store-tables.png -------------------------------------------------------------------------------- /images/chapter18/model-lifecycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter18/model-lifecycle.png -------------------------------------------------------------------------------- /images/chapter28/braintree-client.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter28/braintree-client.png -------------------------------------------------------------------------------- /images/chapter11/no-controller-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter11/no-controller-error.png -------------------------------------------------------------------------------- /images/chapter12/render-hello-world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter12/render-hello-world.png -------------------------------------------------------------------------------- /images/chapter17/many-to-many-group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter17/many-to-many-group.png -------------------------------------------------------------------------------- /images/chapter17/many-to-many-model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter17/many-to-many-model.png -------------------------------------------------------------------------------- /images/chapter17/many-to-many-tables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter17/many-to-many-tables.png -------------------------------------------------------------------------------- /images/chapter14/missing-template-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter14/missing-template-error.png -------------------------------------------------------------------------------- /images/chapter12/controller-view-mapping.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter12/controller-view-mapping.png -------------------------------------------------------------------------------- /images/chapter16/pending-migration-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter16/pending-migration-error.png -------------------------------------------------------------------------------- /images/chapter17/user-store-product-model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter17/user-store-product-model.png -------------------------------------------------------------------------------- /images/chapter13/forbidden-attributes-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter13/forbidden-attributes-error.png -------------------------------------------------------------------------------- /images/chapter17/user-store-product-tables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter17/user-store-product-tables.png -------------------------------------------------------------------------------- /images/chapter12/render-hello-world-with-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter12/render-hello-world-with-view.png -------------------------------------------------------------------------------- /images/chapter12/invalid-authenticity-token-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaochenlong/learn-ruby-on-rails/HEAD/images/chapter12/invalid-authenticity-token-error.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 為你自己學 Ruby on Rails 2 | 3 | 「為你自己學 Ruby on Rails」系列文章。 4 | 5 | 如其標題,學習不需要為公司、不需要為長官、同事、不需要為別人,只為你自己。 6 | 7 | 本 Repo 之 markdown 檔案之後可能不會再維護,之後版本更新(Ruby 2.4.1 及 Rails 5.1)以及新增的章節也將會直接上傳至好讀版本而不再更新至此,好讀版本請麻煩前往 8 | -------------------------------------------------------------------------------- /markdown/chapter22-testing-with-rspec-part-1.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | title: 寫測試讓你更有信心 Part 1 4 | comments: true 5 | permalink: /chapters/22-testing-with-rspec-part-1.html 6 | 7 | --- 8 | 9 | # 寫測試讓你更有信心 Part 1 10 | 11 | ## 什麼是測試 12 | 13 | 很多人以為,所謂的測試,就是請個工讀生、打開瀏覽器、拿著滑鼠點一點就叫測試了(這也是測試沒錯,但並不是我們要介紹的那種測試)。也有人認為,測試就是就在幫程式碼除錯(Debug),所以有寫測試就不會有 bug 了。 14 | 15 | 當然也有人因為覺得寫測試很浪費時間所以反對或不想寫測試。 16 | 17 | 事實上,很多隱藏的細節不是請工讀生或是隨便用滑鼠點一點就測得到的。想想看,如果每次工程師做的任何小修改,如果不完整測試一輪,不知道哪邊會出問題;但整個流程測試一次,實在是相當的浪費時間。生命就該浪費在美好的事物上,不是嗎 :) 18 | 19 | ### 測試是規格 20 | 21 | 我們這裡提的「測試」,指的是 TDD(Test-Driven Development),中文翻譯做「測試驅動開發」。很多人會把重點放在 `Test` 上,但事實上 TDD 是一種 `Development`(開發)的方法,並不是一種「測試方法」。 22 | 23 | ## 為什麼不寫測試? 24 | 25 | 以下是一些經常聽到不寫測試或是覺得測試沒必要的原因: 26 | 27 | ### 1. 光要做功能就沒時間了,哪還有時間寫測試 28 | 29 | 當然,如果你手上這案子是明天或下星期就要交,你所剩的時間只夠勉強把功能寫完,那也許可說沒時間寫測試。 30 | 31 | 不過我們來看個簡單的例子: 32 | 33 | 假設小明跟小華兩個人的技術能力是一樣的,開發功能花的時間會是一樣的。現在有個功能 A,內容是「登入會員帳號然後填寫表格、送出,確認結果是否正確」。 34 | 35 | 小明用原本傳統的方式開發花了 10 分鐘,然後打開瀏覽器,登入、填寫表格,按下送出,很快的完成了,花了 1 分鐘測試,共計 11 分鐘;小華先開始用了 10 分鐘撰寫測試,然後同樣花了 10 分鐘完成功能 A,執行測試,花了 10 秒鐘,所以共計花了 20 分鐘又 10 秒。 36 | 37 | 光從數字上看來,小華花的時間幾乎是小明的 2 倍。 38 | 39 | 但是,通常功能的開發還會有後續的調整,假設在不修改規格的情況下,小明每次的調整花了 1 分鐘修改,然後還是花了 1 分鐘測試,所以每次的調整就是 2 分鐘 40 | 但小華因為已經有寫測試了,所以雖然同樣是花 1 分鐘修改,但只要花 10 秒鐘就可以跑完測試 41 | 42 | 簡單的數學,應該很快就可以看得出來誰花的時間比較多。小華一開始花在撰寫測試的時間,雖然看起來好像會增加整體的開發時間,但隨著時間,專案功能不斷的修改、調整,慢慢的就會會比小明花的時間還少了。 43 | 44 | ### 2. 我是接手別人的專案,之前本來就沒在寫了 45 | 46 | 這的確是個問題,因為如果是在功能完成之後再補測試,一來是不知道哪些功能該要測試,再來就是因為是順著原本的功能來寫的測試,容易有盲點,例如本來的邏輯是有問題的,即使是正確的測試,原本功能的邏輯還是有問題的。 47 | 48 | ### 3. 測試很難維護,很脆弱,隨便弄一下就壞了 49 | 50 | 本來可以正常運作的測試,在你加了一個功能之後測試就壞掉了,那是測試的問題還是你的問題呢? 51 | 52 | ### 真正不寫測試的理由 53 | 54 | 其實大部份真正的理由,大多是不知道怎麼寫測試,或是不知道哪些該測試,甚至是根本不知道有測試這回事。 55 | 56 | 再次強調,TDD 的重點在於 `D`,也就是 `Development`(開發) 57 | 58 | > 不要為了測試而測試,你寫的是「規格」(Spec) 59 | 60 | 接下來,就讓我們開始動手寫測試吧! 61 | 62 | -------------------------------------------------------------------------------- /markdown/chapter20-background-job.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | title: 背景工作及工作排程 4 | comments: true 5 | permalink: /chapters/20-background-job.html 6 | 7 | --- 8 | 9 | # 背景工作及工作排程 10 | 11 | 有些程式在執行的時候需要比較長的時間,例如前面章節介紹的寄發 Email,當使用外部的郵件伺服器的時候,有時候會要等幾秒甚至幾十秒才會繼續往下走,這樣不僅使用者體驗不是很好,也可能讓網站不正常運作(例如使用者在點下送出的時候,以為按下去沒效果結果多按了很多下)。 12 | 13 | 所以,通常這種比較耗時的工作,會先把要執行的工作先存起來,然後把使用者畫面轉往已完成頁面,待主機比較有空檔或稍晚再執行剛剛存下來的工作。 14 | 15 | Rails 有內建一個叫做 `ActiveJob` 的類別可以來處理這樣的事情。 16 | 17 | ## 新增工作 18 | 19 | 要新增一個工作(Job)相當容易,跟之前 Scaffold 一樣,使用 Rails 內建的產生器,一行就搞定了: 20 | 21 | $ rails g job user_confirm_email 22 | invoke test_unit 23 | create test/jobs/user_confirm_email_job_test.rb 24 | create app/jobs/user_confirm_email_job.rb 25 | 26 | 這行指令會在 `app/jobs` 目錄下新增一個檔案。讓我們看一下這個檔案的內容: 27 | 28 | ```ruby 29 | class UserConfirmEmailJob < ApplicationJob 30 | queue_as :default 31 | 32 | def perform(*args) 33 | # Do something later 34 | end 35 | end 36 | ``` 37 | 38 | 檔案的內容其實滿簡單的,產生的檔案(扣除測試的話)也就一個而已,所以也不一定要用產生器,直接手動建立也可。 39 | 40 | 其中,`queue_as` 方法是指這件工作急不急,預設值是 `:default`,如果這件工作不急,可把 `:default` 改成 `:low_priority`,如果是急件則可設定成 `:urgent`。 41 | 42 | 然後這個 `perform` 方法就是「真正在做事情的地方」,稍微改一下 `perform` 的內容: 43 | 44 | ```ruby 45 | class UserConfirmEmailJob < ApplicationJob 46 | queue_as :default 47 | 48 | def perform(user) 49 | # 在這裡寄發確認信... 50 | end 51 | end 52 | ``` 53 | 54 | 這裡 `perform` 方法期待會接收一個 User 物件,然後寄發信件給這個使用者(寄信方法可見前一個章節)。 55 | 56 | ## 開始工作 57 | 58 | 舉例來說,如果我想要在「成功建立使用者之後寄發確認信件」,可以這樣做: 59 | 60 | ```ruby 61 | class UsersController < ApplicationController 62 | # ...[略]... 63 | 64 | def create 65 | @user = User.new(user_params) 66 | if @user.save 67 | UserConfirmEmailJob.perform_later(@user) 68 | redirect_to @user, notice: 'User was successfully created.' 69 | else 70 | render :new 71 | end 72 | end 73 | 74 | # ...[略]... 75 | end 76 | ``` 77 | 78 | 這樣就行了,其中這行: 79 | 80 | ```ruby 81 | UserConfirmEmailJob.perform_later(@user) 82 | ``` 83 | 84 | 意思是這個工作「待會做」,即使這個工作需要耗時 10 秒鐘,Rails 也不會真的在這卡 10 秒鐘才往下執行,而是先把這個工作「排隊」排到某個地方(如果是設定成 `:urgent` 的話就是可以插隊的意思),然後程式繼續往下執行,所以使用者的體驗就會感覺好像馬上完成了,事實上是待會才會做這件事。 85 | 86 | 至於這個待會是多待會,就是看稍會系統比較不忙的時候做。 87 | 88 | 另外,如果想要指定什麼時候做的話,可以這樣寫: 89 | 90 | ```ruby 91 | # 這樣是 5 秒之後做 92 | UserConfirmEmailJob.set(wait: 5.seconds).perform_later(@user) 93 | 94 | # 這樣是「明天下午有空再做」 95 | UserConfirmEmailJob.set(wait_until: Date.tomorrow.noon).perform_later(@user) 96 | ``` 97 | 98 | ## 「排隊」是在哪邊排隊? 99 | 100 | 前面說到的「排隊」(Queue)其實可以在好幾種地方排隊,預設是會把排程放在記憶體裡,但如果萬一伺服器當機或重開機,這個排隊的資料就不見了。在實務上常會另外設置可以排隊的地方,常見的有 `Sidekiq` 跟 `Delayed Job`,我們來試一下比較容易設定的 `delayed_job` 101 | 102 | Delayed::Job 網址:https://github.com/collectiveidea/delayed_job 103 | 104 | 請照頁面說明,安裝 `delayed_job_active_record`: 105 | 106 | ```ruby 107 | gem 'delayed_job_active_record' 108 | ``` 109 | 110 | > 注意:別忘了執行 `bundle install` 確認安裝套件,同時也可能需要重新啟動 `rails server` 111 | 112 | 這個 gem 會建立一個叫做 `delayed_jobs` 的資料表專門存放工作的資料表: 113 | 114 | $ rails generate delayed_job:active_record 115 | 116 | 記得執行 `rails db:migrate` 指令,把 Migration 轉換成資料表。接下來,要改一下 `ActiveJob` 內建存放工作的地方,請編輯 `app/configs/application.rb`: 117 | 118 | ```ruby 119 | module MyBlog 120 | class Application < Rails::Application 121 | config.active_job.queue_adapter = :delayed_job 122 | end 123 | end 124 | ``` 125 | 126 | 加上的這行: 127 | 128 | ```ruby 129 | config.active_job.queue_adapter = :delayed_job 130 | ``` 131 | 132 | 意思就是把工作透過 `delayed_job` 存到資料表,然後就可以稍候再執行了。 133 | 134 | -------------------------------------------------------------------------------- /markdown/chapter19-send-email.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | title: 寄發信件 4 | comments: true 5 | permalink: /chapters/19-send-email.html 6 | 7 | --- 8 | 9 | # 寄發信件 10 | 11 | ## 寄發信件 12 | 13 | 在 Rails 要寄發信件其實滿容易的,有內建的類別(ActionMailer) 可以輕鬆的完成這件事。在 Rails 內建的產生器中,除了我們常用的 `scaffold`、`controller`、`model` 以及 `migration` 之外,也可使用 `mailer` 這個產生器來幫你建立寄信功能所需要的基本架構。首先,讓我們先用 Rails 內建的 mailer 產生器來產生需要的檔案: 14 | 15 | $ rails g mailer Contact 16 | create app/mailers/contact_mailer.rb 17 | invoke erb 18 | create app/views/contact_mailer 19 | identical app/views/layouts/mailer.text.erb 20 | identical app/views/layouts/mailer.html.erb 21 | invoke test_unit 22 | create test/mailers/contact_mailer_test.rb 23 | create test/mailers/previews/contact_mailer_preview.rb 24 | 25 | 透過 mailer 產生器,建立了一個 `ContactMailer` 類別以及 `app/views/contact_mailer` 目錄。先讓我們看一下 `app/mailers` 目錄,現在裡面應該有 2 個檔案,檔名分別是 `application_mailer.rb` 跟 `contact_mailer.rb`。打開看一下 `application_mail.erb` 檔案的內容: 26 | 27 | ```ruby 28 | class ApplicationMailer < ActionMailer::Base 29 | default from: 'from@example.com' 30 | layout 'mailer' 31 | end 32 | ``` 33 | 34 | `default` 方法後面接的 `from: 'from@example.com'`,意思就是如果沒有特別指定的話,預設信件的寄件者就是 `from@example.com`。下一行的 `layout` 是指會去找 `app/views/layouts/mailer` 這個樣版。 35 | 36 | 我們再看一下 `contact_mailer.rb` 檔案的內容: 37 | 38 | ```ruby 39 | class ContactMailer < ApplicationMailer 40 | end 41 | ``` 42 | 43 | 咦?有沒發現好像在哪看過這樣的東西?其實 Mailer 的檔案結構,跟 Controller 有點像: 44 | 45 | | | Controller | Mailer | 46 | |----------|:----------------------------------------|:----------------------------------| 47 | | 類別 | UsersController | ContactMailer | 48 | | 繼承類別 | ApplicationController | ApplicationMailer | 49 | | 檔案位置 | app/contollers/users_controller.rb | app/mailers/contact_mailer.rb | 50 | | Layout | app/views/layouts/application.html.erb | app/views/layouts/mailer.html.erb | 51 | | View | app/views/users/*.erb | /app/views/contact_mailer/*.erb | 52 | 53 | ### mailer 上的 action? 54 | 55 | 即然說 Mailer 跟 Controller 有點像,在 Mailer 也有類似像 action 之類的角色,寫起來大概像這樣: 56 | 57 | ```ruby 58 | class ContactMailer < ApplicationMailer 59 | def say_hello_to(user) 60 | @user = user 61 | mail to:@user.email, subject:"你好!!" 62 | end 63 | end 64 | ``` 65 | 66 | 在 `ContactMailer` 類別裡定義了一個叫做 `say_hello_to` 的方法,並傳入一個 user 物件做為參數。其中真正進行寄信的是 `mail` 那行方法。咦?等等,那信件內容呢?跟 Controller 相比較起來,信件內容大概就是跟 View 差不多的角色。讓我們在 `app/views/contact_mailer` 目錄裡新增一個跟剛剛這個方法「同名」的檔案(檔案名稱:say_hello_to.html.erb),並且加上以下內容: 67 | 68 | ```erb 69 | <%= @user.name %>,你好: 70 | 71 | 今天你也有過得開心嗎! 72 | 73 | 謝謝你的來信,再見! 74 | ``` 75 | 76 | 寫起來的手感是不是真的跟一般的 View 很像呢?它一樣也可以從 Mailer 取得實體變數,並輸出在這個檔案裡。 77 | 78 | ### 準備寄發! 79 | 80 | 準備來寄信吧!我希望可以在成功新增 User 的當下,就寄一封通知信給這位使用者: 81 | 82 | ```ruby 83 | class UsersController < ApplicationController 84 | # ...[略] 85 | def create 86 | @user = User.new(user_params) 87 | if @user.save 88 | ContactMailer.say_hello_to(@user).deliver_now 89 | redirect_to @user, notice: 'User was successfully created.' 90 | else 91 | render :new 92 | end 93 | end 94 | # ...[略] 95 | end 96 | ``` 97 | 98 | 其中這行: 99 | 100 | ```ruby 101 | ContactMailer.say_hello_to(@user).deliver_now 102 | ``` 103 | 104 | 就是寄信的地方了。 105 | 106 | 有發現有點怪怪的地方嗎?我們在 `ContactMailer` 這個類別裡定義實體方法 `say_hello_to`,為什麼這邊用起來變類別方法了?其實這算是 ActionMailer 幫你做的魔術,只要是繼承自 ActionMailer 的類別,定義在 Mailer 裡的實體方法,都會被轉換成類別方法,使用起來更方便。 107 | 108 | 試一下: 109 | 110 | ![image](/images/chapter19/sendmail-1.png) 111 | 112 | 按下送出之後,過沒多久就收到信了: 113 | 114 | ![image](/images/chapter19/sendmail-2.png) 115 | 116 | -------------------------------------------------------------------------------- /markdown/chapter10-mvc.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | title: Model、View、Controller 三分天下 4 | comments: true 5 | permalink: /chapters/10-mvc.html 6 | 7 | --- 8 | 9 | # Model、View、Controller 三分天下 10 | 11 | - [為什麼要這麼麻煩?](#why-mvc) 12 | - [圖解 MVC](#mvc-flow) 13 | - [目錄結構](#project-folders) 14 | 15 | Rails 的專案是採用 Model、View、Controller(簡稱 MVC)的方式設計的。當年我在開發 PHP 專案的時候都沒有這麼麻煩,就是在一個 .php 檔案就處理完查詢資料庫、展示資料等工作(那時候沒有使用其它框架)。所以在我剛開始學習 Rails 的時候一直有這個疑問,就是「為什麼 Rails 要搞這麼複雜?檔案分得這麼細要一直切換實在很麻煩耶!」 16 | 17 | ## 為什麼要這麼麻煩? 18 | 19 | ### 分工容易 20 | 21 | 拆解成 MVC 結構之後,雖然檔案變多、變分散了,但也因此更容易進行分工,當團隊人數增加,每個人可以在各自負責的部份進行開發,較不易互相衝突、干擾。 22 | 23 | ### 開發慣例 24 | 25 | 另一個好處,就是因為整個 Rails 專案都是遵循 MVC 的結構,所以即使是不同程度的開發者寫出來的 Rails 專案,Controller 通常會放在 `app/controllers` 目錄裡,Model 應該也會放在 `app/models` 裡,不會有太大的差別。 26 | 27 | ## 圖解 MVC 28 | 29 | 我們用一張圖來說明 Rails 裡的 MVC 是怎麼運作的: 30 | 31 | ![image](/images/chapter10/mvc.png) 32 | 33 | 1. 當有使用者輸入網址,連到你的網站的時候,第一關會遇到的是路徑對照表(Route,檔案 `config/routes.rb`)。 34 | 2. 在這個路徑對照表裡,記錄著這個網站對外開放的路徑對照表。Rails 會根據使用者輸入的網址及參數,比對這個路徑對照表的資料,然後告訴你應該去找哪個 Controller 上的哪個 Action;或是在對照表裡查不到相關資料,然後就會告訴你 `HTTP 404` 找不到頁面。 35 | 3. 在 Controller 上通常會有好幾個 Action,其實這些 Action 說穿了就是一般的方法而已。透過路徑對照表,找到了對應的 Action,這個 Action 會決定要做什麼事。 36 | 4. 舉例個子來說,在這個 Action 可能會需要查閱「目前所有的商品列表」,接著它就會去請 Model 幫忙要資料。 37 | 5. 雖然 Model 本身並不是資料庫,但它可以幫你把你跟 Model 說的「人話」轉成資料庫看得懂的資料庫查詢語言(SQL)。 38 | 6. 透過資料庫查詢語言,Model 從資料庫那邊取得你想要的資料。 39 | 7. Model 把這包資料交回 Controller/Action 手上。 40 | 8. 雖然 Controller/Action 拿到資料了,但目前這包東西還沒美化、整理過,還不適合給使用者看,所以 Controller/Action 需要跟 View 借一下畫面,讓資料更適合閱讀。 41 | 9. Controller/Action 把資料跟 View 的畫面組合,最後呈現給使用者看。 42 | 43 | 最後第 8、第 9 步,大概就是「I have a data, I have a template, um!... 秀出查詢結果」之類的概念吧。 44 | 45 | ## 目錄結構 46 | 47 | 針對 Rails 的 Route + MVC 的組合,介紹一下這些角色在專案裡對應的目錄結構及慣例。 48 | 49 | ### Route 50 | 51 | 跟 MVC 相比,Route 相對的較為單純,全部的路徑設定都放在 `config/routes.rb` 這個檔案裡: 52 | 53 | ![image](/images/chapter10/folder-config.png) 54 | 55 | `config` 目錄裡面除了 `routes.rb` 之外,基本上跟整個專案設定有關的幾乎都是放在這裡,例如 `database.yml` 就是專門用來設定資料庫連線資訊的地方。關於 Route 的使用會在下一篇做更詳細的介紹。 56 | 57 | ### Controller 58 | 59 | Controller 就是放在專案的 `app/controllers` 目錄裡: 60 | 61 | ![image](/images/chapter10/folder-controller.png) 62 | 63 | 通常每個 Controller 會有自己獨立的檔案,而且檔案的名字跟類別的名字是對得起來的。規則很簡單,就是「大寫字元改成底線加小寫」。舉個例子來說,如果類別的名稱如果叫做 `PostsController` 的話,那這個檔案的名字就是會是 `posts_controller.rb`。 64 | 65 | 如果有興趣,你也可以進到 `rails console` 裡使用字串類別的 `underscore` 以及 `camelcase` 兩個方法來玩看看: 66 | 67 | $ rails console 68 | Running via Spring preloader in process 38922 69 | Loading development environment (Rails 5.0.1) 70 | >> "PostsController".underscore 71 | => "posts_controller" 72 | 73 | >> "UsersController".underscore 74 | => "users_controller" 75 | 76 | >> "posts_controller".camelcase 77 | => "PostsController" 78 | 79 | 另外,如果你打開每個 Controller 的內容,會發現預設都是繼承自 `ApplicationController` 這個類別。根據前面的規則,這個檔案的檔名自然就是 `application_controller.rb` 了 80 | 81 | ### Model 82 | 83 | 跟 Model 相關的檔案都放在 `app/models` 目錄裡: 84 | 85 | ![image](/images/chapter10/folder-model.png) 86 | 87 | 它的類別與檔名規則跟 Controller 是一樣的,例如 `Post` Model,它的檔名是 `post.rb`;如果是 `UserStory` 的話,則是 `user_story.rb`,以此類推。 88 | 89 | 另外,如果資料庫中有 Model 相對應的資料表(Table)的話,資料表的命名慣例是「小寫 + 複數」。簡單整理如下: 90 | 91 | | Model 類別名稱 | 檔案名稱 | 資料表名稱 | 92 | |----------------|--------------------|---------------| 93 | | User | user.rb | users | 94 | | Post | post.rb | posts | 95 | | ProductItem | product_item.rb | product_items | 96 | 97 | 當然資料表的命名慣例是可以修改的,但沒必要的話通常不會特別去改它,儘量維持 Rails 的「慣例優於設定」(CoC, Convention Over Configuration)的原則。 98 | 99 | ### View 100 | 101 | View 的工作主要是負責畫面輸出,通常是一群 HTML 之類的檔案,就放在 `app/views/` 目錄底下,而且預設會隨著 Controller 的名字而集中在某個資料夾: 102 | 103 | ![image](/images/chapter10/folder-view.png) 104 | 105 | 舉例來說,跟 `PostsController` 相關的 View,就會放在 `app/views/posts` 目錄裡。如果執行的是 `PostsController` 的 `index` Action,沒特別聲明 render 方法的話,預設會去找 `app/views/posts/index.html.erb` 這個檔案。 106 | 107 | 而這個 `index.html.erb` 的附檔名本身也是有特別意義的: 108 | 109 | 1. `erb` 是 Embedded Ruby 的縮寫,表示這個檔案會由 Ruby 標準函式庫中的 [ERB](http://ruby-doc.org/stdlib/libdoc/erb/rdoc/ERB.html) 樣版引擎進行解讀。你可以在這個檔案裡寫一些 Ruby 語法,例如陣列的 `each` 或 `map` 方法以及像是產生超連結的 `link_to` 方法。 110 | 2. `html` 表示這個檔案在被 ERB 樣版引擎處理後會被輸出成 HTML。 111 | 112 | 大概了解 Route 跟 MVC 的運作方式,以及每個角色的所在目錄後,下個章節就可以準備來寫一些簡單的程式碼,熟悉一下 Rails 開發的手感。 113 | 114 | -------------------------------------------------------------------------------- /markdown/chapter00-about.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | title: 寫在最前面 4 | comments: true 5 | permalink: /chapters/00-about.html 6 | 7 | --- 8 | 9 | # 寫在最前面 10 | 11 | - [為什麼要寫這本書](#why-this-book) 12 | - [誰適合本書](#who-need-this-book) 13 | - [本書內容](#content) 14 | - [如何使用這本書](#how-to-use-this-book) 15 | - [軟體版本](#version) 16 | - [程式碼慣例](#code-convention) 17 | - [程式範例及錯誤更正](#errata) 18 | - [關於我](#about-me) 19 | 20 | ## 為什麼要寫這本書 21 | 22 | Ruby on Rails(以下簡稱 Rails)是一個非常具有生產力的網站開發框架,透過 Rails 本身的設計以及大量的外部第三方套件,可以很快的就把一個網站應用程式的雛型做出來,所以許多創業圈的朋友會選擇使用它來打造產品也是因為這個原因,快速的先把產品做出來,放到市場上試試水溫。 23 | 24 | 不過 Ruby 這個程式語言以及 Rails 這個網站開發框架,都隱藏了許多的細節,所以用起來雖然感覺很好寫、好用,但其實不太好學,在到上手之前的那段路如果沒人指導的話會走得有點辛苦,特別是對完全沒有技術背景基礎的新手來說難度更高。我自己是醫學院畢業的學生,身為不務正業的非資訊相關科班生,我完全可以體會從新手在這條路上會吃哪些苦頭。 因為所有的技術都得自己想辦法摸索、研究,所以也因此知道新手在學習的時候通常在哪邊會跌倒、踩到地雷。在近幾年[五倍紅寶石](https://5xruby.tw)以及國內各大專院校的課程教學中,觀察、整理出新手容易卡關的點,希望這本書可以幫大家快速的度過新手的撞牆期。 25 | 26 | 在 2007 年的時候,我買了第一本 Ruby 的參考書,當時看完之後只覺得 Ruby 這個程式語言的語法很有趣,但平日公司的業務用的是 ASP 跟 PHP,我不知道這個能在我日常工作上幫到什麼忙。直到 2009 年接觸到了 Rails 之後才發現,原來 Ruby 加上 Rails 之後可以讓開發變得這麼有趣,而且可以這麼有生產力,能讓我在短時間之內就把想做的東西做出來,有更多的時間可以玩樂、做自己想做的事(寫更多的程式..)。 27 | 28 | 也許因為個性的關係,在學習新事物的過程中如果有疑惑的地方,總是希望可以搞懂為止,否則知其然而不知其所以然是沒辦法真的把一門技術搞懂。也因為這樣,本書在撰寫的時候也發揮了我愛囉嗦的專長,即使是簡單的小地方,也希望可以解釋的夠清楚。期望可以不只可以教大家如何寫(How),也能讓大家知道在寫什麼(What),以及為什麼(Why)要這樣寫。 29 | 30 | 至於 Rails 的優、缺點就先不多提了,如人飲水,冷暖自知,還請各位自行來體會 Rails 有趣(或不有趣)的地方,這也是本書最主要的目的。 31 | 32 | 雖然本書是以中文撰寫,但程式語言的專有名詞大多還是英文,所以這些名詞或是常用的口語我還是會儘量使用英文來表示。一來是大家的翻譯可能不一樣,例如 "Default" 在繁體中文翻做「預設」,在簡體中文則是翻譯成「默認」;二來也是因為有些英文字翻譯了反而沒有原文貼切,例如 "Context"、"Meta Programming" 等字。最重要的一點,是希望各位能儘早習慣這些英文,因為實際在業界工作時,很多第一手的資料都是英文的,儘早習慣英文對大家絕對是有幫助的。 33 | 34 | 很多人會比較各種程式語言或開發框架的優劣,比較誰的效能好、誰的功能強大、程式碼可讀性高等等的比較,但這種「戰爭」是戰不完的,而且本身也沒有太大的意義,更何況我個人對我不精通的語言我也沒那個份量來批評。在本書中或多或少會提到「我當初在某些程式語言是如何實作,但在 Ruby/Rails 是這樣做的」之類的比較,這並非比較誰優誰劣,僅為了給曾經寫過該程式語言的朋友們能更輕易的體會我想表達的意思。 35 | 36 | 再次強調,各種程式語言或工具之間並沒有絕對的好或不好的問題,只有適不適合的問題。只要能解決問題的,不管是冷門或熱門,都是好的工具。 37 | 38 | ## 誰適合本書 39 | 40 | 不管您是新手或老鳥,只要你對 Rails 這個網站開發框架有興趣都適合。如果您本身已經有其它程式語言或 Web 開發的經驗,在閱讀本書的前半段應該會相對的比較輕鬆。 41 | 42 | ## 本書內容 43 | 44 | ### 會包括以下內容 45 | 46 | - Ruby / Rails 簡介、環境安裝。 47 | - 套件安裝、使用 48 | - Rails 的專案架構 49 | - 其它實用功能,例如 Email 寄發、工作排程、購物車、訂單處理、金流串接等 50 | 51 | ### 不會包括以下內容 52 | 53 | - 不會教你怎麼寫 HTML/CSS/Javascript。 54 | - 不會教你怎麼使用 Git。 55 | - 不會教你所有的 Ruby 語法或是進階的 Meta Programming 技巧。 56 | 57 | 因篇幅有限,沒辦法包山包海,所以以上內容不會收納在本書內容裡,且上面所列的每一個主題,都可以是獨立的一本書,建議再找更專業的參考書籍。 58 | 59 | ### 你需要準備什麼? 60 | 61 | - 一台可以工作的電腦(不一定要 Mac) 62 | - 一款順手的文字編輯器 63 | - 這樣就夠了 :) 64 | 65 | ## 如何使用這本書 66 | 67 | 本書主要分以下幾部份: 68 | 69 | 1. 開發工具、環境安裝篇 70 | 2. Ruby 基本篇 71 | 3. Rails 入門篇 72 | 4. 實作篇 73 | 74 | 雖然每個章節多少都還是會跟前面的章節有相關,但也不一定要依序從第一章開始閱讀(當然這也是一種方式),可依自己需要跳過部份章節。 75 | 76 | ## 軟體版本 77 | 78 | 隨著時間,Ruby 跟 Rails 的版本可能會有些微不同,本書在撰寫當下所使用的版本為: 79 | 80 | - Ruby 2.4.0 81 | - Rails 5.0.1 82 | 83 | ## 程式碼慣例 84 | 85 | 在開發 Ruby/Rails 程式的時候會有很多機會需要在終端機(Terminal)模式下輸入指令,例如: 86 | 87 | $ ruby hello.rb 88 | hello, world 89 | 90 | 或是這樣: 91 | 92 | $ ruby -v 93 | ruby 2.4.0p0 (2016-12-24 revision 57164) [x86_64-darwin15] 94 | 95 | 在最前面的 `$` 符號是系統提示字元,意思是告訴各位這是一個需要在終端機環境下自己手動輸入的指令,而下一行則是這個指令執行的結果。實際在輸入指令的時候請不要跟著輸入 `$`,不然會出現 `command not found` 的錯誤訊息。 96 | 97 | 有時候你可能會看到這樣寫: 98 | 99 | >> puts "Hello, Ruby" 100 | Hello, Ruby 101 | 102 | 這裡的 `>>` 則是表示這行指令是在 `irb` 或 `rails console` 的環境下輸入的,同樣也不需要跟著輸入這個 `>>` 符號。另外,有時會在程式碼的結尾加上一些註解,例如: 103 | 104 | ```ruby 105 | def calc(n) 106 | n * n 107 | end 108 | 109 | puts calc(4) # => 16 110 | ``` 111 | 112 | 在最後一行加上去的註解是說明或是表示這行程式的輸出結果,各位可以不需要跟著輸入。最後,Ruby 目前有好幾種分支實作品(例如 JRuby、mruby、IronRuby 等),各分支實作品也有好幾種版本,如果沒有特別註明,本書中提到的 Ruby 指的都是 CRuby,也就是最常見的 Ruby 版本,目前最新版本是 2.4.0。 113 | 114 | ## 程式範例及錯誤更正 115 | 116 | 本書所有的程式碼在 `Ruby 2.4.0` 及 `Rails 5.0.1` 的環境下均已測試可正常執行,檔案可在我的 GitHub 帳號取得。隨著 Ruby 及 Rails 的版本演進,或是作業系統的不同,範例程式執行的結果可能會有些微的差異(甚至是錯誤)。若有任何問題,或是有哪邊寫錯,還請各位先進不吝留言或來信、留言指教。 117 | 118 | 最後,希望各位會喜歡本書,一起來學習、體驗 Rails 這個極富生產力的網站開發框架 :) 119 | 120 | ## 關於我 121 | 122 | 高見龍,這看起來有點像武俠小說的名字不是筆名,而是我父母給我的本名。目前是兩個小朋友的爸爸,是個愛寫程式而且希望可以寫一輩子程式的阿宅。 123 | 124 | * [五倍紅寶石](https://5xruby.tw)創辦人及負責人 125 | * Blog: 126 | * Facebook: 127 | * Twitter: 128 | * Github: 129 | * Email: eddie@5xruby.tw 130 | 131 | 若發現本書內容有誤,歡迎直接來信,或到 GitHub 上發 Pull Request 修正 :) 132 | 133 | -------------------------------------------------------------------------------- /markdown/chapter01-ecosystem-and-introduction.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | title: 生態圈及簡介 4 | comments: true 5 | permalink: /chapters/01-ecosystem-and-introduction.html 6 | 7 | --- 8 | 9 | # 生態圈及簡介 10 | 11 | - [Ruby 生態圈](#ruby-ecosystem) 12 | - [關於 Ruby](#about-ruby) 13 | - [關於 Rails](#about-rails) 14 | - [常見問題](#faq) 15 | 16 | ## Ruby 生態圈 17 | 18 | Ruby 這個程式語言可以說是因為 Rails 的盛行而興起的也不為過,我認識大部份的人會開始學習 Ruby 或知道 Ruby 這個程式語言,大多是因為 Rails 的緣故。事實上,在 Rails 風行之前,Ruby 這個程式語言可能全世界幾乎只有日本的工程師在使用。 19 | 20 | ## 關於 Ruby 21 | 22 | ### 什麼是 Ruby? 23 | 24 | 很多人因為聽聞 Rails 可以快速開發網站而開始知道有 Ruby 這個程式語言,所以會認為 Ruby 就是用來開發網站,或是以為 Ruby 是個最近幾年才發明的程式語言。事實上 Ruby 是一種泛用的腳本式程式語言,從資料分析、繪圖、3D 建模、系統管理、遊戲開發等程式都可以使用 Ruby 來開發,而且它的年紀已經超過 20 年了。 25 | 26 | Ruby 是由一位名叫[松本行弘](https://zh.wikipedia.org/wiki/%E6%9D%BE%E6%9C%AC%E8%A1%8C%E5%BC%98)的日本人所發明(日文:まつもとゆきひろ,網路上大家通常稱他 Matz)。Ruby 參考了 Perl、Lisp 及 Smalltalk 等程式語言的設計,是一款物件化非常徹底的程式語言。在 1995 年釋出了第一個版本,在早期實際使用 Ruby 在工作上的開發者並不多,相關的技術文件也大多是日文居多,直到 Rails 開始風行之後,才慢慢的有越來越多人關注它。 27 | 28 | ### 為什麼選擇 Ruby 29 | 30 | 引用一句已故大師 Alan Perlis 的話: 31 | 32 | > "A language that doesn't affect the way you think about programming is not worth knowing" — Alan Perlis 33 | 34 | 中文意思是: 35 | 36 | >「如果某種程式語言不會影響你寫程式的思考方式的話,那就不值得去學習它。」 37 | 38 | Ruby 是個很容易學、很容易上手的程式語言,語法寫起來也很自然、有趣,也因為 Ruby 的自然語法,寫久了真的會影響你在寫程式時候的思考或設計方式。 39 | 40 | 因為 Ruby 的語法寫起來很自然,所以用 Ruby 寫出來的程式碼的可閱讀性也相當高。不管是接手別人的專案,或是維護自己幾個月前寫的系統,比較好的程式碼可讀性對開發者來說可以減少不少負擔。 41 | 42 | 另外,現在全世界的 Ruby 社群都相當活躍,要找什麼套件幾乎都有熱心人士幫忙寫好了。除了可以免費取得之外,連原始程式碼都公開給你看。在台灣,Ruby 社群也是十分活躍,每個月甚至每週都有實體的線下聚會,也有大型的國際程式研討會 [RubyConf Taiwan](http://rubyconf.tw),每年都有不少國內外的 Ruby 開發者前來與會,連 Ruby 的發明人松本行弘也會遠從日本來台灣參加。 43 | 44 | 在本文撰寫的當下,官方最新推出的 Ruby 穩定版本是 2.4.0 版,較舊版本的 Ruby(1.8、1.9 或更早之前)的部份功能也可能會被提到,但以下文章仍會以 2.4.0 版本為主。Ruby 2 系列對之前的版本有向下相容的特性,原本在 1.9 版可以正常執行的程式碼,在 2.0 應該也可以正常運作。 45 | 46 | ### 誰在用 Ruby? 47 | 48 | 很多人在評估程式語言的優劣,是看有哪些大公司、單位在使用它,或是使用的開發者人數。老實說我個人不是很關心這個問題,有些人覺得 Ruby 並不是很流行,在 [TIOBE](http://www.tiobe.com) 網站上的排名也不是非常前面,但我個人認為,好的東西不一定要流行,只要能完成任務的工具就是好工具。在 iPhone 還沒流行之前,誰也沒料到開發 iOS app 的 Objective-C 這個語法看起來很奇怪的程式語言有一天可以這麼熱門。 49 | 50 | ## 關於 Rails 51 | 52 | ### 什麼是 Rails? 53 | 54 | Rails 是一款使用 Ruby 程式語言所開發出來的網站開發框架(Web Framework),作者是名為 David Heinemeier Hansson(簡稱 DHH) 的丹麥人。當年他在開發自家的產品的同時,發現好像可以把一些網站開發常用的模組或函式庫組成一個框架,利用這個框架可以大大的縮短網站應用程式開發的時間。DHH 在 2005 年年底釋出第一個版本,並在研討會現場展示如何使用 Rails 在 15 分鐘內開發出一個 Blog,讓所有的人眼睛為之一亮,在那之後 Rails 便慢慢的風行到全世界,現在世面上常見的網頁開發框架的設計,多少也直接或間接的受了 Rails 的影響。 55 | 56 | 一開始的時候大家會把 Ruby on Rails 簡稱為「RoR」,不過因為「RoR」實在不好發音,後來大家開始慢慢的改稱之 Rails,包括本書也是。 57 | 58 | 有些朋友在學習 Rails 過程中曾問道「即然 Rails 這麼方便,那有必要學 Ruby 嗎?」。我的建議是:「是的,有必要。你也許不需要把 Ruby 學得非常熟、不需要知道 Ruby 裡所有的方法,但至少你該學會在 Rails 專案裡常看到的 Ruby 語法」。 59 | 60 | 很多人一開始可能搞不清楚 Ruby 跟 Rails 之間的關係,如果打個比方的話,大家也許看過或玩過樂高(Lego)積木,Rails 就像是一塊一塊的積木,可以讓你很快的把城堡蓋起來;而 Ruby 則像是積木的原料(塑膠),沒有原料就不會有這個積木。 61 | 62 | Ruby 是一款設計很特別、寫起來也很特別的程式語言,如果能花時間更去深入 Ruby 這個程式語言特別的點,相信在寫 Rails 的時候可以寫出更漂亮、簡潔、有效率的語法。 63 | 64 | ### Rails 設計哲學 65 | 66 | Rails 的兩大設計哲學: 67 | 68 | - 慣例優於設定(Convention over Configuration, CoC) 69 | - 不要做重複的事(Don't Repeat Yourself, DRY) 70 | 71 | #### 慣例優於設定(Convention over Configuration, CoC) 72 | 73 | 所謂的「慣例」就像是不成文的規定,當遇到某種情況的時候我們會用特定的方式來解決問題,或是該把某個功能的程式碼放在什麼地方,不過即使不照著慣例寫,也有別的方法可以達到一樣的目的。 74 | 75 | 在 Rails 裡有相當多這樣的慣例,例如像是專案的目錄結構、資料表的關連及命名等,順著 Rails 的慣例,程式碼可以變得更簡潔、優雅。甚至可以說在學習 Rails 的過程,除了學習 Ruby/Rails 的語法之外,也是在學習 Rails 的慣例。 76 | 77 | #### 不要做重複的事(Don't Repeat Yourself, DRY) 78 | 79 | 如果有些程式碼或結構一直重複的出現,就應該把重複的部份抽離出來,整理成為一個方法、類別或模組。這樣不僅可以重複使用,也會因此變得比較好維護,Bug 也比較容易被發現。 80 | 81 | ## 常見問題 82 | 83 | ### 聽說寫 Ruby/Rails 要先買 Mac 電腦? 84 | 85 | 其實不需要的,使用 Mac OS 作業系統只是在開發環境上比較方便,但並不是必需品,即使用一般的 PC 安裝 Linux/Ubuntu 系統一樣可以進行開發。事實上,最後網站部署的環境也是 Linux/Ubuntu,所以不要聽信「要先學 Ruby/Rails 要先買 Mac」之類的江湖謠言。 86 | 87 | 當然,如果經濟許可,使用 Mac 電腦開發 Ruby/Rails 專案是件還滿開心的事。 88 | 89 | ### 聽說 Ruby/Rails 很慢? 90 | 91 | 這點的確不否認,以各家的程式語言來說,Ruby 的確不是最快的程式語言,Rails 也為了功能的完整性,本身也是一個體積有點肥胖的框架。即便如此,Ruby/Rails 並沒有慢到不堪使用的程度。 92 | 93 | Ruby 的重點,在於可以用自然、簡短的語法開發你想要的功能,而 Rails 的重點則是可以快速的打造出產品雛型,早點上線試一下水溫,試一下商業模式是否可以運作,如果真的有人買單,再來擔心效能也不遲。流量如果真的做起來的話,需要比較好的效能的部份也可再考慮改用其它程式語言改寫。 94 | 95 | 線上遊戲有一句名言「死人沒有 DPS」,意思就是有再強大的武器,躺在地上看星星也是沒用的。同理,若使用強大效能的程式語言或框架但開發時程較長而錯過市場時機,效能再快、再好也是沒有用的。 96 | 97 | ### 用 Windows 系統可以嗎? 98 | 99 | 如果只是練習,使用 Windows 作業系統是可以的,有打包好的懶人安裝包([Ruby Installer](https://rubyinstaller.org/) 及 [Rails Installer](http://railsinstaller.org/en)),但在 Windows 平台可能常常會遇到套件安裝失敗或不支援的問題。如果您平常的工作還是在 Windows 平台的話,建議您可以使用 [VirtualBox](https://www.virtualbox.org/) 在 Windows 上安裝個 Linux/Ubuntu 的虛擬作業環境,可以少一些麻煩。 100 | 101 | ### 一定要安裝在自己的電腦裡嗎? 102 | 103 | 也許因為某些因素,你無法在電腦上安裝 Ruby/Rails 環境(例如設備是借來的、公司內部管制,或是設備的記憶體不夠多,跑不動 VirtualBox 之類的軟體等),也有其它線上的環境可以讓你練習。 104 | 105 | 如果只是想練一下 Ruby 的話,可以試試 [Try Ruby](http://tryruby.org/) 網站: 106 | 107 | ![Try Ruby](/images/chapter01/try_ruby.jpg) 108 | 109 | 在畫面的左邊會一直出現要你完成的題目,右邊輸入區則是類似 Ruby 的 irb 環境。 110 | 111 | 如果想要在線上寫 Rails 的話,[c9.io](https://c9.io/) 這個線上平台也是個很不錯的選項: 112 | 113 | ![c9.io](/images/chapter01/c9.io.png) 114 | 115 | c9.io 除了有線上開發環境外,也可直接在線上有預覽的功能。 116 | 117 | 事實上,在各種不同的硬體及作業系統安裝 Ruby/Rails 環境,對剛入門的新手來說是個很大的挑戰,所以不管在我們學校或是企業的教育訓練課程中,我們也常推薦大家直接使用 c9.io 之類的線上平台來進行練習。 118 | 119 | -------------------------------------------------------------------------------- /markdown/chapter21-api-mode.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | title: API 模式 4 | comments: true 5 | permalink: /chapters/21-api-mode.html 6 | 7 | --- 8 | 9 | # API 模式 10 | 11 | - [輸出成 JSON 格式 - 使用 render](#output-json-with-render) 12 | - [輸出成 JSON 格式 - 使用 Jbuilder](#output-json-with-jbuilder) 13 | - [API-Only 模式](#api-only) 14 | 15 | 在開發手機應用程式或是一些前端比較吃重的網站應用程式時,常會需要跟後端伺服器交換資料,交換資料的格式常見的有 JSON 或 XML 等格式,這個交換介面又稱之 API(Application Programming Interface)。雖然 Rails 的強項之一是資料的 CRUD(新增、修改、刪除)功能,但其實要拿來做 API(Application Programming Interface)也是相當合適的。 16 | 17 | ## 輸出成 JSON 格式 - 使用 render 18 | 19 | 舉個 User scaffold 的例子: 20 | 21 | ```ruby 22 | class UsersController < ApplicationController 23 | # ...[略]... 24 | 25 | def index 26 | @users = User.all 27 | end 28 | 29 | # ...[略]... 30 | end 31 | ``` 32 | 33 | 這邊的 `index` Action 如果沒特別聲明 `render` 方法,它就會去 `app/views/users/` 目錄下找同名的 `index.html.erb` 來呈現畫面,這個我們前面都介紹過。但如果不需要畫面,可以把 `index` Action 改成這樣: 34 | 35 | ```ruby 36 | def index 37 | @users = User.all 38 | render json: @users 39 | end 40 | ``` 41 | 42 | 原本的樣子長這樣: 43 | 44 | ![image](/images/chapter21/index-html.png) 45 | 46 | 這樣就會直接在 Controller 階段就直接以 JSON 格式輸出,輸出結果如下: 47 | 48 | ![image](/images/chapter21/index-json.png) 49 | 50 | 排版整理後結果如下: 51 | 52 | ```json 53 | [ 54 | { 55 | "id":1, 56 | "name":"孫悟空", 57 | "email":"eddie@5xruby.tw", 58 | "created_at":"2017-01-06T08:24:00.045Z", 59 | "updated_at":"2017-01-06T08:24:00.045Z" 60 | }, 61 | { 62 | "id":2, 63 | "name":"貝曲達", 64 | "email":"hi@5xruby.tw", 65 | "created_at":"2017-01-06T08:24:08.318Z", 66 | "updated_at":"2017-01-06T08:24:08.318Z" 67 | }, 68 | { 69 | "id":3, 70 | "name":"丁小雨", 71 | "email":"rain@5xruby.tw", 72 | "created_at":"2017-01-06T08:24:27.352Z", 73 | "updated_at":"2017-01-06T08:24:27.352Z" 74 | } 75 | ] 76 | ``` 77 | 78 | 如果你原本是開發手機應用程式的工程師或前端工程師,不需要拜託別人,簡單幾行就可以取得你所需要的 JSON 格式資料了。 79 | 80 | 但上面這樣的寫法,原本 `/users` 的頁面就不見了,也就是說如果要同時可以呈現 HTML 跟 JSON 的話,得另外設計網址,有點麻煩...讓我們接著看看另一種做法。 81 | 82 | ## 輸出成 JSON 格式 - 使用 Jbuilder 83 | 84 | 不知道大家有沒注意到,其實在使用 Scaffold 產生一堆檔案的時候,在 View 的目錄裡有多一些特別的檔案: 85 | 86 | ![image](/images/chapter21/views.png) 87 | 88 | 這幾個結尾是 `.json.jbuilder` 的檔案,就跟 `.html.erb` 的概念一樣,`.html.erb` 是指會使用 ERB 樣版引擎來解讀這個檔案,並轉換成 HTML 格式;而 `.json.jbuilder` 則是使用 [Jbuilder](https://github.com/rails/jbuilder) 這個 gem,把結果輸出成 JSON 格式。 89 | 90 | 讓我們看一下 `app/views/users/index.json.jbuilder` 這個檔案的內容: 91 | 92 | ```ruby 93 | json.array! @users, partial: 'users/user', as: :user 94 | ``` 95 | 96 | 看來是去 render `users/_user` 這個檔案,裡面的內容是: 97 | 98 | ``` 99 | json.extract! user, :id, :name, :email, :created_at, :updated_at 100 | json.url user_url(user, format: :json) 101 | ``` 102 | 103 | 意思大概就是會輸出 `user` 物件的 `id`、`name`...`updated_at` 等欄位的資料。 104 | 105 | 原本網址 `http://localhost:3000/users` 可以看到使用者列表,如果把網址後面加上 `.json` 變成 `http://localhost:3000/users.json` 就能得到跟前面 `render json: @users` 一樣的結果。 106 | 107 | 不只如此,`/users/2` 會用 HTML 格式輸出 2 號使用者資料,如果是 `/users/2.json` 則是會以 JSON 格式輸出這筆資料。 108 | 109 | 另外,像是如果我只想印出姓名跟 Email,可以把 `users/_user.json.jbuilder` 的內容修改成這樣: 110 | 111 | ``` 112 | json.extract! user, :name, :email 113 | ``` 114 | 115 | 輸出結果就會變成(經過排版結果): 116 | 117 | ```json 118 | [ 119 | { 120 | "name":"孫悟空", 121 | "email":"eddie@5xruby.tw" 122 | }, 123 | { 124 | "name":"貝曲達", 125 | "email":"hi@5xruby.tw" 126 | }, 127 | { 128 | "name":"丁小雨", 129 | "email":"rain@5xruby.tw" 130 | } 131 | ] 132 | ``` 133 | 134 | 使用 `render json: @users` 的寫法只能一口氣把 `@users` 的內容全部印出來,而使用 Jbuilder 則是可以微調要輸出的欄位。另一個好處就是原本 `/users` 的頁面還是可保持 HTML 的頁面呈現,只有 `/users.json` 的時候才會以 JSON 格式輸出。 135 | 136 | ### 為什麼加 `.json` 就可以了? 137 | 138 | 其實這算是 Route 做的好事,先看一下 `rails routes` 的結果: 139 | 140 | $ rails routes 141 | Prefix Verb URI Pattern Controller#Action 142 | users GET /users(.:format) users#index 143 | POST /users(.:format) users#create 144 | new_user GET /users/new(.:format) users#new 145 | edit_user GET /users/:id/edit(.:format) users#edit 146 | user GET /users/:id(.:format) users#show 147 | PATCH /users/:id(.:format) users#update 148 | PUT /users/:id(.:format) users#update 149 | DELETE /users/:id(.:format) users#destroy 150 | 151 | 有注意到後面那個 `(.:format)` 嗎?就是這個東西。假設你輸入網址 `/users.html` 的時候,後面的附檔名就會被捕捉成 `format`,然後如果在 Controller 的 Action 沒有特別聲明要以什麼方式 render 的話,就會依據這個 `format` 去找對應的檔案,也就是說會找到 app/views/index.`html`.erb。(還記得結果是 `.html.erb` 的意思嗎?) 152 | 153 | 同理可證,當網址是輸入 `/users.json` 的時候,它會去找 app/views/index.`json`.jbuilder 來輸出結果。 如果後面沒有附檔名,例如 `/users`,則是預設會去找 `html` 的樣版。 154 | 155 | ## API-Only 模式 156 | 157 | Rails 方便歸方便,它本身是個有點肥大的框架,這是一直以來 Rails 被詬病的地方。在 Rails 5 之後,Rails 加入了 API only 的模式,讓你在產生專案的時候減少一些不必要的套件及 middleware。讓我們產生一個全新的專案: 158 | 159 | $ rails new my_blog --api 160 | 161 | 後面加上了 `--api` 參數產生出來的專案,因為僅需要做 API 的輸出,所以不僅用到的 gem 變少了,連用到的 middleware 也比較少一點,在效能的提昇上是有一些幫助的。 162 | 163 | 更多詳細資料請參閱 http://guides.rubyonrails.org/api_app.html 164 | 165 | 但即使如此,Rails 畢竟是一個功能完整的網站開發框架,改成 API only 也只是從很胖的胖子變成普通胖的胖子,如果真的有效能考量,而且沒有要用到那麼完整的功能,也許可直接考慮使用更輕量化的工具,例如 Sinatra (http://www.sinatrarb.com/)。 166 | 167 | -------------------------------------------------------------------------------- /markdown/chapter27-order.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | title: 訂單處理 4 | comments: true 5 | permalink: /chapters/27-order.html 6 | 7 | --- 8 | 9 | # 訂單處理 10 | 11 | 有了購物車功能,客人順利的下單、結帳,訂單也成立了,接下來就是訂單的處理了。 12 | 13 | - [訂單要處理什麼?](#what-order-do) 14 | - [有限狀態機](#state-machine) 15 | 16 | ## 訂單要處理什麼? 17 | 18 | 通常一筆訂單上面會有: 19 | 20 | 1. 商品(Product)資訊 21 | 2. 訂購數量(quantity) 22 | 3. 狀態 23 | 24 | 商品資訊跟訂購數量其實跟前一個章節購物車的 `CartItem` 的概念有點像,我們這裡要特別介紹的是訂單的「狀態」。訂單的狀態可以很單純也可以很複雜,雖然訂單的狀態欄位的資訊你可以選用字串來表示,但很多時候訂單狀態的改變,通常還會伴隨著一些變化。 25 | 26 | 另外,訂單狀態的變化也可能有多種變化,看一下這張圖: 27 | 28 | ![image](/images/chapter27/state-machine.png) 29 | 30 | 雖然照正常流程來說,商品需要先「退貨」才能辦理「退款」,但如果是在狀態 2 的「已付款」,還沒來得及出貨結果客人就先按了退款,這時候也是一樣要進行退款流程;或是,商品要辦理退貨,理論上也是要等商品到貨才能退,但可能在運作的過程中客人就想退貨了,也是該要進行退貨流程。最重要的是,如果訂單是「待處理」狀態,它不應會被直接改成「已到貨」。 31 | 32 | 如果我們只是用單純的字串在做這個欄位的變化,然後程式裡面是直接修改這個欄位的內容,很容易造成訂單的靈異現像,例如明明還沒付錢的訂單卻被要求要退款。 33 | 34 | ## 有限狀態機 35 | 36 | 有一個有趣的數學模型:[有限狀態機](https://zh.wikipedia.org/wiki/%E6%9C%89%E9%99%90%E7%8A%B6%E6%80%81%E6%9C%BA),又簡稱「狀態機」,剛好可以解決我們上面提到的問題。 37 | 38 | ![image](/images/chapter27/fsm.png) 39 | 40 | 用平常每天大家都會經過的門當例子,狀態機就是「已經關上的門,只能打開不能關;已經打開的門,只能關不能打開」,或是開車為例:「在開車打檔的時候,應該是要 1 檔、2 檔、3 檔依續往上打,而不應該是在 4 檔突然打 P 檔」。 41 | 42 | 在狀態機的模型下,狀態理論上只能順著我們設計的路線走,所以不會有「待處理」的訂單突然就變成「已到貨」狀態的情況發生。 43 | 44 | ### 使用 AASM 45 | 46 | 如果要自己用 `if...else...` 來設計狀態機,那會非常的辛苦,而且不容易維護。還好有人做出一個叫做 [AASM](https://github.com/aasm/aasm) 的 gem 可以很快的幫我們做好這件事。 47 | 48 | 請把 `aasm` 放到 Gemfile 裡,別忘了執行 `bundle install` 以完成完裝。 49 | 50 | ### 訂單狀態 51 | 52 | AASM 的使用方式滿單純的,假設我們有一個叫做 `Order` 的 Model,我們可以透過 AASM 提供的方法來定義可能發生的「狀態」: 53 | 54 | ```ruby 55 | class Order < ApplicationRecord 56 | include AASM 57 | 58 | aasm do 59 | state :pending, initial: true 60 | state :paid, :shipping, :delivered, :returned, :refunded 61 | end 62 | end 63 | ``` 64 | 65 | 說明: 66 | 67 | 1. `state :pending, initial: true` 表示是這個 Model 的初始狀態 68 | 2. 存放狀態的欄位名稱預設叫做 `aasm_state`,型態是字串,但如果你的 Order Model 的狀態欄位不叫這個名字的話,要不可以透過 Migration 改個名字,或是直接這樣修改也可以: 69 | 70 | ```ruby 71 | class Order < ApplicationRecord 72 | include AASM 73 | 74 | aasm column: :state do 75 | state :pending, initial: true 76 | state :paid, :shipping, :delivered, :returned, :refunded 77 | end 78 | end 79 | ``` 80 | 81 | 如果你原來的狀態欄位叫 `state` 的話,這樣的修改就可以對得起來了。 82 | 83 | ### 定義事件 84 | 85 | 除了定義狀態之外,還能定義「事件」,基本上訂單的「狀態」都是由觸發「事件」而改變的,而不是直接修改狀態的內容: 86 | 87 | ```ruby 88 | class Order < ApplicationRecord 89 | include AASM 90 | 91 | aasm do 92 | state :pending, initial: true 93 | state :paid, :shipping, :delivered, :returned, :refunded 94 | 95 | event :pay do 96 | transitions from: :pending, to: :paid 97 | end 98 | 99 | event :ship do 100 | transitions from: :paid, to: :shipping 101 | end 102 | 103 | event :delivering do 104 | transitions from: :shipping, to: :delivered 105 | end 106 | 107 | event :return do 108 | transitions from: [:delivered, :shipping], to: :returned 109 | end 110 | 111 | event :refund do 112 | transitions from: [:paid, :returned], to: :refunded 113 | end 114 | end 115 | end 116 | ``` 117 | 118 | 這裡訂義了 `pay`、`ship`、`delivering`、`return` 以及 `refund` 等事件,其中 `return` 跟 `refund` 事件可以在 2 個以上的不同的狀態觸發(例如在「已付款」跟「已退貨」都可以變成「已退款」)。 119 | 120 | ### 試用 121 | 122 | 進到 `rails console` 來試一下用起來的樣子吧: 123 | 124 | $ rails console 125 | >> o1 = Order.create 126 | (0.1ms) begin transaction 127 | SQL (2.2ms) INSERT INTO "orders" ("aasm_state", "created_at", "updated_at") VALUES (?, ?, ?) [["aasm_state", "pending"], ["created_at", 2017-01-12 15:11:52 UTC], ["updated_at", 2017-01-12 15:11:52 UTC]] 128 | (0.7ms) commit transaction 129 | => # 130 | 131 | 使用 `Order.create` 先隨便建立一組訂單,你會發現這個訂單的狀態是 `pending`。接下來,AASM 有根據你定義的「狀態」送了一些好用的方法,例如: 132 | 133 | >> o1.pending? 134 | => true 135 | 136 | 問它目前是 `pending` 嗎?它回答「是」 137 | 138 | >> o1.paid? 139 | => false 140 | 141 | 問它目前是不是已付款 `paid` 狀態嗎?它說「不是」 142 | 143 | >> o1.may_pay? 144 | => true 145 | 146 | 繼續問它,請問這筆訂單現在可以付錢嗎?它說「可以喔」 147 | 148 | >> o1.may_delivering? 149 | => false 150 | 151 | 那可以出貨嗎?它說「不行」。這樣的回答相當合理,因為我們的設定中,沒付錢的訂單本來就不應該直接出貨。即然沒付錢,那現在讓我們觸發 `pay` 事件來付錢吧: 152 | 153 | >> o1.pay 154 | => true 155 | 156 | 這時候看一下這筆訂單的狀態: 157 | 158 | >> o1 159 | => # 160 | 161 | 它的狀態度成 `paid` 了,再問問它的狀態: 162 | 163 | >> o1.pending? 164 | => false 165 | >> o1.paid? 166 | => true 167 | 168 | 回答都是正確的了。 169 | 170 | 你在這裡看到的一些問號方法以及 `may_` 方法,都是 AASM 在你定義狀態及事件的時候,自動送給你的,相當方便而且防呆。 171 | 172 | 如果想要在某個事件完成之後接著做另一件事,例如「退款之後發送簡訊通知購買人」,可以在事件裡面加上 `after` 方法: 173 | 174 | ```ruby 175 | class Order < ApplicationRecord 176 | include AASM 177 | 178 | aasm do 179 | state :pending, initial: true 180 | state :paid, :shipping, :delivered, :returned, :refunded 181 | 182 | #...[略]... 183 | 184 | event :refund do 185 | transitions from: [:paid, :returned], to: :refunded 186 | 187 | after do 188 | # 發送簡訊通知... 189 | end 190 | end 191 | 192 | end 193 | end 194 | ``` 195 | 196 | 這樣就會在做完 `refund` 退款事件後,接著做 `after` 方法指定的事了。 197 | 198 | 更多詳細使用方法及 Callback,請見 [AASM](https://github.com/aasm/aasm) 的說明頁面。 199 | 200 | -------------------------------------------------------------------------------- /markdown/chapter03-command-line-tools.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | title: 開發工具與常用命令列指令 4 | comments: true 5 | permalink: /chapters/03-command-line-tools.html 6 | 7 | --- 8 | 9 | # 開發工具與常用命令列指令 10 | 11 | 在上一個章節介紹安裝 Ruby 及 Rails,在正式開始寫我們第一個應用程式之前,先介紹一下開發工具以及在開發 Rails 專案過程中常會用到的指令。 12 | 13 | - [開發工具](#dev-tools) 14 | - [常用命令列指令](#command-line) 15 | - [不要害怕指令、不要害怕錯誤](#dont-be-scared-of-command-line) 16 | 17 | ## 開發工具 18 | 19 | 剛接觸 Ruby 或 Rails 的朋友常會問道:「我是 Ruby/Rails 的新手,請問有像 Apple 的 Xcode、Microsoft 的 Visual Studio 或至少像 Dreamweaver 之類方便或視覺化的整合開發工具(Integrated Development Environment, IDE)嗎?」 20 | 21 | 簡單的答案:「目前沒有」 22 | 23 | 那大家都用什麼工具在開發?其實每種程式語言的開發環境都不相同,大部份的 Ruby/Rails 開發者,不太使用這樣的東西。 但其實不是不用,而是幾乎沒有這樣的工具可以用。 24 | 25 | 如果程式語法不熟,沒有程式碼提醒或是語法自動補完的工具怎麼辦?Ruby/Rails 語法不熟,就多查手冊、多用、多寫幾次就會熟了。身為一名稱職的開發者,也不應該太過依賴這樣的提醒功能。別太依賴工具,蹲好馬步、練好正拳把基礎打穩才是正途,別被開發工具寵壞了。而且 Ruby/Rails 的語法都短短的,語法本身也相當直覺、易懂,沒有程式碼提醒或語法自動補完也不是太大的問題。 26 | 27 | Ruby/Rails 的開發者只要手上有任何一款文字編輯器就能進行開發(就跟龍五手上只要有槍....類似的概念吧),雖然沒有好用的開發工具對新手來說是個不小的門檻,但根據幾年在學校或課堂上教授 Ruby/Rails 課程的經驗來看,這都不是真正造成學習者會卡關的地方。 28 | 29 | 以下介紹幾款曾經使用比較順手的文字編輯器。 30 | 31 | ### Sublime Text 32 | 33 | [Sublime Text](https://www.sublimetext.com/) 是一款商業軟體,雖然需要付費購買,但即使沒有付費也可使用(超佛心!),它的優點除了有程式碼上色之外,好用的外掛也非常多。 34 | 35 | ### Atom 36 | 37 | [Atom](https://atom.io/) 是由 GitHub 出資的開發的編輯器,不僅完全免費,連原始碼都直接開放了,同樣也有程式碼上色功能,外掛也越來越豐富。 38 | 39 | ### Vim / Emacs 40 | 41 | 這兩款文字編輯器的年紀已經有三、四十歲了,說不定都比大家還要老。雖然很老,但到現在還是很多開發者會使用,而且各有各的擁護者,一款稱之「編輯器之神」,另一款稱「神之編輯器」(可參閱「[編輯器之戰](https://zh.wikipedia.org/wiki/%E7%BC%96%E8%BE%91%E5%99%A8%E4%B9%8B%E6%88%98)」)。 42 | 43 | 我自己目前主要使用 Vim,並不是說它特別強大,主要是它跟終端機可以無縫整合(因為 Vim 本身就在終端機裡)。在 Rails 專案開發過程中,有很多機會需要在終端機環境下輸入指令,所以對我來說開發起來比較順手,另外主要的原因是因為已經用習慣了。 44 | 45 | ### RubyMine 46 | 47 | 說沒有 IDE 其實是騙人的,還是有商業公司推出一套名為 [RubyMine](https://www.jetbrains.com/ruby/) 的整合開發工具。它的優點可以提醒或自動完成語法,對 Ruby 語法還不熟的新手來說應該有幫助;但缺點是執行速度有比較慢一點點,另一個不太算缺點的缺點就是它的收費比其它的軟體要來得貴一些。 48 | 49 | ## 常用命令列指令 50 | 51 | 在 Rails 的開發過程中,許多指令都是在終端機(Terminal)環境操作。由於大部份的初學者較習慣圖形介面工具,不熟悉指令該怎麼輸入,或是輸入的指令是什麼意思,這點是讓新手覺得容易挫折的地方。以下介紹幾個在終端機環境常會用到的指令。 52 | 53 | | 指令 | 說明 | 54 | | ------------- |:-------------------------| 55 | | cd | 切換目錄 | 56 | | pwd | 取得目前所在的位置 | 57 | | ls | 列出目前的檔案列表 | 58 | | mkdir | 建立新的目錄 | 59 | | touch | 建立檔案 | 60 | | cp | 複製檔案 | 61 | | mv | 移動檔案 | 62 | | rm | 刪除檔案 | 63 | | sudo | 暫時取得權限 | 64 | 65 | ### 目錄切換 66 | 67 | 在 Rails 專案開發過程中,指令需要下在正確的目錄裡才能正常運作,所以學會目錄的切換是很重要的。 68 | 69 | # 切換到 /tmp 目錄(絕對路徑) 70 | $ cd /tmp 71 | 72 | # 切換到 my_project 目錄(相對路徑) 73 | $ cd my_project 74 | 75 | # 往上一層目錄移動 76 | $ cd .. 77 | 78 | # 切換到使用者的 home 目錄中的 project 裡的 namecards 目錄 79 | $ cd ~/project/namecards/ 80 | 81 | # 顯示目前所在目錄 82 | $ pwd 83 | /tmp 84 | 85 | ### 檔案列表 86 | 87 | `ls` 指令可列出在目前目錄所有的檔案及目錄,後面接的 `-al` 參數,`a` 是指連小數點開頭的檔案(例如.gitignore)也會顯示,`l` 則是完整檔案的權限、擁有者以及建立、修改時間: 88 | 89 | $ ls -al 90 | total 56 91 | drwxr-xr-x 18 user wheel 612 Dec 18 02:20 . 92 | drwxrwxrwt 24 root wheel 816 Dec 18 02:19 .. 93 | -rw-r--r-- 1 user wheel 543 Dec 18 02:19 .gitignore 94 | -rw-r--r-- 1 user wheel 1729 Dec 18 02:19 Gemfile 95 | -rw-r--r-- 1 user wheel 4331 Dec 18 02:20 Gemfile.lock 96 | -rw-r--r-- 1 user wheel 374 Dec 18 02:19 README.md 97 | -rw-r--r-- 1 user wheel 227 Dec 18 02:19 Rakefile 98 | drwxr-xr-x 10 user wheel 340 Dec 18 02:19 app 99 | drwxr-xr-x 8 user wheel 272 Dec 18 02:20 bin 100 | drwxr-xr-x 14 user wheel 476 Dec 18 02:19 config 101 | -rw-r--r-- 1 user wheel 130 Dec 18 02:19 config.ru 102 | drwxr-xr-x 4 user wheel 136 Dec 18 02:41 db 103 | drwxr-xr-x 4 user wheel 136 Dec 18 02:19 lib 104 | drwxr-xr-x 4 user wheel 136 Dec 18 02:23 log 105 | drwxr-xr-x 9 user wheel 306 Dec 18 02:19 public 106 | drwxr-xr-x 9 user wheel 306 Dec 18 02:19 test 107 | drwxr-xr-x 7 user wheel 238 Dec 18 02:23 tmp 108 | drwxr-xr-x 3 user wheel 102 Dec 18 02:19 vendor 109 | 110 | ### 建立檔案、目錄 111 | 112 | $ touch index.html 113 | 114 | 如果 index.html 這個檔案本來不存在,`touch` 指令會建立一個名為 index.html 的空白檔案;如果本來就已經存在,則只會改變這個檔案的最後修改時間,並不會變更其內容。 115 | 116 | $ mkdir demo 117 | 118 | `mkdir` 指令會在目前所在目錄,建立一個名為 demo 的目錄。 119 | 120 | ### 檔案操作 121 | 122 | # 複製 index.html 成 about.html 123 | $ cp index.html about.html 124 | 125 | # 把 index.html 更名成 info.html 126 | $ mv index.html info.html 127 | 128 | # 刪除 index.html 129 | $ rm index.html 130 | 131 | # 刪除在這個目錄裡所有的 html 檔 132 | $ rm *.html 133 | 134 | ### 取得權限 135 | 136 | 有些指令需要有系統管理權限(root 權限)才能執行(例如要幫使用者變更密碼)。這時可在指令前面再加上 `sudo` 指令,只要你本身有可以使用 `sudo` 指令的權限,就可暫時的透過這個指令取得 root 權限: 137 | 138 | $ sudo passwd john 139 | 140 | ## 不要害怕指令、不要害怕錯誤 141 | 142 | 在 Rails 專案開發過程中會在終端機環境輸入許多指令,對新手來說是個不小的障礙。但請不要擔心,在開發過程用到的指令其實都不會太複雜,應該多用幾次就能上手,千萬不要因為指令輸入錯誤而造成挫折。 143 | 另外,在終端機執行指令後,不管成功或失敗,通常都會有訊息顯示在指令之後,這些訊息請多花幾秒鐘仔細的閱讀。很多的新手以為看到訊息就等於是指令執行成功,但事實上可能是錯誤訊息。 144 | 145 | 看到錯誤訊息不用擔心,因為通常答案就在錯誤訊息中,舉個例子來說: 146 | 147 | ![image](/images/chapter03/pending_migration.png) 148 | 149 | 這紅紅的錯誤訊息 `ActiveRecord::PendingMigrationError` 看起來一開始有點嚇人,但仔細看,它底下寫著一行貼心小提示: 150 | 151 | Migrations are pending. To resolve this issue, run: bin/rails db:migrate RAILS_ENV=development 152 | 153 | 意思就是說你有個 Migration 還沒處理,只要執行一下 `rails db:migrate` 就解決了。 154 | 155 | 不要害怕輸入指令,不要害怕錯誤訊息,加油! 156 | 157 | -------------------------------------------------------------------------------- /markdown/chapter09-using-gems.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | title: 使用套件(gem)讓開發更有效率 4 | comments: true 5 | permalink: /chapters/09-using-gems.html 6 | 7 | --- 8 | 9 | # 使用套件(gem)讓開發更有效率 10 | 11 | - [安裝套件](#install-gem) 12 | - [使用 gem 來加速開發](#using-gem) 13 | - [小結](#note) 14 | 15 | 在開放原始碼的世界,有非常多厲害開發者願意無私的貢獻程式碼,而這些程式碼大多會打包成好用的套件,在 Ruby 的世界,我們稱它為 gem。所有 gem 的詳細資訊,都可在 [RubyGems](https://rubygems.org/) 網站上找得到: 16 | 17 | ![image](/images/chapter09/rubygems.png) 18 | 19 | ## 安裝套件 20 | 21 | 在 Ruby 要安裝套件超簡單的,只要 `gem install` 指令加上套件的名字,敲完按下 Enter 鍵,就自動會連上網路、下載套件、安裝套件,一氣呵成。例如我想安裝一個名為 `takami` 的套件: 22 | 23 | $ gem install takami 24 | Fetching: takami-0.0.1.gem (100%) 25 | Successfully installed takami-0.0.1 26 | Parsing documentation for takami-0.0.1 27 | Installing ri documentation for takami-0.0.1 28 | Done installing documentation for takami after 0 seconds 29 | 1 gem installed 30 | 31 | 如果該套件又有需要其它套件,它也會一併順便一起下載、安裝。這個 `takami` 是我自己寫的 gem,裡面沒有任何功能,僅是上課時教同學們怎麼把程式碼打包成 gem 的範例,所以可安心安裝!((咦?!) 32 | 33 | ### 所以我說那個套件呢? 34 | 35 | 安裝 gem 很簡單,但安裝好了之的那些檔案放哪去了?執行 `gem env` 可列出目前在這台電腦的設定: 36 | 37 | $ gem env 38 | RubyGems Environment: 39 | - RUBYGEMS VERSION: 2.6.8 40 | - RUBY VERSION: 2.4.0 (2016-12-24 patchlevel 0) [x86_64-darwin15] 41 | - INSTALLATION DIRECTORY: /Users/user/.rvm/gems/ruby-2.4.0 42 | - USER INSTALLATION DIRECTORY: /Users/user/.gem/ruby/2.4.0 43 | - RUBY EXECUTABLE: /Users/user/.rvm/rubies/ruby-2.4.0/bin/ruby 44 | - EXECUTABLE DIRECTORY: /Users/user/.rvm/gems/ruby-2.4.0/bin 45 | - SPEC CACHE DIRECTORY: /Users/user/.gem/specs 46 | - SYSTEM CONFIGURATION DIRECTORY: /Users/user/.rvm/rubies/ruby-2.4.0/etc 47 | - RUBYGEMS PLATFORMS: 48 | - ruby 49 | - x86_64-darwin-15 50 | ... 略 ... 51 | 52 | 那個 `INSTALLATION DIRECTORY` 就是 gem 安裝的地方,裡面翻一下應該就可以找得到剛剛安裝的 `takami` 套件了。因為我是使用 [RVM](https://rvm.io/),所以 gem 的安裝路徑會在 .rvm 目錄裡。 53 | 54 | ### 使用 gem 55 | 56 | gem 裝好了要怎麼使用呢?剛好趁這個機會介紹一個我很喜歡的 gem:[Faker](https://github.com/stympy/faker)。這個套件可以快速的產生很多種的看起來像真的「假資料」。 57 | 58 | 安裝一下套件: 59 | 60 | gem install faker 61 | 62 | 安裝完成之後,開 Ruby 內附的互動小工具 `irb` 來試玩一下: 63 | 64 | $ irb 65 | # 先 require 這個套件 66 | >> require 'faker' 67 | => true 68 | 69 | # 產生假的 Email 70 | >> Faker::Internet.email 71 | => "lynn.raynor@grahamcartwright.net" 72 | 73 | >> Faker::Internet.email 74 | => "guiseppe@jones.net" 75 | 76 | # 連權利遊戲的假資料都有 77 | >> Faker::GameOfThrones.character 78 | => "Ned Stark" 79 | 80 | >> Faker::GameOfThrones.character 81 | => "Stannis Baratheon" 82 | 83 | 做測試的時候用這個 gem 來產生假資料相當方便! 84 | 85 | ## 在 Rails 專案裡使用 gem 86 | 87 | 如果要在 Rails 專案中使用 gem 的話,需要把要使用的 gem 標註在專案目錄下的 `Gemfile`。打開 `Gemfile`,大概會長得像這樣: 88 | 89 | ```ruby 90 | source 'https://rubygems.org' 91 | 92 | git_source(:github) do |repo_name| 93 | repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/") 94 | "https://github.com/#{repo_name}.git" 95 | end 96 | 97 | gem 'rails', '~> 5.0.1' 98 | gem 'sqlite3' 99 | gem 'puma', '~> 3.0' 100 | gem 'sass-rails', '~> 5.0' 101 | gem 'uglifier', '>= 1.3.0' 102 | gem 'coffee-rails', '~> 4.2' 103 | 104 | gem 'jquery-rails' 105 | gem 'turbolinks', '~> 5' 106 | gem 'jbuilder', '~> 2.5' 107 | 108 | group :development, :test do 109 | gem 'byebug', platform: :mri 110 | end 111 | 112 | group :development do 113 | gem 'web-console', '>= 3.3.0' 114 | gem 'listen', '~> 3.0.5' 115 | gem 'spring' 116 | gem 'spring-watcher-listen', '~> 2.0.0' 117 | end 118 | 119 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 120 | gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] 121 | ``` 122 | 123 | 在這個檔案裡,你可以看到有些 gem 的後面有加註版本號碼,有的沒有,這分別代表不同的意思: 124 | 125 | ### 沒加註版號 126 | 127 | 先從最簡單的來看。當後面沒有加註版本號碼的時候,像這樣: 128 | 129 | ```ruby 130 | gem 'sqlite3' 131 | gem 'jquery-rails' 132 | ``` 133 | 134 | 這樣的寫法將會在安裝的時候選用「最新的穩定(stable)版本」,要注意這裡的重點是「穩定」而不是「最新」。以 Rails 來說,假設最新的版本是 5.0.2 beta 4,但最新的「穩定」版本是 5.0.1 版,當沒有加註版本號的時候,它會選擇安裝 5.0.1 版本。 135 | 136 | ### 加註明確版號 137 | 138 | 例如像這樣: 139 | 140 | ```ruby 141 | gem "rails", "5.0.1" 142 | ``` 143 | 144 | 這相當明顯了,這就是說「我要安裝 rails 5.0.1 版」,應該不需要特別解釋。 145 | 146 | ### 大於、小於版號 147 | 148 | ```ruby 149 | gem 'uglifier', '>= 1.3.0' 150 | ``` 151 | 152 | 我想這個光用看的就猜得出來,就是要選用大於或等於 1.3.0 版本。如果是這樣: 153 | 154 | ```ruby 155 | gem 'rails', '>= 5.0.0.beta4', '< 5.1' 156 | ``` 157 | 158 | 則是會選用在 5.0.0.beta4 跟 5.1 之間的版本。 159 | 160 | ### 差不多... 161 | 162 | ```ruby 163 | gem 'coffee-rails', '~> 4.1.0' 164 | ``` 165 | 166 | 這是指會選用 4.1.0 以上,但 4.2 以下(不含括 4.2)的最新版本。 167 | 168 | 為什麼這麼麻煩?舉個例子來說,例如版本號 `4.2.6`,`4`、`2`、`6` 三個數字分別代表主要版號(Major)、次要版號(Minor)以及修訂版號(Patch),分別表示: 169 | 170 | * 主要版號:功能大改,公開的 API 做了不少修正,通常無法向下相容 171 | * 次要版號:加了某些新功能,但不影響其它功能,向下相容 172 | * 修訂版號:對現有的功能做了小幅度的修正,可向下相容 173 | 174 | 這是個不成文的規定(語義化版本),雖然沒有強制,但幾乎大部份的 gem 作者都會依照這個規範。這個 `~>` 「差不多」的寫法,可以確保不會因為套件昇級而把原本正常運作的系統弄壞了。 175 | 176 | ## 使用 gem 來加速開發 177 | 178 | 介紹完了 Gemfile 裡的內容,接下讓我們利用現有的 gem 來加速開發,舉個例子來說: 179 | 180 | ![image](/images/chapter09/paging-01.png) 181 | 182 | 這個頁面的資料太多了,如果我只想呈現每頁 5 筆資料,通常得自己算每頁幾筆、現在是第幾頁、總共有幾頁這些數字(我數學不好,很不擅長算這種)。有位好心又很厲害的大大做了一個專門計算分頁的套件稱為 [Kaminari](https://github.com/amatsuda/kaminari),可以很輕鬆的完成這件事: 183 | 184 | ### Step 1: 安裝套件 185 | 186 | 打開 `Gemfile`,加上這行: 187 | 188 | ``` 189 | gem 'kaminari' 190 | ``` 191 | 192 | > 重要:更新 Gemfile 檔案內容後,別忘了要到該專案目錄底下執行 `bundle install` 指令,確保所有套件都有正常安裝。 193 | 194 | ### Step 2: 修改程式碼 195 | 196 | 打開專案的 `app/controllers/posts_controller.rb` 檔案,把原來在 `index` 方法的 `Post.all` 做一些調整: 197 | 198 | ```ruby 199 | class PostsController < ApplicationController 200 | before_action :set_post, only: [:show, :edit, :update, :destroy] 201 | 202 | # GET /posts 203 | # GET /posts.json 204 | def index 205 | @posts = Post.page(params[:page]).per(5) 206 | end 207 | 208 | ... [略] ... 209 | end 210 | ``` 211 | 212 | 那個 `page` 方法,是 Kaminari 這個套件專門拿來做分頁的方法,後面的 `per(5)` 就是「每頁有 5 筆資料」的意思。重新整理一下瀏覽器,應該會看到只剩 5 筆了: 213 | 214 | ![image](/images/chapter09/paging-02.png) 215 | 216 | (如果發生 page 方法找不到之類的錯誤訊息,可能重新啟動 Rails Server 之後就正常了) 217 | 218 | 但這樣還不夠,在畫面上還少了「上一頁」、「下一頁」的功能啊!沒關係,這個套件也幫你做好了。打開檔案 `app/views/posts/index.html.erb`,找一個你想要放分頁器的地方: 219 | 220 | ```erb 221 |

<%= notice %>

222 | 223 |

Posts

224 | 225 | 226 | 227 | ...[略]... 228 | 229 |
230 |
231 | 232 | <%= paginate @posts %> 233 | 234 |
235 | <%= link_to 'New Post', new_post_path %> 236 | ``` 237 | 238 | 那行 `<%= paginate @posts %>` 會幫你把分頁器做出來。重新整理一下畫面: 239 | 240 | ![image](/images/chapter09/paging-03.png) 241 | 242 | 就這樣,寫沒幾行程式碼就把分頁功能做完了! 243 | 244 | ## 小結 245 | 246 | 善用現有的套件可以大幅的縮短開發時程。這些 gem 的作者通常很愛現(稱讚意味),他們大多會在說明文件裡詳細介紹這個套件怎麼用(怕你不會用),所以,使用套件前應詳閱公開說明書(README),如果有任何問題,也都歡迎留 issue 給作者們,通常很快就會被解答。 247 | 248 | 另外,有兩個網站推薦給大家參考: 249 | 250 | - [RailsCasts](http://railscasts.com/) 251 | - [GoRails](https://gorails.com/) 252 | 253 | 這兩個網站的影片都有介紹怎麼使用 gem,雖然 RailsCasts 網站已停止更新,但網站上的內容仍非常有參考價值。 254 | 255 | -------------------------------------------------------------------------------- /markdown/chapter26-shopping-cart-part-2.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | title: 購物車 Part 2 4 | comments: true 5 | permalink: /chapters/26-shopping-cart-part-2.html 6 | 7 | --- 8 | 9 | # 購物車 Part 2 10 | 11 | - [先寫測試,再寫程式](#tdd) 12 | 13 | 接續前一個章節,繼續把後續的功能以 TDD 的方式完成。 14 | 15 | > 本章節程式碼可於 GitHub 上取得 https://github.com/kaochenlong/shopping_mall 16 | 17 | ## 先寫測試,再寫程式 18 | 19 | ### 測試 Step 4 20 | 21 | 在開始下一個測試之前,先看一下這個測試: 22 | 23 | ```ruby 24 | require 'rails_helper' 25 | 26 | RSpec.describe Cart, type: :model do 27 | describe "購物車基本功能" do 28 | # ...[略]... 29 | it "每個 Cart Item 都可以計算它自己的金額(小計)" do 30 | end 31 | 32 | # ...[略]... 33 | end 34 | end 35 | ``` 36 | 37 | 這個看起來是跟 `CartItem` 比較有關,雖然要全部寫在同一個 spec 檔裡面也不是不行,但隨著測試越來越多,建議還是另外開一 spec 來做這件事。一樣使用產生器來產生 spec 檔案: 38 | 39 | $ rails g rspec:model CartItem 40 | Running via Spring preloader in process 96855 41 | create spec/models/cart_item_spec.rb 42 | invoke factory_girl 43 | create spec/factories/cart_items.rb 44 | 45 | 然後把 Cart Item 相關的測試移過去: 46 | 47 | ```ruby 48 | require 'rails_helper' 49 | 50 | RSpec.describe CartItem, type: :model do 51 | it "每個 Cart Item 都可以計算它自己的金額(小計)" do 52 | p1 = Product.create(title:"七龍珠", price: 80) # 建立商品 1 53 | p2 = Product.create(title:"冒險野郎", price: 200) # 建立商品 2 54 | 55 | cart = Cart.new 56 | 3.times { cart.add_item(p1.id) } # 加 3 次商品 1 57 | 4.times { cart.add_item(p2.id) } # 加 4 次商品 2 58 | 2.times { cart.add_item(p1.id) } # 再加 2 次商品 1 59 | 60 | expect(cart.items.first.price).to be 400 # 第 1 條 cart item 的價錢應該是 400 塊 61 | expect(cart.items.second.price).to be 800 # 第 2 條 cart item 應該是 800 塊 62 | end 63 | end 64 | ``` 65 | 66 | 在測試裡,期待 `CartItem` 本身可以計算自己這條 item 的價錢的能力。這時候跑測試,自然是一定會失敗的... 67 | 68 | ### 實作 Step 4 69 | 70 | ```ruby 71 | class CartItem 72 | attr_reader :product_id, :quantity 73 | 74 | def initialize(product_id, quantity = 1) 75 | @product_id = product_id 76 | @quantity = quantity 77 | end 78 | 79 | def increment(n = 1) 80 | @quantity += n 81 | end 82 | 83 | def product 84 | Product.find_by(id: product_id) 85 | end 86 | 87 | def price 88 | product.price * quantity 89 | end 90 | end 91 | ``` 92 | 93 | 說明: 94 | 95 | 因為目前每個 item 本身都可以知道對應到的商品以及數量,所以只要一行: 96 | 97 | ```ruby 98 | product.price * quantity 99 | ``` 100 | 101 | 就可以算出這個 item 的價錢了。 102 | 103 | ### 測試 Step 5 104 | 105 | 即然每個 CartItem 都可以自己算錢,接下來要讓整台購物車也能算錢就會不太難做了。測試如下: 106 | 107 | ```ruby 108 | require 'rails_helper' 109 | 110 | RSpec.describe Cart, type: :model do 111 | describe "購物車基本功能" do 112 | #...[略]... 113 | 114 | it "可以計算整台購物車的總消費金額" do 115 | cart = Cart.new 116 | p1 = Product.create(title:"七龍珠", price: 80) # 建立商品 1 117 | p2 = Product.create(title:"冒險野郎", price: 200) # 建立商品 2 118 | 119 | 3.times { 120 | cart.add_item(p1.id) 121 | cart.add_item(p2.id) 122 | } 123 | 124 | expect(cart.total_price).to be 840 125 | end 126 | end 127 | 128 | # ...[略]... 129 | end 130 | ``` 131 | 132 | 商品 1 跟商品 2 各買了 3 份,整台購物車的 `total_price` 應該是 840 元。 133 | 134 | ### 實作 Step 5 135 | 136 | ```ruby 137 | class Cart 138 | attr_reader :items 139 | 140 | def initialize 141 | @items = [] 142 | end 143 | 144 | def add_item(product_id) 145 | found_item = items.find { |item| item.product_id == product_id } 146 | 147 | if found_item 148 | found_item.increment 149 | else 150 | @items << CartItem.new(product_id) 151 | end 152 | end 153 | 154 | def empty? 155 | items.empty? 156 | end 157 | 158 | def total_price 159 | items.reduce(0) { |sum, item| sum + item.price } 160 | end 161 | end 162 | ``` 163 | 164 | 在 `Cart` 類別加上了 `total_price` 方法,並且用 Ruby 內建的 `reduce` 方法來計算所有 item 的價錢,這樣測試應該就可以通過了。 165 | 166 | ### 測試 Step 6 167 | 168 | 基本功能做完了,接下來要做的是比較進階的功能。因為預計會使用 Session 在存購物車的資料,所以會需要把購物車物件轉換成 Hash 格式: 169 | 170 | ```ruby 171 | require 'rails_helper' 172 | 173 | RSpec.describe Cart, type: :model do 174 | # ...[略]... 175 | 176 | describe "購物車進階功能" do 177 | it "可以將購物車內容轉換成 Hash,存到 Session 裡" do 178 | cart = Cart.new 179 | 3.times { cart.add_item(2) } # 新增商品 id 2 180 | 4.times { cart.add_item(5) } # 新增商品 id 5 181 | 182 | expect(cart.serialize).to eq session_hash 183 | end 184 | 185 | it "可以把 Session 的內容(Hash 格式),還原成購物車的內容" do 186 | end 187 | end 188 | 189 | private 190 | def session_hash 191 | { 192 | "items" => [ 193 | {"product_id" => 2, "quantity" => 3}, 194 | {"product_id" => 5, "quantity" => 4} 195 | ] 196 | } 197 | end 198 | end 199 | ``` 200 | 201 | 在這個測試,我手動把商品 id 2 以及 5 透過 `add_item` 方法丟到購物車裡,然後期待購物車的 `serialize` 方法可以回傳一個格式正確的 Hash... 202 | 203 | ### 實作 Step 6 204 | 205 | 為了符合預期的規格,在這裡我定義了一個 `serialize` 的方法,想辦法讓它回傳期望的 Hash 格式: 206 | 207 | ```ruby 208 | class Cart 209 | # ...[略]... 210 | 211 | def serialize 212 | all_items = items.map { |item| 213 | { "product_id" => item.product_id, "quantity" => item.quantity} 214 | } 215 | 216 | { "items" => all_items } 217 | end 218 | end 219 | ``` 220 | 221 | 要把物件收集成一個陣列雖然常會使用 `.each` 方法,但使用 `.map` 方法寫起來會更漂亮一點。如果沒打錯字的話,這個測試應該可以順利通過。 222 | 223 | ### 測試 Step 7 224 | 225 | 可以由購物車物件轉成 Hash,接下來是要可以反向的把 Hash 轉成購物車: 226 | 227 | ```ruby 228 | require 'rails_helper' 229 | 230 | RSpec.describe Cart, type: :model do 231 | describe "購物車基本功能" do 232 | # ...[略]... 233 | end 234 | 235 | describe "購物車進階功能" do 236 | it "可以將購物車內容轉換成 Hash,存到 Session 裡" do 237 | # ...[略]... 238 | end 239 | 240 | it "可以把 Session 的內容(Hash 格式),還原成購物車的內容" do 241 | cart = Cart.from_hash(session_hash) 242 | 243 | expect(cart.items.first.product_id).to be 2 244 | expect(cart.items.first.quantity).to be 3 245 | expect(cart.items.second.product_id).to be 5 246 | expect(cart.items.second.quantity).to be 4 247 | end 248 | end 249 | 250 | private 251 | def session_hash 252 | { 253 | "items" => [ 254 | {"product_id" => 2, "quantity" => 3}, 255 | {"product_id" => 5, "quantity" => 4} 256 | ] 257 | } 258 | end 259 | end 260 | ``` 261 | 262 | 期待購物車有個類別方法 `from_hash`,可以接收一個 Hash 轉回購物車物件,並且期待轉回來的商品跟數量都是正確的... 263 | 264 | ### 實作 Step 7 265 | 266 | 這個實作可能會比前面幾個要來得複雜一點: 267 | 268 | ```ruby 269 | class Cart 270 | attr_reader :items 271 | 272 | def initialize(items = []) 273 | @items = items 274 | end 275 | 276 | def add_item(product_id) 277 | #...[略]... 278 | end 279 | 280 | def empty? 281 | #...[略]... 282 | end 283 | 284 | def total_price 285 | #...[略]... 286 | end 287 | 288 | def serialize 289 | #...[略]... 290 | end 291 | 292 | def self.from_hash(hash) 293 | if hash.nil? 294 | new [] 295 | else 296 | new hash["items"].map { |item_hash| 297 | CartItem.new(item_hash["product_id"], item_hash["quantity"]) 298 | } 299 | end 300 | end 301 | end 302 | ``` 303 | 304 | 說明: 305 | 306 | 1. 因為期望 `from_hash` 是類別方法,所以在定義的時候加上了 `self.` 307 | 2. 在 `self.from_hash` 方法中,不管傳進來的 Hash 是空的還是有資料,最終都還是呼叫 `new` 方法產生一個 `Cart` 實體,並且把傳入的 Hash 的內容轉換成 `CartItem` 物件。 308 | 3. 因此,在 `Cart` 類別的 `initialize` 方法需要稍做調整,讓它可以接收一個參數,並把參數直接指定給 `@items` 實體變數。 309 | 310 | 這樣測試應該就可以通過了!YES! 311 | 312 | ## 小結 313 | 314 | 請記得,TDD(Test-Driven Development)的重點在於「Development」而不在於「Test」,它是一種「測試先行」的「開發方法」。c 使用 TDD 方式進行開發一開始會有不小的阻力,畢竟跟平常的開發習慣不同,但逐漸習慣後會開始嘗到甜頭,並慢慢建立自信。甚到當你熟悉這個流程之後,沒先寫測試就開始實作反而會覺得晚上睡不著覺。 315 | 316 | -------------------------------------------------------------------------------- /markdown/chapter28-payment.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | title: 金流串接(使用 Paypal) 4 | comments: true 5 | permalink: /chapters/28-payment.html 6 | 7 | --- 8 | 9 | # 金流串接(使用 Paypal) 10 | 11 | - [使用 Braintree - 前端](#front-end) 12 | - [使用 Braintree - 後端](#back-end) 13 | 14 | 有購物車、有訂單處理,接下來就是準備要你的客人付錢了。國內、外的金流廠商有非常多,本章節將以 Paypal 為例,示範如何串接金流。一般來說,國外的金流服務在串接上是比較簡單的,不僅設計比較先進,文件也比較清楚。雖然國外的金流服務比較容易串接,但有可能會遇到以下問題: 15 | 16 | 1. 國內某些信用卡沒辦法刷 17 | 2. 可能需要有國外的銀行帳戶才能收款 18 | 19 | 但不管是要串接哪一家的金流服務,文件是一定都得要看的。以下我們將使用 [Braintree](https://www.braintreepayments.com/) 所提供的服務來串接 Paypal 金流。 20 | 21 | ## 使用 Braintree - 前端 22 | 23 | 因為我們這是練習用的範例,所以在登入的時候選的是「Sandbox」模式,在這個模式下的任何消費都不會真的刷卡或匯款。登入之後的樣子像這樣: 24 | 25 | ![image](/images/chapter28/dash-board.png) 26 | 27 | 點擊上方選單的「Help」→「API documentation」,可以在這個頁面查到串接金流服務的所有說明及範例。 28 | 29 | 使用 Braintree 服務需要在前、後台都做一些設定才能使用,在前端的 `Client SDKs`,選擇 `Web/JavaScript`,整個 Client 端的 SDK 運作原理如它文件上附的這張圖: 30 | 31 | ![image](/images/chapter28/braintree-client.png) 32 | 33 | 說明: 34 | 35 | 1. 打開瀏覽器,它會先跟網頁伺服器要一組 token,這個伺服器在這裡就是我們的 Rails 專案。 36 | 2. 我們的網頁伺服器產生一組 token 給瀏覽器(或手機)。 37 | 3. 當在頁面上填完信用卡號以及有效日期後,按下送出,這時候會再跟 Braintree 伺服器要一組 nonce(隨機數)。 38 | 4. 瀏覽器取得這組 nonce 後會傳給我們自己的網頁伺服器,然後我們的伺服器就會把所有相關資訊組合成一包資訊傳給 Braintree,進行刷卡。 39 | 40 | 到這裡是前端頁面在做的事情。但第 2 步因為尚未完成,所以待會這個步驟的資訊會先用假的 token 替代。 41 | 42 | 要注意的是,目前 Braintree 的 JavaScript 的 SDK 有 v2 跟 v3 兩個版本: 43 | 44 | ![image](/images/chapter28/v2-v3.png) 45 | 46 | 其中 v3 版本較多地方可以客制化,但 v2 版本使用上較為簡單,以下將使用 v2 版本做為範例。為了簡化流程,我直接在商品頁面的 `show` 頁面進行刷卡,我直接在該頁面加上它文件的範例: 47 | 48 | ```erb 49 |

<%= notice %>

50 | 51 |

52 | Title: 53 | <%= @product.title %> 54 |

55 | 56 |

57 | Description: 58 | <%= @product.description %> 59 |

60 | 61 |

62 | Price: 63 | <%= @product.price %> 64 |

65 | 66 | <%= link_to 'Back', products_path, class:'btn btn-default' %> 67 |
68 | 69 | <%= form_for(@product, url: checkout_product_path(@product), method: :post) do |f| %> 70 |
71 | <%= f.submit "確認付款", class:"btn btn-default btn-danger" %> 72 | <% end %> 73 | 74 | 75 | 82 | ``` 83 | 84 | 說明: 85 | 86 | 1. Form 裡面必須有一個 id 為 `payment-form` 的元素,好讓底下的 JavaScript 可以對到這個名字。不一定要用 `payment-form` 這個名字,但如果換的話,底下的 `braintree.setup` 那段範例裡的名字也要跟著換。 87 | 2. 文件的範例只是一般的 HTML form,但在 Rails 傳送表單的時候會順便檢查 CSRF Token,所以通常會用 `form_for` 或 `form_tag` 來產生 HTML form。 88 | 3. 接續 2,為了讓 `form_for` 裡的 `checkout_product_path` 可以正常運作,我在 `config/routes.rb` 加了一些修改: 89 | 90 | ```ruby 91 | Rails.application.routes.draw do 92 | resources :products do 93 | member do 94 | post :checkout 95 | end 96 | end 97 | end 98 | ``` 99 | 100 | 重新整理頁面,這時候的畫面會變成這樣: 101 | 102 | ![image](/images/chapter28/payment-form.png) 103 | 104 | ### 測試卡號 105 | 106 | Braintree 有提供一組測試用的卡號: 107 | 108 | 卡號:4111-1111-1111-1111 (第一個 4,剩下按 1 按到底) 109 | 日期:只要超過今天的日期就行了 110 | 111 | 填完卡號以及有效期限按下送出後,沒意外的話應該會看到這個錯誤畫面: 112 | 113 | ![image](/images/chapter28/post-error.png) 114 | 115 | 那是因為我們還沒有寫這個 `checkout` Action,所以有這個錯誤訊息是正常的。到這裡,前端頁面的設定算是完成一部份了。為什麼說一部份?因為那個 `clientToken` 目前還是寫死的,它應該由我們的伺服器來傳給它才對,這也就是我們下一步要做的事。 116 | 117 | ## 使用 Braintree - 後端 118 | 119 | 後端要做幾件事情: 120 | 121 | 1. 安裝 `braintree` gem 122 | 2. 設定金鑰 123 | 3. 產生 `clientToken` 124 | 4. 接收到前端頁面 POST 過來的資訊,準備進行刷卡 125 | 126 | ### 1. 安裝 `braintree` gem 127 | 128 | 這個步驟還滿簡單的,只要在 Gemfile 裡加上 `gem 'braintree'` 就行了,存檔後記得執行 `bundle install` 指令確定有正確安裝完成。 129 | 130 | ### 2. 設定金鑰 131 | 132 | 接下來要設定一些金鑰資訊以便讓我們產生下一步所需的 Client Token。以我們在開發環境為例,請打開 `config/environments/development.rb` 檔案,在最下方加上這幾行: 133 | 134 | ```ruby 135 | Rails.application.configure do 136 | #...[略]... 137 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker 138 | end 139 | 140 | Braintree::Configuration.environment = :sandbox 141 | Braintree::Configuration.merchant_id = "use_your_merchant_id" 142 | Braintree::Configuration.public_key = "use_your_public_key" 143 | Braintree::Configuration.private_key = "use_your_private_key" 144 | ``` 145 | 146 | 其中 `use_your_merchant_id`、`use_your_public_key` 以及 `use_your_private_key` 這三個資訊,請到 Braintree 的上方選單「Account」→「My User」頁面,下方有一個「API Keys, Tokenization Keys, Encryption Keys」段落,裡面可以新增或取得所需的資訊: 147 | 148 | ![image](/images/chapter28/api-keys.png) 149 | 150 | > 注意:修改過 `config` 目錄下的檔案,通常都需要重新啟動 `rails server` 才會生效。 151 | 152 | ### 3. 產生 `clientToken` 153 | 154 | 回到 ProductsController 的 show Action,加上這行: 155 | 156 | ```ruby 157 | class ProductsController < ApplicationController 158 | #...[略]... 159 | 160 | def show 161 | @client_token = Braintree::ClientToken.generate 162 | end 163 | 164 | #...[略]... 165 | end 166 | ``` 167 | 168 | 產生一個 `@client_token` 的實體變數,準備給 View 使用。然後回到 `show.html.erb` 檔案,把剛剛很長而且寫死的那個 `clientToken` 換成我們剛剛產生的資訊: 169 | 170 | ```erb 171 |

<%= notice %>

172 | 173 |

174 | Title: 175 | <%= @product.title %> 176 |

177 | 178 |

179 | Description: 180 | <%= @product.description %> 181 |

182 | 183 |

184 | Price: 185 | <%= @product.price %> 186 |

187 | 188 | <%= link_to 'Back', products_path, class:'btn btn-default' %> 189 |
190 | 191 | <%= form_for(@product, url: checkout_product_path(@product), method: :post) do |f| %> 192 |
193 | <%= f.submit "確認付款", class:"btn btn-default btn-danger" %> 194 | <% end %> 195 | 196 | 197 | 204 | ``` 205 | 206 | 這樣就可以準備來刷卡了! 207 | 208 | ### 4. 接收到前端頁面 POST 過來的資訊,準備進行刷卡 209 | 210 | 按下送出,這時候它會去找 `checkout` Action,所以我們現在來完成這段功能。當按下送出之後,`checkout` Action 會收到的 `params` 的內容如下: 211 | 212 | ```ruby 213 | Parameters: {"utf8"=>"✓", "authenticity_token"=>"PdmlFcBf6AmjyNg9bM6nh3wppzdC3xPZGBRmv7NR58DKcYh4DsoV804YKI9pyfU+FyrzzRbh0iq6Tg9K/BL9ZA==", "payment_method_nonce"=>"47571afc-8316-0b1d-1619-24f29754a320", "id"=>"1"} 214 | ``` 215 | 216 | 這段資訊裡面我們會需要的是 `payment_method_nonce` 以及 `id`,所以只要截取這兩個資訊出來就行了: 217 | 218 | > 你有發現這串 params 裡面沒有「信用卡卡號」嗎? 219 | 220 | ```ruby 221 | class ProductsController < ApplicationController 222 | before_action :set_product, only: [:show, :edit, :update, :destroy, :checkout] 223 | #...[略]... 224 | 225 | def checkout 226 | if @product 227 | nonce = params[:payment_method_nonce] 228 | 229 | result = Braintree::Transaction.sale( 230 | amount: @product.price, 231 | payment_method_nonce: nonce 232 | ) 233 | 234 | if result 235 | redirect_to products_path, notice: "刷卡成功" 236 | else 237 | # 錯誤處理 238 | end 239 | else 240 | # 錯誤處理 241 | end 242 | end 243 | 244 | #...[略]... 245 | end 246 | ``` 247 | 248 | 說明: 249 | 250 | 1. 因為 `checkout` Action 也需要先把要刷卡的那項商品挑出來,所以在 `before_action` 也把它掛上去 251 | 2. `Braintree::Transaction.sale` 方法需傳入「金額」以及 Client 頁面傳過來的那個隨機數(nonce) 252 | 3. 如果刷卡錯誤需適時的提醒使用者哪裡發生錯誤 253 | 254 | 如果一切順利,應該就會轉往商品列表頁面了。 255 | 256 | 這時候回到 Braintree 的後台主頁面,可以看到我們的確刷了 100 塊錢: 257 | 258 | ![image](/images/chapter28/paid.png) 259 | 260 | ## 小結 261 | 262 | Braintree 算是相當容易串接的服務,不過在上面的例子我們僅傳了消費金額給 Braintree,事實上在實務上還需要傳更多資訊過去,例如訂單編號等,這樣店家才會知道到底賣了什麼、賣給誰。更多詳細使用方法,請參閱 Braintree 的 API 手冊。 263 | 264 | -------------------------------------------------------------------------------- /markdown/chapter24-organize-your-code.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | title: 程式碼整理 4 | comments: true 5 | permalink: /chapters/24-organize-your-code.html 6 | 7 | --- 8 | 9 | # 程式碼整理 10 | 11 | 當 Rails 專案成長到一定程度後,如果沒有好好的整理程式碼,很有可能發生重複的程式碼到處散落的情況。接下來這個章節是要介紹如何使用 Ruby 跟 Rails 內建的方法或設計來整理重複的程式碼。 12 | 13 | ## 在 View 出現有點複雜或重複的邏輯 14 | 15 | 先看一下這個畫面: 16 | 17 | ![image](/images/chapter24/user-list-1.png) 18 | 19 | 因為某些因素,在設計使用者性別(Gender)欄位的時候,可能會用數字 `1` 表示男生,用數字 `0` 表示女生。如果我想直接印出「男」、「女」字樣,可能會這樣寫: 20 | 21 | ```ruby 22 | 23 | <% @users.each do |user| %> 24 | 25 | <%= user.name %> 26 | <%= user.email %> 27 | 28 | <% if user.gender == 1 %> 29 | 男 30 | <% else %> 31 | 女 32 | <% end %> 33 | 34 | <%= link_to 'Show', user %> 35 | <%= link_to 'Edit', edit_user_path(user) %> 36 | <%= link_to 'Destroy', user, method: :delete, data: { confirm: 'Are you sure?' } %> 37 | 38 | <% end %> 39 | 40 | ``` 41 | 42 | 這裡使用 `if...else...` 判斷 `user.gender` 的值然後印出字樣,以結果來看是沒問題,但在開發 Rails 專案的時候,以 MVC 的結構來說,儘量不要讓 View 有邏輯判斷,View 的工作,就是乖乖的輸出資料就好。 43 | 44 | ### 1. 使用 View Helper 45 | 46 | 在前面第 14 章有介紹到如何使用 View Helper 來把這段邏輯藏起來: 47 | 48 | ```ruby 49 | # 檔案:app/helpers/users_helper.rb 50 | 51 | module UsersHelper 52 | def print_gender(user) 53 | if user.gender == 1 54 | "男" 55 | else 56 | "女" 57 | end 58 | end 59 | end 60 | ``` 61 | 62 | 這樣一來,原來那段 View 的寫法就可改成: 63 | 64 | ```erb 65 | <% @users.each do |user| %> 66 | 67 | <%= user.name %> 68 | <%= user.email %> 69 | <%= print_gender(user) %> 70 | <%= link_to 'Show', user %> 71 | <%= link_to 'Edit', edit_user_path(user) %> 72 | <%= link_to 'Destroy', user, method: :delete, data: { confirm: 'Are you sure?' } %> 73 | 74 | <% end %> 75 | ``` 76 | 77 | 這樣一來,原來的 `if..else..` 邏輯就可以被包到 View Helper 裡,而且其它頁面要用也可以用得上。 78 | 79 | ### 2. 在 Model 上新增實體方法 80 | 81 | 除了使用 View Helper,以上面這個例子來說,也可在 User Model 裡直接新增一個實體方法: 82 | 83 | ```ruby 84 | class User < ApplicationRecord 85 | validates :name, presence: true 86 | 87 | def show_gender 88 | if gender == 1 89 | "男" 90 | else 91 | "女" 92 | end 93 | end 94 | end 95 | ``` 96 | 97 | 然後 View 就可改寫成: 98 | 99 | ```erb 100 | <% @users.each do |user| %> 101 | 102 | <%= user.name %> 103 | <%= user.email %> 104 | <%= user.show_gender %> 105 | <%= link_to 'Show', user %> 106 | <%= link_to 'Edit', edit_user_path(user) %> 107 | <%= link_to 'Destroy', user, method: :delete, data: { confirm: 'Are you sure?' } %> 108 | 109 | <% end %> 110 | ``` 111 | 112 | 哪種做法比較好? 113 | 114 | 如果這個邏輯可能跟其它同一個 View 的變數有關,我會選擇第 1 種做法;如果就只是像這個例子一樣,資料的呈現僅與自身 Model 有關,我個人會比較偏好第 2 種寫法。 115 | 116 | ## 在 Controller 好幾個 Action 都看到在做一樣的事 117 | 118 | 舉個例子來說: 119 | 120 | ```ruby 121 | class UsersController < ApplicationController 122 | def show 123 | @user = User.find(params[:id]) 124 | end 125 | 126 | def edit 127 | @user = User.find(params[:id]) 128 | end 129 | 130 | def update 131 | @user = User.find(params[:id]) 132 | respond_to do |format| 133 | #...[略]... 134 | end 135 | end 136 | 137 | def destroy 138 | @user = User.find(params[:id]) 139 | @user.destroy 140 | respond_to do |format| 141 | #...[略]... 142 | end 143 | end 144 | end 145 | ``` 146 | 147 | 在這個 Controller 裡,`show`、`edit`、`update` 以及 `destroy` 都有用 `User.find(params[:id])` 的方法在查詢使用者,像這種在同一個 Controller 裡有好幾個 Action 都在做類似的事,可以使用 Controller 內建的 Callback,例如: 148 | 149 | ```rubbby 150 | class UsersController < ApplicationController 151 | before_action :set_user, only: [:show, :edit, :update, :destroy] 152 | 153 | #...[略]... 154 | 155 | private 156 | def set_user 157 | @user = User.find(params[:id]) 158 | end 159 | end 160 | ``` 161 | 162 | 定義一個 `set_user` 方法(通常會掛在 `private` 區塊),然後掛在 `before_action` 這個 Callback 上,並且僅在 `show`、`edit`、`update` 以及 `destroy` 這 4 個 Action 執行前先執行。 163 | 164 | 其它可以用的 Callback 還有 `after_action` 跟 `around_action` 等方法,更多詳細內容可參考 http://api.rubyonrails.org/classes/AbstractController/Callbacks/ClassMethods.html 165 | 166 | ## 在 Controller 看到有點長的連續技 167 | 168 | 不知道大家有沒有在 Controller 看過類似這樣的程式碼: 169 | 170 | ```ruby 171 | class UsersController < ApplicationController 172 | def index 173 | @users = User.where(gender: 0, city: 'Taipei').where("age >= 18") 174 | end 175 | end 176 | ``` 177 | 178 | 雖然看得出來大概是要查「住台北的成年女性」的使用者,但這樣寫等於是把這個查詢的「邏輯」寫在 Controller 裡了,如果在別的 Controller 要查一樣的資料,就又得再複製、貼上一次。 179 | 180 | Rails 的 Model 有提供 `Scope` 或類別方法可以把這個邏輯包起來: 181 | 182 | ```ruby 183 | class User < ApplicationRecord 184 | validates :name, presence: true 185 | 186 | scope :adult_female_live_in_taipei, -> { where(gender: 0, city: 'Taipei').where("age >= 18") } 187 | end 188 | ``` 189 | 190 | 這樣一來原來那段就可簡化成: 191 | 192 | ```ruby 193 | class UsersController < ApplicationController 194 | def index 195 | @users = User.adult_female_live_in_taipei 196 | end 197 | end 198 | ``` 199 | 200 | 不僅在每個地方都可以使用,而且光看方法名字就大概可以猜得出來是要查什麼資料。 201 | 202 | ## 好幾個 Controller 或 Model 都有一樣的功能 203 | 204 | 如果我們做了後台管理系統,應該會希望「所有後台管理系統的 Controller 在 `before_action` 的地方都要先檢查有沒有登入」。當然,你可以在每個後台 Controller 都加上權限控管,但也可考慮使用物件導向程式設計的「繼承」來解決這件事。 205 | 206 | 在 Rails 的 Controller,如果沒有特別改過,預設應該是繼承自 `ApplicationController` 這個類別,大概像這樣: 207 | 208 | ![image](/images/chapter24/inheritance-1.png) 209 | 210 | 但如果想讓每個後台管理系統都會在 `before_action` 做某件事,可以額外新增一個 `Admin::BaseController` 類別: 211 | 212 | ```ruby 213 | class Admin::BaseController < ApplicationController 214 | before_action :do_something 215 | 216 | private 217 | def do_something 218 | #.... 219 | end 220 | end 221 | ``` 222 | 223 | 然後讓所有後台的 Controller 都改繼承這個 `Admin::BaseController`: 224 | 225 | ```ruby 226 | class Admin::UsersController < Admin::BaseController 227 | #...[略]... 228 | end 229 | ``` 230 | 231 | 原來的關係圖就會變成像這樣: 232 | 233 | ![image](/images/chapter24/inheritance-2.png) 234 | 235 | 利用物件導向的繼承功能,可以把共同的程式碼集中在上層類別。 236 | 237 | ## 繼承雖然容易用,但不是每個 Controller 或 Model 都需要這個功能... 238 | 239 | 雖然繼承可以「把重複的程式碼寫在上層類別」,但很多時候並不是每個 Controller 或 Model 都想要有這個功能。就跟在第 8 章物件導向程式設計章節的「模組」一樣,有需要這個功能才引進來。 240 | 241 | Rails 有提供 `Concern` 的功能,可以把「共同的行為」集中起來,有需要的再「引入」,而不使用繼承。就是「不要為了想要會飛就去當鳥的小孩」的概念: 242 | 243 | 舉個例子,我有 User 跟 AdminUser 這兩個 Model,我希望這兩個 Model 都: 244 | 245 | 1. 都有 `has_one :profile` 設定 246 | 2. 都有 `show_gender` 方法可以顯示性別字串 247 | 2. 在在新增帳號的時候都可以對輸入的密碼加密 248 | 249 | ```ruby 250 | module Profileable 251 | extend ActiveSupport::Concern 252 | 253 | included do 254 | has_one :profile 255 | before_create :encrypt_user_password 256 | end 257 | 258 | module ClassMethods 259 | end 260 | 261 | def show_gender 262 | if gender == 1 263 | "男" 264 | else 265 | "女 " 266 | end 267 | end 268 | 269 | private 270 | def encrypt_user_password 271 | # 對密碼加密... 272 | end 273 | end 274 | ``` 275 | 276 | 其實 Concern 就是 Ruby 裡 Module 的概念,說明如下: 277 | 278 | 1. `included do ... end` 裡面放的是當這個 Module 被 include 的時候會做的事 279 | 2. `ClassMethod` 這個 Module 裡面可以定義方法,但定義的方法會直接變成類別方法 280 | 3. `show_gender` 方法被 include 之後就會變成該類別的實體方法 281 | 282 | 接下來,在 `User` Model 加上一行: 283 | 284 | ```ruby 285 | class User < ApplicationRecord 286 | include Profileable 287 | end 288 | ``` 289 | 290 | 另外 `AdminUser` Model 也可以加上這行: 291 | 292 | ```ruby 293 | class AdminUser < ApplicationRecord 294 | include Profileable 295 | end 296 | ``` 297 | 298 | 這樣一來,這兩個 Model 就都有 `Profileable` 這個 Module 所提供的功能了。 299 | 300 | -------------------------------------------------------------------------------- /markdown/chapter18-model-validation-and-callback.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | title: Model 驗證及回呼 4 | comments: true 5 | permalink: /chapters/18-model-validation-and-callback.html 6 | 7 | --- 8 | 9 | # Model 驗證及回呼 10 | 11 | - [資料驗證(Validation)](#validation) 12 | - [回呼(Callback)](#callback) 13 | 14 | ## 資料驗證(Validation) 15 | 16 | 開發網站應用程式,資料的正確性是很重要的。大家應該都不會想被有意或無意的在資料庫裡塞了奇怪的資料,所以通常都會加上資料驗證的機制,確保寫入的資料是符合規定的。 17 | 18 | ### 驗證該在哪裡做? 19 | 20 | 那,這個資料驗證機制該寫在哪裡比較好呢?有常見的選項有: 21 | 22 | 1. 前端驗證:在 HTML 頁面使用 JavaScript 在使用者填寫資料的時候就先檢查。 23 | 2. 後端驗證:資料傳進來在寫入資料庫之前之後再檢查。 24 | 3. 資料庫驗證:直接由資料庫本身所提供的功能來做資料驗證。 25 | 26 | 雖然前端驗證簡單容易做,但如果如果使用者關掉 JavaScript 功能,或是直接檢視 HTML 原始碼,自己做一個一樣的表單,一樣可以送資料進來,所以即使在 HTML 頁面已經有驗證,在寫入資料庫前還是得再做一次檢查。 27 | 28 | 資料庫驗證雖然可行,但缺點不見得每種資料庫系統都有提供一樣的功能,一但換了資料庫系統可能會沒辦法正常運作。但如果這個資料庫還有跟其它程式語言開發的系統共用的話,資料庫驗證比較能確保不管是哪個系統寫進來的資料都是正確的了。 29 | 30 | 資料驗證這件事在 Rails 的 MVC 三分天下的架構中,Controller 跟 Model 都可以做這件事,要在 View 裡寫 JavaScript 做檢查也可以,但這件事如果交給 Controller 或 View 來做的話,一來會讓程式碼的邏輯變得更複雜,二來這個驗證也不容易被重複使用,也不容易被測試,所以資料機制寫在 Model 裡是比較合理而且單純的。 31 | 32 | ### 在 Model 裡加上驗證 33 | 34 | 假設我們有一個叫做 `Article` 的 Model,然後我們希望每篇文章的文章標題(title)是必填資訊,那我們可以這樣寫: 35 | 36 | ```ruby 37 | class Article < ApplicationRecord 38 | validates :title, presence: true 39 | end 40 | ``` 41 | 42 | 中間那行的意思是「title 這個欄位為必填欄位」,讓我們開 `rails console` 起來試一下: 43 | 44 | $ rails console 45 | >> a1 = Article.new 46 | => #
47 | 48 | 先用 `new` 方法建立一個 Article 物件,然後用 `errors` 方法看一下這個物件有沒有什麼狀況: 49 | 50 | >> a1.errors.any? 51 | => false 52 | 53 | 看來沒什麼問題。接著試著呼叫 `save` 方法把這顆物件存入資料表: 54 | 55 | >> a1.save 56 | (0.2ms) begin transaction 57 | (0.1ms) rollback transaction 58 | => false 59 | 60 | 失敗了,並且回傳 false,來看看到底是哪邊有問題: 61 | 62 | >> a1.errors.any? 63 | => true 64 | 65 | 原本沒問題的,在 `save` 之後變得有問題了。來看看錯誤訊息是什麼: 66 | 67 | >> a1.errors.full_messages 68 | => ["Title can't be blank"] 69 | 70 | 除了 `validates :title, presence: true` 之外,還有另一種寫法: 71 | 72 | ```ruby 73 | class Article < ActiveRecord::Base 74 | validates_presence_of :title 75 | end 76 | ``` 77 | 78 | 效果也是一樣的。 79 | 80 | 除了 `presence` 之外,Rails 還有提供其它像是 `uniqueness`、`length` 或 `numericality` 等便利的驗證器,使用方法可直接參考 Rails Guide 的 [Validations 章節](http://guides.rubyonrails.org/active_record_validations.html)。 81 | 82 | ### 不是每個方法都會被驗證... 83 | 84 | 雖然驗證功能很方便,但並不是每種方法都會觸發驗證,僅有以下這些方法會觸發驗證: 85 | 86 | - create 87 | - create! 88 | - save 89 | - save! 90 | - update 91 | - update! 92 | 93 | 其它方法不會經過驗證流程喔 94 | 95 | 像 `toggle!` 或 `increment!` 等方法會跳過驗證流程。 96 | 97 | > 有驚嘆號版本的,如果驗證未通過會產生錯誤訊息,而沒有驚嘆號版本則僅會回傳該 Model 的一個空物件。 98 | 99 | 如果想要主動的跳過驗證的話,也可在呼叫 `save` 的時候加上 `validate: false` 的參,像這樣: 100 | 101 | ```ruby 102 | user1 = User.new 103 | user1.save(validate: false) 104 | ``` 105 | 106 | ### 驗證沒過的時候... 107 | 108 | 當資料驗證沒過的時候,可以透過該物件本身的 `errors` 方法得知。一樣先做一顆新的 User 物件,其中 `name` 欄位為必填欄位: 109 | 110 | $ rails console 111 | >> user1 = User.new 112 | => # 113 | 114 | 試著呼叫 `save` 方法,要把這筆資料寫入資料表: 115 | 116 | >> user1.save 117 | (0.3ms) begin transaction 118 | (0.1ms) rollback transaction 119 | => false 120 | 121 | 失敗了!這時候可以透過 `errors` 方法看一下到底是哪裡出錯: 122 | 123 | >> user1.errors 124 | => #["can't be blank"]}, @details={:name=>[{:error=>:blank}]}> 125 | >> user1.errors.full_messages 126 | => ["Name can't be blank"] 127 | 128 | 喔,原來是 `name` 欄位沒填寫。 129 | 130 | ### 在 `save` 的時候才發生驗證錯誤嗎? 131 | 132 | 有些人可能以為要呼叫 `save` 或 `create` 方法,試圖把資料寫入資料表的時候才會發生驗證錯誤,其實上不用寫入資料表也可以知道這筆資料是否有效。先用 `new` 方法建立一個 User 物件: 133 | 134 | >> user1 = User.new 135 | => # 136 | 137 | 這時候檢查一下是不是有錯誤訊息: 138 | 139 | >> user1.errors.any? 140 | => false 141 | 142 | 很好!沒有任何錯誤訊息。這時候用 `valid?` 方法問一下這筆資料是否能通過驗證: 143 | 144 | >> user1.valid? 145 | => false 146 | 147 | 啊,沒通過驗證!再回頭看一下是不是有錯誤訊息: 148 | 149 | >> user1.errors.any? 150 | => true 151 | >> user1.errors.full_messages 152 | => ["Name can't be blank"] 153 | 154 | 即使沒有執行 `save` 方法,也是會觸發驗證的。 155 | 156 | 157 | ### 自訂驗證器 Validator 158 | 159 | 現有的驗證器不夠用嗎?有幾種方式可以自訂驗證器: 160 | 161 | #### 1. 寫一個方法,掛到 `validate` 方法上: 162 | 163 | ```ruby 164 | class User < ActiveRecord::Base 165 | validate :name_validator 166 | 167 | private 168 | def name_validator 169 | unless name.starts_with? 'Ruby' 170 | errors[:name] << "必需是 Ruby 開頭喔!" 171 | end 172 | end 173 | end 174 | ``` 175 | 176 | > 注意:這個方法是 `validate`,不是 `validates` 喔 177 | 178 | 這種寫法滿簡單的,就是直接寫一個一般的方法(通常會放在 `private` 區塊),當條件不符規定的時候,就在 `errors` 這個 Hash 裡面塞錯誤訊息。用起來就跟一般的驗證器差不多: 179 | 180 | $ rails console 181 | Running via Spring preloader in process 4628 182 | Loading development environment (Rails 5.0.1) 183 | >> user1 = User.new(name: "孫悟空") 184 | => # 185 | >> user1.save 186 | (0.1ms) begin transaction 187 | (0.3ms) rollback transaction 188 | => false 189 | >> user1.errors.full_messages 190 | => ["Name 必需是 Ruby 開頭喔!"] 191 | 192 | 193 | #### 2. 遵循 Rails 的驗證器規則: 194 | 195 | 想寫出這樣的語法嗎? 196 | 197 | ```ruby 198 | class User < ActiveRecord::Base 199 | validates :name, presence: true, begin_with_ruby: true 200 | end 201 | ``` 202 | 203 | 這個驗證器可以跟其它內建的驗證器一起混著使用,使用起來會更簡潔。要寫這樣的驗證器需要符合 Rails Validator 的命名規則: 204 | 205 | 1. 參數是 `begin_with_ruby` 的話,類別名稱則是 `BeginWithRuby` 加上 `Validator`,並繼承自 `ActiveModel::EachValidator` 類別。 206 | 2. 必須實作 `validate_each` 方法。 207 | 208 | 大概像這樣: 209 | 210 | ```ruby 211 | class BeginWithRubyValidator < ActiveModel::EachValidator 212 | def validate_each(record, attribute, value) 213 | unless value.starts_with? 'Ruby' 214 | record.errors[attribute] << "必需是 Ruby 開頭喔!" 215 | end 216 | end 217 | end 218 | ``` 219 | 220 | 然後在使用的時候,就是跟一般的 `validates` 差不多: 221 | 222 | ```ruby 223 | class User < ActiveRecord::Base 224 | validates :name, begin_with_ruby: true 225 | end 226 | ``` 227 | 228 | 在 `rails console` 試一下效果: 229 | 230 | $ rails console 231 | Running via Spring preloader in process 4750 232 | Loading development environment (Rails 5.0.1) 233 | >> user1 = User.new(name: "孫悟空") 234 | => # 235 | >> user1.save 236 | (0.1ms) begin transaction 237 | (0.0ms) rollback transaction 238 | => false 239 | >> user1.errors.full_messages 240 | => ["Name 必需是 Ruby 開頭喔!"] 241 | 242 | 243 | ## 回呼(Callback) 244 | 245 | 資料在要存到資料表的過程中,其實不是直接把資料放進去這麼簡單。不同的行為(例如存檔、或刪除)可能會有不同的流程,舉個例子來說,當呼叫 `save` 方法的時候,整個資料寫入的過程大概會是以下的流程: 246 | 247 | ![image](/images/chapter18/model-lifecycle.png) 248 | 249 | 其中,顏色比較深的那幾個流程是有機會可以掛上一些方法,又稱之回呼(Callback),可以在這些流程執行的時候做一些事,像是這樣: 250 | 251 | ```ruby 252 | require 'digest' 253 | 254 | class User < ActiveRecord::Base 255 | before_create :encrypt_email 256 | 257 | private 258 | def encrypt_email 259 | self.email = Digest::MD5.hexdigest(email) 260 | end 261 | end 262 | ``` 263 | 264 | 上面這段範例可以在建立使用者資料之前,先對 email 進行 MD5 加密。 265 | 266 | > 注意:`before_save` 跟 `before_create` 的差別,在於 `before_save` 是每次存檔的時候都會經過,但 `before_create` 只有在「新增」的時候才會觸發。 267 | 268 | 除了這樣的寫法,如果內容單純的話,也是可以使用 Block 的方式來寫: 269 | 270 | ```ruby 271 | require 'digest' 272 | class User < ActiveRecord::Base 273 | before_create do 274 | self.email = Digest::MD5.hexdigest(email) 275 | end 276 | end 277 | ``` 278 | 279 | 關於其它回呼的使用方式,可參考 Rails Guide 上關於 [Callback 章節](http://guides.rubyonrails.org/active_record_callbacks.html)的說明 280 | 281 | -------------------------------------------------------------------------------- /markdown/chapter02-environment-setup.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | title: 環境設定 4 | comments: true 5 | permalink: /chapters/02-environment-setup.html 6 | 7 | --- 8 | 9 | # 環境設定 10 | 11 | 要開始用 Ruby 或 Rails 來開發網站之前,第一件事(其實可能也是最難的事?)就是要先搞定開發環境。 12 | 13 | - [安裝 Ruby](#install-ruby) 14 | - [用 RVM 來管理 Ruby 版本](#use-rvm) 15 | - [安裝 Rails](#install-rails) 16 | - [建立 Rails 專案](#build-rails-project) 17 | 18 | ## 安裝 Ruby 19 | 20 | ### Unix/Linux 作業系統 21 | 22 | 如果您使用的是 Ubuntu 之類的系統,可以直接使用 `apt-get` 來安裝 Ruby: 23 | 24 | $ sudo apt-get install ruby 25 | 26 | 如果是 CentOS 之類的系統,則是使用 `yum` 來安裝: 27 | 28 | $ sudo yum install ruby 29 | 30 | 因為使用了 `sudo` 指令,所以你應該會被提示需要輸入目前的登入密碼。 31 | 32 | ### Mac 作業系統 33 | 34 | 如果是 Mac 作業系統,比較新的版本均已內建 Ruby 2.0 版本,如果沒有的話,建議可使用 [Homebrew](http://brew.sh/) 這個套件管理工具來安裝 Ruby: 35 | 36 | $ brew install ruby 37 | 38 | ### Windows 作業系統 39 | 40 | 在 Windows 平台可根據您的需求,選擇安裝 [Ruby Installer](http://rubyinstaller.org/) 或 [Rails Installer](http://railsinstaller.org/en),基本上 Rails Installer 跟 Ruby Installer 沒太大的差別,只是後者多加入了 Rails 開發的相關工具(例如 Git、Bundler 等): 41 | 42 | ### 其它系統 43 | 44 | 更多其它平台的安裝方式,或是想要直接下載原始碼自行編譯,請參閱 [Ruby 官方網站](https://www.ruby-lang.org/)的安裝說明。 45 | 46 | ## 用 RVM 來管理 Ruby 版本 47 | 48 | Ruby 有許多的版本(1.8/1.9/2.0/2.1/2.2/2.3/2.4)以及眾多的分支實作品(例如 JRuby/IronRuby/Rubinius/Macruby/mruby 等),算一算有不少排列組合,如果想要在自己機器上安裝不同版本會有點麻煩,而且萬一亂裝把工作環境弄壞了還得花時間重建。如果是要裝在伺服器上,如果你不是系統管理員,還不一定有足夠的權限可以安裝。使用 VirtualBox 的軟體可以來模擬作業環境,玩壞了隨時都可以很快的還原或重建一個新的,即使這樣還是有點麻煩。 49 | 50 | 如果各位跟我一樣都喜歡玩些新玩具,但又擔心環境被弄髒弄壞,推薦大家可以試試 [RVM](https://rvm.io/)(Ruby Version Manager)。有 RVM 的幫忙,你可以安心的在你的電腦裡同時安裝多個不同版本的 Ruby/Rails 而不會搞混,隨時都可以輕鬆的切換。 51 | 52 | RVM 是把程式安裝在你的的個人帳號目錄下,不需要的時候就整個 `~/.rvm` 資料夾刪除就行了,不會影響原來系統的設定。也就是因為 RVM 是安裝在你的個人帳號底下,所以你在安裝過程中不需要管理者(root)的權限就可以安裝其它相關的套件。 53 | 54 | ### 安裝 RVM 55 | 56 | RVM 的安裝滿簡單的,只要二行指令即可完成。安裝步驟請直接參閱 [RVM 官網](https://rvm.io/)的安裝說明。 57 | 58 | ### 使用 RVM 59 | 60 | 接著我們來看一些在 RVM 裡常用的指令。在終端機下輸入 `rvm list known` 會列出目前有哪些可以安裝的列表: 61 | 62 | $ rvm list known 63 | # MRI Rubies 64 | [ruby-]1.8.6[-p420] 65 | [ruby-]1.8.7[-head] # security released on head 66 | [ruby-]1.9.1[-p431] 67 | [ruby-]1.9.2[-p330] 68 | [ruby-]1.9.3[-p551] 69 | [ruby-]2.0.0[-p648] 70 | [ruby-]2.1[.10] 71 | [ruby-]2.2[.6] 72 | [ruby-]2.3[.3] 73 | [ruby-]2.4[.0] 74 | ruby-head 75 | 76 | [...略...] 77 | 78 | macruby[-0.12] 79 | macruby-nightly 80 | macruby-head 81 | 82 | # IronRuby 83 | ironruby[-1.1.3] 84 | ironruby-head 85 | 86 | 幾乎目前常見的 Ruby 分支實作品都有。列表裡的中括號表示那些是可以省略的,所以如果你這樣輸入: 87 | 88 | $ rvm install 2.3 89 | 90 | RVM 會自動找 `[ruby-]2.4[.0]` 這個版本的 Ruby 來安裝。前面提到可以安裝多個不同的版本,所以如果你喜歡,也可以再裝個 `1.9.3` 的版本: 91 | 92 | $ rvm install 1.9.3 93 | 94 | 安裝完成後,我們可以使用 `rvm list` 來查看目前電腦裡已經安裝哪些版本的 Ruby: 95 | 96 | $ rvm list 97 | 98 | ruby-1.9.3-p551 [ x86_64 ] 99 | ruby-2.2.1 [ x86_64 ] 100 | ruby-2.2.2 [ x86_64 ] 101 | ruby-2.3.0 [ x86_64 ] 102 | ruby-2.3.1 [ x86_64 ] 103 | ruby-2.3.3 [ x86_64 ] 104 | =* ruby-2.4.0 [ x86_64 ] 105 | 106 | # => - current 107 | # =* - current && default 108 | # * - default 109 | 110 | 因為工作上需求,所以在我的電腦上裝了好幾個版本的 Ruby。在 2.4.0 版前面的 `=*` 符號則是表示我目前正在使用這個版本。你可以在終端機下輸入這個指令,看看目前 Ruby 的版本: 111 | 112 | $ ruby -v 113 | ruby 2.4.0p0 (2016-12-24 revision 57164) [x86_64-darwin15] 114 | 115 | 如果要切換到其它版本的 Ruby,例如想要切換到 1.9.3 版本: 116 | 117 | $ rvm use 1.9.3 118 | 119 | 想少打幾個字的話,`use` 也可以省略: 120 | 121 | $ rvm 1.9.3 122 | 123 | 再來看一下Ruby的版本: 124 | 125 | $ ruby -v 126 | ruby 1.9.3p551 (2014-11-13 revision 48407) [x86_64-darwin13.4.0] 127 | 128 | 這樣就切換到 Ruby 1.9.3 了,相當便利!不過有個小問題,就是使用 RVM 指定的 Ruby 版本會在每次開啟新的終端機視窗的時候變回預設值(也就是變回系統內建的 Ruby 版本),所以如果你希望每次開終端機視窗的時候都會自動切到 `2.4.0` 版的話: 129 | 130 | $ rvm 2.4.0 --default 131 | 132 | 這樣之後每次開終端機視窗就會自動幫你切換到 2.4.0 版了。如果想切回到原來系統內建的版本,只要執行這個指令: 133 | 134 | $ rvm system 135 | 136 | 想移除某個版本的 Ruby 的話: 137 | 138 | $ rvm uninstall 2.4.0 139 | 140 | 這樣就可以把 `2.4.0` 版本移除掉了。如果是整個 RVM 都不想要了,只要把個人帳號 home 資料夾底下的 `.rvm` 資料夾整個移除,就會整個清潔溜溜了,完全不會動到系統內建的 Ruby。 141 | 142 | ### 運作原理 143 | 144 | 你也許會好奇為什麼 RVM 可以這麼神奇的切換 Ruby 的環境。讓我們來把系統的 PATH 變數印出來看看: 145 | 146 | $ echo $PATH 147 | /Users/user/.rvm/gems/ruby-2.4.0/bin:/Users/user/.rvm/gems/ruby...[略]... 148 | 149 | 然後查看一下 Ruby 的位置: 150 | 151 | $ which ruby 152 | /Users/user/.rvm/rubies/ruby-2.4.0/bin/ruby 153 | 154 | 再查一下 Ruby 版本: 155 | 156 | $ ruby -v 157 | ruby 2.4.0p0 (2016-12-24 revision 57164) [x86_64-darwin15] 158 | 159 | (以上內容是我自己電腦裡的設定,應該跟各位的環境不同) 160 | 161 | 接下來把 RVM 切換到 1.9.3 版本: 162 | 163 | $ rvm 1.9.3 164 | 165 | 再重複把剛剛的那些資訊印出來: 166 | 167 | $ echo $PATH 168 | /Users/user/.rvm/gems/ruby-1.9.3-p551/bin:/Users/user/.rvm/gems...[略]... 169 | 170 | $ which ruby 171 | /Users/user/.rvm/rubies/ruby-1.9.3-p551/bin/ruby 172 | 173 | $ ruby -v 174 | ruby 1.9.3p551 (2014-11-13 revision 48407) [x86_64-darwin13.4.0] 175 | 176 | 仔細看上面的輸出結果,就會發現其實 RVM 是把不同版本的 Ruby 安裝在你的個人帳號底下的 `.rvm` 目錄裡。當你切換不同版本的 Ruby 的時候,RVM 會幫你把系統預設的 PATH 的最前面加上這個 `.rvm` 的資料夾。接下來當你在終端機底下輸入 `ruby` 指令時,系統原本的 `/usr/bin/ruby` 因為在 PATH 的比較後面的位置,所以系統只會先找到 RVM 版本的 Ruby(也就是原來系統的 Ruby 被鬼摭眼了)。如果各位有興趣,也可以試著輸入 `rvm info` 指令來看看 RVM 幫你做了哪些設定。 177 | 178 | ### 除了 RVM 之外... 179 | 180 | 我自己個人習慣使用 RVM,除了 RVM 之外還有其它的選擇,例如 [rbenv](https://github.com/rbenv/rbenv) 及 [chruby](https://github.com/postmodern/chruby),這些 Ruby 版本管理工具各有其優、缺點,還請大家自己去試用看看,然後選一套自己覺得順手的來用吧。 181 | 182 | ## 安裝 Rails 183 | 184 | 完成 Ruby 安裝後,接下來就準備來安裝 Rails。在開放原始碼的圈子,有非常多的善心人士開發好了功能強大又可免費取用的套件,在 Ruby 的世界我們稱它叫 `gem`。Ruby on Rails 這個網站開發框架本身也是一個 gem(更準確的說,應該是一群 gem 的集合體),要安裝 rails 的話,只要使用 `gem install` 指令加上套件名稱即可,像這樣: 185 | 186 | $ gem install rails 187 | Fetching: i18n-0.7.0.gem (100%) 188 | Successfully installed i18n-0.7.0 189 | Fetching: thread_safe-0.3.5.gem (100%) 190 | Successfully installed thread_safe-0.3.5 191 | Fetching: tzinfo-1.2.2.gem (100%) 192 | Successfully installed tzinfo-1.2.2 193 | Fetching: concurrent-ruby-1.0.4.gem (100%) 194 | Successfully installed concurrent-ruby-1.0.4 195 | Fetching: activesupport-5.0.1.gem (100%) 196 | Successfully installed activesupport-5.0.1 197 | ...[略]... 198 | 36 gems installed 199 | 200 | 從安裝過程的訊息可大概看到 `5.0.1` 的字樣。如果過程沒發生錯誤訊息的話,接下來確認一下是不是安裝了正確的版本: 201 | 202 | $ rails -v 203 | Rails 5.0.1 204 | 205 | 搞定!接下來,我們就要用它來建立第一個 Rails 專案了。 206 | 207 | ## 建立 Rails 專案 208 | 209 | Rails 安裝完成後,接下來就用它來產生一個名為 `hello_rails` 的 Rails 專案,建立新專案用的是 `new` 這個參數: 210 | 211 | $ rails new hello_rails 212 | create 213 | create README.md 214 | create Rakefile 215 | create config.ru 216 | create .gitignore 217 | create Gemfile 218 | create app 219 | ...[略]... 220 | create vendor/assets/javascripts/.keep 221 | create vendor/assets/stylesheets 222 | create vendor/assets/stylesheets/.keep 223 | remove config/initializers/cors.rb 224 | run bundle install 225 | Fetching gem metadata from https://rubygems.org/.......... 226 | Fetching version metadata from https://rubygems.org/.. 227 | Fetching dependency metadata from https://rubygems.org/. 228 | ...[略]... 229 | Using rails 5.0.1 230 | Installing sass-rails 5.0.6 231 | Bundle complete! 15 Gemfile dependencies, 62 gems now installed. 232 | Use `bundle show [gemname]` to see where a bundled gem is installed. 233 | run bundle exec spring binstub --all 234 | * bin/rake: spring inserted 235 | * bin/rails: spring inserted 236 | 237 | `rails new hello_rails` 這個幫你產生了一個名為 `hello_rails` 的目錄,接下來請使用 `cd` 指令進到剛剛產生的這個目錄: 238 | 239 | $ cd hello_rails 240 | 241 | 進到這個專案之後,什麼事都不用做,直接啟動 Rails 附的 web server: 242 | 243 | $ rails server 244 | => Booting Puma 245 | => Rails 5.0.1 application starting in development on http://localhost:3000 246 | => Run `rails server -h` for more startup options 247 | Puma starting in single mode... 248 | * Version 3.6.2 (ruby 2.4.0-p0), codename: Sleepy Sunday Serenity 249 | * Min threads: 5, max threads: 5 250 | * Environment: development 251 | * Listening on tcp://localhost:3000 252 | Use Ctrl-C to stop 253 | 254 | 打開瀏覽器,連上網址 `http://localhost:3000/` ,你應該可以看到這個畫面: 255 | 256 | ![image](/images/chapter02/welcome_page.png) 257 | 258 | 恭喜你,你已經順利把 Ruby 跟 Rails 安裝完成,而且建立了一個 Rails 專案並順利跑起來了。雖然環境安裝看起來只是一小步,但這一小步對第一次接觸終端機、要打一堆指令的人來說已經是不小的一步了。 259 | 260 | -------------------------------------------------------------------------------- /markdown/chapter08-ruby-basic-4.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | title: 類別(Class)與模組(Module) 4 | comments: true 5 | permalink: /chapters/08-ruby-basic-4.html 6 | 7 | --- 8 | 9 | # 類別(Class)與模組(Module) 10 | 11 | Rails 不是一種程式語言,它是一種用 Ruby 這個程式語言所開發出來的網頁開發框架(Web Framework)。 12 | 13 | 接下來幾個章節的目的並不是要詳細的介紹 Ruby 這個程式語言所有的功能,而是希望讓大家對 Ruby 有足夠的基本認識,之後大家在閱讀或撰寫 Rails 專案的時候,會比較知道 Rails 在寫些什麼。 14 | 15 | - [類別(Class)](#class) 16 | - [模組(Module)](#module) 17 | 18 | ## 類別(Class) 19 | 20 | Ruby 是一款物件導向程式語言(Objected-Oriented Programming, OOP),在 Ruby 的世界裡,幾乎所有的東西都是物件。但,到底什麼是「物件」? 21 | 22 | ### 什麼是物件? 23 | 24 | > 物件(object) = 狀態(state) + 行為(behavior) 25 | 26 | 在現實生活中,路上跑的車子、天上飛的鳥,你我他,看得到、摸得到的都可通稱為之物件(Object)。物件會有狀態跟行為,例如我這個人會有是「黑色頭髮」、「黃色皮膚」、「年紀 18 歲(?)」等狀態,也會有「吃飯」、「睡覺」、「走路」、「講話」等行為。 27 | 28 | 為了讓大家更容易學習程式設計,許多程式語言都有引進物件的概念,讓程式架構更容易組織、整理。而且 Ruby 又是一款物件化很徹底的程式語言,在 Ruby 的世界,看的到的幾乎都是物件,數字 1、字串 "hello",陣列、Hash 都是物件。 29 | 30 | ### 等等,為什麼說「幾乎」? 31 | 32 | 「在 Ruby 裡所有東西都是物件」,但其實也是有例外的,在 Ruby 裡的 Block 就不是物件,Block 本身沒辦法單獨的存活在 Ruby 的世界裡。 33 | 34 | ### 什麼是類別? 35 | 36 | 大家也許在夜市有看過有人在賣雞蛋糕,有小貓、小狗或其它可愛動物造型,只要把調配好的麵粉糊倒進模具,壓一下,幾分鐘後就會有香噴噴又造型可愛的雞蛋糕可以吃了。 37 | 38 | ![image](/images/chapter08/cake_maker.jpg) 39 | photo by [Bryan Liu](https://www.flickr.com/photos/bryanliu99/) 40 | 41 | 那個烤盤模具,就是類別(Class)的概念。如果沒意外,一樣形狀的模具,放一樣的原料進去,做出來雞蛋糕的型狀應該都會長得一樣。而這個做出來的雞蛋糕,以物件導向程式設計的概念來說便稱之「實體(instance)」。 42 | 43 | ### 定義類別 44 | 45 | 在 Ruby 要定義一個類別,使用的關鍵字是 `class`: 46 | 47 | ```ruby 48 | class 類別的名字 49 | #... 50 | end 51 | ``` 52 | 53 | 如果我想定義一個小貓類別,順便在裡面先定義好一些方法,就可以這樣寫: 54 | 55 | ```ruby 56 | class Cat 57 | def eat(food) 58 | puts "#{food} 好好吃!!" 59 | end 60 | end 61 | ``` 62 | 63 | 其中,類別的名字規定必須是常數,也就是必須是大寫英文字母開頭。有了 `Cat` 類別之後,就可以用這個類別的 `new` 方法來產生實體: 64 | 65 | ```ruby 66 | kitty = Cat.new 67 | kitty.eat "鮪魚罐頭" #=> 印出「鮪魚罐頭 好好吃!!」 68 | 69 | nancy = Cat.new 70 | nancy.eat "小魚餅干" #=> 印出「小魚餅干 好好吃!!」 71 | ``` 72 | 73 | 在這裡我用 Cat 類別做了兩個不同的實體,分別叫做 `kitty` 跟 `nancy`,這兩個物件因為都是用 `Cat` 類別做出來的,所以都有 `eat` 方法。 74 | 75 | ### 初始化 76 | 77 | 一樣形狀的烤盤,放入不同的原料就可以做出不同口味的雞蛋糕。一樣的概念,在使用 `new` 方法製作實體的時候,也可以順便傳參數進去。 78 | 79 | ```ruby 80 | class Cat 81 | def initialize(name, gender) 82 | @name = name 83 | @gender = gender 84 | end 85 | 86 | def say_hello 87 | puts "hello, my name is #{@name}" 88 | end 89 | end 90 | 91 | kitty = Cat.new("kitty", "female") 92 | kitty.say_hello # => hello, my name is kitty 93 | ``` 94 | 95 | 如果要透過 `new` 方法傳參數進來,在類別裡面必須有個名為 `initialize` 的方法來接收傳進來的參數。在 `initialize` 方法裡,常見的手法是會把參數傳進來給內部的實體變數(instance variable)。 96 | 97 | ### 實體變數(instance variable) 98 | 99 | 在 Ruby 裡的實體變數是有一個 `@` 開頭的變數,顧名思義,是活在每個實體裡的變數,而且每個實體之間互不相影響。 100 | 101 | 以前面這段為例,`@name` 跟 `@gender` 就是實體變數。 102 | 103 | 在 Rails 專案中,實體變數常用的地方是 Controller 與 View 之間的溝通,例如以下這個例子,這是一個很常見的 Controller 的例子: 104 | 105 | ```ruby 106 | class PostsController < ApplicationController 107 | def index 108 | @posts = Post.all # 取得所有的 Post 資料 109 | end 110 | end 111 | ``` 112 | 113 | 更多細詳內容待後面的 MVC(Model, View, Controler)章節再說明。 114 | 115 | ### 取用實體變數 116 | 117 | Ruby 的實體變數沒辦法直接從外部取用,像這樣直接取用會發生錯誤訊息: 118 | 119 | ```ruby 120 | kitty = Cat.new("kitty", "female") 121 | kitty.name = "nancy" # 這會發生錯誤 122 | puts kitty.name # 這也會發生錯誤 123 | ``` 124 | 125 | Ruby 並沒有「屬性」(property/attribute)這樣的東西,要取用實體變數,需要另外定義的方法才行: 126 | 127 | ```ruby 128 | class Cat 129 | def initialize(name, gender) 130 | @name = name 131 | @gender = gender 132 | end 133 | 134 | def say_hello 135 | puts "hello, my name is #{@name}" 136 | end 137 | 138 | def name 139 | @name 140 | end 141 | 142 | def name=(new_name) 143 | @name = new_name 144 | end 145 | end 146 | 147 | kitty = Cat.new("kitty", "female") 148 | kitty.name = "nancy" 149 | puts kitty.name # => nancy 150 | ``` 151 | 152 | 這裡定義的 `name` 以及 `name=` 方法(是的,你沒看錯,等號 `=` 也是方法的一部份)就是負責回傳及設定 `@name` 這個實體變數的。 153 | 154 | 每次要這樣取用或設定都要這麼麻煩嗎?還好,怕麻煩的工程師有另外定義了三個方法來解決這件事,分別是 `attr_reader`、`attr_writer` 以及 `attr_accessor`。這三個方法分別會做出「讀取」、「設定」以及「讀取+設定」的方法出來,所以原來的有點囉嗦的寫法就可改成這樣: 155 | 156 | ```ruby 157 | class Cat 158 | attr_accessor :name 159 | 160 | def initialize(name, gender) 161 | @name = name 162 | @gender = gender 163 | end 164 | 165 | def say_hello 166 | puts "hello, my name is #{@name}" 167 | end 168 | end 169 | ``` 170 | 171 | ### 實體方法與類別方法 172 | 173 | 依據方法作用的對像不同,有分實體方法(instance method)及類別方法(class method),舉個例子來說: 174 | 175 | ```ruby 176 | kitty = Cat.new("kitty", "female") 177 | kitty.say_hello 178 | ``` 179 | 180 | 這個 `say_hello` 是作用在 `kitty` 這個實體,所以稱這個 `say_hello` 為實體方法。如果是這樣: 181 | 182 | ```ruby 183 | class PostsController < ApplicationController 184 | def index 185 | @posts = Post.all # 取得所有的 Post 資料 186 | end 187 | end 188 | ``` 189 | 190 | 這裡的 `all` 方法是直接作用在 `Post` 這個類別上,故稱之類別方法。在 Ruby 要定義類別方法有幾種寫法,其中一種比較簡單的,就是在前面加上 `self`: 191 | 192 | ```ruby 193 | class Cat 194 | def self.all 195 | # ... 196 | end 197 | end 198 | ``` 199 | 200 | 這樣就可以直接用 `Cat.all` 的方式呼叫了。 201 | 202 | ### 繼承 (Inheritance) 203 | 204 | 到目前為止的範例都是只有單一類別,但在真實的世界裡其實是更複雜的,像是如果想要再加入一個小狗類別: 205 | 206 | ```ruby 207 | class Cat 208 | def eat(food) 209 | puts "#{food} 好好吃!!" 210 | end 211 | end 212 | 213 | class Dog 214 | def eat(food) 215 | puts "#{food} 好好吃!!" 216 | end 217 | end 218 | ``` 219 | 220 | 不管是 Cat 或 Dog 類別都有定義了一樣功能的 `eat` 方法,在物件導向的概念裡,通常會把相同功能的方法移到上一層的類別裡,然後再去繼承它: 221 | 222 | ``` 223 | class Animal 224 | def eat(food) 225 | puts "#{food} 好好吃!!" 226 | end 227 | end 228 | 229 | class Cat < Animal 230 | end 231 | 232 | class Dog < Animal 233 | end 234 | ``` 235 | 236 | 在這裡我定義了一個 Animal 類別,然後讓 Cat 跟 Dog 都去繼承它,那個小於符號 `<` 就是繼承的意思。這樣一來,就算 Cat 跟 Dog 類別空空的什麼都沒寫,也一樣都可以執行 `eat` 方法。雖然 Cat 跟 Dog 是不同的類別,但我們可以說「Cat 是一種 Animal,Dog 也是一種 Animal」,利用這樣的設計,可以把程式碼整理得更漂亮,不會寫出一堆重複的程式碼。 237 | 238 | ### 開放類別(Open Class) 239 | 240 | 大家請先看一下這段程式碼: 241 | 242 | ```ruby 243 | class Cat 244 | def abc 245 | # ... 246 | end 247 | end 248 | 249 | class Cat 250 | def xyz 251 | # ... 252 | end 253 | end 254 | 255 | kitty = Cat.new 256 | kitty.abc # => 會發生什麼事? 257 | kitty.xyz # => 會發生什麼事? 258 | ``` 259 | 260 | 一個不小心,定義了兩個 `Cat` 類別,所以你可能會猜,後面寫的類別會蓋掉前面先寫的類別,所以 `kitty.xyz` 可正常運作,但 `kitty.abc` 會出錯。 261 | 262 | 在 Ruby 裡,如果遇到兩個一樣名字的類別,其實並不會「覆蓋」,而是會進行「融合」,上面這兩個類別最後會變成: 263 | 264 | ```ruby 265 | class Cat 266 | def abc 267 | # ... 268 | end 269 | 270 | def xyz 271 | # ... 272 | end 273 | end 274 | ``` 275 | 276 | 然後 `abc` 跟 `xyz` 兩個方法都可以正常執行。利用這個特性,可以做出有趣的效果: 277 | 278 | ```ruby 279 | class String 280 | def say_hello 281 | "hi, I am #{self}" 282 | end 283 | end 284 | 285 | puts "eddie".say_hello # => hi, I am eddie 286 | puts "kitty".say_hello # => hi, I am kitty 287 | ``` 288 | 289 | 在這裡定義了一個 `say_hello` 方法,在那之後所有的字串就都有 `say_hello` 方法可以用了。等等,那個 `String` 類別不是內建的類別嗎?是的,你沒看錯,在 Ruby 即使是內建的類別,也是可以幫它「加料」的,這個技巧稱之開放類別(Open Class)。 290 | 291 | 這是我個人很喜歡的功能,雖然有些人會認為這樣感覺很恐怖,竟然連內建的類別都可以修改,但我想大家都是大人了,應該不會沒事亂 open 然後去惡搞自己或自己的同事吧。事實上,Rails 本身也正是利用這個特性,讓程式碼的可讀性變得更好,例如: 292 | 293 | ```ruby 294 | puts 3.days.ago # => Wed, 21 Dec 2016 12:06:13 UTC +00:00 295 | puts 10.megabyte # => 10485760 296 | ``` 297 | 298 | 這樣不是很酷嗎 :) 299 | 300 | ## 模組(Module) 301 | 302 | 如果我有一隻小貓類別,我想要這個小貓類別有飛行功能,應該怎麼做?也許你會想到用「繼承」的做法: 303 | 304 | > 我只要讓小貓類別去繼承小鳥類別就好啦,反正小鳥會飛,所以繼承之後的小貓就會飛了! 305 | 306 | 1. 直接寫一個有飛行功能的小鳥類別,然後再叫小貓類別去繼承它? 307 | 308 | 2. 直接把飛行功能寫在小貓類別裡? 309 | 310 | 第 1 種做法的設計有點怪怪的,好好的貓不當,為什麼要去當鳥?為了想要有飛行功能就去當別人家的小孩... 311 | 312 | 第 2 種做法看來似乎可行,但如果之後又有個「我希望我的這個小狗類別也會飛!」的需求,那這樣又得在小狗類別裡寫一段飛行功能,程式碼沒辦法共用。 313 | 314 | 這時候,模組就可以派上用場了。 315 | 316 | ### 飛行模組 317 | 318 | 在 Ruby 定義模組,使用的是 `module` 這個關鍵字: 319 | 320 | ```ruby 321 | module Flyable 322 | def fly 323 | puts "I can fly!" 324 | end 325 | end 326 | ``` 327 | 328 | 寫起來的手感跟類別一樣,連模組名字的規定也跟類別一樣,必須是常數(也就是大字英文字母開頭)。定義好了之後,如果要把它拿來用,只要用 `include` 這個方法: 329 | 330 | ```ruby 331 | class Cat 332 | include Flyable 333 | end 334 | 335 | kitty = Cat.new 336 | kitty.fly # => I can fly! 337 | ``` 338 | 339 | 就可以把這個飛行模組掛上去,然後小貓就會飛了!如果之後小狗類別也想要會飛的話,只要這樣: 340 | 341 | ```ruby 342 | class Dog 343 | include Flyable 344 | end 345 | ``` 346 | 347 | 小狗也會飛了。 348 | 349 | ### 要用繼承還是要用模組? 350 | 351 | 基本上,如果你發現你要做的這個功能,它可能在很多不同體系的類別裡都會用得到,那你可以考慮把功能包在模組裡,然後在必要的時候再 include 進來即可。但如果你還是不知道到底類別跟模組有什麼差別,我再舉二個例子。 352 | 353 | 不知道大家有沒看過[火影忍者](http://zh.wikipedia.org/wiki/%E7%81%AB%E5%BD%B1%E5%BF%8D%E8%80%85)這部漫畫,漫畫裡的主人公之一,宇智波佐助,因為他們家族血統的關係,他寫輪眼這個功能是天生就有的,這個功能是從他的家族「繼承」來的。而佐助的老師,旗木卡卡西,他雖然也有寫輪眼功能,但他的寫輪眼並非繼承來的,事實上是他在年輕時候 include 了某個寫輪眼模組,所以才有這個效果。 354 | 355 | 另一個例子,[海賊王](http://zh.wikipedia.org/wiki/ONE_PIECE)漫畫裡,魯夫本來是普通人,但在偶然的機會下,他 include 了橡膠果實之後,他就有了橡膠人的能力了,並不是因為他老爸是橡膠人所以他才是橡膠人。 356 | 357 | ### 在 Rails 專案中,模組用在哪些地方? 358 | 359 | 在 Rails 專案中其實還不少地方有用到模組,主要有用在 View 的 Helper 以及 Model 跟 Controller 的 Concern,這待到後面的 Rails 章節再做詳細的介紹。 360 | 361 | -------------------------------------------------------------------------------- /markdown/chapter04-your-first-rails-application.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | title: 第一個應用程式(使用 Scaffold) 4 | comments: true 5 | permalink: /chapters/04-your-first-rails-application.html 6 | 7 | --- 8 | 9 | # 第一個應用程式(使用 Scaffold) 10 | 11 | - [使用者功能](#user-scaffold) 12 | - [文章功能](#post-scaffold) 13 | - [Rails 常用快速鍵](#rails-shortcuts) 14 | - [小結](#note) 15 | 16 | 在上一篇,我們建立了一個全新的 Rails 專案,讓我們接著用這個專案繼續往下做。 17 | 18 | ## 你的第一個 Rails 應用程式(Blog 系統) 19 | 20 | 新手上路,讓我們來做一個讓使用者可以發文的 Blog 系統吧!先想一下這個系統的使用者故事(User Story)大概會長什麼樣子: 21 | 22 | - 可以新增使用者(User) 23 | - 每個使用者(User)可以新增、修改或刪除文章(Post) 24 | 25 | 雖然 Ruby 的世界有非常多厲害的套件(gem),像是如果要做會員系統,只要用 [devise](https://github.com/plataformatec/devise) 就可以在幾分鐘甚至是幾十秒內就把會員註冊、登入、登出等基本的功能完成。不過這裡我們先不用任何套件,僅靠 Rails 內建的功能來完成它。 26 | 27 | ## 使用者功能 28 | 29 | ### Step 1: 使用 Scaffold 30 | 31 | 我們先想一下使用者的資料大概會長什麼樣子: 32 | 33 | | 欄位名稱 | 資料型態 | 說明 | 34 | |:---------|:-------------:|:---------------| 35 | |name | 字串(string)| 使用者姓名 | 36 | |email | 字串(string)| 使用者 Email | 37 | |tel | 字串(string)| 聯絡電話 | 38 | 39 | 接下來,我們使用 Rails 內建的 Scaffold 功能來幫我們產生需要的檔案。切換到終端機畫面輸入指令: 40 | 41 | $ rails generate scaffold User name:string email:string tel:string 42 | Running via Spring preloader in process 17922 43 | invoke active_record 44 | create db/migrate/20161220041724_create_users.rb 45 | create app/models/user.rb 46 | invoke test_unit 47 | create test/models/user_test.rb 48 | create test/fixtures/users.yml 49 | invoke resource_route 50 | ..[略].. 51 | create app/views/users/_user.json.jbuilder 52 | invoke assets 53 | invoke coffee 54 | create app/assets/javascripts/users.coffee 55 | invoke scss 56 | create app/assets/stylesheets/users.scss 57 | invoke scss 58 | create app/assets/stylesheets/scaffolds.scss 59 | 60 | 打上面這個指令的時候,記得要先用 `cd` 指令切到 Rails 專案目錄裡,不然會出現不正確的訊息。這個 Scaffold 指令產生了一堆檔案,我們在後面的章節會再做更詳細的介紹,現在你只要先記得這個指令會幫你把 User 的新增、修改、刪除功能一口氣都做出來。 61 | 62 | 工程師其實有著懶惰的美德,所以上面這串很長的指令,可以濃縮成更簡單的樣子: 63 | 64 | 1. `generate` 可以簡寫成 `g` 65 | 2. 如果資料型態是 `string`,可以省略,但如果是其它型態不能省略。 66 | 67 | 所以原來的指令: 68 | 69 | $ rails generate scaffold User name:string email:string tel:string 70 | 71 | 可以簡寫成: 72 | 73 | $ rails g scaffold User name email tel 74 | 75 | ### Step 2 把描述具現化 76 | 77 | 在上一步產生的一堆檔案裡,有一個特別的檔案,在專案的 `db/migrate` 目錄裡,有個可能長得像 `20161220041724_create_users.rb` 的檔案(前面的數字是時間,所以應該會跟各位的檔名不太一樣),裡面的內容大概長這樣: 78 | 79 | ```ruby 80 | class CreateUsers < ActiveRecord::Migration[5.0] 81 | def change 82 | create_table :users do |t| 83 | t.string :name 84 | t.string :email 85 | t.string :tel 86 | 87 | t.timestamps 88 | end 89 | end 90 | end 91 | ``` 92 | 93 | 內容現在看不懂沒關係,之後會再介紹,但大概可以從文字猜得出來它是要建立一個表格(table),裡面有 `name`、`email` 以及 `tel` 三個欄位,分別都是字串(string)型態。 94 | 95 | 在 Rails 專案,這個檔案稱之遷移檔(migration file),是個很重要的檔案,我們會在後面的章節再介紹。 96 | 97 | 現在要做的,就是執行這個遷移檔的描述,在資料庫建立一個名為 `users` 的表格,好讓我們把使用者的資料放進去。 98 | 99 | $ rails db:migrate 100 | == 20161220041724 CreateUsers: migrating ====================================== 101 | -- create_table(:users) 102 | -> 0.0012s 103 | == 20161220041724 CreateUsers: migrated (0.0013s) ============================= 104 | 105 | 這個指令就是做這件事,這樣就把 `users` 表格建好囉! 106 | 107 | 要注意的的是,在 Rails 5 之前,用的指令是 `rake db:migrate`,在 Rails 5 之後,雖然原來的 `rake` 指令也可用,但為了統一,所以許多原來的 `rake` 指令都搬到 `rails` 底下了。 108 | 109 | ### Step 3 啟動 Rails Server 110 | 111 | 到這裡,其實使用者的新增、修改、刪除功能已經完成了!這時候只要啟動 Rails Server 就行了。 112 | 113 | $ rails server 114 | => Booting Puma 115 | => Rails 5.0.1 application starting in development on http://localhost:3000 116 | => Run `rails server -h` for more startup options 117 | Puma starting in single mode... 118 | * Version 3.6.2 (ruby 2.4.0-p0), codename: Sleepy Sunday Serenity 119 | * Min threads: 5, max threads: 5 120 | * Environment: development 121 | * Listening on tcp://localhost:3000 122 | Use Ctrl-C to stop 123 | 124 | 如果想要少打幾個字,`rails server` 指令也可簡化成 `rails s`。接著打開瀏覽器,連上網址 `http://localhost:3000/users`,應該可以看到這個畫面: 125 | 126 | ![image](/images/chapter04/user-scaffold-1.png) 127 | 128 | 試著輸入一些資料資料: 129 | 130 | ![image](/images/chapter04/user-scaffold-2.png) 131 | 132 | 你會發現你根本沒寫到什麼程式碼,一個簡單的 Scaffold 指令,已經把整個新增、修改、刪除的功能都完成了: 133 | 134 | ![image](/images/chapter04/user-scaffold-3.png) 135 | 136 | 相當神奇吧! 137 | 138 | ## 文章功能 139 | 140 | 完成了使用者功能,接著是文章(Post)功能,大致上也是依樣畫葫蘆,但還會加上一些這兩個功能之間的關連性。 141 | 142 | 再讓我們先想一下文章的資料大概會長什麼樣子: 143 | 144 | | 欄位名稱 | 資料型態 | 說明 | 145 | |:------------|:--------------:|:-------------| 146 | |title | 字串(string) | 文章標題 | 147 | |content | 文字(text) | 內文 | 148 | |user_id | 數字(integer)| 使用者編號 | 149 | |is_available | 布林(boolean)| 文章是否上線 | 150 | 151 | 這裡有幾個需要解釋的地方: 152 | 153 | 1. 文字(text)跟字串(string)不同的地方,是在於 `text` 型態可以存放更多的內容(因為通常文章不會只有短短幾個字)。 154 | 2. `user_id` 欄位的目的,是為了可以讓該編文章跟某位使用者連結在一起。 155 | 156 | ### Step 1 使用 Scaffold 157 | 158 | 根據上面這個表格,我們使用 Scaffold 來產生相對應的功能: 159 | 160 | $ rails g scaffold Post title content:text user:references is_available:boolean 161 | Running via Spring preloader in process 18657 162 | invoke active_record 163 | create db/migrate/20161220050455_create_posts.rb 164 | create app/models/post.rb 165 | invoke test_unit 166 | create test/models/post_test.rb 167 | create test/fixtures/posts.yml 168 | invoke resource_route 169 | ..[略].. 170 | invoke assets 171 | invoke coffee 172 | create app/assets/javascripts/posts.coffee 173 | invoke scss 174 | create app/assets/stylesheets/posts.scss 175 | invoke scss 176 | identical app/assets/stylesheets/scaffolds.scss 177 | 178 | 注意事項: 179 | 180 | 1. 除了 `string` 型態之外,其它型態不能省略。 181 | 2. 雖然 `user_id` 也可以用 `user_id:integer`,但使用 `user:references` 會幫你完成更多細節,這部份也一樣會在後面的章節介紹。 182 | 183 | ### Step 2 別忘了把描述具現化 184 | 185 | 跟前面的 User 一樣,Scaffold 又再產生了一個新的遷移檔,所以別忘了再讓這個描述檔執行一下: 186 | 187 | $ rails db:migrate 188 | == 20161220050455 CreatePosts: migrating ====================================== 189 | -- create_table(:posts) 190 | -> 0.0056s 191 | == 20161220050455 CreatePosts: migrated (0.0057s) ============================= 192 | 193 | ### Step 3 檢視成果 194 | 195 | 如果你剛剛的 Rails Server 還沒關掉(通常在開發過程不會特別關掉),打開網址 `http://localhost:3000/posts`: 196 | 197 | ![image](/images/chapter04/post-scaffold-1.png) 198 | 199 | 在 User 的欄位先填寫數字 `1`,表示是 1 號使用者: 200 | 201 | ![image](/images/chapter04/post-scaffold-2.png) 202 | 203 | 這個 User 欄位其實不該讓使用者自己填空,至少是要自動帶入或是使用下拉選單,不過暫時先這樣。然後就可以看到: 204 | 205 | ![image](/images/chapter04/post-scaffold-3.png) 206 | 207 | 這裡出現了看起來有點像亂碼的東西 `#`,事實上它是一個使用者物件,我們可以修正一下程式碼,讓它顯示出使用者的姓名: 208 | 209 | 請打開專案的 `app/views/posts/index.html.erb` 檔案,把第 21 行的 `post.user` 改成 `post.user.name`,像這樣: 210 | 211 | ```erb 212 | <% @posts.each do |post| %> 213 | 214 | <%= post.title %> 215 | <%= post.content %> 216 | <%= post.user.name %> 217 | <%= post.is_available %> 218 | <%= link_to 'Show', post %> 219 | <%= link_to 'Edit', edit_post_path(post) %> 220 | <%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %> 221 | 222 | <% end %> 223 | ``` 224 | 225 | 應該就可以正常顯示了: 226 | 227 | ![image](/images/chapter04/post-scaffold-4.png) 228 | 229 | 其實這裡還有一些效能問題(N+1 Query),不過也讓我們留到以後再說明。 230 | 231 | ## Rails 常用快速鍵 232 | 233 | Rails 專案裡常用到的指令都有簡寫,可以讓你少敲幾個字: 234 | 235 | | 原本的指令 | 簡寫 | 用途 | 236 | |-----------------|----------|------------------------------------------------------------------------------------| 237 | | rails generate | rails g | 用來產生各種需要的檔案,例如 scaffold、controller、model 等等 | 238 | | rails destroy | rails d | 可刪除產生器所產生的檔案 | 239 | | rails server | rails s | 啟動 Rails 伺服器,讓你可以檢視目前專案的成課 | 240 | | rails console | rails c | 進類似乎 Ruby 的 IRB 介面,但是有載入整個 Rails 專案的環境,可以在這裡直接操作資料 | 241 | | rails dbconsole | rails db | 直接進到資料庫裡,使用 SQL 語法對資料庫進行存取 | 242 | | bundle install | bundle | 安裝套件 | 243 | | rake test | rake | 執行測試 | 244 | 245 | ## 小結 246 | 247 | Scaffold 好用歸好用,我當年第一次接觸 Rails 就是被 Scaffold 給騙進來的。但實際在工作的時候不見得常用,比較常見是使用產生器(generator)各別建立 Controller 或 Model,畢竟 Scaffold 一口氣生出太多用不到的檔案,有種用牛刀殺小雞的感覺。 248 | 249 | 基本上 Rails 是不可能靠用聽的或用看的就學得會的,一定多要練習,建議有空可試著照 Rails Guide 的這篇 [Getting Started](http://guides.rubyonrails.org/getting_started.html) 操練一遍,應該就對 Rails 更有概念了,加油! 250 | 251 | -------------------------------------------------------------------------------- /markdown/chapter16-model-migration.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | title: Model Migration 4 | comments: true 5 | permalink: /chapters/16-model-migration.html 6 | 7 | --- 8 | 9 | # Model Migration 10 | 11 | - [什麼是 Migration](#what-is-migration) 12 | - [新增 Migration](#add-migration) 13 | - [修改 Migration](#update-migration) 14 | - [種子資料](#seed-data) 15 | 16 | 資料遷移(Migration)是很多剛接觸 Rails 的新手容易卡關的地方,對 Migration 常見的誤解有: 17 | 18 | 1. Migration 就是資料庫。 19 | 2. 只要在 Migration 修改欄位後,網頁上自動就會呈現修改後的效果。 20 | 3. 如果 Migration 有寫錯,只要修改之後再重新執行 `rails db:migrate` 指令就行了。 21 | 22 | ## 什麼是 Migration 23 | 24 | Migration 是用來描述「資料庫的架構長什麼樣子」的檔案,它會隨著專案開發的過程中逐漸增加。想像一下這個對話內容: 25 | 26 | > 同事 A:「嘿,我剛剛建立了一個 User 資料表喔」 27 | > 28 | > 同事 B:「好,那我待會要建一個 Product 資料表用來放產品資訊的」 29 | > 30 | > 同事 C:「咦?等等,這個 User 資料表少一個地址欄位啦,我要加上去喔」 31 | > 32 | > 同事 A:「Product 資料表的 name 欄位不太好記,我要把它改名成 title 喔」 33 | > 34 | > 同事 C:「User 資料表的這個 flag 欄位好像都沒用到,我要把這個欄位刪除掉喔」 35 | 36 | 這段對話就是所謂的 Migration,上面這樣其實就是發生了 5 次的 Migration,每個 Migration 都是一個描述檔案。透過 Git 共同開發,每位同事應該都能拿到一樣的 Migration 描述檔,只要一個執令就可以同步資料庫的結構,比較不會有「同事 A 直接在 Server 上修改某個欄位的名字,但同事 B 不知情而造成程式無法順利執行」的情況發生。 37 | 38 | 使用 Migration 的另一個好處,是因為 Migration 檔案應該都會進 Git 版本控制,所以整個資料庫的設計過程全部都可以一目了然。而且假設原本使用 MySQL 資料庫,突然被公司長官要求要換成 PostgreSQL,只要沒有用到太特別或某些資料庫專屬的特異功能,通常只要一行指令就可以再重建資料庫。 39 | 40 | ## 新增 Migration 41 | 42 | 要新增 Migration 有好幾種管道,例如透過 `rails generate` 指令產生 Model 或 Scaffold 都會順便產生一個 Migration 檔。讓我們先用 generate 產生一個 Model 吧: 43 | 44 | $ rails g model Article title content:text is_online:boolean 45 | Running via Spring preloader in process 1480 46 | invoke active_record 47 | create db/migrate/20161231224701_create_articles.rb 48 | create app/models/article.rb 49 | invoke test_unit 50 | create test/models/article_test.rb 51 | create test/fixtures/articles.yml 52 | 53 | 除了 Model 本體外,這個指令也產生了一個名為 `20161231224701_create_articles.rb` 的 Migration 檔案,其中檔名前面的 `20161231224701` 是這個指令執行時候的時間戳記。讓我們看一下這個 Migration 檔案的內容: 54 | 55 | ```ruby 56 | class CreateArticles < ActiveRecord::Migration[5.0] 57 | def change 58 | create_table :articles do |t| 59 | t.string :title 60 | t.text :content 61 | t.boolean :is_online 62 | 63 | t.timestamps 64 | end 65 | end 66 | end 67 | ``` 68 | 69 | Migration 檔的內容本質上就是一個 Ruby 程式,從語法大概能猜得出來要建立一個 `articles` 表格,並且有 title、content 以及 is_online 這幾個欄位。 70 | 71 | 除了這幾個欄位外,在最後一行還有一個 `t.timestamps` 的語法,這個會幫你在這個表格分別建立出 `created_at` 以及 `updated_at` 兩個時間戳記欄位,分別會在資料新增及更新的時候把當下的時間寫進去。如果覺得這個資料表不需要這樣的時間欄位的話,亦可直接把這行刪除。 72 | 73 | 有了 Migration,記得要執行 `rails db:migrate` 指令,這樣就會把這些描述轉換成真實的資料表: 74 | 75 | $ rails db:migrate 76 | == 20161231224701 CreateArticles: migrating =================================== 77 | -- create_table(:articles) 78 | -> 0.0050s 79 | == 20161231224701 CreateArticles: migrated (0.0051s) ========================== 80 | 81 | ### 如果你忘了執行 rails db:migrate 指令... 82 | 83 | 在 Rails 專案中如果有 Migration 檔案還沒有「處理」過,在你開瀏覽器檢視頁面的時候會看到「ActiveRecord::PendingMigrationError」的錯誤訊息: 84 | 85 | ![image](/images/chapter16/pending-migration-error.png) 86 | 87 | 不用太擔心,這時候只要執行一下 `rails db:migrate` 指令就可以解決問題了 :) 88 | 89 | ### 想要在其它環境下執行 Migrate 90 | 91 | 預設的 `rails db:migrate` 是會在 development 模式執行,如果你想在 production 或 test 環境執行的話,只要改一下環境變數就行,像這樣: 92 | 93 | $ RAILS_ENV=production rails db:migrate 94 | 95 | 這樣就會以 production 模式來執行 Migration 了。 96 | 97 | ## 修改 Migration 98 | 99 | 剛執行完一個 Migration,才發現欄位名字打錯了,想要修改該怎麼做?直覺做法是「修改剛剛那個 Migration 檔案,存檔後再執行一次 `rails db:migrate` 吧」 100 | 101 | 但這方法行不通的,因為 `rails db:migrate` 這個指令只會針對還沒執行過的 Migration 檔案有效果,已經做過的 Migration ,再做一次是不會有反應的,所以即使修改同一個 Migration 檔再重新執行是沒用的。 102 | 103 | 那怎辦?其實做法有好幾款,其中一款,就是執行 Rollback 指令,把執行過的 Migration 倒回去: 104 | 105 | $ rails db:rollback 106 | == 20161231224701 CreateArticles: reverting =================================== 107 | -- drop_table(:articles) 108 | -> 0.0024s 109 | == 20161231224701 CreateArticles: reverted (0.0079s) ========================== 110 | 111 | 這樣就可以「倒轉」一個 Migration。如果一次想要倒轉 3 個 Migration,可以加上 `STEP=3` 參數: 112 | 113 | $ rails db:rollback STEP=3 114 | 115 | 雖然我們在 Migration 裡只有寫 `create_table` 語法,但上面這個指令會自動幫我們執行 `drop_table` 來刪除新增的資料表。 116 | 117 | 在執行 Rollback 的時候,如果正向 Migration 是建立資料表,那逆轉 Migration 就是刪除資料表;同理,如果正向是新增欄位,逆轉就會是刪除欄位。 118 | 119 | > 注意:Rollback 是有風險的! 120 | > 121 | > 因為 Rollback 通常會造成刪除資料表或是刪除欄位的效果,所以如果原本該資料表或該欄位已經有資料的話,請儘量不要使用 Rollback 方式來修正 Migration,建議直接再新增一個 Migration 來進行修正。 122 | 123 | ### Rails 怎麼知道哪些 Migration 有做過? 124 | 125 | 其實在資料庫裡有一個名為 `schema_migrations` 的資料表,裡面有記錄哪些 Migration 已經做過的。除了可以直接進這個資料表看之外,也可使用這個指令查看: 126 | 127 | $ rails db:migrate:status 128 | database: /private/tmp/my_candidates/db/development.sqlite3 129 | 130 | Status Migration ID Migration Name 131 | -------------------------------------------------- 132 | up 20161229084544 Create candidates 133 | down 20161231224701 Create articles 134 | down 20170101064253 Create comments 135 | 136 | 其中狀態是 `up` 的表示這個 Migration 已執行過,`down` 則是尚未執行。 137 | 138 | ### 手工產生 Migration 139 | 140 | 當想要修正 Migration 的時候,前面提到 Rollback 後修改再重做一次 Migration 的做法其實是不太推薦的,因為這樣做除了可能會刪除原有的資料之外,如果這個專案還有跟其它人協同開發,你也得要求其它同事 Rollback 重做一次,這實在會造成別人的困擾。 141 | 142 | 除非這個案子剛開始,或是只有你自己一個人在做,否則要進行資料庫結構修改的事,建議另外新增一個 Migration 來修正。例如我想要幫 `articles` 資料表新增一個名為 `photo` 的字串欄位: 143 | 144 | $ rails g migration add_photo_to_articles 145 | Running via Spring preloader in process 7437 146 | invoke active_record 147 | create db/migrate/20170101081107_add_photo_to_articles.rb 148 | 149 | 這邊的 `add_photo_to_articles` 並不一定要這樣寫,你要使用 `abc` 或 `xyz` 都沒問題,但建議使用一眼就看得出意圖的寫法跟單字,日後在維護的時候比較容易依檔名就知道到底這次 Migration 做了什麼事。讓我們看看剛剛產生的那個的 Migration 檔: 150 | 151 | ```ruby 152 | class AddPhotoToArticles < ActiveRecord::Migration[5.0] 153 | def change 154 | end 155 | end 156 | ``` 157 | 158 | 其實產生器也只幫你產生了一個空殼而已,沒有真正的實作,所以接下來就要在裡面寫上我想要加的欄位: 159 | 160 | ```ruby 161 | class AddPhotoToArticles < ActiveRecord::Migration[5.0] 162 | def change 163 | add_column :articles, :photo, :string 164 | end 165 | end 166 | ``` 167 | 168 | `add_column` 這個方法,第一個參數是「資料表名稱」(注意:不是 Model 名稱喔),第二個參數是「要新增的欄位名稱」,第三個參數是這個欄位的「資料型態」。完成並存檔之後,則可繼續執行 Migration: 169 | 170 | $ rails db:migrate 171 | == 20170101081107 AddPhotoToArticles: migrating =============================== 172 | -- add_column(:articles, :photo, :string) 173 | -> 0.0004s 174 | == 20170101081107 AddPhotoToArticles: migrated (0.0004s) ====================== 175 | 176 | 這樣一來,其它同事透過 Git 收到這個 Migration 檔的時候,同樣只要執行 `rails db:migrate` 指令,就可以跟你有一樣的資料表結構了。 177 | 178 | ### 魔術 Migration 產生器 179 | 180 | Migration 的產生其實還滿神奇的,當你的檔名符合某些字樣的時候,例如 `add ... to ...` 或是 `remove ... from ...`,後面再加一些欄位,可以自動幫你產生一個寫好的 Migration 檔案,例如這樣: 181 | 182 | $ rails g migration add_candidate_id_to_articles candidate_id:integer:index 183 | Running via Spring preloader in process 7765 184 | invoke active_record 185 | create db/migrate/20170101081538_add_candidate_id_to_articles.rb 186 | 187 | 我要 `add` 一個欄位 `to` articles 這個表格,同時幫這個欄位加上索引。看一下它幫我們產生的 Migration 檔: 188 | 189 | ```ruby 190 | class AddCandidateIdToArticles < ActiveRecord::Migration[5.0] 191 | def change 192 | add_column :articles, :candidate_id, :integer 193 | add_index :articles, :candidate_id 194 | end 195 | end 196 | ``` 197 | 198 | 突然就很魔術的寫好了! 199 | 200 | 雖然說這樣挺方便,但我沒辦法記得太多這樣的魔術寫法,我個人比較偏好開一個 Migration 再慢慢自己寫,反正也不會慢到哪裡去。如果你對這樣的寫法有興趣,請查閱 Rails Guide 的 [Migration 章節](http://guides.rubyonrails.org/active_record_migrations.html) 201 | 202 | ### schema.rb 是什麼東西? 203 | 204 | 在執行 `rails db:migrate` 指令之後,在專案的 `db` 目錄裡有個名為 `schema.rb` 的檔案,內容可能長得像這樣: 205 | 206 | ```ruby 207 | ActiveRecord::Schema.define(version: 20170101081538) do 208 | # ...[略]... 209 | create_table "candidates", force: :cascade do |t| 210 | t.string "name" 211 | t.string "party" 212 | t.integer "age" 213 | t.text "politics" 214 | t.integer "votes", default: 0 215 | t.datetime "created_at", null: false 216 | t.datetime "updated_at", null: false 217 | end 218 | # ...[略]... 219 | end 220 | ``` 221 | 222 | 這個檔案是你在執行 `rails db:migrate` 指令的時候順便一起產生的,你不需要也沒必要手動修改這個檔案。從這個檔案可以看得出來每個資料表的名字與欄位名稱、型態。這個檔案通常會在版本控制系統裡,如果有些比較老舊的專案,中間有些 Migration 檔因為不明原因壞掉了而無法順利執行 `rails db:migrate`,這時候也可透過 `rails db:schema:load` 把資料表建回來。 223 | 224 | 另外,因為這個檔案的內容是由 `rails db:migrate` 指令產生,所以偶爾會遇到新手「Migration 寫好但還沒存檔就執行」的狀況,這時候從這個 `schema.rb` 檔案就可以看得出來。 225 | 226 | ### 「Migration 寫好但還沒存檔就執行」會怎樣? 227 | 228 | 其實不會怎樣,就只是執行了一個空的 Migration 而已。 229 | 230 | 但因為執行過的 Migration 檔不會再重複執行,所以有些對 Migration 還不熟的新手,以為已經正確的執行了 Migration 檔,但事實上根本就是執行了一個空的 Migration,這時候即使再存檔也沒效果了。 231 | 232 | 所以常會發生「奇怪,怎麼明明 Migration 檔案裡就有寫這些欄位,但為什麼 schema.rb 檔案裡卻沒有」的情況。 233 | 234 | ## 種子資料 235 | 236 | 前面對 Migration 的介紹,好像都是在建立或修改資料庫的結構,事實上如果你想要的話,也是可以在 Migration 的過程順便寫資料進去的,像是這樣: 237 | 238 | ```ruby 239 | class CreateArticles < ActiveRecord::Migration[5.0] 240 | def change 241 | create_table :articles do |t| 242 | t.string :title 243 | t.text :content 244 | t.boolean :is_online 245 | 246 | t.timestamps 247 | end 248 | end 249 | 250 | Article.create(title: "五倍紅寶石 part 1", content: "斷開鎖鍊吧!") 251 | Article.create(title: "五倍紅寶石 part 2", content: "斷開魂結吧!") 252 | end 253 | ``` 254 | 255 | 這樣的技巧常用在建立資料表的時候順便建立初始資料,例如預設的系統管理帳號。以上面這段範例來說,當執行 `rails db:migrate` 的同時也會順便一併新增兩筆資料到 `articles` 資料表。 256 | 257 | 雖然這樣可以寫入預設資料沒錯,但 Migration 的特性之一,就是已經處理過的 Migration 不會再執行(除非 Rollback 回去),所以如果要重新再重建這些預設資料會有點麻煩。在 Rails 裡有個更適合做這件事的地方,就是在 `db/seeds.rb` 這個檔案,請直接編輯這個檔案的內容: 258 | 259 | ```ruby 260 | Article.create(title: "五倍紅寶石 part 1", content: "斷開鎖鍊吧!") 261 | Article.create(title: "五倍紅寶石 part 2", content: "斷開魂結吧!") 262 | ``` 263 | 264 | 存檔後,執行 `rails db:seed` 指令,就可以把資料寫進資料庫裡了,不管是目的性或是實用性來說,把預設資料放在這裡都是比較好的做法。 265 | 266 | 另外,`rails db:setup` 指令其實除了建立資料庫之外,也隱含了執行 `rails db:seed` 的指令,所以如果是全新的資料庫,執行 `rails db:setup` 可一口氣把資料表建完,順便把預設資料寫入。 267 | 268 | -------------------------------------------------------------------------------- /markdown/chapter12-controllers.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | title: Controller 4 | comments: true 5 | permalink: /chapters/12-controllers.html 6 | 7 | --- 8 | 9 | # Controller 10 | 11 | - [向你的用戶說聲哈囉](#say-hello-world) 12 | - [Params 變數](#params) 13 | - [實作練習:BMI 計算器](#bmi-calculator) 14 | 15 | ## 向你的用戶說聲哈囉 16 | 17 | 接續前一章,Route 解讀網址之後,會把工作轉往指定的 Controller 及 Action。在這個小節我們會試著在畫面上跟使用者說聲哈囉,熟悉一下 Route、Controller 以及 View 是怎麼運作的。 18 | 19 | ### Controller 是幹嘛的 20 | 21 | Controller 中文可翻譯成「控制器」,顧名思義,就是用來控制流程用的。它可能需要跟 Model 要資料,可能需要跟 View 要 HTML template 來玩填空遊戲,或是可能需要存取外部服務(例如金流串接)等,這大多是 Controller 要做的工作。 22 | 23 | ### 命名慣例 24 | 25 | 在 Rails 的慣例中,Controller 的命名會根據 Route 是使用複數的 `resources` 還是單數 `resource` 方法而定。如果在 Route 是使用複數型態,例如: 26 | 27 | ```ruby 28 | Rails.application.routes.draw do 29 | resources :posts 30 | resources :users 31 | end 32 | ``` 33 | 34 | 在沒有特別指定 Resources 的 `controller` 參數的情況下,預設會對到的 Controller 就會是 `PostsController` 或是 `UsersController` 這樣的複數型態;反之,如果使用的是單數 `resource`,對到的就會是單數命名的 Controller。 35 | 36 | ### 第 0 步 - 新增 Controller 37 | 38 | 在開始之前,讓我們使用 Rails 內建的產生器做一個全新的 Controller: 39 | 40 | $ rails g controller pages 41 | Running via Spring preloader in process 16503 42 | create app/controllers/pages_controller.rb 43 | invoke erb 44 | create app/views/pages 45 | invoke test_unit 46 | create test/controllers/pages_controller_test.rb 47 | invoke helper 48 | create app/helpers/pages_helper.rb 49 | invoke test_unit 50 | invoke assets 51 | invoke coffee 52 | create app/assets/javascripts/pages.coffee 53 | invoke scss 54 | create app/assets/stylesheets/pages.scss 55 | 56 | 上面這行指令會幫你做出一個 `PagesController`,以及一些其它對應的檔案、目錄。Controller 的內容如下: 57 | 58 | ```ruby 59 | class PagesController < ApplicationController 60 | end 61 | ``` 62 | 63 | 這個 Controller 裡什麼內容都沒有,就只有繼承自 `ApplicationController` 而已。所以如果上手之後,也不一定要用產生器來幫你產生 Controller,直接自己手動新增也行。 64 | 65 | ### 第 1 步 - 新增 Route 66 | 67 | 別忘了,使用者想要看到你網站上的內容,第一步是要問過 Route,所以我們先在 Route 上簡單的加上一條: 68 | 69 | ```ruby 70 | Rails.application.routes.draw do 71 | get "/hello_world", to: "pages#hello" 72 | 73 | resources :posts 74 | resources :users 75 | end 76 | ``` 77 | 78 | 當使用者輸入 `/hello_world` 網址的時候,會交給 `PagesController` 的 `hello` 方法處理。(是的,其實網址跟 Controller 上的 Action 不一定要同名) 79 | 80 | ### 第 2 步 - 把文字印出來吧! 81 | 82 | 有了 Route 之後,接下來回到 Controller 把 `hello` 這個 Action 加上去: 83 | 84 | ```ruby 85 | class PagesController < ApplicationController 86 | def hello 87 | render plain: "

你好,世界!

" 88 | end 89 | end 90 | ``` 91 | 92 | 在 `hello` 方法裡要把文字輸出到瀏覽器上,不是使用 `return` 也不是使用 `puts`,而是使用 `render` 方法,後面的 `plain` 參數是指要輸出一個一般的文字內容到畫面上。 93 | 94 | 有些剛開始學 Rails 的新朋友可能會想這樣做: 95 | 96 | ```ruby 97 | class PagesController < ApplicationController 98 | def hello 99 | render plain: "

你好,世界!

" 100 | puts "---- 你好 ----" 101 | end 102 | end 103 | ``` 104 | 105 | 使用 `puts` 方法把資料直接輸出在畫面上,看起來很直覺,但這樣不會有效果。事實上並不是 `puts` 方法不能用,它的確可以把東西印出來,只是不是印在瀏覽器上給你看到,而是印在 Rails 的 log 裡,仔細看一下正在執行 `rails server` 的那個畫面是不是有這樣的東西: 106 | 107 | ![image](/images/chapter12/puts-to-console.png) 108 | 109 | 你就可以發現有這樣的畫面: 110 | 111 | ![image](/images/chapter12/render-hello-world.png) 112 | 113 | ### 第 3 步 - 把工作交給 View 吧 114 | 115 | 雖然在第 2 步這樣可以直接在 Action 裡透過 `render` 方法把資料輸出在畫面上沒錯,但如果遇到比較複雜的 HTML 通常就不會用這個方式了。在 Controller 裡的 Action,如果沒有特別指定 `render` 方法或參數的話,它會到 `app/views/` 的目錄找「 Controller 名字」目錄裡的 Action 同名檔案。以這個例子來說,它會去找 `app/views/pages/hello.html.erb`。 116 | 117 | ![image](/images/chapter12/controller-view-mapping.png) 118 | 119 | 如果這個 `hello.html.erb` 不存在,就自己手動建一個吧。即然輸出的事情交給 View,原來 `hello` 這個 Action 的 `render` 方法就可以拿掉: 120 | 121 | ```ruby 122 | class PagesController < ApplicationController 123 | def hello 124 | end 125 | end 126 | ``` 127 | 128 | 就這樣空空的,然後編輯 `app/views/pages/hello.html.erb` 129 | 130 | ```erb 131 |

你好,世界

132 |

我是設計師也看得懂的檔案喔

133 | ``` 134 | 135 | 重新整理,應該就會看到跟剛才的差別: 136 | 137 | ![image](/images/chapter12/render-hello-world-with-view.png) 138 | 139 | 這樣的好處是不用把 HTML 都寫在 Controller 裡(事實上也很少人會這麼做),再來就是要跟設計師合作的時候也比較方便。 140 | 141 | ## Params 參數 142 | 143 | 接下來我們看看怎麼傳參數給 Controller。當使用者輸入網址這樣的網址: 144 | 145 | /hello_world?name=5xruby&price=100&staff=20 146 | 147 | 畫面的輸出雖然沒變,但後面跟的那串東西會被當做參數傳進一個特別的變數叫做 `params`。這是 Rails 預先幫我們定義好的,它可以捕捉到這個頁面的資訊。讓我們在剛剛的 `hello` Action 裡加一些料: 148 | 149 | 150 | ```ruby 151 | class PagesController < ApplicationController 152 | def hello 153 | render json: params 154 | end 155 | end 156 | ``` 157 | 158 | 使用 `render` 方法,把 `params` 這個變數用 `JSON` 的方式印出來,可以看到這個結果: 159 | 160 | ![image](/images/chapter12/render-params-1.png) 161 | 162 | Rails 會把剛剛後面那串東西,整理成一個類似 Hash 的東西,例如我只想要 `name` 參數的話: 163 | 164 | ```ruby 165 | class PagesController < ApplicationController 166 | def hello 167 | render plain: params["name"] 168 | end 169 | end 170 | ``` 171 | 172 | ![image](/images/chapter12/render-params-2.png) 173 | 174 | 不管是 GET 或是 POST 方式傳過來的參數,都會被收集到這個 `params` 裡。 175 | 176 | ## 實作練習:BMI 計算器 177 | 178 | 大概知道 Route、Controller、View 以及 Params 的使用方法後,接下來我們來做一個可以計算 BMI(Body Mass Index,身體質量指數)的計算機。 179 | 180 | ### 第 0 步 - 新增 Controller 及 Route 181 | 182 | 先用產生器把 Controller 做出來: 183 | 184 | $ rails g controller bmi index  21:53:32 185 | Running via Spring preloader in process 18198 186 | create app/controllers/bmi_controller.rb 187 | route get 'bmi/index' 188 | invoke erb 189 | create app/views/bmi 190 | create app/views/bmi/index.html.erb 191 | invoke test_unit 192 | create test/controllers/bmi_controller_test.rb 193 | invoke helper 194 | create app/helpers/bmi_helper.rb 195 | invoke test_unit 196 | invoke assets 197 | invoke coffee 198 | create app/assets/javascripts/bmi.coffee 199 | invoke scss 200 | create app/assets/stylesheets/bmi.scss 201 | 202 | 203 | 跟前面稍微有點不一樣的是在 Controller 後面多加了 `index` 這個參數,這樣會自動幫你做幾件事: 204 | 205 | #### 1 - 幫你加上 Route 206 | 207 | ```ruby 208 | Rails.application.routes.draw do 209 | get 'bmi/index' 210 | 211 | get "hello_world", to: "pages#hello" 212 | resources :posts 213 | resources :users 214 | end 215 | ``` 216 | 217 | 多加了 `get 'bmi/index` 條路徑。但我不是很喜歡這樣的路徑,所以請把它改成: 218 | 219 | ```ruby 220 | Rails.application.routes.draw do 221 | get "bmi", to: "bmi#index" 222 | 223 | get "hello_world", to: "pages#hello" 224 | resources :posts 225 | resources :users 226 | end 227 | ``` 228 | 229 | 這時候輸入路徑 `/bmi` 應該可以看到這個畫面: 230 | 231 | ![image](/images/chapter12/bmi-1.png) 232 | 233 | #### 2 - 自動幫 Controller 加上 `index` Action: 234 | 235 | ```ruby 236 | class BmiController < ApplicationController 237 | def index 238 | end 239 | end 240 | ``` 241 | 242 | #### 3 - 自動幫你產生 `app/views/bmi/index.html.erb` 檔案 243 | 244 | 內容如下: 245 | 246 | ```erb 247 |

Bmi#index

248 |

Find me in app/views/bmi/index.html.erb

249 | ``` 250 | 251 | ### 第 1 步 - 建立表單 252 | 253 | 編輯 `app/views/bmi/index.html.erb` 如下: 254 | 255 | ```erb 256 |

BMI 計算機

257 | 258 | <%= form_tag '/bmi/result' do %> 259 | 身高:<%= text_field_tag 'body_height' %> 公分
260 | 體重:<%= text_field_tag 'body_weight' %> 公斤
261 | <%= submit_tag "開始計算" %> 262 | <% end %> 263 | ``` 264 | 265 | 這裡有幾個需要稍做說明的地方: 266 | 1. `form_tag` 會被轉換成 HTML 的 `
` 標籤 267 | 2. `text_field_tag` 會被轉換成 HTML 的 `` 標籤。 268 | 3. `submit_tag` 會被轉換成 HTML 的 `` 標籤。 269 | 270 | 以上這些方法都統稱為 `View Helper`,更多相關的使用方法參考 [Form Helper](http://api.rubyonrails.org/classes/ActionView/Helpers/FormTagHelper.html) 的 API 手冊。這時候的畫面會長得像這樣: 271 | 272 | ![image](/images/chapter12/bmi-2.png) 273 | 274 | 在繼續之前,先讓我們檢視一下這一頁的原始碼,仔細看一下跟表單有關的部份,稍做整理如下: 275 | 276 | ```html 277 | 278 | 279 | 280 | 身高: 公分
281 | 體重: 公斤
282 | 283 |
284 | ``` 285 | 286 | 這邊有一段名字叫做 `authenticity_token` 的隱藏 input 標籤,不只內容看起來像是亂碼,而且每次重新整理又會得到不一樣的值,這個是做什麼用的呢? 287 | 288 | 我在一開始接觸網路的時候做的工作是網路行銷,因為工作的關係,常常需要撰寫讓網友們票選或是填寫資料抽獎之類的程式。稍微有點技術底子的參加者,只要檢視網頁的原始碼,就可以看得出來這個表單要用什麼方式(GET 或 POST)、要送到什麼地方,以及要送的資料欄位名稱。有心人士只要寫一個簡單的小程式,仿照原頁面送資料到指定的地方,就可能可以造成灌票或是大量留言、灌水的情況,影響活動的公平性。若因此而再加一些驗證規則,反而又提高了一般參加者的的門檻。 289 | 290 | 如果這個活動網站是用 Rails 開發的,Rails 預設在處理表單的時候會檢查這個 `authenticity_token` 是不是由本站所產生的,如果沒有這個欄位,或是這個欄位的值經 Rails 核對後發現並不是本身所產生,就會出現這個錯誤訊息: 291 | 292 | ![image](/images/chapter12/invalid-authenticity-token-error.png) 293 | 294 | 不管是 `form_tag` 或是下個章節才介紹的 `form_for`,在產生 `
` 標籤的時候都會自動幫你加上並產生 `authenticity_token` 的欄位,確保比較不會太容易被有心人士所破壞。 295 | 296 | ### 第 2 步 - 新增 Route 297 | 298 | 這時候當我們按下送出的時候會得到 `Routing Error` 的錯誤訊息,那是因為我們還沒有這個路徑,所以現在來補做一下。在 Route 裡加上一行: 299 | 300 | ```ruby 301 | Rails.application.routes.draw do 302 | get "bmi", to: "bmi#index" 303 | post "bmi/result", to: "bmi#result" 304 | 305 | get "hello_world", to: "pages#hello" 306 | resources :posts 307 | resources :users 308 | end 309 | ``` 310 | 311 | 這樣可讓 Route 可以接到 `POST` 並轉往 `BmiController` 的 `result` Action。 312 | 313 | ### 第 3 步 - 計算 314 | 315 | Route 有了,接下來就是把 `result` Action 的內容補上去: 316 | 317 | ```ruby 318 | class BmiController < ApplicationController 319 | def index 320 | end 321 | 322 | def result 323 | height = params[:body_height].to_f / 100 # 把單位換算成公尺 324 | weight = params[:body_weight].to_f 325 | 326 | # BMI 計算公式: BMI = 體重(單位:公斤) / 身高平方(單位:公尺). 327 | @bmi = (weight / (height * height)).round(2) 328 | end 329 | end 330 | ``` 331 | 332 | BMI 的計算公式還滿單純的,不過要注意的是: 333 | 334 | 1. 透過 `params` 取得的資料預設型態是字串,所以需要使用 `.to_i` 或 `.to_f` 轉換成數字。這裡因為需要使用除法計算到小數點以下所以使用 `.to_f` 方法進行轉換。 335 | 2. 計算完的結果存成實體變數 `@bmi`,以便讓 View 可以取用。 336 | 337 | ### 第 4 步 - 呈現結果 338 | 339 | 最後一步,把結果印出來。編輯檔案 `app/views/bmi/result.html.erb` (如果檔案不存在請直接手動建立): 340 | 341 | ```erb 342 |

您的 BMI 值為:<%= @bmi %>

343 | ``` 344 | 345 | 搞定!試玩一下,我輸入身高 178 公分、體重 80 公斤: 346 | 347 | ![image](/images/chapter12/bmi-3.png) 348 | 349 | 按下送出即可得到計算結果: 350 | 351 | ![image](/images/chapter12/bmi-4.png) 352 | 353 | ## 小結 354 | 355 | 雖然這個計算機的功能相當陽春,而且也很多地方需要改善,例如防呆機制,或是根據計算結果嘲諷一下 BMI 值過高的胖子。但如果你能理解這個例子裡 Route、Controller、View 之間的基本運作原理,當下回遇到更複雜的應用程式開發相信也是可以迎刃而解。 356 | 357 | > 以上實作完整程式碼可在[我的 GitHub 帳號](https://github.com/kaochenlong/hello_rails)取得。 358 | 359 | -------------------------------------------------------------------------------- /markdown/chapter25-shopping-cart-part-1.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | title: 購物車 Part 1 4 | comments: true 5 | permalink: /chapters/25-shopping-cart-part-1.html 6 | 7 | --- 8 | 9 | # 購物車 Part 1 10 | 11 | - [功能設計](#requirement) 12 | - [測試環境設定](#rspec-env) 13 | - [先寫測試,再寫程式](#tdd) 14 | 15 | 終於進入真正的實作階段了,接下來讓我們來實作一個很多電子商務網站都會有的購物車功能吧。 16 | 17 | 在這個實作範例中,我們將使用: 18 | 19 | 1. TDD(Test-Driven Development)方式進行開發。 20 | 2. 購物車的內容不會建立資料表。 21 | 22 | 關於第 2 點,有些人會習慣使用建立資料表來存購物車的資料,不過我們這裡選擇使用 Session 來存放資料。 23 | 24 | > 本章節程式碼可於 GitHub 上取得 https://github.com/kaochenlong/shopping_mall 25 | 26 | ## 功能設計 27 | 28 | ![image](/images/chapter25/cart.png) 29 | 30 | 說明: 31 | 32 | 一台購物車(Cart,①)會有很多的購買項目(CartItem ②),每個購買項目都有一項商品(Product ③)以及數量(Quantity ④) 33 | 34 | ### 基本功能 35 | 36 | 1. 可以把商品丟到到購物車裡,然後購物車裡就有東西了。 37 | 2. 如果加了相同種類的商品到購物車裡,購買項目(CartItem)並不會增加,但商品的數量會改變。 38 | 3. 商品可以放到購物車裡,也可以再拿出來。 39 | 4. 每個 Cart Item 都可以計算它自己的金額(小計)。 40 | 5. 可以計算整台購物車的總消費金額。 41 | 6. 特別活動可能可搭配折扣(例如聖誕節的時候全面打 9 折,或是滿額滿千送百)。 42 | 43 | ### 進階功能 44 | 45 | 因為購物車將以 Session 方式儲存,所以: 46 | 47 | 1. 可以將購物車內容轉換成 Hash,存到 Session 裡 48 | 2. 也可以把 Session 的內容(Hash 格式),還原成購物車的內容。 49 | 50 | ## 測試環境設定 51 | 52 | 在 `Gemfile` 裡加上需要的 gem: 53 | 54 | ```ruby 55 | source 'https://rubygems.org' 56 | 57 | # ...[略]... 58 | 59 | group :development, :test do 60 | gem 'byebug', platform: :mri 61 | gem 'rspec-rails' 62 | gem 'factory_girl_rails' 63 | gem 'faker' 64 | end 65 | 66 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 67 | gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] 68 | ``` 69 | 70 | 存檔後別忘了執行 `bundle install`,確保套件有安裝成功。其中,本體是 `rspec-rails`,其它像 `factory_girl_rails` 以及 `faker` 則是輔助產生測試資料用的。 71 | 72 | Gem 安裝完成之後,接著照 `rspec-rails` 的說明頁面,安裝 `rspec` 到 Rails 專案裡: 73 | 74 | $ rails g rspec:install 75 | Running via Spring preloader in process 85482 76 | create .rspec 77 | create spec 78 | create spec/spec_helper.rb 79 | create spec/rails_helper.rb 80 | 81 | 這個指令會建立一個 `spec` 目錄,並且建立 2 個 Helper 檔案。接著試一下執行 `rspec` 指令: 82 | 83 | $ rspec 84 | No examples found. 85 | 86 | Finished in 0.00036 seconds (files took 0.15527 seconds to load) 87 | 0 examples, 0 failures 88 | 89 | 這時候因為都還沒有任何測試,所以這個結果是正常的。 90 | 91 | 另外,因為我們會用 Rspec 取代原本內建的測試,所以原本專案裡的 `test` 目錄也可移除。 92 | 93 | 94 | ## 先寫測試,再寫程式 95 | 96 | ### 先寫測試 97 | 98 | 我們在第 23 章是自己手動建立 `bank_account_spec.rb` 檔案,但如果有安裝 `rspec-rails` 的話,可以請產生器幫我們做這件事。首先,先產生一個針對 Cart 這個 Model 的測試: 99 | 100 | $ rails g rspec:model Cart 101 | Running via Spring preloader in process 86133 102 | create spec/models/cart_spec.rb 103 | invoke factory_girl 104 | create spec/factories/carts.rb 105 | 106 | 接著執行測試看看,應該是會失敗(錯誤訊息是「沒有 Cart 這個常數」): 107 | 108 | $ rspec 109 | /private/tmp/shopping_mall/spec/models/cart_spec.rb:3:in `': uninitialized constant Cart (NameError) 110 | from /Users/user/.rvm/gems/ruby-2.4.0/gems/rspec-core-3.5.4/lib/rspec/core/configuration.rb:1435:in `load' 111 | ...[略]... 112 | from /Users/user/.rvm/gems/ruby-2.4.0/bin/ruby_executable_hooks:15:in `eval' 113 | from /Users/user/.rvm/gems/ruby-2.4.0/bin/ruby_executable_hooks:15:in `
' 114 | 115 | 不意外,因為我們根本還沒寫。 116 | 117 | ### 建立 Cart Model 118 | 119 | 首先,大家看到 Model 這個字,就會以為跟資料庫有關,然後就準備用 `rails g model Cart...` 指令來產生這個檔案了。事實上並不需要的,因為我們這個購物車只需要是個一般的純 Ruby 類別就行了,並不需要資料庫功能。所以請自己手動新增一個 `cart.rb` 檔案到 `app/models` 目錄裡: 120 | 121 | ```ruby 122 | # 檔案:app/models/cart.rb 123 | class Cart 124 | end 125 | ``` 126 | 127 | 什麼內容還不用寫,也不需要繼承自 Rails 的類別,只要先定義一個 `Cart` 類別就好。接著執行 `rspec` 指令,跑一下測試: 128 | 129 | $ rspec 130 | * 131 | 132 | Pending: (Failures listed here are expected and do not affect your suite's status) 133 | 134 | 1) Cart add some examples to (or delete) /private/tmp/shopping_mall/spec/models/cart_spec.rb 135 | # Not yet implemented 136 | # ./spec/models/cart_spec.rb:4 137 | 138 | 139 | Finished in 0.0008 seconds (files took 3.31 seconds to load) 140 | 1 example, 0 failures, 1 pending 141 | 142 | 這樣剛才找不到 `Cart` 類別的問題就解決了。這個 `pending` 的訊息是因為在 `spec/models/cart_spec.rb` 檔案裡有一行 `pending`,把它刪掉再執行一次就不會有錯誤訊息了。 143 | 144 | ### 把規格轉成測試 145 | 146 | 回到 `spec/models/cart_spec.rb` 檔案,把我們要測試的內容補上去: 147 | 148 | ```ruby 149 | require 'rails_helper' 150 | 151 | RSpec.describe Cart, type: :model do 152 | describe "購物車基本功能" do 153 | it "可以把商品丟到到購物車裡,然後購物車裡就有東西了" do 154 | end 155 | 156 | it "如果加了相同種類的商品到購物車裡,購買項目(CartItem)並不會增加,但商品的數量會改變" do 157 | end 158 | 159 | it "商品可以放到購物車裡,也可以再拿出來" do 160 | end 161 | 162 | it "每個 Cart Item 都可以計算它自己的金額(小計)" do 163 | end 164 | 165 | it "可以計算整台購物車的總消費金額" do 166 | end 167 | 168 | it "特別活動可能可搭配折扣(例如聖誕節的時候全面打 9 折,或是滿額滿千送百)" do 169 | end 170 | end 171 | 172 | 173 | describe "購物車進階功能" do 174 | it "可以將購物車內容轉換成 Hash,存到 Session 裡" do 175 | end 176 | 177 | it "可以把 Session 的內容(Hash 格式),還原成購物車的內容" do 178 | end 179 | end 180 | end 181 | ``` 182 | 183 | ### 測試 Step 1 184 | 185 | 先從第一個測試開始寫吧: 186 | 187 | ```ruby 188 | require 'rails_helper' 189 | 190 | RSpec.describe Cart, type: :model do 191 | describe "購物車基本功能" do 192 | it "可以把商品丟到到購物車裡,然後購物車裡就有東西了" do 193 | cart = Cart.new # 新增一台購物車 194 | cart.add_item 1 # 隨便丟一個東西到購物車裡 195 | expect(cart.empty?).to be false # 它應該不是空的 196 | end 197 | 198 | #...[略]... 199 | end 200 | ``` 201 | 202 | 這時候執行測試,應該是會失敗的,因為 `add_item` 跟 `empty?` 方法都還沒有寫。 203 | 204 | ### 實作 Step 1 205 | 206 | 回到 `Cart` 類別加上這兩個方法,想辦法通過測試: 207 | 208 | ```ruby 209 | class Cart 210 | def initialize 211 | @items = [] 212 | end 213 | 214 | def add_item(product_id) 215 | @items << product_id 216 | end 217 | 218 | def empty? 219 | @items.empty? 220 | end 221 | end 222 | ``` 223 | 224 | 說明: 225 | 226 | 1. 在 `initialize` 的時候初始化一個空陣列 `@items` 227 | 2. 在 `add_item` 的時候,把傳進來的東西往 `@items` 陣列裡丟 228 | 3. `empty?` 方法則是回傳 `@items` 陣列是不是空的 229 | 230 | 沒打錯字的話,應該可以通過測試。 231 | 232 | ### 測試 Step 2 233 | 234 | 繼續加測試: 235 | 236 | ```ruby 237 | require 'rails_helper' 238 | 239 | RSpec.describe Cart, type: :model do 240 | describe "購物車基本功能" do 241 | # ...[略] 242 | 243 | it "如果加了相同種類的商品到購物車裡,購買項目(CartItem)並不會增加,但商品的數量會改變" do 244 | cart = Cart.new # 新增一台購物車 245 | 3.times { cart.add_item(1) } # 加了 3 次的 1 246 | 5.times { cart.add_item(2) } # 加了 5 次的 2 247 | 2.times { cart.add_item(3) } # 加了 2 次的 3 248 | 249 | expect(cart.items.length).to be 3 # 總共應該會有 3 個 item 250 | expect(cart.items.first.quantity).to be 3 # 第 1 個 item 的數量會是 3 251 | expect(cart.items.second.quantity).to be 5 # 第 2 個 item 的數量會是 5 252 | end 253 | 254 | # ...[略]... 255 | end 256 | end 257 | ``` 258 | 259 | ### 實作 Step 2 260 | 261 | 一樣想辦法來通過測試。我們在 step 1 寫的 `add_item` 不僅沒有任何檢查,只是一股腦兒的把收到的 `product_id` 的往 `@items` 陣列裡丟,而且光靠 `product_id` 也不足以記錄傳進來的數量,看起來會需要另外的類別來存放收到的商品以及數量。先修改原來 `Cart` 類別的內容: 262 | 263 | ```ruby 264 | class Cart 265 | attr_reader :items 266 | 267 | def initialize 268 | @items = [] 269 | end 270 | 271 | def add_item(product_id) 272 | found_item = items.find { |item| item.product_id == product_id } 273 | 274 | if found_item 275 | found_item.increment 276 | else 277 | @items << CartItem.new(product_id) 278 | end 279 | end 280 | 281 | def empty? 282 | items.empty? 283 | end 284 | end 285 | ``` 286 | 287 | 說明: 288 | 289 | 1. 加了一個 `items` 的 `attr_reader`,讓內、外部的存取更方便一些。 290 | 2. `add_item` 方法裡使用 `find` 方法來找看看是不是有 item 的 product_id 跟傳進來的 product_id 是一樣的。這個 `find` 方法不是一般 Model 的 find 方法,它只是一個一般的陣列方法,可以找到是否有符合條件的元素。 291 | 3. 如果找到的話,就叫那個 `found_item` 增加數量 292 | 4. 找不到的話,就是用 `CartItem` 類別包一個物件,然後丟往 `@items` 陣列。 293 | 294 | 接下來,跟新增 `Cart` 類別一樣,新增一個 `cart_item.rb` 在 `app/models` 底下,而且也同樣因為不需要用到資料庫相關的功能,所以也不需要繼承自 Rails 的類別: 295 | 296 | ```ruby 297 | # 檔案:app/models/cart_item.rb 298 | 299 | class CartItem 300 | attr_reader :product_id, :quantity 301 | 302 | def initialize(product_id, quantity = 1) 303 | @product_id = product_id 304 | @quantity = quantity 305 | end 306 | 307 | def increment(n = 1) 308 | @quantity += n 309 | end 310 | end 311 | ``` 312 | 313 | 說明: 314 | 315 | 1. 加了 2 個 `attr_reader`,分別是 `product_id` 以及 `quantity`,方便外部取用。 316 | 2. 在初始化的時候會接收 `product_id` 以及 `quantity` 兩個參數,但如果沒有傳 `quantity` 則是使用預設值 1 317 | 3. `increment` 方法會接收一次要新增的數量,預設值 1 318 | 319 | 這樣一來測試應該就可以通過了 320 | 321 | ### 測試 Step 3 322 | 323 | 繼續測試: 324 | 325 | ```ruby 326 | require 'rails_helper' 327 | 328 | RSpec.describe Cart, type: :model do 329 | describe "購物車基本功能" do 330 | # ...[略]... 331 | 332 | it "商品可以放到購物車裡,也可以再拿出來" do 333 | cart = Cart.new 334 | p1 = Product.create(title:"七龍珠") # 建立商品 1 335 | p2 = Product.create(title:"冒險野郎") # 建立商品 2 336 | 337 | 4.times { cart.add_item(p1.id) } # 放了 4 次的商品 1 338 | 2.times { cart.add_item(p2.id) } # 放了 2 次的商品 2 339 | 340 | expect(cart.items.first.product_id).to be p1.id # 第 1 個 item 的商品 id 應該會等於商品 1 的 id 341 | expect(cart.items.second.product_id).to be p2.id # 第 2 個 item 的商品 id 應該會等於商品 2 的 id 342 | expect(cart.items.first.product).to be_a Product # 第 1 個 item 拿出來的東西應該是一種商品 343 | end 344 | end 345 | 346 | # ...[略]... 347 | end 348 | ``` 349 | 350 | 執行測試,沒意外應該是會出錯的... 351 | 352 | ### 實作 Step 3 353 | 354 | 因為我們要透過 `Product` Model 來建立資料,然後放到購物車裡,所以這時候就真的需要請產生器來幫我們建一個 `Product` Model 了: 355 | 356 | $ rails g model Product title description:text price:integer 357 | Running via Spring preloader in process 88733 358 | invoke active_record 359 | create db/migrate/20170110151200_create_products.rb 360 | create app/models/product.rb 361 | invoke rspec 362 | create spec/models/product_spec.rb 363 | invoke factory_girl 364 | create spec/factories/products.rb 365 | 366 | 別忘了執行 `rails db:migrate` 喔! 367 | 368 | 接下來,回頭來幫 `CartItem` 加上 `product` 方法,讓它可以根據目前這條 item 的 `product_id` 查出產品是什麼: 369 | 370 | ```ruby 371 | class CartItem 372 | attr_reader :product_id, :quantity 373 | 374 | def initialize(product_id, quantity = 1) 375 | @product_id = product_id 376 | @quantity = quantity 377 | end 378 | 379 | def increment(n = 1) 380 | @quantity += n 381 | end 382 | 383 | def product 384 | Product.find_by(id: product_id) 385 | end 386 | end 387 | ``` 388 | 389 | 如此一來,透過 `Cart` 類別的 `add_item` 方法,把 A 商品加到購物車裡,拿出來之後還是 A 商品,而不是放蘋果進去,結拿拿出來變香蕉。 390 | 391 | ### 小結 392 | 393 | TDD 的手感: 394 | 395 | 1. 確認規格,把規格轉成測試 396 | 2. 執行測試,一定會失敗!(除非你有小精靈) 397 | 3. 想辦法讓測試通過 398 | 4. 回到第一步 399 | 400 | 寫測試的時候,先不用擔心寫得好不好看或優不優雅,先用最直覺的方式把你想測的內容寫出來,然後把實作內容做出來,想辦法通過測試。到這裡,我們大概完成了購物車一半的功能,下個章節將會繼續完成另外一半... 401 | 402 | > 本章節程式碼可於 GitHub 上取得 https://github.com/kaochenlong/shopping_mall 403 | 404 | -------------------------------------------------------------------------------- /markdown/chapter07-ruby-basic-3.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | title: 方法與程式碼區塊(block) 4 | comments: true 5 | permalink: /chapters/07-ruby-basic-3.html 6 | 7 | --- 8 | 9 | # 方法與程式碼區塊(block) 10 | 11 | Rails 不是一種程式語言,它是一種用 Ruby 這個程式語言所開發出來的網頁開發框架(Web Framework)。 12 | 13 | 接下來幾個章節的目的並不是要詳細的介紹 Ruby 這個程式語言所有的功能,而是希望讓大家對 Ruby 有足夠的基本認識,之後大家在閱讀或撰寫 Rails 專案的時候,會比較知道 Rails 在寫些什麼。 14 | 15 | - [方法(Method)](#method) 16 | - [程式碼區塊((Block)](#block) 17 | 18 | ## 方法(Method) 19 | 20 | ### 定義方法 21 | 22 | 在 Ruby 定義方法,使用的是 `def` 這個關鍵字: 23 | 24 | ```ruby 25 | def say_hello_to(name) 26 | puts "hello, #{name}" 27 | end 28 | ``` 29 | 30 | 這樣就定義了一個 `say_hello_to` 方法,後面的 `name` 是這個方法的參數(parameter),不限定只能傳一個,如果要傳多個參數可使用逗號分開。方法的命名慣例跟一般的區域變數差不多,是使用小寫加底線的組合。 31 | 32 | ### 呼叫方法 33 | 34 | 要執行已經定義的方法,只要直接呼叫方法的名字即可: 35 | 36 | ```ruby 37 | say_hello_to("帥哥") # => hello, 帥哥 38 | ``` 39 | 40 | 也可視情況省略小括號: 41 | 42 | ```ruby 43 | say_hello_to "帥哥" # => hello, 帥哥 44 | ``` 45 | 46 | 在 Ruby 執行方法,經常省略小括號,目的是為了讓程式碼看起來更不像程式碼,反而像是一般的文章。 47 | 48 | ### 參數預設值 49 | 50 | 在定義方法時,可幫參數加上預設值: 51 | 52 | ```ruby 53 | def say_something(message = "something") 54 | "message: #{message}" 55 | end 56 | 57 | p say_something "hi" # => message: hi 58 | p say_something # => message: something 59 | ``` 60 | 61 | 如果有正確傳參數給方法,那就會使用傳進去的參數;如果沒有,則使用預設值。 62 | 63 | ### 方法的回傳值 64 | 65 | 有時候你會希望方法在接收參數並在執行完成之後,回傳執行之後的結果,例如我們可以寫一個 BMI(Body Mass Index,身體質量指數)方法,它可以接收身高與體重,並回傳計算結果: 66 | 67 | ```ruby 68 | # BMI值計算公式: BMI = 體重(單位:公斤)/ 身高平方(單位:公尺) 69 | 70 | def bmi_calculator(height, weight) 71 | return weight / height ** 2 72 | end 73 | 74 | puts bmi_calculator(1.70, 80) # => 27.681 75 | ``` 76 | 77 | 上面這段範例中的 `return` 是指這個方法執行完成之後,把最後的計算結果回傳給呼叫它的方法,在這個範例裡也就是 `puts`,然後會被印出來在畫面上。 78 | 79 | 在 Ruby 方法裡,最後一行的執行結果會自動被回傳,所以上面這個例子的 `return` 也是可以省略的,像這樣: 80 | 81 | ```ruby 82 | def bmi_calculator(height, weight) 83 | weight / height ** 2 84 | end 85 | ``` 86 | 87 | ### puts 不是 return,也沒有回傳值 88 | 89 | 對程式新手來說,有可能會寫出這樣的語法: 90 | 91 | ```ruby 92 | def bmi_calculator(height, weight) 93 | puts weight / height ** 2 94 | end 95 | ``` 96 | 97 | 執行 `bmi_calculator` 方法,的確是會印出內容,但會印出內容是因為在方法裡面直接 `puts` 把內容印出來,並不是因為這個方法回傳所造成的。事實上,`puts` 方法本身是沒有回傳值的喔。 98 | 99 | ### 問號跟驚嘆號也是方法的一部份 100 | 101 | 在 Ruby 定義方法時,方法的名字一般除了使用英文、底線及數字的組合外,也可以使用問號 `?` 跟驚嘆號 `!`(其實等號 `=` 也可以),但僅能放在方法名字的最後面,像這樣: 102 | 103 | ```ruby 104 | def is_adult?(age) 105 | age >= 18 106 | end 107 | ``` 108 | 109 | 在使用的時候跟一般的方法沒什麼差別,但別忘了要把問號加上去: 110 | 111 | ```ruby 112 | if is_adult?(20) 113 | puts "你是成年人了!" 114 | end 115 | ``` 116 | 117 | 在通常會使用問號,慣例上是表示這個方法會回傳布林值(true 或 false),不管是 Ruby 或 Rails,都很常可以看到這樣的慣例: 118 | 119 | ```ruby 120 | puts "".empty? # => true 121 | puts [1, 2, 3, 4, 5].include?(3) # => true 122 | puts "Ruby".start_with?("Ru") # => true 123 | ``` 124 | 125 | 而使用驚嘆號,通常是表示使用這個方法可能會有「副作用」或「驚喜」,舉個例子來說,像是陣列有個叫做 `reverse` 的方法,它可以產生一個跟原來陣列的相反排序的新陣列: 126 | 127 | ```ruby 128 | original_list = [1, 2, 3, 4, 5] 129 | reversed_list = original_list.reverse 130 | 131 | p reversed_list # => [5, 4, 3, 2, 1] 132 | p original_list # => [1, 2, 3, 4, 5] 133 | ``` 134 | 135 | `reverse` 方法會回傳一個新的陣列回來,不會影響原來的資料。但如果是呼叫有驚嘆號版本的 `reverse!` 就不同了: 136 | 137 | ```ruby 138 | original_list = [1, 2, 3, 4, 5] 139 | reversed_list = original_list.reverse! 140 | 141 | p reversed_list # => [5, 4, 3, 2, 1] 142 | p original_list # => [5, 4, 3, 2, 1] 143 | ``` 144 | 145 | `reverse!` 方法除了會回傳一個陣列之外,原來的陣列也會直接跟著一起被影響了。所以如果你這個方法可能會有一些意外驚喜,在慣例上通常會加上一個驚嘆號,提醒一下使用這個方法的人。 146 | 147 | ### 問題:是變數還是方法? 148 | 149 | 因為 Ruby 在執行方法的時候可以適時的省略小括號,可以讓你的方法寫起來像是個區域變數一樣。不過想一下這個情況: 150 | 151 | ```ruby 152 | age = 18 153 | 154 | def age 155 | 20 156 | end 157 | 158 | puts age # => 會得到 18 還是 20? 159 | ``` 160 | 161 | 這裡有個區域變數 `age` 指向數字 18,也有一個方法叫 `age` 會回傳數字 20,請問你認為是會印出 18 還是 20? 162 | 163 | 答案是 18,因為 Ruby 在同一個範圍內,如果遇到同名的區域變數及方法,會以區域變數優先。那如果想要得到 20 的話該怎麼辦?其實超簡單的,就是把最後一行的 `puts age` 改成 `puts age()` 就行了。大家寫 Ruby 省略小括號省到已經習慣了,都忘了其實呼叫方法的基本招使用小括號,這反而是在其它程式語言不太會有的困擾 :) 164 | 165 | ### 問題:參數有幾個? 166 | 167 | 在 Rails 裡常會看到 `link_to` 這樣寫: 168 | 169 | ```erb 170 | <%= link_to '刪除', user, method: :delete, data: { confirm: 'sure?' }, class:'btn' %> 171 | ``` 172 | 173 | 你看得出來上面這段範例中,`link_to` 方法共有幾個參數嗎?如果你是用逗號的數量數出來是 5 個,那你就需要繼續往下看了 :) 174 | 175 | Ruby 很愛省略東西,像是方法的小括號,所以原來上面的 `link_to` 語法原本應該長這樣: 176 | 177 | ```erb 178 | <%= link_to('刪除', user, method: :delete, data: { confirm: 'sure?' }, class:'btn') %> 179 | ``` 180 | 181 | 除了常常省略小括號外,偶爾也會省略大括號。在 Ruby 中如果最後一個參數是 Hash 的話,它的大括號是可以省略的。舉個例子來說: 182 | 183 | ```ruby 184 | def say_hello_to(name, options = {}) 185 | # do something 186 | end 187 | ``` 188 | 189 | 如果要使用這個方法,可以這樣寫: 190 | 191 | ```ruby 192 | say_hello_to "eddie", {age: 18, favorite: 'ruby'} 193 | ``` 194 | 195 | 又,因為最後一個參數是 Hash,所以 Hash 的大括號也可省略: 196 | 197 | ```ruby 198 | say_hello_to "eddie", age: 18, favorite: 'ruby' 199 | ``` 200 | 201 | 如果你了解有什麼東西被省略的話,一開始的那段 link_to 的範例還原之後會變成: 202 | 203 | ```erb 204 | <%= link_to('刪除', user, {method: :delete, data: { confirm: 'sure?' }, class:'btn'}) %> 205 | ``` 206 | 207 | 所以,其實參數個數只有 3 個,最後一個參數是一個 Hash。也因為最後一個是 Hash,Hash 本身是沒有順序的,所以 Hash 裡的 `method` 要放後面或是 `class` 要放前面其實都可已。 208 | 209 | Ruby 的語法可以適時的省略小括號、大括號以及 return,程式碼寫起來雖然會更像在寫文章,但對新手來說可能會容易混淆,需要花一點時間了解到底省略了哪些東西。 210 | 211 | ## 程式碼區塊(Block) 212 | 213 | Block 在 Ruby 或 Rails 裡大量的被使用,像是在使用迴圈的時候,可能都寫過這樣的程式碼: 214 | 215 | ```ruby 216 | 5.times { puts "Hello, Ruby" } # 這會印 5 次的 Hello Ruby 217 | 218 | friends = ["魯夫", "孫悟空", "黑崎一護", "旋渦嗚人"] 219 | friends.each do |friend| 220 | puts friend # 這會把陣列裡的元素一個一個印出來 221 | end 222 | ``` 223 | 224 | 其中,那個大括號 `{ ... }` 以及 `do ... end`,在 Ruby 稱之一個程式碼區塊(Block) 225 | 226 | ### Block 不是物件 227 | 228 | 我們常說,在 Ruby 裡,幾乎什麼東西都是物件,但其實還是有少數的例外,例如 Block 就不是物件。 Block 沒有辦法單獨的存在,也沒辦法把它指定給某個變數,像這樣的寫法都會造成語法錯誤(Syntax Error): 229 | 230 | ```ruby 231 | { puts "Hello, Ruby" } # 這樣會產生語法錯誤 232 | action = { puts "Hello, Ruby" } # 這樣也會產生語法錯誤 233 | ``` 234 | 235 | ### Block 不是參數 236 | 237 | Block 通常得像寄生蟲一樣依附或寄生在其它的方法或物件(或是使用某些類別把它物件化),但它不是參數,例如: 238 | 239 | ```ruby 240 | def say_hello_to(name) 241 | # do something here 242 | end 243 | 244 | say_hello_to("悟空") { 245 | puts "這裡是 Block" 246 | } 247 | 248 | # 或是 do ... end 寫法 249 | say_hello_to("悟空") do 250 | puts "這裡是 Block" 251 | end 252 | ``` 253 | 254 | Block 不是參數,在上面這段範例中,`name` 才是參數,但 Block 不是。上面這段程式碼執行之後不會有任何錯誤,但 Block 裡要執行的動作也不會執行。 255 | 256 | ### 如何執行 Block 的內容? 257 | 258 | 想像一下這段的對話: 259 | 260 | > 某 Block:「嘿,say_hello_to 方法,我要掛在你身上囉」 261 | 262 | > say_hello_to :「隨便啊,你要掛就讓你掛,但要不要讓你執行是我決定的!」 263 | 264 | 如果想要讓附掛的 Block 執行的話,可使用 `yield` 方法,暫時把控制權交棒給 Block,等 Block 執行結束後再把控制權交回來: 265 | 266 | ```ruby 267 | def say_hello 268 | puts "開始" 269 | yield # 把控制權暫時讓給 Block 270 | puts "結束" 271 | end 272 | 273 | say_hello { 274 | puts "這裡是 Block" 275 | } 276 | ``` 277 | 278 | 執行上面這段範例會得到: 279 | 280 | 開始 281 | 這裡是 Block 282 | 結束 283 | 284 | ### 傳參數給 Block 285 | 286 | 有時候你會看到像這樣的寫法: 287 | 288 | ```ruby 289 | 5.times do |i| 290 | puts i 291 | end 292 | ``` 293 | 294 | 那個 `|i|` 是什麼呢?這個在兩根看起來像牆壁中間的 `i`,是在這個 Block 裡專屬的區域變數,Block 執行結束後就會失效了: 295 | 296 | ```ruby 297 | 5.times do |i| 298 | puts i # 這個變數 i 只有在 Block 裡有效,會依序印出數字 0 到 4 299 | end 300 | 301 | puts i # 離開 Block 之後就失效,出現找不到變數的錯誤(NameError) 302 | ``` 303 | 304 | 所以,到底是這個 i 是怎麼來的?事實上,它就只是你在使用 yield 方法把控制權轉讓給 Block 的時候,順便把值帶給 Block 而已: 305 | 306 | ```ruby 307 | def say_hello 308 | puts "開始" 309 | yield 123 # 把控制權暫時讓給 Block,並且傳數字 123 給 Block 310 | puts "結束" 311 | end 312 | 313 | say_hello { |x| # 這個 x 是來自 yield 方法 314 | puts "這裡是 Block,我收到了 #{x}" 315 | } 316 | ``` 317 | 318 | 下回大家再看到 `|i|` 的寫法,應該就知道它是什麼意思了。 319 | 320 | ### Block 的回傳值 321 | 322 | 其實 `yield` 方法除了把控制權暫時的讓給後面的 Block 之外,Block 最後一行的執行結果也會自動變成 Block 的回傳值,所以可把 Block 當做判斷內容: 323 | 324 | ```ruby 325 | def pick(list) 326 | result = [] 327 | list.each do |i| 328 | result << i if yield(i) # 如果 yield 的回傳值是 true 的話... 329 | end 330 | result 331 | end 332 | 333 | p pick([*1..10]) { |x| x % 2 == 0 } # => [2, 4, 6, 8, 10] 334 | p pick([*1..10]) { |x| x < 5 } # => [1, 2, 3, 4] 335 | ``` 336 | 337 | 上面這段範例的 pick 方法,會根據 Block 的條件,挑出符合條件的元素。 338 | 339 | ### 用 return 回傳 Block 的結果? 340 | 341 | Block 的最後一行執行結果自動會變成 Block 的回傳值,這裡並不是省略了 return,而是不能使用 return 回傳結果。所以如果你在上面那個例子,在 Block 裡試圖用 `return` 回傳結果,像這樣: 342 | 343 | ```ruby 344 | pick([*1..10]) { |x| return x % 2 == 0 } 345 | ``` 346 | 347 | 這會產生 LocalJumpError 錯誤。因為,Block 並不是一個方法,所以它不知道你要 Return 到哪裡去而造成錯誤。 348 | 349 | ### 問題:`5.times { ... }` 很好用,但你能自己土砲一個類似的方法嗎? 350 | 351 | ```ruby 352 | def my_times(n) 353 | i = 0 354 | while n > i 355 | i += 1 356 | yield i 357 | end 358 | end 359 | 360 | my_times(5) { |num| 361 | puts "hello, #{num}xRuby" 362 | } 363 | 364 | # 得到結果 365 | # hello, 1xRuby 366 | # hello, 2xRuby 367 | # hello, 3xRuby 368 | # hello, 4xRuby 369 | # hello, 5xRuby 370 | ``` 371 | 372 | 做法就是在執行 `while` 迴圈的同時,不斷的把數字透過 `yield` 傳出來,這樣就可以做出一個類似 `5.times { ... }` 的效果了。 373 | 374 | ### 大括號跟 do ... end 的差別 375 | 376 | 大部份的情況,Block 的大括號的寫法跟 `do ... end` 寫法是可以互換的,像這樣: 377 | 378 | ```ruby 379 | # 使用 do .. end 寫法 380 | 5.times do 381 | puts "哈囉,世界" 382 | end 383 | 384 | # 使用大括號寫法 385 | 5.times { 386 | puts "哈囉,世界" 387 | } 388 | ``` 389 | 390 | 如果 Block 的內容如果有多行,通常會建議使用 `do .. end` 寫法,如果只有一行,則建議使用大括號寫法,可讓語法看起來精簡一些: 391 | 392 | ```ruby 393 | # 使用大括號一行寫法 394 | 5.times { puts "哈囉,世界" } 395 | ``` 396 | 397 | 但事實上,這兩種情況是有一些微妙的差別的,並不是所有情況都互相交換。看看這段程式碼範例: 398 | 399 | ```ruby 400 | p [*1..10].map { |i| i * 2 } 401 | # => 得到 [2, 4, 6, 8, 10, 12, 14, 16, 18, 20] 402 | 403 | p [*1..10].map do |i| i * 2 end 404 | # => 得到 405 | ``` 406 | 407 | 會造成不同結果的原因,有點像是數學的「先乘除後加減」的規則,大括號的優先順序較高: 408 | 409 | ```ruby 410 | p [*1..10].map { |i| i * 2 } 411 | 412 | # 還原省略的小括號 413 | p([*1..10].map { |i| i * 2 }) 414 | ``` 415 | 416 | 但 `do ... end` 的優先順序較低,會有不一樣的解讀: 417 | 418 | ```ruby 419 | p [*1..10].map do |i| i * 2 end 420 | 421 | # 還原省略的小括號 422 | p([*1..10].map) do |i| i * 2 end 423 | ``` 424 | 425 | 因為優先順序較低,所以變成先跟 p 結合了,造成後面附掛的 Block 就不會被處理了。 426 | 427 | ### 把 Block 物件化 428 | 429 | 前面提到,Block 本身並不是物件,它沒辦法單獨的存在 Ruby 的世界裡,需要依附在方法或物件後面。 430 | 431 | 但其實也是可以把 Block 物件化,例如使用 `Proc` 類別: 432 | 433 | ```ruby 434 | greeting = Proc.new { puts "哈囉,世界" } # 使用 Proc 類別可把 Block 物件化 435 | ``` 436 | 437 | 要使用它的時候,只要執行這個物件上的 `call`: 438 | 439 | ```ruby 440 | greeting.call # 印出 "哈囉,世界" 441 | ``` 442 | 443 | 如果要帶參數也可以: 444 | 445 | ```ruby 446 | say_hello_to = Proc.new { |name| puts "你好,#{name}"} 447 | say_hello_to.call("尼特羅會長") 448 | ``` 449 | 450 | ### Proc 呼叫方式 451 | 452 | 要執行一個 Proc 物件,可以使用 `call` 方法,但其實還有其它好幾種使用方法,例如: 453 | 454 | ```ruby 455 | say_hello_to.call("尼特羅會長") # 使用 call 方法 456 | say_hello_to.("尼特羅會長") # 使用小括號(注意,有多一個小數點) 457 | say_hello_to["尼特羅會長"] # 使用中括號 458 | say_hello_to === "尼特羅會長" # 使用三個等號 459 | say_hello_to.yield "尼特羅會長" # 使用 yield 方法 460 | ``` 461 | 462 | 這幾種方式都可以呼叫 Proc 物件。 463 | 464 | 如果是第一次接觸 Ruby 的朋友,使用 Block 一開始可能會有點不習慣。不過因為在 Ruby 或 Rails 的專案裡,Block 被用到的機會非常高,儘早熟悉 Block 的使用是很有幫助的喔,加油! 465 | 466 | -------------------------------------------------------------------------------- /markdown/chapter23-testing-with-rspec-part-2.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | title: 寫測試讓你更有信心 Part 2 4 | comments: true 5 | permalink: /chapters/23-testing-with-rspec-part-2.html 6 | 7 | --- 8 | 9 | # 寫測試讓你更有信心 Part 2 10 | 11 | - [哥寫的不是測試,是規格](#spec) 12 | - [把規格轉成測試](#turn-specs-into-test-code) 13 | - [紅綠燈](#red-green-light) 14 | - [小結](#note) 15 | 16 | 前面介紹了什麼是測試,接下來就讓我們捲起袖子,動手寫測試吧! 17 | 18 | ## 哥寫的不是測試,是規格 19 | 20 | > 客人:「我想要做一個銀行帳戶系統,很簡單的,只要可以存錢、領錢以及顯示餘額就行了」 21 | 22 | 雖然客人開的需求有點「簡單」,但這個時候先不要電腦打開就開始寫 code,先來把規格寫出來吧: 23 | 24 | - 存錢功能 25 | - 原本帳戶有 10 元,存入 5 元之後,帳戶餘額變 15 元 26 | - 原本帳戶有 10 元,存入 -5 元之後,帳戶餘額還是 10 元(不能存入小於等於零的金額) 27 | - 領錢功能 28 | - 原本帳戶有 10 元,領出 5 元之後,帳戶餘額變 5 元 29 | - 原本帳戶有 10 元,試圖領出 20 元,帳戶餘額還是 10 元,但無法領出(餘額不足) 30 | - 原本帳戶有 10 元,領出 -5 元之後,帳戶餘額還是 10 元(不能領出小於或等於零的金額) 31 | 32 | 以上這些就是「規格」。再次強調一次,我們其實不是在「寫測試」,而是在「寫規格」,然後藉由一步一步滿足這些規格的過程來完成系統功能。 33 | 34 | ### 安裝 RSpec 35 | 36 | 在 Ruby/Rails 的世界有好幾套測試用的框架,目前比較受歡迎的主要有 `minitest` 跟 `RSpec`,這邊我們將使用 RSpec 來做介紹。安裝 RSpec 只要一行: 37 | 38 | $ gem install rspec 39 | Successfully installed rspec-3.5.0 40 | Parsing documentation for rspec-3.5.0 41 | Installing ri documentation for rspec-3.5.0 42 | Done installing documentation for rspec after 0 seconds 43 | 1 gem installed 44 | 45 | 這樣就行了。 46 | 47 | ## 把規格轉成測試 48 | 49 | 接下來,我們要把上面提到的規格轉成程式碼。 50 | 51 | 因為這不是一個 Rails 專案,所以可以找一個你喜歡的地方,隨便開一個資料夾,並在裡面新增一個名為 `bank_account_spec.rb` 的檔案,然後試著把上面的「規格」轉換成「測試」: 52 | 53 | ```ruby 54 | # 檔案:bank_account_spec.rb 55 | 56 | RSpec.describe BankAccount do 57 | describe "存錢功能" do 58 | it "原本帳戶有 10 元,存入 5 元之後,帳戶餘額變 15 元" do 59 | end 60 | 61 | it "原本帳戶有 10 元,存入 -5 元之後,帳戶餘額還是 10 元(不能存入小於等於零的金額)" do 62 | end 63 | end 64 | 65 | describe "領錢功能" do 66 | it "原本帳戶有 10 元,領出 5 元之後,帳戶餘額變 5 元" do 67 | end 68 | 69 | it "原本帳戶有 10 元,試圖領出 20 元,帳戶餘額還是 10 元,但無法領出(餘額不足)" do 70 | end 71 | 72 | it "原本帳戶有 10 元,領出 -5 元之後,帳戶餘額還是 10 元(不能領出小於或等於零的金額)" do 73 | end 74 | end 75 | end 76 | ``` 77 | 78 | 說明: 79 | 80 | 1. 檔名不一定要叫這個名字,只是習慣上會在要測試的對象的後面加上 `_spec` 或是 `_test` 以表示是測試用的檔案。 81 | 2. 這只是先把要測試的方向列出來而已,還沒開始寫。 82 | 3. 可以用中文沒關係,重點是清楚就好。 83 | 84 | ## 紅綠燈 85 | 86 | TDD 的流程,大概是一個「紅綠燈」的概念: 87 | 88 | 1. 先寫規格(測試),執行它,這時候一定會發生錯誤(紅燈)。 89 | 2. 實作功能,想辦法通過第 1 步發生錯誤的測試(綠燈)。 90 | 3. 回到第 1 步。 91 | 92 | ### 測試失敗!(紅燈) 93 | 94 | 接下來使用 `rspec` 程式來執行一下剛剛這個「規格」: 95 | 96 | $ rspec bank_account_spec.rb 97 | /private/tmp/bank/bank_spec.rb:1:in `': uninitialized constant BankAccount (NameError) 98 | from /Users/user/.rvm/gems/ruby-2.4.0/gems/rspec-core-3.5.4/lib/rspec/core/configuration.rb:1435:in `load' 99 | from /Users/user/.rvm/gems/ruby-2.4.0/gems/rspec-core-3.5.4/lib/rspec/core/configuration.rb:1435:in `block in load_spec_files' 100 | ...[略]... 101 | from /Users/user/.rvm/gems/ruby-2.4.0/bin/ruby_executable_hooks:15:in `eval' 102 | from /Users/user/.rvm/gems/ruby-2.4.0/bin/ruby_executable_hooks:15:in `
' 103 | 104 | 咦?發生錯誤了!如果你是第一次接觸 TDD 開發流程的話,可能會驚訝「哇!好多錯誤訊息啊!這怎麼解決?」 105 | 106 | 請不要擔心,也請大家要開始習慣,在寫測試的時候,這樣的錯誤是很常見的,甚至應該說這是正常的。仔細看一下錯誤訊息,錯誤訊息是 `uninitialized constant BankAccount`,表示還沒有 `BankAccount` 這個類別。這是當然的,因為我們根本還沒寫啊,這時候執行測試如果會過,如果不是你有養小精靈幫你寫程式,就是你在做夢還沒醒。 107 | 108 | ### 想辦法解決錯誤(綠燈) 109 | 110 | 我們來把 `BankAccount` 類別做出來吧,在同一個目錄下新增一個名為 `bank_account.rb` 的檔案,內容如下: 111 | 112 | ```ruby 113 | class BankAccount 114 | end 115 | ``` 116 | 117 | 再回到剛剛的 `bank_account_spec.rb` 的第一行加上: 118 | 119 | ```ruby 120 | require "./bank_account" 121 | ``` 122 | 123 | `require` 可以引入這個檔案的內容。接著,再執行一次測試: 124 | 125 | $ rspec bank_account_spec.rb 126 | ..... 127 | 128 | Finished in 0.00096 seconds (files took 0.13211 seconds to load) 129 | 5 examples, 0 failures 130 | 131 | 雖然我們的測試裡面還沒有寫任何的內容,但至少沒有剛剛的錯誤訊息了。 132 | 133 | ### 繼續寫測試(紅燈) 134 | 135 | ```ruby 136 | require "./bank_account" 137 | 138 | RSpec.describe BankAccount do 139 | describe "存錢功能" do 140 | it "原本帳戶有 10 元,存入 5 元之後,帳戶餘額變 15 元" do 141 | account = BankAccount.new(10) 142 | account.deposit 5 143 | expect(account.balance).to be 15 144 | end 145 | 146 | it "原本帳戶有 10 元,存入 -5 元之後,帳戶餘額還是 10 元(不能存入小於等於零的金額)" do 147 | end 148 | end 149 | 150 | describe "領錢功能" do 151 | # ... [略] ... 152 | end 153 | end 154 | ``` 155 | 156 | 等等!那個 `deposit` 跟 `balance` 功能是哪來的?其實現在這個當下還沒寫,我們只是先假設(或期待)這個類別有這些功能,而且在最後算出來的餘額數字是對的。 157 | 158 | 所以這時候執行測試,沒意外的話應該會出錯: 159 | 160 | $ rspec bank_account_spec.rb 161 | F.... 162 | 163 | Failures: 164 | 165 | 1) BankAccount 存錢功能 原本帳戶有 10 元,存入 5 元之後,帳戶餘額變 15 元 166 | Failure/Error: account = BankAccount.new(10) 167 | 168 | ArgumentError: 169 | wrong number of arguments (given 1, expected 0) 170 | # ./bank_account_spec.rb:6:in `initialize' 171 | # ./bank_account_spec.rb:6:in `new' 172 | # ./bank_account_spec.rb:6:in `block (3 levels) in ' 173 | 174 | Finished in 0.00122 seconds (files took 0.09661 seconds to load) 175 | 5 examples, 1 failure 176 | 177 | Failed examples: 178 | 179 | rspec ./bank_account_spec.rb:5 # BankAccount 存錢功能 原本帳戶有 10 元,存入 5 元之後,帳戶餘額變 15 元 180 | 181 | 果然出錯了!有錯才是正確的,因為我們根本還沒寫真正的功能啊,這個測試只是先假設我們有這些功能而已。 182 | 183 | ### 繼續想辦法解決錯誤(綠燈) 184 | 185 | 好啦,即然知道這個 `BankAccount` 類別要有 `deposit` 跟 `balance` 功能,那就來寫吧。回到 `bank_account.rb`,先讓我們把上面需要的功能寫出來: 186 | 187 | ```ruby 188 | class BankAccount 189 | def initialize(amount) 190 | @amount = amount 191 | end 192 | 193 | def balance 194 | @amount 195 | end 196 | 197 | def deposit(amount) 198 | @amount += amount 199 | end 200 | end 201 | ``` 202 | 203 | 執行測試: 204 | 205 | $ rspec bank_account_spec.rb 206 | ..... 207 | 208 | Finished in 0.00143 seconds (files took 0.09033 seconds to load) 209 | 5 examples, 0 failures 210 | 211 | 過了!搞定一個測試,再讓我們繼續往下看下一個測試。 212 | 213 | ### 繼續寫測試(紅燈) 214 | 215 | 接下來是存錢功能的第二個測試: 216 | 217 | ```ruby 218 | require "./bank_account" 219 | 220 | RSpec.describe BankAccount do 221 | describe "存錢功能" do 222 | it "原本帳戶有 10 元,存入 5 元之後,帳戶餘額變 15 元" do 223 | account = BankAccount.new(10) 224 | account.deposit 5 225 | expect(account.balance).to be 15 226 | end 227 | 228 | it "原本帳戶有 10 元,存入 -5 元之後,帳戶餘額還是 10 元(不能存入小於等於零的金額)" do 229 | account = BankAccount.new(10) 230 | account.deposit -5 231 | expect(account.balance).to be 10 232 | end 233 | end 234 | 235 | describe "領錢功能" do 236 | #...[略]... 237 | end 238 | end 239 | ``` 240 | 241 | 在寫測試的時候,測試的程式碼不需要寫得很漂亮,只要寫得清楚就好。在我們加上「不能存入小於等於零的金額」的測試之後,執行 `rspec`: 242 | 243 | $ rspec bank_account_spec.rb 244 | .F... 245 | 246 | Failures: 247 | 248 | 1) BankAccount 存錢功能 原本帳戶有 10 元,存入 -5 元之後,帳戶餘額還是 10 元(不能存入小於等於零的金額) 249 | Failure/Error: expect(account.balance).to be 10 250 | 251 | expected # => 10 252 | got # => 5 253 | 254 | ...[略]... 255 | 256 | Finished in 0.02562 seconds (files took 0.09308 seconds to load) 257 | 5 examples, 1 failure 258 | 259 | Failed examples: 260 | 261 | rspec ./bank_account_spec.rb:11 # BankAccount 存錢功能 原本帳戶有 10 元,存入 -5 元之後,帳戶餘額還是 10 元(不能存入小於等於零的金額) 262 | 263 | 咦?又壞了!先想一下,為什麼測試會壞掉? 264 | 265 | ### 繼續想辦法解決錯誤(綠燈) 266 | 267 | 如果回去看我們 `BankAccount` 的 `deposit` 方法的實作就會發現,我們根本沒檢查傳入的金額是不是小於零。修正一下: 268 | 269 | ```ruby 270 | class BankAccount 271 | def initialize(amount) 272 | @amount = amount 273 | end 274 | 275 | def balance 276 | @amount 277 | end 278 | 279 | def deposit(amount) 280 | @amount += amount if amount > 0 281 | end 282 | end 283 | ``` 284 | 285 | 在 `deposit` 方法裡加上了 `if amount > 0` 的判斷後,再執行一次測試: 286 | 287 | $ rspec bank_account_spec.rb 288 | ..... 289 | 290 | Finished in 0.00233 seconds (files took 0.09252 seconds to load) 291 | 5 examples, 0 failures 292 | 293 | 這樣就過了,而且之前的那個測試也沒壞。很好,就是維持這個手感,再讓我們繼續往下一個測試前進。 294 | 295 | ### 再繼續測試 296 | 297 | 避免拖太長的篇幅,我先一口氣先把剩下的三個測試寫完: 298 | 299 | ```ruby 300 | require "./bank_account" 301 | 302 | RSpec.describe BankAccount do 303 | describe "存錢功能" do 304 | # ... [略] ... 305 | end 306 | 307 | describe "領錢功能" do 308 | it "原本帳戶有 10 元,領出 5 元之後,帳戶餘額變 5 元" do 309 | account = BankAccount.new(10) 310 | amount = account.withdraw 5 311 | expect(amount).to be 5 312 | expect(account.balance).to be 5 313 | end 314 | 315 | it "原本帳戶有 10 元,試圖領出 20 元,帳戶餘額還是 10 元,但無法領出(餘額不足)" do 316 | account = BankAccount.new(10) 317 | amount = account.withdraw(20) 318 | expect(amount).to be 0 319 | expect(account.balance).to be 10 320 | end 321 | 322 | it "原本帳戶有 10 元,領出 -5 元之後,帳戶餘額還是 10 元(不能領出小於或等於零的金額)" do 323 | account = BankAccount.new(10) 324 | amount = account.withdraw(-5) 325 | expect(amount).to be 0 326 | expect(account.balance).to be 10 327 | end 328 | end 329 | end 330 | ``` 331 | 332 | 這時候執行測試,想當然爾一定是會發生錯誤訊息的: 333 | 334 | $ rspec bank_account_spec.rb 335 | ..FFF 336 | 337 | Failures: 338 | 339 | 1) BankAccount 領錢功能 原本帳戶有 10 元,領出 5 元之後,帳戶餘額變 5 元 340 | Failure/Error: amount = account.withdraw 5 341 | 342 | NoMethodError: 343 | undefined method `withdraw' for # 344 | # ./bank_account_spec.rb:21:in `block (3 levels) in ' 345 | 346 | 2) BankAccount 領錢功能 原本帳戶有 10 元,試圖領出 20 元,帳戶餘額還是 10 元,但無法領出(餘額不足) 347 | Failure/Error: amount = account.withdraw(20) 348 | 349 | NoMethodError: 350 | undefined method `withdraw' for # 351 | # ./bank_account_spec.rb:28:in `block (3 levels) in ' 352 | 353 | 3) BankAccount 領錢功能 原本帳戶有 10 元,領出 -5 元之後,帳戶餘額還是 10 元(不能領出小於或等於零的金額) 354 | Failure/Error: amount = account.withdraw(-5) 355 | 356 | NoMethodError: 357 | undefined method `withdraw' for # 358 | # ./bank_account_spec.rb:35:in `block (3 levels) in ' 359 | 360 | Finished in 0.00252 seconds (files took 0.09026 seconds to load) 361 | 5 examples, 3 failures 362 | 363 | Failed examples: 364 | 365 | rspec ./bank_account_spec.rb:19 # BankAccount 領錢功能 原本帳戶有 10 元,領出 5 元之後,帳戶餘額變 5 元 366 | rspec ./bank_account_spec.rb:26 # BankAccount 領錢功能 原本帳戶有 10 元,試圖領出 20 元,帳戶餘額還是 10 元,但無法領出(餘額不足) 367 | rspec ./bank_account_spec.rb:33 # BankAccount 領錢功能 原本帳戶有 10 元,領出 -5 元之後,帳戶餘額還是 10 元(不能領出小於或等於零的金額) 368 | 369 | ### 實作領錢功能,繼續想辦法解決錯誤 370 | 371 | `BankAccount` 的存錢功能實作如下: 372 | 373 | ```ruby 374 | class BankAccount 375 | def initialize(amount) 376 | @amount = amount 377 | end 378 | 379 | def balance 380 | @amount 381 | end 382 | 383 | def deposit(amount) 384 | @amount += amount if amount > 0 385 | end 386 | 387 | def withdraw(amount) 388 | if amount > 0 && @amount >= amount 389 | @amount -= amount 390 | amount 391 | else 392 | 0 393 | end 394 | end 395 | end 396 | ``` 397 | 398 | 執行一下測試: 399 | 400 | $ rspec bank_account_spec.rb 401 | ..... 402 | 403 | Finished in 0.00238 seconds (files took 0.09287 seconds to load) 404 | 5 examples, 0 failures 405 | 406 | 三個測試全部都過了,之前寫的存錢功能也沒有因此被弄壞。 407 | 408 | ## 小結 409 | 410 | 雖然這個例子可能有點簡單,但希望藉由這樣一連串的「紅綠燈」練習,可以讓大家更了解寫測試的手感,以及為什麼要寫測試。 411 | 412 | 因為在正式開工之前,先把規格好好的想清楚,可以讓開發者多想幾分鐘,不僅在類別及方法的命名上可以用比較適合的名字,也因為我們的類別或方法也都是照著「規格」寫出來的,所以相對的不會寫出多餘的類別或方法。 413 | 414 | 不過,測試全部都通過也不表示程式就完全不會有 Bug,只能說「目前的程式碼實作都有滿足現有的規格」。但藉由越完整的測試,除了可以減少 Bug 的出現,最重要的是可避免「修改完 A 功能結果 B 功能跟著壞掉」的問題。 415 | 416 | 在 Ruby/Rails 的世界,寫測試是算是業界標準的技能,希望大家可以趕快習慣寫測試的「紅綠燈」手感,藉由測試讓你對你的程式碼實作更有信心! 417 | 418 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /markdown/chapter14-layout-render-and-view-helper.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | title: Layout, Render 與 View Helper 4 | comments: true 5 | permalink: /chapters/14-layout-render-and-view-helper.html 6 | 7 | --- 8 | 9 | # Layout, Render 與 View Helper 10 | 11 | - [版型(Layout)](#layout) 12 | - [局部渲染(Partial Render)](#partial-render) 13 | - [View Helper](#view-helper) 14 | 15 | 在上個章節介紹了 CRUD 的分解動作,接下來這個章節要介紹的是在 Rails 專案 MVC 架構的 V。 16 | 17 | ## 版型 Layout 18 | 19 | 隨便打開一個在 `app/views` 目錄裡的檔案,例如上個章節的候選人列表頁面: 20 | 21 | ```erb 22 |

候選人列表

23 | 24 | <%= link_to "新增候選人", new_candidate_path %> 25 | 26 | 27 | 28 | 29 | 30 | ...[略]... 31 | 35 | 36 | <% end %> 37 | 38 |
投票 32 | <%= link_to "編輯", edit_candidate_path(candidate) %> 33 | <%= link_to "刪除", candidate_path(candidate), method: "delete", data: { confirm: "確認刪除" } %> 34 |
39 | ``` 40 | 41 | 在這個檔案裡,看不到任何 ``、`` 或 `<body>` 之類的 HTML 標籤,但檢視實際網頁的原始碼又都有,這是怎麼回事呢? 42 | 43 | ### yield 44 | 45 | 以這個例子來說,Controller 在處理 View 的時候,並不只是單純的只取用 `index.html.erb`,而是會先取用 Layout 檔案的內容(預設是 `app/views/layouts/application.html.erb`),然後把 `index.html.erb` 的內容填到 `<%= yield %>` 裡。 46 | 47 | ![image](/images/chapter14/layout.png) 48 | 49 | 版型的好處,就是不需要重複的寫一堆長得一樣的 HTML 標籤,例如頁面的頁首跟頁尾通常不會有什麼變化,這種就是版型適用的地方。 50 | 51 | 讓我們看一下 `app/views/layouts/application.html.erb` 的內容: 52 | 53 | ```erb 54 | <!DOCTYPE html> 55 | <html> 56 | <head> 57 | <title>MyCandidates 58 | <%= csrf_meta_tags %> 59 | <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> 60 | <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %> 61 | 62 | 63 | 64 |
65 | <%= yield %> 66 |
67 | 68 | 69 | ``` 70 | 71 | 這裡有幾行需要說明一下: 72 | 73 | 1. `csrf_meta_tags` 方法會在頁面上產生 `` 跟 `` 兩個 `` 標籤,用途主要是確保網站較不容易受到 CSRF(Cross-site request forgery)攻擊。 74 | 2. `stylesheet_link_tag` 方法會轉換成 CSS 的 `` 標籤。 75 | 3. `javascript_include_tag` 方法會轉換成 JavaScript 的 `