目的
- sinatraでのアプリ開発の基本を纏める
- webアプリの開発経験があまりない人、ruby系の経験が少ない(それは僕もそうだが)を対象
前提
ゴール
-
アプリを開発する、MVCの基本的な実装ができる
-
bundlerの導入
-
ルーティング
-
View 画面表示
-
画面とのパラメータの受け渡し
-
DBとの接続、永続化
-
UnitTest(RSpec)
-
次回
-
Herokuから公開できる
手順
ライブラリの読み込み
- bundlerのインストール
- 対象のディレクトリにbundlerをインストール
gem install bundler
- gem ファイルを作成
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]
変数を展開するため#{}
内で行う
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
- パラメータが受け渡されていることを確認
複数のパスパラメータを扱う
- パスパラメータ中で複数のシンボルを受け取ると、
# パスパラメータの受け取り
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
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
を作成する - 共通部分はここに書き込んでおく
<!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コード実行が行われる
- <%= となると、この場所に結果が代入される(ここに結果が表示される)、と言う意味になる
スタイルやjavascriptの静的リソースの読み込み
- /publicディレクトリを作成すると、その下に静的リソースを配置し、view上から読み出すことができる
- 例えば、以下のようなファイルを作る
/public/css/style.css
- view上からは、以下のように読み出すことができる
<link rel="stylesheet" href="css/styles.css">
public直下のパスを指定する
<!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を直接流しての設定する場合手順
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を追加します
gem 'rake'
gem 'activerecord'
gem "sinatra-activerecord"
- 以上を追加し
bundle install
する - Rakeファイルを追加する
- プロジェクトのルートに、以下のファイルを追加する
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にマイグレーションファイル名を指定します。
-
マイグレーションファイルは、プロジェクトに一意である必要がありますが、同じ名称を与えても、自動的に頭にタイムスタンプが追記されるため、重複しないようになっています
-
テーブル構造の変更などを行う場合、マイグレーションファイルを編集したり削除して作り直して流したりなどは、すべきではありません
-
修正するマイグレーションファイルを作成し、更新を行います。
class CreateUsers < ActiveRecord::Migration[5.2]
def change
end
end
- 以下のように追記し、テーブルを作ります
- モデルがuserなので、テーブルはusersとなります
- create_tableでusersを指定します
- テーブルは、idとnameです。idは自動で追加されるようにするため、記載せず
- nameを、string型で定義します
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との接続設定
- 今回、コントローラ上に追記してしまう
ActiveRecord::Base.establish_connection(
adapter: 'sqlite3',
database: './sindb.db'
)
- 先ほど作成したデータベースファイルを、sqlite3をアダプタに読み込む
モデルクラスの作成
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のデータを削除 |
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対策のため、次のライブラリを読み込み、設定する
require 'rack/csrf'
use Rack::Session::Cookie, secret: "{任意の文字列。長めのランダムな文字を定義する}"
use Rack::Csrf, rise:true
- 画面側
<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を利用し、エスケープする -
フォームの実装
- 画面での入力を
<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 を追加する
$(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を読み込む
-
このjavascriptを、共通のlayoutファイルに読み込んでおく
-
jqueryも読み込む (googleのhosted libraryを使用 https://developers.google.com/speed/libraries/)
<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ファイル上からメソッドを呼び出し利用することができる
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
- 画面側
<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
する
group :test do
gem 'rspec'
gem 'rack-test'
end
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 }
- テストケースの作成
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
- テストの動作を確認
- 以上で、最小限の機能ですが、コントローラ、ビュー、モデル、テスト、まで作成が終わりました
- 次は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でも、コメントでも、ご連絡ください)