[Update on 2017/02/23] 雖然許多人都會從 Ruby on Rails、Laravel 或是其他知名的網頁框架學習網頁程式設計,甚至在不會 Ruby 時就直接學 Rails,我個人非常不推薦這種學習方式。這些框架是給已經有程式設計經驗的開發者快速開發新産品用的,往往都有許多複雜的專案結構和設定,而初學者會以為網頁程式設計一定需要這些複雜的工具。我反而推薦 Sinatra 或是 Flask 這類輕量的替代品。使用這種微框架,不需要學習複雜的專案結構和設定,甚至只要單一的檔案即可執行程式。這種漸進式的過程,其實比較適合初學者。
早期的動態網頁 (dynamic web page) 是以 Perl 或其他語言撰寫的 CGI (Common Gateway Interface),後來有數個專門的伺服端語言,像是 PHP 或是 ASP (Active Server Pages) 或是 JSP (JavaServer Pages) 等。後來,隨著 Ruby on Rails 及其他以 MVC (Model-View-Controller) 為架構的 web frameworks,以 framework 協助開發 web application 成為風行的模式。Sinatra 以及其他相似的 micro-frameworks 以 HTTP 動作為基礎,沒有典型的 MVC 架構,用少量的程式碼即可快速開發,是另一種輕量的選擇。
典型的 MVC 架構的 web framework,幫網站開發者規畫程式碼擺放的位置,對於有經驗的開發者來說,很快可以將相關的程式碼串接起來;但是,對於初次接觸 MVC 架構的人來說,要花一段時間才能適應這種架構,程式發生錯誤時,也不容易馬上找出錯誤何在。使用 Sinatra,一開始的網站只是一個單一且簡短的檔案,隨著網站的需求,再逐漸增加程式碼,相當類似學習一個新的程式語言的過程。
在本文中,我們以 Sinatra 為範例,但是,同樣的概念可以在略做修改後,在別的語言的 Sinatra-like framework 上實作。一個 Sinatra 的 "Hello, World" 範例如下:
# app.rb
require 'sinatra'
get '/' do
'Hello, World'
end
{{< / highlight >}}
其實這是 router 和 controller 的混合,但是我們暫時不需要做這樣的區分。如果有寫過 CGI 或 PHP 等語言的人,可能會想在這裡塞入 HTML 碼,但是,比較好的方法,是利用 templating 的技術將程式碼分開。這裡的例子使用 ERB (Embedded Ruby) 這個 templating language:
```ruby
# app.rb
require 'sinatra'
get '/' do
@message = 'Hello, World'
erb :index
end
{{< / highlight >}}
加入 template 如下:
```erb
<!-- views/index.erb -->
<!DOCTYPE html>
<html>
<body>
<p><%= @message %></p>
</body>
</html>
{{< / highlight >}}
雖然我們沒有定義明確的 viewer,但透過這樣的安排,我們將 view 和 controller 做初步的分離。
除了 HTML 外,網站通常也需要適常的放入 CSS 和 JavaScript 程式碼。在 Sinatra 中,只要把這些靜態檔案放入 *public* 資料夾即可。放入後,整個專案的組成如下:
```console
$ tree
├── app.rb
├── public
│ ├── css
│ │ └── bootstrap.min.css
│ └── js
│ ├── bootstrap.min.js
│ └── jquery-1.12.2.min.js
└── views
└── index.erb
{{< / highlight >}}
在 template 的地方適當地引入相關檔案:
```erb
<!-- views/index.erb -->
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="/css/bootstrap.min.css">
</head>
<body>
<p><%= @message %></p>
<script src="/js/jquery-1.12.2.min.js"></script>
<script src="/js/bootstrap.min.js"></script>
</body>
</html>
{{< / highlight >}}
如果要進一步地管理 assets,或是使用 CoffeeScript 及 SCSS 等進階的方案,可以使用 **Sprocket** 等套件,有興趣的讀者可以自行查閱相關資料。
到目前為止,我們已經可以處理靜態網頁了,不過,我們還想進一步連接資料庫。雖然,我們也可以直接利用某個特定資料庫的 adaptor 來連接資料庫,然後自行撰寫相關的 SQL 敘述,但是這不是一個好主意,因為:
- 每個 adaptor 的語法略有不同,如果更換資料庫時,必需一併重寫相關的程式碼。如果在網站規模漸大時,要在許多地方修改程式碼是一件耗費心力的工作。
- 每種資料庫的 SQL 語法略有不同,所以,除了更換 adaptor 部分的語法,其中的 SQL 敘述也要一併更新。
- 如果這是一份多人共同開發的網站,每台電腦,包括開發用的、測試用的和實際上線用的,要重建數個資料庫是件相當繁複的工作。
比較好的方法,是另外撰寫一個 model 類別,將實際和資料庫互動的行為藏在其中。假設我們要建立一個處理 TODO 清單的網站,可能的例子如下:
```ruby
# a model pseudo-code
class TODOModel
def initialize
# Connect to database
end
def create_todo(message, category, time)
# Create new todo item
end
def retrieve_todo(id)
# Retrieve todo item
end
def retrieve_todos
# Retrieve all todo items
end
def update_todo(message, catetory, time)
# Update todo item
end
def delete_todo(id)
# Delete todo item
end
def delete_todos
# Delete all todo items
end
end
{{< / highlight >}}
然後,再另外提供 SQL dump 檔案,用來重建資料庫。
不過,Ruby 社群有更好的方案,利用 **ActiveRecord** 等套件,將這些不同資料庫間的差異抽象化,我們只要設定好想連接的資料庫,其餘的程式碼可以共用,而且也可以處理資料庫重建的過程。
在這裡,我們仍然以 TODO 清單為例;然而,為了方便示範,我們使用 SQLite,在實際上線的系統,應該使用 MySQL/MariaDB 或是 PostgreSQL 等可以適當地應對多人連線的資料庫。我們會逐一講解建置的過程,不過,如果想直接觀看完成品,可以到[這裡](https://github.com/opensourcedoc/sinatra-todolist-demo)。
首先,下載 `sqlite3`、`activerecord`、`sinatra-activerecord` 和 `rake`,其中第一個套件是連接資料庫的 adaptor,第二個套件是 ORM (Object-Relational Mapping),也就是將資料庫抽象化的主力套件,第三個套件為本專案增加一些建置資料庫的動件,rake 則是流程自動化軟體。
接著,在 *config/database.yml* 中設定資料庫,並在主要的 app 中引入。
```yaml
development:
adapter: sqlite3
database: "todos.sqlite3.db"
host: localhost
# app.rb
set :environment, :development
set :database_file, "config/database.yml"
require 'active_record'
接著,定義 Rakefile 的內容,以便 rake
呼叫。
require './app.rb'
require 'sinatra/activerecord'
require 'sinatra/activerecord/rake'
接著,在 db/migrate 資料夾中,建立 Migration 定義檔。
# 201603211457_create_todos.rb
# Modify the file name according to your situation
class CreateTodos < ActiveRecord::Migration
def up
create_table :todos do |t|
t.string :category
t.text :body
t.timestamps null: false
end
end
def down
drop_table :todos
end
end
在終端機中,呼叫 rake db:migrate
以建立資料庫。
接著,在主要 app 中定義相關的 CRUD (Create, Retrieve, Update, Delete) 動作。
# app.rb
get '/' do
@todos = Todo.all()
erb :index
end
get '/create/?' do
erb :create
end
# Create TODO item
post '/create/?' do
@todo = Todo.new(params[:todo])
if @todo.save
redirect '/'
else
erb :create
end
end
get '/update/:id/?' do
@todo = Todo.find_by_id(params[:id])
erb :update
end
# Update TODO item
post '/update/:id/?' do
todo = Todo.find_by_id(params[:id])
todo.body = params[:todo][:body]
todo.category = params[:todo][:category]
todo.save
redirect '/'
end
# Delete TODO item
post '/delete/:id/?' do
Todo.destroy(params[:id])
redirect '/'
end
# Delete all TODO items
post '/clear/?' do
# Truncate SQLite table
ActiveRecord::Base.connection.execute <<-SQL
DELETE FROM todos
SQL
redirect '/'
end
接著,在相對應的 template 中,建置相關的 view,這裡以 index.erb 為例:
<ul class="list-group">
<% @todos.each do |todo| %>
<li class="list-group-item">
<form class="form-inline" role="form">
<div class="row">
<div class="col-md-6">
<%= todo.body %>
<span class="badge"><%= todo.category %></span>
</div>
<div class="col-md-6 text-right">
<button class="btn btn-warning btn-sm" type="submit"
formaction="/delete/<%= todo.id %>"
formmethod="post">
Delete
</button>
<button class="btn btn-info btn-sm" type="submit"
formaction="/update/<%= todo.id %>"
formmethod="get">
Update
</button>
</div>
</div>
</form>
</li>
<% end %>
</ul>
<form class="form-inline" role="form">
<button class="btn btn-info" type="submit" formaction="/create" formmethod="get">Create TODO</button>
<button class="btn btn-warning" type="submit" formaction="/clear" formmethod="post">Clear TODOs</button>
</form>
至於其他的 view,有興趣的讀者可以到這裡觀看相關程式碼,這裡便不再贅述。
當然,我們這個網站還欠缺許多功能,像是使用者管理等,其他的功能就留給有興趣的讀者自行發揮。不過,到這裡,我們可以了解,Sinatra 或其他類似的 micro-frameworks,的確能夠從頭開始,建立一個包含資料庫的動態網站。典型的 MVC 架構的 web framework,像是 Ruby on Rails,一開始就幫你規畫好程式架構了,而 Sinatra-like framework 這種從頭開始堆積木的方式,倒是另外一種趣味。那麼,什麼時候適合使用 Sinatra-like framework 呢?
- 想要建立一個原型 (prototype),以展示産品的概念。如果一開始就直接建立整個大型網站,當結果不如預期時,重來的代價太大。這時候,就可以使用 Sinatra-like framework 快速地建立一個試用品。
- 對於沒有複雜架構的中小型網站,使用 Sinatra-like framework 相當適合。像是一些只有簡單頁面的 web app,使用這種 framework,可以很快就完成一個網站。
- 建立 RESTful API。對於不需要使用者介面的 RESTful 網站,可以專注在 business logic 上,相當適合使用 Sinatra-like framework 來建立。
- 做為教育和學習網站開發的教具。由於 Sinatra-like framework 是從單一檔案開始,對於學習者來說,比較能夠漸入佳境,而不會被複雜的架構搞混。
不過,Sinatra-like framework 也不是適用所有的情境。當網站的規模越來越大,開發者其實多多少少在重覆一些別的 framework 已建立好的程式碼架構,一些建立網站常碰到的情境,其實在比較成熟的 framework,常常已有相關套件,可以很快就解決。如果預期可能會有這樣的結果,不如一開始就採用一個有 MVC 架構的 framework。如果是多人開發,程式碼的架構會更加重要,這時候,Sinatra-like framework 太過自由的撰碼方式,反而是不利的。有關更多 Ruby on Rails vs. Sinatra 的討論,可見這裡。
後記
Padrino 是一個基於 Sinatra 的 MVC 架構 framework,其模組化的設計,使得其中的模組,可單獨抽取出來和 Sinatra 混用,也可以用來建立完整的網站。不過,文件相對稀少,能見度也是相對低。我還在試著了解這個 framework,如果有新的心得或想法,也會再來和大家分享。