宅學習撰文 Ruby on Rails
====
簡介
----
Ruby 是由 Matz 松本行弘(Yukihiro Matsumoto, 別名Matz)在1995年發明的程式語言。之後在2004年7月時,大衛·漢森把Basecamp(37signals公司的作品)分離出一個框架並命名為Rails,但由於此框架實現的語言是Ruby,所以又普遍被稱為Ruby on Rails。
很多想接觸Ruby的新手確實都是慕名Ruby on Rails而來的,不過以筆者的經驗上會建議先花點時間玩Ruby這個程式語言,畢竟框架(不限任何一種語言)的使用難度會比直接操作程式碼高,而且也有不使用框架的簡易架設方式。下方會分成兩個章節來分別敘述Ruby語言以及Ruby on Rails框架,以便讓人確認讀者認知自己需要的是什麼。
Ruby語言
----
> 以下內容為摘錄中文版Ruby介紹並補上筆者的看法
> 摘錄網址:https://www.ruby-lang.org/zh_tw/about/
> 下方程式碼若沒有註明 edited from 就是筆者自行寫出或是由摘錄網址引用的。
根據官方網站的說法,Ruby是當初創造者結合他喜歡的語言(Perl、Smalltalk、Eiffel、Ada、以及 Lisp)而產生的新語言,所以具備了函數式(數學函數)與指令式(命令列表)程式撰寫的特性。而且Ruby屬於開放原始碼的程式語言,只要你具備C/C\+\+的基本流程控制理解,就可以在[Ruby-doc](https://ruby-doc.org/)上觀看該函數/命令實作的C\+\+程式碼。
> "Ruby is simple in appearance, but is very complex inside, just like our human body."
> Matz on Fri, 12 May 2000
> http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/2773
對於一個有接觸Pseudo code的程式撰寫者來說,Ruby很容易使用較短或是較自然的語句實現需求,但相對的程式語言的概念就不容易接觸。這部分歸功於他把所有的變數轉變成物件,藉由物件導向的概念,我們可以簡易的寫出與Pseudo code的程式碼。以下是將五個人的成績從高到低排序後取出倒數第二的方法。
```ruby
# 這是單行註解,score裡面有五筆成績
score = [75, 80, 60, 55, 92]
=begin
這是Ruby多行註解的方法
puts: 印出內容(後面必須為字串)
連續技能如下:
1. sort: 排序
2. reverse: 反序(改成從大排到小)
3. [-2]: 取出倒數第二
3. to_s: 把數字轉成字串
=end
puts score.sort.reverse[-2].to_s
# 會印出60
```
你可以在[這裡](https://www.tutorialspoint.com/execute_ruby_online.php)實行這段程式碼,或是透過以下的安裝教學來在你的電腦上執行Ruby程式碼。
#### 安裝與第一次執行
Ruby的安裝請依照[台灣官方網站](https://www.ruby-lang.org/zh_tw/documentation/installation/)的指示來選擇一個安裝,以下列表為安裝Ruby的注意事項。
* 若你是透過[RubyInstaller - for windows](https://rubyinstaller.org/downloads/)安裝的話,請務必選擇"with Devkit"的項目安裝,Rails或是其他套件的下載與安裝工具都包含在內。
* 在安裝完成後,請確保以下指令能在Console運作,若無法使用的話請去參考環境變數的設定
* ``ruby``: 這是ruby在執行程式的指令,後面補上一個``.rb``檔就可以執行Ruby程式
* ``irb``: 這是ruby的互動介面,你可以直接測試指令的效果或是讀取ruby程式來除錯
* ``gem``: 如果你要管理套件的話,需要用此開頭的指令來管理你的ruby套件
Ruby雖然有很多IDE整合,但筆者偏好編輯器與Console運作,以下是一個Ruby程式的執行流程:
1. 用你習慣的編輯器來編寫你的程式碼(也可以複製上面的程式測試看看),存成``檔名.rb``
2. 使用cmd(或是其他作業系統的Console)切換到該程式的資料夾,執行``ruby 檔名.rb``
3. 如果能看到畫面就是執行成功了。
以下我們會透過小分類說明Ruby語言的特性,若您使用的語言具備該特性,可以看看Ruby是怎樣呈現該特性的。
#### 萬物即物件 --- 物件導向
> "I wanted a scripting language that was more powerful than Perl, and more object-oriented than Python. That's why I decided to design my own language."
> Matz on 11/29/2001
> [http://www.linuxdevcenter.com/pub/a/linux/2001/11/29/ruby.html](http://web.archive.org/web/20180225100349/http://www.linuxdevcenter.com/pub/a/linux/2001/11/29/ruby.html)
通常程式語言與物件導向的概念會分成兩門課教學,但Ruby中的每個程式碼片段與交互作用都是透過物件來實現的。例如以下範例就可以看到Ruby對數字可以呼叫一個方法來實現迴圈,這樣就代表這個數字具備一個物件的「方法」。
```ruby
5.times { print "We *love* Ruby -- it's outrageous!" }
```
至於物件的可擴充性,我們可以用下面的程式碼來演示。
若javascript要實現此方法,必須要對該型態物件撰寫prototype來實現。
C\+\+中的operator overloading也可以實現,但是在執行期間沒有確認型態相等的話,Ruby會直接報型態錯誤(C\+\+在數字上可以強制轉換成字元碼,但Ruby只能在數字後面補上``.chr``來實現)。
```ruby
# 以下為擴充數字的加法功能
class Numeric
def plus(x)
self.+(x)
end
end
# .plus實現
y = 5.plus 6
# y 等於 11
```
#### 區塊的規範與使用
Ruby跟Python一樣不使用分號作為程式結尾,但是Ruby需要用end作為區塊規範。
Ruby與Python一樣都是屬於對縮排很敏感的語言,在撰寫時請務必讓編輯器糾正你的縮排使用。
modifier與do...end都是Ruby在區塊規範上的亮點,您可以看以下程式碼實現這兩種方法。
```ruby
# edited from: http://rubylearning.com/satishtalim/simple_constructs.html
# ruby 可以不使用return關鍵字,如果要強制回傳的話就必須先聲明。
# 透過modifier我們就可以將簡單的if與句化簡成比較自然順口的寫法。
def is_leap(year)
return true if year % 400 == 0 # 如果year能被400整除就是閏年
return false if year % 100 == 0 # 如果year能被100整除就不是閏年
return true if year % 4 == 0 # 如果year能被4整除就是閏年
false # 能到這裡的就一定不是閏年,可以不用寫return
end
years = [2000, 2012, 2018] # 給定一個陣列,裡面有很多個年份
years.each do |year| # 透過 each 來遍歷所有元素。
leap = is_leap(year)? "是" : "不是" # 簡易的三元運算式
puts "#{year}#{leap}閏年。" # 字串格式化貼合
end
# 以下是執行結果:
# 2000是閏年。
# 2012是閏年。
# 2018不是閏年。
```
#### 變數與Mixin
Ruby只能使用單繼承,這點跟Java與Objective-C一樣,但是Ruby有模組(Module)的概念(在Objective-C中被稱為Category,也有人覺得跟Java中Interface的概念差不多)。
在以下範例中我們透過混入``Enumerable``來實行map的概念,你可以看到此物件只有在裏頭的members上有陣列,卻可以透過對該物件的map來把members的內容改成小寫。
```ruby
# edited from: https://stackoverflow.com/questions/7220634/how-do-i-use-the-enumerable-mixin-in-my-class
class ATeam
# 使用Enumerable
include Enumerable
# 初始化的時候,把參數的內容全部塞進指定變數,這裡的星號跟C++的variable length parameter概念類似
def initialize(*members)
# 變數前面有"@"就代表他是實體變數,在物件內都可以使用
@members = members
end
# 跟Enumerable說明他該怎樣做遍歷,block.call是用來指明該元素要回傳到Enumerable做處理。
# Enumerable會用宣告好的each來一筆一筆取得資料。
def each(&block)
@members.each do |member|
block.call(member)
end
# or
# @members.each(&block)
end
end
# 使用ATeam新增一個物件,我們把成員一次丟進去。
# 這裡的變數沒有任何符號,屬於global
ateam = ATeam.new("Face", "B.A. Barracus", "Murdoch", "Hannibal")
# 我們使用map(Enumerable的其中一個方法)來將所有成員名字都弄成小寫,並列出來
# "&:方法" 與 "元素.方法" 同義,只是前者可以不用另外再寫匿名函數
p ateam.map(&:downcase)
# 輸出結果:
# ["face", "b.a. barracus", "murdoch", "hannibal"]
# p 與 puts 的用途不同,之後會再說明。
```
Ruby的變數宣告有四種,分別條列如下,請注意宣告可用區塊(block scope),仍需要注意變數一樣是先宣告後才能使用(沒先宣告的變數通常會用nil代替,後面會介紹nil):
1. 物件變數(class variable, ``@@variable``):只能在物件(class)的初始化以及子物件(sub class)被引用,除此之外的外部區塊都不能被引用。
2. 實體變數(instance variable, ``@variable``):只能在特定物件(specific object)內的所有方法(method)被引用,定義區塊的部份不能使用。
3. 全域變數(global variable, ``$variable``):在Ruby程式碼的任務區塊皆可以使用。
4. 區物變數(local variable, ``variable``):遵守C/C\+\+的變數宣告區塊,使用時請注意區塊(簡易判別︰只要你的宣告與調用之間有經過end就要注意)。
#### 額外補充
以下是官方介紹的特性
* Ruby 具有例外處理(exception handling)的能力。就如 Java 或 Python 一樣,可以讓使用者輕鬆的處理錯誤狀況。
* Ruby 對於所有的物件具有一個真正的標記-清除(mark and sweep)式的垃圾收集器(garbage collector)。使用者不必去維護擴充函式庫中的 參考計數器(reference counts)。如 Matz 說的:”這樣有益健康”。
* 在 Ruby 中撰寫 C 的擴充程式比在 Perl 或 Python 中方便,擁有許多方便的 API 可以讓 C 呼叫 Ruby。這樣可以將 Ruby 當成腳本語言,嵌入到其他軟體之中。它也具有 SWIG 的呼叫界面。
* 如果作業系統支援,Ruby 可以動態的載入擴充函式庫。
* Ruby 具有與作業系統無關的多線程(threading)能力。可以在所有可以執行 Ruby 的平台上都能夠達到多線程的目標,而不必管作業系統是否支援,就算是 MS-DOS 也行。
* Ruby 具有高度的移植性:它大部份是在 GNU/Linux 上發展出來,但是可以執行於多種的作業系統如: UNIX、Mac OS X、Windows、DOS、BeOS、OS/2 等。
以下是補充前面沒有介紹的部份
> ``p 變數`` 與 ``puts 變數`` 雖然有時候結果一樣,但效果是不一樣的
p 會將後面的變數進行``.inspect``呼叫,通常這呼叫會把變數進行``.to_s``的呼叫,所以通常``p 變數``會與``puts 變數.to_s``等效,不過為了養成正確的型態確認,請使用後者來印出變數(PHP中的echo),至於前者你可以當作除錯用(PHP中的var_dump)。
Ref: https://stackoverflow.com/questions/1255324/p-vs-puts-in-ruby
> nil與變數的關係
nil是拉丁文中的「零」,但跟實際上的0比起來他更像無的概念(就跟0與null的差別一樣),如果你還是無法分辨的話用下圖記憶會比較快(NaN: javascript中的Not a Number)。
![javascript's zero, null and Nan](https://i.imgur.com/SYu3QEY.png)
Ruby不管哪個變數都是物件,對應到``null``的空物件就是``nil``,你可以從下方的連結發現這物件仍然有實作方法。
Ref: https://ruby-doc.org/core-2.5.1/NilClass.html
MVC
----
> 這或許是一個軟工的常識,但實際上是一個MVC各自表述。
> Ref: [MVC是一個巨大誤會](http://blog.turn.tw/?p=1539)
定義的部份來自[wikipedia](https://zh.wikipedia.org/wiki/MVC),筆者會解釋Rails使用的結構比較向哪一種,只要看懂那種就可以理解Rails的結構了。
* Model(模型):對一個資料的封裝,一筆資料可能涉及到型態以及一些業務邏輯,在存取時我們希望有一個窗口來存提資料。在資料有變動的時候,Model會發送一次報告,讓旁聽的View以及Controller做後續的處理(前提是View跟Controller有申請旁聽)。
* View(視圖):能夠實現資料有目的的顯示。在 View 中一般沒有程式上的邏輯。為了實現 View 上的重新整理功能,View 需要存取它監視的資料模型(Model),因此應該事先在被它監視的資料那裡註冊。
* Controller(控制器):不同層面間的組織作用,用於控制應用程式的流程。它處理事件並作出回應。「事件」包括用戶的行為和資料 Model 上的改變。
#### Rails的變形:Model2
> Ref: http://www.bogotobogo.com/RubyOnRails/RubyOnRails_Model_View_Controller_MVC.php
> 裡頭有一張圖可以明確說明以下定義
真正要實作MVC架構只能在Client端程式(或是Client + Server整合程式),對於只有Server端的Rails,只能實現變形版的Model2,以下會說明Rails的Model2實作,不另做Model2定義:
* Model(模型):在Rails中,Controller與View不需要申請旁聽,只需要在Controller呼叫時處理好對資料庫的存取以及商業邏輯處理即可。
* View(視圖):在Rails中輸出資料或是設計好的頁面,一樣聽候Controller的差遣。
* Controller(控制器):在經過Router呼叫後,控制器會處理使用者的所有功能,通常會透過View來回傳資料,也通常會透過Model來做資料存提。
對於Server-side的程式中,Router才是核心,畢竟使用者是用網址來呼叫伺服器的。
Rails
----
Rails是Ruby的一個套件(Gem),所以在學完Ruby之後,只需要補上Model2的知識就可以開發出Hello, World!等級的Rails網站。以下的指令僅限於Command Line,你可以根據指令輸入順序來建立你的網站。
### ``gem install rails``
這是用來安裝Rails的指令,在你安裝好ruby後需要使用這套件來部屬rails的相依工具。你會看到在安裝時產生很多相依套件,直到最後出現`` XXX gems installed ``才算安裝完成。
### ``rails new myapp``
rails中與專案相關的指令都是rails開頭,而這指令是透過``myapp``這名稱建立一個Rails網站。你會看到一連串建立目錄與檔案的條目,在工作目錄下會出現一個名為``myapp``的資料夾。(目錄詳細結構後面會說明)
### ``cd myapp & bundle install``
``cd``是讓你把工作目錄切換到專案底下,``bundle install``是rails在做部屬時的批次命令。每一個專案第一次執行前都必須跑這條指令來確保套件都有到位。
如果你在這邊遇到有關mysql的error,這就代表需要補上與mysql相關的函式庫,請依照[此Stack Overflow](https://stackoverflow.com/questions/19014117/ruby-mysql2-gem-installation-on-windows-7)實作。如果你的網站不需要資料庫,除了忽略此部分之餘,記得用編輯器打開``Gemfile``,把mysql那一列開頭用``#``註解掉。
### ``rails server``
這是用來啟動開發版本的伺服器,也就是說你的網站出錯時會直接把錯誤訊息投射到網站上。在執行後,你的console會變成一個log用的視窗。當你的rails程式有用到印出指令或是存取資料庫時,log會全部記錄下來,這樣有助於會SQL指令的開發者直接除錯。
### ``rails console``
這是rails針對專案使用的debug窗口,與前者的差別是,你可以在這裡測試你要用於專案的邏輯是否符合所需。如果你要根據你的邏輯對資料庫塞入大量資料,可以在寫完ruby指令稿後,透過這邊的console進行批次處理。
Rails開發流程
----
> 參考自:https://railsbook.tw/chapters/12-controllers.html
以下會介紹一個rails功能的開發順序,如果涉及到資料庫存取則會在步驟一之前有Model建立的流程。
### 新增controller
``rails generate controller pages``
他會在controllers底下建立一個名為``pages_controller.rb``的檔案,這是一個繼承``ApplicationController``的class,你可以在裡面新增自訂的function。對於需要開發RESTful的開發者,請參考後方scaffold介紹。
### 新增功能
在controller的class底下,你可以撰寫你的function。假設我們在``page_controller.rb``下開發一個hello的功能,大概會長這樣:
```ruby
class PagesController < ApplicationController
def hello
render plain: "<h1>你好,世界!</h1>"
end
end
```
> 請注意,使用puts不會出現在網頁上,只會出現在log視窗。
controller可以不用回傳,必須使用render來輸出到網頁上,但如果是開發網站可以不用在這裡render所有內容,你可以新增一些變數以便網頁輸出使用。
### 網頁輸出
自訂的function在執行結束後會去``views``底下找到對應的檔案(目錄為``views/pages/你的function名稱.html.erb``,不使用scaffold建立的話要自己新增)。你可以使用基本的html開發,但如果你的function內有變數的話,可以透過一些與法置入(ActionView範疇,請參見:https://ihower.tw/rails/actionview.html)
### 掛上你的功能
當你完成以上開發手續後,你必須讓這功能透過網址來存取這功能。請打開``config/routes.rb``,並作如下的新增:
```ruby
Rails.application.routes.draw do
# 其餘部分
get "/hello_world", to: "pages#hello"
end
```
這樣就會把以下網址串連到你的功能上:
http://<你的網域與埠號>/hello_world
Migration
----
中文翻譯成遷移,其實就是資料庫的欄位版本控制(不包含資料)。換句話說就是你的所有欄位更動都必須有一個目標功能,例如會員系統(使用者資料表)或是發文系統(貼文資料表)。
ApplicationRecord
----
> 原名ActiveRecord,到了Rails 5後就改名了,不過由於很多功能都差不多,所以大部分文件都沒有即時做成Rails 5的版本
> Ref: https://github.com/rails/rails/pull/22567
若要實作Model,你必須繼承ApplicationRecord,他包含了以下功能:
* ORM(Object Relational Mapping):在程式裡面,資料是物件化組成;在資料庫裡面,資料是關聯性組成。透過此功能,我們可以把物件中的屬性與資料庫中的關聯欄位做對應(包含資料與型態)。
* Naming, schema轉換:資料表內有複數,所以他會把你的資料表名稱做複數化(pluralize),如果你用ApplicationRecord宣告一些特殊欄位(例如建立於、更新於,甚至是外鍵綁定)時,會幫你自動處理。
* 存提語法: ApplicationRecord在資料的CRUD上有專屬的函式,若要撰寫自己的SQL語法也可以。
* 驗證:資料要存進資料庫之前,除了可以在Controller做驗證以外,也可以在ApplicationRecord可以做型態或是非空欄位的處理。
* 遷移:每次更動資料庫的欄位時,必須預先寫好變動內容,再交由ApplicationRecord更新資料庫。
在以上功能的作用下,你就可以達成:
* 只要學會Ruby的連續技能以及ApplicationRecord的函數方法,不用一句SQL語法就可以存取資料庫。
* 用很簡單的方法來防範錯誤資料的存入,甚至是簡易的SQL Injection。
* 每次資料庫欄位的更動都在掌握範圍內,甚至可以做版本控管。
詳細語法與撰寫方法你可以從這邊查看:[ActiveRecord Basics](http://guides.rubyonrails.org/active_record_basics.html)
你也可以把上面的功能寫進你的controller內,這樣就可以達成基本的Model2功能了。
Scaffold
----
當CRUD與RESTful功能已經成為基本配備時,Rails這邊有一個加速開發功能叫Scaffold,目的是把你建立一個RESTful所需的命名一次到位。
``rails generate scaffold User name:string email:string tel:string``
這個指令一口氣完成以下事情,在不做細部調整下就已經省下很多時間了。
- 建立一個可以用來部屬資料表的migration
- 建立一個對應該資料表的model
- 建立一個可以對該資料表做RESTful的controller
- 建立四個頁面(顯示所有資料、顯示單筆資料、新增一筆資料、修改一筆資料)
- 把這RESTful功能掛上route.rb
最後補上``rake db:migrate``就可以開測試伺服器出來做細部修整了。
詳細的介紹可以觀看這篇文章:https://railsbook.tw/chapters/04-your-first-rails-application.html