web  

Mar 21, 2016 • Michael Chen

[Update on 2017/02/23] 雖然許多人都會從 Ruby on RailsLaravel 或是其他知名的網頁框架學習網頁程式設計,甚至在不會 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

其實這是 router 和 controller 的混合,但是我們暫時不需要做這樣的區分。如果有寫過 CGI 或 PHP 等語言的人,可能會想在這裡塞入 HTML 碼,但是,比較好的方法,是利用 templating 的技術將程式碼分開。這裡的例子使用 ERB (Embedded Ruby) 這個 templating language:

# app.rb
require 'sinatra'

get '/' do
  @message = 'Hello, World'
  erb :index
end

加入 template 如下:

<!-- views/index.erb -->
<!DOCTYPE html>
<html>
<body>
<p><%= @message %></p>
</body>
</html>

雖然我們沒有定義明確的 viewer,但透過這樣的安排,我們將 view 和 controller 做初步的分離。

除了 HTML 外,網站通常也需要適常的放入 CSS 和 JavaScript 程式碼。在 Sinatra 中,只要把這些靜態檔案放入 public 資料夾即可。放入後,整個專案的組成如下:

$ tree
├── app.rb
├── public
│   ├── css
│   │   └── bootstrap.min.css
│   └── js
│       ├── bootstrap.min.js
│       └── jquery-1.12.2.min.js
└── views
     └── index.erb

在 template 的地方適當地引入相關檔案:

<!-- 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>

如果要進一步地管理 assets,或是使用 CoffeeScript 及 SCSS 等進階的方案,可以使用 Sprocket 等套件,有興趣的讀者可以自行查閱相關資料。

到目前為止,我們已經可以處理靜態網頁了,不過,我們還想進一步連接資料庫。雖然,我們也可以直接利用某個特定資料庫的 adaptor 來連接資料庫,然後自行撰寫相關的 SQL 敘述,但是這不是一個好主意,因為:

比較好的方法,是另外撰寫一個 model 類別,將實際和資料庫互動的行為藏在其中。假設我們要建立一個處理 TODO 清單的網站,可能的例子如下:

# 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

然後,再另外提供 SQL dump 檔案,用來重建資料庫。

不過,Ruby 社群有更好的方案,利用 ActiveRecord 等套件,將這些不同資料庫間的差異抽象化,我們只要設定好想連接的資料庫,其餘的程式碼可以共用,而且也可以處理資料庫重建的過程。

在這裡,我們仍然以 TODO 清單為例;然而,為了方便示範,我們使用 SQLite,在實際上線的系統,應該使用 MySQL/MariaDB 或是 PostgreSQL 等可以適當地應對多人連線的資料庫。我們會逐一講解建置的過程,不過,如果想直接觀看完成品,可以到這裡

首先,下載 sqlite3activerecordsinatra-activerecordrake,其中第一個套件是連接資料庫的 adaptor,第二個套件是 ORM (Object-Relational Mapping),也就是將資料庫抽象化的主力套件,第三個套件為本專案增加一些建置資料庫的動件,rake 則是流程自動化軟體。

接著,在 config/database.yml 中設定資料庫,並在主要的 app 中引入。

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 呢?

不過,Sinatra-like framework 也不是適用所有的情境。當網站的規模越來越大,開發者其實多多少少在重覆一些別的 framework 已建立好的程式碼架構,一些建立網站常碰到的情境,其實在比較成熟的 framework,常常已有相關套件,可以很快就解決。如果預期可能會有這樣的結果,不如一開始就採用一個有 MVC 架構的 framework。如果是多人開發,程式碼的架構會更加重要,這時候,Sinatra-like framework 太過自由的撰碼方式,反而是不利的。有關更多 Ruby on Rails vs. Sinatra 的討論,可見這裡

後記

Padrino 是一個基於 Sinatra 的 MVC 架構 framework,其模組化的設計,使得其中的模組,可單獨抽取出來和 Sinatra 混用,也可以用來建立完整的網站。不過,文件相對稀少,能見度也是相對低。小弟我還在試著了解這個 framework,如果有新的心得或想法,也會再來和大家分享。