Giới thiệu thư viện Sinatra qua việc xây dựng một ứng dụng đơn giản

Sinatra là một DSL được viết bằng Ruby cho phép tạo ứng dụng web một cách nhanh chóng với chi phí thấp. Bài viết sẽ giới thiệu một số tính năng của Sinatra và sử dụng thư viện này để xây dựng một ứng dụng demo.

Giới thiệu

Việc khởi tạo ứng dụng với Sinatra khá đơn giản. Chúng ta hãy cùng xem xét ví dụ sau:

  • cài đặt gem sinatra:
  gem install sinatra
  • tạo tệp app.rb:
  require "sinatra"

  get "/" do
    "Hello world!"
  end
  • chạy server:
  ruby app.rb

Như vậy, chỉ với một vài bước, chúng ta đã có thể tạo ra một ứng dụng web và sử dụng nó. Tuy nhiên, ứng dụng này vô cùng đơn giản, vậy hãy tìm hiểu xem Sinatra cung cấp những gì để ta có thể tạo ra một ứng dụng phức tạp hơn.

Route

Ở trong tệp app.rb phía trên có chứa 3 dòng:

get "/" do
  "Hello world!"
end

Ba dòng này định nghĩa một block dùng để xử lý HTTP request với phương thức GET và URI là "/". Xâu "Hello world!" chính là nội dung phản hồi của ứng dụng trả về cho phía client. Toàn bộ block này là một route của Sinatra.

Như vậy, route của Sinatra là một block dùng để xử lý các HTTP request với phương thức và uri nào đó, nội dung trả về cho client sẽ được xử lý trong block này. Tương ứng với các HTTP method khác, Sinatra cho phép khai báo các block tương ứng:

post "/uri" do
  # code goes here
end

put "/uri" do
  # code goes here
end

patch "/uri" do
  # code goes here
end

delete "/uri" do
  # code goes here
end
...

Trong phần uri của block có thể chứa các tham số, các tham số này được truy cập thông qua hash params. Ví dụ:

get "/posts/:id" do
  @post = Post.find params[:id]
end

URI cũng có thể chứa ký tự * (wildcard). Phần tham số tương ứng với ký tự này được truy cập qua mảng params[:splat]. Ví dụ:

get "/posts/*/comments/*" do
  @post = Post.find params[:splat][0]
  @comment = Comment.find params[:splat][1]
end

Ngoài ra còn có khá nhiều cách sử dụng uri khác có thể tham khảo tại đây.

View

Template

Phần view là phần nội dung trang web được trả về cho client. Ngoài cách trả về trực tiếp qua xâu ký tự như ở ví dụ ở phần giới thiệu, Sinatra cho phép định nghĩa các template và trả về nội dung của các template này. Các template được đặt trong thư mục views của ứng dụng. Với ví dụ đầu tiên, chúng ta có thể tạo thư mục views và tạo tệp home.erb bên trong thư mục này với nội dung như sau:

Rendered from template.

Sửa tệp app.rb như sau:

require "sinatra"

get "/" do
  erb :home
end

Khởi động lại server (chạy lại lệnh ruby app.rb), tải lại trang web, ta có kết quả: img-2.png

Với ví dụ trên, ứng dụng đã sử dụng template dạng Erubis. Sinatra cũng hỗ trợ nhiều định dạng template khác, có thể xem ở đây.

Sinatra cho phép định nghĩa một template chính dùng để chứa các template khác. Template này có tên layout.erb. Trong layout.erb sẽ chứa lời gọi đến method yield dùng để render các template khác. Tiếp tục với ví dụ đầu, chúng ta tạo tệp layout.erb với nội dung sau:

<!DOCTYPE html>
<html>
  <head>
    <title>Note</title>
  </head>
  <body>
    <%= yield %>
  </body>
</html>

Tải lại trang web, ta thấy phần tiêu đề đổi thành "Note".

Ngoài ra Sinatra cũng cho phép chỉ định layout khác với layout.erb, ví dụ:

get "/" do
  erb :home, layout: :other_layout
end

Trong đó :other_layout được định nghĩa bằng template other_layout.erb.

Truyền biến sang view

Các biến instance ở các block xử lý HTTP request có thể được sử dụng ngay trong các template:

# app.rb
...

get "/posts/:id" do
  @post = Post.find params[:id]
  erb :show
end
# show.erb
...
<p><%= @post.content %></p>
...

Chúng ta có thể truyền biến vào view thông qua hash locals:

get "/" do
  str = "This is a string"
  erb :home, locals: {string: str}
end
# home.erb
...
<p><%= string %></p>
...

CSS và Javascript

Các tệp css và javascript được đặt trong thư mục public của ứng dụng. Các tệp này sẽ được sử dụng trong template bằng việc khai báo các thẻ <link><script>.

Xây dựng ứng dụng

Ứng dụng demo của chúng ta có chức năng lưu trữ các ghi chú cho người dùng. Các chức năng của ứng dụng bao gồm tạo mới, sửa (nội dung, đánh dấu/hủy đánh dấu hoàn thành), xóa các ghi chú.

