LoginSignup
24
24

More than 5 years have passed since last update.

sinatraでの最小限のアプリを作る

Last updated at Posted at 2018-07-16

目的

  • sinatraでのアプリ開発の基本を纏める
  • webアプリの開発経験があまりない人、ruby系の経験が少ない(それは僕もそうだが)を対象

前提

ゴール

  • アプリを開発する、MVCの基本的な実装ができる
    • bundlerの導入
    • ルーティング
    • View 画面表示
    • 画面とのパラメータの受け渡し
    • DBとの接続、永続化
    • UnitTest(RSpec)
  • 次回

    • Herokuから公開できる
  • よく見かけるリストに登録するアプリを例にする
    スクリーンショット 2018-07-16 16.16.31.png

手順

ライブラリの読み込み

  • bundlerのインストール
    • 対象のディレクトリにbundlerをインストール
gem install bundler
  • gem ファイルを作成
Gemfile
source 'https://rubygems.org'
gem 'sinatra', :github => 'sinatra/sinatra'

gem 'rake'
gem 'sinatra-contrib'
gem 'activerecord'
gem 'sqlite3'

group :test do
  gem 'rspec'
  gem 'rack-test'
end
  • gemをインストール bundle install > - bundlerのパスが変わっていることがある。その場合bundlerを再インストール
    > https://qiita.com/nakanowax/items/fe07e8ccd1721befebeb > - gemがinstallされていないエラーは、bundle installし直す

ルーティング

  • getリクエスト
  • /(ルート)にルーティングする場合は以下のようになる
get '/' do
  'Hello wolrd'
end

パスパラメータの受け渡し

  • getリクエストから値を受け取る場合は、
    get '/std/:arg' do パスの最後にシンボルで受け取る
  • パラメータの取得は、paramsハッシュからシンボルを指定して取得する
    params[:arg] 変数を展開するため#{}内で行う
ruby
require 'sinatra'
require 'sinatra/reloader'

get '/' do
  'Hello wolrd'
end

# パスパラメータの受け取り
get '/std/:arg' do
  "display args#{params[:arg]}"
end

# ブロック引数で受け取る方法も可能
#get '/std/:arg' do |arg|
#  "display args#{arg}"
#end
  • std/にパスパラメータ_testを渡してブラウザからgethttp://localhost:4567/std/_test

スクリーンショット 2018-07-14 15.21.01.png

  • パラメータが受け渡されていることを確認

複数のパスパラメータを扱う

  • パスパラメータ中で複数のシンボルを受け取ると、
# パスパラメータの受け取り
get '/std/:id/:usr' do |id, usr|
  "display args ID : #{id} User : #{usr}"
end

http://localhost:4567/std/5/testuser
- この場合、パラメータが途中までの場合、 例えばhttp://localhost:4567/std/5 では、パターンがアンマッチとしてエラーになる
- パラメータをオプショナルにするには、任意にする要素の直後に?を指定する

# 可変にする
get '/stdopt/:id/?:usr?' do |id, usr|
  "stdopt args ID:#{id} User:#{usr}"
end

スクリーンショット 2018-07-14 15.52.22.png
- - /と:usrを任意に設定しているので、以下の場合でも、表示がされている
スクリーンショット 2018-07-14 15.52.30.png

View

  • テンプレートで画面を作る
  • viewsと言うdirectryを作成し、以下に.erbファイルを作成する
mkdir views
touch views/index.erb

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>HTML 5 complete</title>
<!--[if IE]>
<script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>

<![endif]-->
<style>
  article, aside, dialog, figure, footer, header,
  hgroup, menu, nav, section { display: block; }
</style>
</head>
<body>
<h1>TITLE</h1>
<p>Hello World</p>
</body>
</html>
  • この画面にルーティングする
get '/' do
  erb:index
end
  • erbにシンボルでviews以下のファイル名を指定すると、そのerbファイルが読み込まれる

テンプレートの共通化

  • viewsの中にviews/layout.erbを作成する
  • 共通部分はここに書き込んでおく
layout.erb
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title><%= @title%></title>
<!--[if IE]>
<script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>

