【在主画面加入捷径】
       
【选择语系】
繁中 简中

技术杂谈:为什么该 (或不该) 用网页微框架 (Sinatra-like framework)

【赞助商连结】

    [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 叙述,但是这不是一个好主意,因为:

    • 每个 adaptor 的语法略有不同,如果更换数据库时,必需一并重写相关的程序代码。如果在网站规模渐大时,要在许多地方修改程序代码是一件耗费心力的工作。
    • 每种数据库的 SQL 语法略有不同,所以,除了更换 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 呢?

    • 想要建立一个原型 (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,如果有新的心得或想法,也会再来和大家分享。

    【赞助商连结】