Mỗi ghi chú sẽ gồm các trường dữ liệu: id, nội dung, dấu hoàn thành, dấu thời gian. img-3.png

Khởi tạo ứng dụng

  • tạo thư mục note-app chứa mã nguồn của ứng dụng
  • tạo tệp Gemfile trong thư mục note-app để quản lý các gem cần thiết:
  source "https://rubygems.org"

  gem "sinatra"
  gem "shotgun"

Do Sinatra không tự động nạp lại nội dung của tệp chứa code chính của ứng dụng khi tệp này bị thay đổi nên chúng ta sử dụng gem shotgun để tránh phải khởi động lại server mỗi khi có thay đổi. Sử dụng gem này bằng cách chạy server với lệnh: shotgun app.rb thay vì ruby app.rb. Server sẽ lắng nghe ở cổng 9393, để đổi cổng thành 4567 có thể thêm tham số -p 4567 vào sau lệnh khởi động server.

Chạy bundle install để cài đặt các gem.

  • tạo tệp app.rb:
  require "sinatra"
  • tạo thư mục views và tệp layout.erb bên trong thư mục này với nội dung:
    <!DOCTYPE html>
    <html>
      <head>
        <title>Note</title>
        <link rel="stylesheet" type="text/css" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
      </head>
      <body>
        <div class="container">
          <div class="col-md-6 col-md-offset-3">
            <a href="/"><h1>Note</h1></a>
          </div>
          <%= yield %>
        </div>
      </body>
    </html>

Cài đặt cơ sở dữ liệu

Chúng ta sẽ dùng sqlite3 và gem sinatra-activerecord để quản lý CSDL. Hướng dẫn sử dụng sinatra-activerecord có thể xem ở đây.

  • Sửa Gemfile và cài đặt gem:
  ...
  gem "sqlite3"
  gem "sinatra-activerecord"
  gem "rake"
  • Sửa app.rb
  ...
  require "sinatra/activerecord"

  set :database, {adapter: "sqlite3", database: "noteapp.sqlite3"}
  • Tạo Rakefile:
    require "sinatra/activerecord/rake"

    namespace :db do
      task :load_config do
        require "./app"
      end
    end
  • Tạo cơ sở dữ liệu: bundle exec rake db:create
  • Tạo migration mới dùng để tạo bảng notes: bundle exec rake db:create_migration NAME=create_notes. Tệp migration sẽ được sinh ra trong thư mục db/migrate/. Sửa tệp này như sau:
    class CreateNotes < ActiveRecord::Migration
      def change
        create_table :notes do |t|
          t.text :content, null: false
          t.boolean :done, null: false, default: false
          t.timestamps null: false
        end
      end
    end

Chạy bundle exec rake db:migrate để thực hiện migration.

  • Tạo tệp note.rb trong thư mục models để quản lý bảng notes:
    class Note < ActiveRecord::Base
      validates :content, presence: true
    end
  • Sửa tệp app.rb:
    ...
    require "sinatra/activerecord"
    require "./models/note"

    ...
  • Tạo tệp seeds.rb trong thư mục db để khởi tạo dữ liệu ban đầu:
    3.times do |n|
      Note.create content: "Content of note #{n}", done: n % 2 == 0
    end

Chạy bundle exec rake db:seed để tạo dữ liệu.

Cài đặt các chức năng

Hiển thị các ghi chú

Việc hiển thị sẽ được xử lý bởi route có phương thức là get và uri là "/". Trong route này, danh sách của tất cả các ghi chú sẽ được lấy ra và lưu vào trong biến @notes. Biến này sẽ được sử dụng bởi template home để hiển thị ra các ghi chú.

  • Sửa tệp app.rb:
    ...
    get "/" do
      @notes = Note.all
      erb :home
    end
  • Tạo tệp "views/home.erb"
    <div class="col-md-6 col-md-offset-3">
      <ul class="list-group" id="list-notes">
        <% @notes.each do |note| %>
          <li class="list-group-item clearfix">
            <div class="col-md-11">
              <p><%= note.content %></p>
              <p><i>Created at: <%= note.created_at %></i></p>
            </div>
            <div class="col-md-1 done-sign">
              <% if note.done? %>
                <span class="glyphicon glyphicon-ok"></span>
              <% end %>
            </div>
          </li>
        <% end %>
      </ul>
    </div>
  • Chúng ta sẽ sử dụng style.css để tùy biến một chút giao diện. Tạo thư mục public và tệp style.css bên trong với nội dung:
    #list-notes {
      margin-top: 5%;
    }

    .done-sign span{
      font-size: 1.5em;
      color: green;
    }
  • Thêm tệp này vào layout.erb:
    ...
    <head>
      ...
      <link rel="stylesheet" type="text/css" href="style.css">
    </head>
    ...
  • Khởi động server (shotgun app.rb -p 4567) và truy cập localhost:4567: img-4.png

Tạo mới ghi chú

Chức năng này sẽ cho phép người dùng nhập nội dung của ghi chú và gửi lên server để tạo ghi chú mới. Route sẽ xử lý phương thức post và uri là "/notes", lấy thông tin gửi lên của client ở biến hash params để tạo ghi chú và chuyển hướng về uri "/".