<![endif]-->
<style>
  article, aside, dialog, figure, footer, header,
  hgroup, menu, nav, section { display: block; }
</style>
</head>
<body>
  <%= yield %>
</body>
</html>

  • 共通部分のHTMLを記載し、
  • 個別実装する部分は、yieldに埋め込まれる。
  • この場合、body要素を個別実装するので、bodyの中身だけ個別に書けば、yieldに組み込まれるようになる
:index
<h1><%= @title %></h1>
<p>Hello World</p>

  • ルーティング 画面への値受け渡し
get '/' do
  @title = "index"
  erb:index
end
  • 画面に受け渡したい値は、ここでインスタンス変数に代入する。
  • 画面、テンプレートファイル上で、その変数へ<%= @変数 %>で受け渡すことができる
  • erbでは<% %>でrubyコード実行が行われる
  • <%= となると、この場所に結果が代入される(ここに結果が表示される)、と言う意味になる スクリーンショット 2018-07-14 16.38.51.png

スタイルやjavascriptの静的リソースの読み込み

  • /publicディレクトリを作成すると、その下に静的リソースを配置し、view上から読み出すことができる
  • 例えば、以下のようなファイルを作る
    /public/css/style.css
  • view上からは、以下のように読み出すことができる
    <link rel="stylesheet" href="css/styles.css">
    public直下のパスを指定する
layout.erb
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title><%= @title%></title>
<!--[if IE]>
<script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>

<![endif]-->
<link rel="stylesheet" href="css/styles.css">
<style>
  article, aside, dialog, figure, footer, header,
  hgroup, menu, nav, section { display: block; }
</style>
</head>
<body>
  <%= yield %>
</body>
</html>

