位元詩人 技術雜談:為什麼該 (或不該) 用網頁微框架 (Sinatra-like framework)

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

[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
{{< / 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,如果有新的心得或想法,也會再來和大家分享。

關於作者

身為資訊領域碩士,位元詩人 (ByteBard) 認為開發應用程式的目的是為社會帶來價值。如果在這個過程中該軟體能成為永續經營的項目,那就是開發者和使用者雙贏的局面。

位元詩人喜歡用開源技術來解決各式各樣的問題,但必要時對專有技術也不排斥。閒暇之餘,位元詩人將所學寫成文章,放在這個網站上和大家分享。