Trước hết, chúng ta sẽ sửa tệp home.erb để tạo thêm một form nhập liệu cho phép nhập nội dung của ghi chú mới và gửi lên server:

<div class="col-md-6 col-md-offset-3">
  <form action="/notes" method="post" id="new-note-form">
    <textarea placeholder="Write note here..." class="form-control" name="content"></textarea>
    <input type="submit" class="btn-primary form-control" value="Create">
  </form>
...

Sau đó, tạo thêm một route mới trong app.rb. Tương ứng với thẻ <textarea> có namecontent, biến params[:content] sẽ chứa nội dung của ghi chú mới:

...
post "/notes" do
  @note = Note.new content: params[:content]
  @note.save
  redirect "/"
end

Chỉnh sửa một chút css trong tệp style.css:

...
#new-note-form input[type=submit] {
  margin-top: 2%;
  margin-bottom: 2%;
}

Tải lại trang web và tạo một ghi chú mới: img-5.png

Sửa ghi chú

Để sửa một ghi chú, ta cần hai route cho phép hiển thị trang sửa ghi chú và xử lý nội dung mới của ghi chú được gửi lên từ client.

Trước hết, thêm đường dẫn đến trang sửa ghi chú cho mỗi ghi chú được hiển thị ở template home:

...
<p><i>Created at: <%= note.created_at %></i></p>
<div>
  <a class="btn-xs" href="/notes/<%=note.id %>/edit">Edit</a>
</div>
...

Tạo tiếp hai route trong app.rb. Route thứ nhất xử lý hiển thị trang sửa ghi chú có phương thức get và uri "/note/:id/edit". Route này sẽ tìm ghi chú có id bằng với id trong uri để hiển thị thông tin của ghi chú đó. Route thứ hai xử lý thông tin gửi lên của client để cập nhật ghi chú có phương thức là put và uri "/notes/:id". Route này sẽ tìm ghi chú có id tương ứng, cập nhật nội dung và dấu hoàn thành của ghi chú:

...
get "/notes/:id/edit" do
  @note = Note.find params[:id]
  erb :edit
end

put "/notes/:id" do
  @note = Note.find params[:id]
  if @note.update_attributes(content: params[:content], done: params[:done])
    redirect "/"
  else
    erb :edit
  end
end

Tạo template edit.erb để hiển thị trang chỉnh sửa:

<div class="col-md-6 col-md-offset-3">
  <form action="/notes/<%= @note.id %>" method="post">
    <input type="hidden" name="_method" value="put">
    <textarea placeholder="Write note here..." class="form-control" name="content">
      <%= @note.content %>
    </textarea>
    <input type="hidden" name="done" value="0">
    <input type="checkbox" name="done" <%= "checked='1'" if @note.done? %>>
    <label>Done</label>
    <input type="submit" value="Update" class="btn-primary form-control">
  </form>
</div>

Do HTML form chỉ hỗ trợ hai phương thức GET và POST nên để sử dụng phương thức PUT hoặc DELETE, ta cần thêm một thẻ <input> kiểu hidden có name là _method và value là put hoặc delete tương ứng. Sinatra sẽ sử dụng các giá trị này để biết được phương thức thực sự.

Một chú ý nữa là khi cập nhật ghi chú mà check box không được tích chọn, giá trị của check box gửi lên server sẽ là null. Nếu cập nhật giá trị này vào CSDL sẽ gây lỗi do trường done có thuộc tính not null. Để xử lý trường hợp này, ta có thể kiểm tra trên server hoặc thêm một thẻ <input> kiểu hidden có cùng name với check box và đặc trước check box. Nếu check box không được tích, giá trị của check box sẽ được thay bằng giá trị của thẻ input này.

Tải lại trang web và sửa một ghi chú bất kỳ: img-6.png img-7.png

Xóa ghi chú

Chức năng xóa ghi chú sẽ được xử lý bởi route có phương thức là delete và uri là "/notes/:id". Route sẽ xóa ghi chú có id tương ứng và điều hướng về uri "/". Chúng ta sửa app.rb như sau:

...
delete "/notes/:id" do
  Note.destroy params[:id]
  redirect "/"
end

Thêm form có phương thức delete vào các ghi chú được hiển thị ở template home:

...
<a class="btn-xs" href="/notes/<%=note.id %>/edit">Edit</a>
<form action="/notes/<%= note.id %>" method="post" class="delete-form">
  <input type="hidden" name="_method" value="delete">
  <input type="submit" value="Delete" class="btn-xs btn-link">
</form>
...

Chỉnh sửa lại giao diện (style.css):

...
.delete-form {
  display: inline;
}

Tải lại trang và xóa một ghi chú bất kỳ: img-8.png img-9.png

Kết luận

Bài viết trên đây đã giới thiệu một số tính năng của thư viện Sinatra và hướng dẫn tạo một ứng dụng đơn giản với thư viện này. Bài viết còn đơn giản và có thể còn thiếu sót, rất mong nhận được ý kiến đóng góp của mọi người.

Mã nguồn của ứng dụng có thể xem tại: https://github.com/hieuns/note-app