Model SQLite3データベースの準備

  • SQLを直接流す方法と、マイグレーション機能を追加する方法2パターンあります
    • ライトにやるならSQLで直接DDLを流す方法でOK
      この手順でやるとしても、次のエントリー「Herokuでsinatraアプリを公開する手順」で、別途マイグレーション可能な設定を追加します
    • あとでHerokuにデプロイする場合や、DBの保守を楽に行いたい場合は、マイグレーションを設定すると良いかと思います
    • 手順上どちらを選んでも問題ありません
      SQLでDDLを直接流しての設定する場合手順
    • sinatra固有の方法と言う訳ではなく、単純にsqlファイルを流し、DBを用意
    • seeds.sql
      drop table if exists users;
      create table users (
        id integer primary key,
        name text
      );
      
      insert into users (name) values ('test yamanaka');
      insert into users (name) values ('test icheda');
      insert into users (name) values ('test shinagawa');
      
      
      • sqlファイルを流し、sindb、を作成 
      
      sqlite3 sindb.db < seeds.sql
      sqlite3 sindb.db
      Enter ".help" for usage hints.
      sqlite> .schema
      CREATE TABLE users (
        id integer primary key,
        name text
      );
      sqlite> select * from users;
      1|comment 1
      2|comment 2
      3|comment 3
      sqlite> .quit
      
      • テーブル作成、テストでーたの導入が行われていることを確認
      • sqlite3は、sqlite3 {dbファイル}でプロンプトに入れる
      • .schemaでスキーマを確認することができる
      • .quitで終了できる
      • 後日、migrationでやる方法を調べて差し替える。
      • 後日、RDBを別で用意する方法を追記する

      マイグレーションの設定手順
      • 以下の手順を追加すると、seedsなど使わずに、rakeコマンドでDBの設定ができるようになります
      • Gemfileに以下のgemを追加します
      Gemfile
      gem 'rake'
      gem 'activerecord'
      gem "sinatra-activerecord"
      
      • 以上を追加しbundle installする
      • Rakeファイルを追加する
        • プロジェクトのルートに、以下のファイルを追加する
      Rakefile
      require 'sinatra/activerecord'
      require 'sinatra/activerecord/rake'
      
      • Rakeコマンドを実行し、結果が以下となっていることを確認してください
      $ bundle exec rake -T
      rake db:create              # Creates the database from DATABASE_URL or config/datab...
      rake db:create_migration    # Create a migration (parameters: NAME, VERSION)
      rake db:drop                # Drops the database from DATABASE_URL or config/databas...
      rake db:environment:set     # Set the environment value for the database
      rake db:fixtures:load       # Loads fixtures into the current environment's database
      rake db:migrate             # Migrate the database (options: VERSION=x, VERBOSE=fals...
      rake db:migrate:status      # Display status of migrations
      rake db:rollback            # Rolls the schema back to the previous version (specify...
      rake db:schema:cache:clear  # Clears a db/schema_cache.yml file
      rake db:schema:cache:dump   # Creates a db/schema_cache.yml file
      rake db:schema:dump         # Creates a db/schema.rb file that is portable against a...
      rake db:schema:load         # Loads a schema.rb file into the database
      rake db:seed                # Loads the seed data from db/seeds.rb
      rake db:setup               # Creates the database, loads the schema, and initialize...
      rake db:structure:dump      # Dumps the database structure to db/structure.sql
      rake db:structure:load      # Recreates the databases from the structure.sql file
      rake db:version             # Retrieves the current schema version number
      
      • マイグレーションファイルの作成

        • 実際にDBのテーブルを準備したり、SQLの代わりにデータの追加や更新を行うことができます
        • その命令を実際に記載するテンプレートを生成させます
      • bundle exec rake db:create_migration NAME=create_users

        • NAMEにマイグレーションファイル名を指定します。
        • マイグレーションファイルは、プロジェクトに一意である必要がありますが、同じ名称を与えても、自動的に頭にタイムスタンプが追記されるため、重複しないようになっています
        • テーブル構造の変更などを行う場合、マイグレーションファイルを編集したり削除して作り直して流したりなどは、すべきではありません
        • 修正するマイグレーションファイルを作成し、更新を行います。
      db/migrate/20180718105504_create_users.rb
      class CreateUsers < ActiveRecord::Migration[5.2]
        def change
        end
      end
      
      • 以下のように追記し、テーブルを作ります
        • モデルがuserなので、テーブルはusersとなります
        • create_tableでusersを指定します
        • テーブルは、idとnameです。idは自動で追加されるようにするため、記載せず
        • nameを、string型で定義します
      db/migrate/20180718105504_create_users.rb
      class CreateUsers < ActiveRecord::Migration[5.2]
        def change
          create_table :users do |t| 
            t.string :name
          end
        end
      end
      
      • マイグレーションを、以下のコマンドで実行すると、スキーマが作成されます
        • bundle exec rake db:migrate

      ActiveRecordの準備

      DBとの接続設定

      • 今回、コントローラ上に追記してしまう
      sinatra_study.rb
      ActiveRecord::Base.establish_connection(
        adapter: 'sqlite3',
        database: './sindb.db'
      )
      
      • 先ほど作成したデータベースファイルを、sqlite3をアダプタに読み込む ### モデルクラスの作成
      sinatra_study.rb
      class User < ActiveRecord::Base
        validates :name, presence: true
      end
      
      • modelクラス
        • ActiveRecordには命名規則があって、
        • テーブル名は複数形にする
        • 対応するクラス名は、単数形にする。このクラスがたくさん入るからテーブルは複数になる、というイメージ
        • 今回テーブルがusersとなりUserがクラス名となる
        • 本当はmodelパッケージを別に分けて、そこにactiverecordクラスを集約してしまいたいが、sinatraはrailsのような形でmodelをサポートしていないように見える。この辺りのSinatraでのベストプラクティスを探したい。
      • バリデーションの追加
        • ActiveRecordの機能として、登録されるデータの事前チェックを行うことができる。これでからの文字が登録されてしまったり、不正な登録が行われることを避ける
        • validates :name, presence: true nameカラムに登録される値が、presence:trueで、存在していなければならない、と定義している

      CLUDの実装

      • コントローラ側
        • 登録、表示、削除だけを作成
        • /をgetリクエスト時、登録されているUser一覧を全件取得する
        • postリクエストで/create が行われた時、新規データを追加する
        • postリクエストで/destroy が呼ばれた時、そのデータを削除する
      メソッド パス パラメータ 処理
      get / なし 全件を取得し、画面に返す
      post /create name 受け取ったnameのデータを作成し、Activerecordがidを自動採番
      post /destroy id 受け取ったIDのデータを削除
      sinatra_study.rb
      get '/' do
        @title = "User List"
        @users = User.all
        erb :index
      end
      
      post '/create' do
        User.create(name: params[:name])
        redirect to('/')
      end
      
      post '/destroy' do
        User.find(params[:id]).destroy
      end
      
      

      - Model操作
       - ActiveRecordの使い方あたり、別途以下にまとめています
       - Active Recordの基本的な操作メモ

      • また、csrf対策、XSS対策のため、次のライブラリを読み込み、設定する
      sinatra_study.rb
      require 'rack/csrf'
      
      use Rack::Session::Cookie, secret: "{任意の文字列。長めのランダムな文字を定義する}"
      use Rack::Csrf, rise:true
      
      • 画面側
      index.erb
      <h1><%= @title %></h1>
      <ul>
        <% @users.each do |user| %>
        <li data-id="<%= user.id %>" data-token="<%= Rack::Csrf.csrf_token(env) %>>
          <%= Rack::Utils.escape_html(user.name) %>
          <span class="delete">[x]</span>
        </li>
        <% end %>
      </ul>
      
      • まず、h1では、@titleでコントローラから受け取ったインスタンス変数を表示
      • ulのリスト要素内で、liリスト要素を繰り返し作成する
        <% @users.each do |user| %> {リスト要素} end で@user.eachで繰り返し要素を取得
        一つ一つのuserオブジェクトが|user|に入る
      • <li data-id="<%= user.id %>"> リスト要素のIDを、ユーザーIDに設定し、あとで取得しやすくする
      • <%= Rack::Utils.escape_html(user.name) %> ユーザー名を表示する、単純に書けばuser.name だけでいいが、ここにjavascriptが仕込まればばあい、実行されてしまうのを防ぐため、Rack::Utilsを利用し、エスケープする

      • フォームの実装
         - 画面での入力を

      index.erb
      <form action="/create" method="post">
        <%= Rack::Csrf.csrf_tag(env) %> 
        <input type="text" name="name">
        <input type="submit" value="add user">
      </form>
      
      • Ajaxでのリクエスト
        • li要素に、要素を一つ追加し、クリックイベントを取得したら削除をpostする
        • <span class="delete">[x]</span> をli要素に追加する
        • javascriptを追加する。先述のように、静的リソースは、publcに配置をすれば、読み取ることができるので、 public/js/main.js を追加する
      public/js/main.js
      $(function() {
        $('.delete').on('click', function() {
          var li = $(this).parent();
          if (!confirm('削除しますか?')) {
            return;
          }
          $.post('/destroy', {
            id: li.data('id'),
            _csrf: li.data('token')
          }, function() {
            li.fadeOut(800);
          });
        });
      });
      
      
      • $('.delete').on('click', function() {
      • クラスを読み取り、deleteクラスの要素がclickされた場合、処理が行われる
      • var li = $(this).parent();
      • li変数に、そのオブジェクト(span)の親(li)要素を読み取る
      • confirmで、OKでない選択をしたら、そのままreturnする。OKなら継続しpostリクエストを行う
      • $.post('/destroy', {id: li.data('id'), _csrf: li.data('token')},function() {li.fadeOut(800);});

        • 第一引数:postリクエストで/destroyを実行し
        • 第二引数:パラメータに、idにli要素のid data-idで指定したもの<li data-id="<%= user.id %>" _csrfに同じようにtokenを渡す
        • 第三引数:dom操作の処理を行う。データが削除されたため、同じようにli要素を削除する
        • csrfで渡す要素名がわからない場合、ブラウザのdeveloperコンソールでソースを確認し、この要素がなんてidが振られているかを確認し、それに合わせる
      • jsを読み込む

      /views/layout.html
      <body>
        <%= yield %>
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
        <script src="js/main.js"></script>
      </body>
      
      • csrf対策とは
        • csrf攻撃を防ぐため、トークンを画面に事前に送っておき、postが帰ってくるとき、トークンが一致しているかどうかを確認する。トークンが一致していなければ、クッキーを奪った第三者が不正にpostリクエストを行なっている可能性があるため、403エラーとなる
        • 実装方法
        • すでに記載の通り、コントローラ側(sinatra_study)でrack/csrfライブラリを読み込み、設定を行う
        • 画面では、ポストを行う要素にcsrf対策トークンを埋め込む
          <li data-id="<%= user.id %>" data-token="<%= Rack::Csrf.csrf_token(env) %>>
        • Ajaxからは、次のようにトークン情報をpostパラメータに含めている$.post('/destroy', {id: li.data('id'), _csrf: li.data('token')},function() {li.fadeOut(800);});
        • コントローラ側では、見えないところでこれらの認可処理が行われているので、個別の実装は不要になっている

      処理の共通化 helperの作成

      • 共通で利用する処理を、helperに切り出し再利用することができる
      • 画面上で行なっているRack::Csrf、Rack::Utils系の処理を、コントローラでhelpers に定義すると、viewのerbファイル上からメソッドを呼び出し利用することができる
      sinatra_study.rb
      helpers do
        def csrf_tag
          Rack::Csrf.csrf_tag(env)
        end
        def csrf_token
          Rack::Csrf.csrf_token(env)
        end
        def h(src)
          Rack::Utils.escape_html(src)
        end
      end
      
      • 画面側
      index.erb
      <h1><%= @title %></h1>
      <ul>
        <% @users.each do |user| %>
        <li data-id="<%= user.id %>" data-token="<%=csrf_token%>"> <=
          <%= h(user.name) %>                                      <=
          <span class="delete">[x]</span>
        </li>
        <% end %>
      </ul>
      
      <form action="/create" method="post">
        <%= csrf_tag %>                                            <=
        <input type="text" name="name">
        <input type="submit" value="add user">
      </form>
      

      RSpec テストの準備

      • テストフレームワーク、RSpecを導入する
      • 必要となるライブラリをGemfileに追記し、bundle installする
      Gemfile
      group :test do
        gem 'rspec'
        gem 'rack-test'
      end
      
      • specディレクトリを作成し、helperとテストケースのファイルを作成する
        スクリーンショット 2018-07-16 17.54.57.png

      • spec_helperは、テストの設定を行うファイル

      spec_helper.rb
      require 'rack/test'
      require 'rspec'
      
      ENV['RACK_ENV'] = 'test'
      
      require File.expand_path '../../sinatra_study.rb', __FILE__
      
      module RSpecMixin
        include Rack::Test::Methods
        def app() Sinatra::Application end
      end
      
      RSpec.configure { |c| c.include RSpecMixin }
      
      • テストケースの作成
      sinatra_study_spec.rb
      require File.expand_path '../spec_helper.rb', __FILE__
      
      describe "新規ユーザの追加が行われることを確認" do
        it "ルートページが正しく表示されること" do
          get '/'
          expect(last_response).to be_ok
        end
      end
      
      
      • テストケースは、~_spec.rbという命名規則に従う
      • 冒頭のこの箇所は require File.expand_path '../spec_helper.rb', __FILE__ 第一引数で指定した相対パスを、絶対パス化して読み込むことができる。

        • ../とすると、自身のファイルを含まず、同じ階層./のディレクトリにある別のファイルspec_helperをrequireする
      • テストを実行する

      $ bundle exec rspec spec
      .
      Finished in 0.21486 seconds (files took 4.04 seconds to load)
      1 example, 0 failures
      
      • テストの動作を確認

      スクリーンショット 2018-07-16 17.59.11.png

      • 以上で、最小限の機能ですが、コントローラ、ビュー、モデル、テスト、まで作成が終わりました
      • 次はhelokuにあげて公開します

      Herokuでsinatraアプリを公開する手順

      • 別エントリーにまとめました
        • herokuのために、DBをpostgresに変更し、マイグレーションをできるように変更します
        • herokuのアカウントの準備とリポジトリ・サーバを準備します
        • 必要な設定ファイルを追加します https://qiita.com/yukihigasi/items/3b2307061e15996a84ca

      参考

      公式
      http://sinatrarb.com/intro.html
      SinatraでとりあえずWebページを立ち上げてみる
      https://qiita.com/k-ta-yamada/items/9e35c5f8b31862267e01
      Sinatra入門
      https://qiita.com/kimioka0/items/751e460cbb59c70379c6
      ドットインストール sinatra入門
      https://dotinstall.com/lessons/basic_sinatra_v2
      HTML雛形
      http://www.html5.jp/html5doctor/html-5-boilerplates.html
      sinatraにないdb:seedを行う
      冪等性を担保する仕組みとしてseed-fuを使い、それをsinatraに組み込む方法
      https://qiita.com/hogesuke_1/items/56922d172e4d215e0eb4
      Herokuに速攻デプロイするSinatraアプリテンプレートをつくる #1
      http://totutotu.hatenablog.com/entry/2015/06/10/Heroku%E3%81%AB%E9%80%9F%E6%94%BB%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4%E3%81%99%E3%82%8BSinatra%E3%82%A2%E3%83%97%E3%83%AA%E3%83%86%E3%83%B3%E3%83%97%E3%83%AC%E3%83%BC%E3%83%88%E3%82%92%E3%81%A4
      Sinatra Best Practices
      https://blog.carbonfive.com/2013/06/24/sinatra-best-practices-part-one/
      SinatraのインストールとRSpecでテストする(rspec)
      http://48n.jp/blog/2016/04/20/try-sinatra/

      その他

      • なぜかtmuxエラー
      • 発生事象
        • ActiveRecordによるdestroyメソッド実行時にエラーが発生する
        • ログ上、なぜかtmuxのmanが表示され、そのあとにエラーログが出ている
        • ログも残せず、原因を記録できていない
      • 解消

        • tmuxのセッションを切り、接続し直したら解決
        • 原因はよくわからない
      • WEBRickが起動しない

      • 発生事象

        • ruby {起動ファイル名} で立ち上がらない
        • gemのインストールが足りない、というエラーが出続ける。
      • 原因と対策

        • 異なるbundlerが使われていることがある which bunderで正しいパスのbundlerが使われているか確認する
        • bundlerを再インストールするのが早いかもしれない
      • テーブルが存在しないエラー

      • 発生事象

        • /でUser.allでアクティブレコードへの接続が行われた瞬間エラーとなる
        • エラー内容は、usersテーブルが存在しないという内容
      • 原因

        • どこまで本当の原因がわからないが、以下二つをした時に治った
        • 別の.dbファイルが、残っていたため、削除をした
        • activerecordのdbファイルの指定パスを、カレントパスを表記しなかったら治った
      ActiveRecord::Base.establish_connection(
        adapter: 'sqlite3',
        database: './herdb.db'
      )
      
      ActiveRecord::Base.establish_connection(
        adapter: 'sqlite3',
        database: 'herdb.db'
      )
      
      • しかし、これを元に戻してリトライしたら正常に表示していたため、原因ではなさそう。取り急ぎ以上を実行すると、治る可能性がある。

      その他2(留意事項とできなかったこと)

      • 今後の課題として
        • Integrationテストをどう実行するのか調べる
        • インストールしたライブラリを管理する場所を明示的に管理できていない気がする
        • migration
        • rakeタスクを作り、雛形とテストの雛形を生成できるようにしておきたい scaffold的なものがないのか探す
        • ActiveRecordでのバリデーション時の、エラーのフラッシュ表示などはどうやったら簡単に実装できるのか調べる
        • helperを別クラスに切り出すべきだった

      その他3

      • 本記事の作成の意図として、「何かサービスを作ってみたいけど、どうしていいかわからない」
        みたいな人で集まってプログラミングサークルみたいなのを作ったら、面白いかなと思っていて
        その時に、サポートするにあたって最小限の必要知識をまとめておく
      • スキルを高め合うための(プロ向け)サークル、というより、
        非エンジニアだったり、経験が浅かったり、高齢者だったり、敷居が高くて近づけないと思っていた人が、
        日常生活で感じている課題や問題を解決するアイデアを、「サービス化」して解決にチャレンジする
        そういう人たちのためのサークル、あったら有意義かなと。
        (本当にやるかわからないですが、ニーズがあるならば。。。興味があれば、twitterでも、コメントでも、ご連絡ください)
24
24
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
24