何度もくどいですが、前回までのReactチュートリアルの三目並べをせっかくなので勉強も兼ねてRuby Railsで作ってみようと思います。
RubyおよびRailsはProgateの無料レッスンをこなした程度です。
無理やりerbでやらせて、ボタンを押下するたびにformをsubmitしたら、超もっさりになりました・・・。
- そもそも小規模なので、クライアントで完結させた方がよい
- 使うにしてもクライアントはReactなり他のフレームワークやライブラリに任せればよい
- やり取りはajaxにして、Rails側はデータの出し入れだけやればよい
にすればよかたかも・・・。
#開発環境
Rails環境:以前の記事で構築した環境を使用する
エディタ:VSCode
#M(Model)V(View)C(Controller)を用意する
##Controller
ターミナルで以下を実行して、コントローラーを追加します。
rails generate controller tictactoes
ファイルの内容はデフォルトのindexアクションがあるのみ
class TictactoesController < ApplicationController
def index
end
end
##View
コントローラー作成の際に以下にtictactoes
ディレクトリが作成されているので、そこにindex.html.erb
を作成します。
htmlの構成は前回まで使用していたindex.html
をコピーします。
RailsだとDOCTYPE
やhtml
、body
等の共通部分は自動的に作成してくれる?ようなので、必要最低限をコピーしました。
<head>
<!--<link rel="stylesheet" href="css/index.css" />-->
<!--<script src="js/index.js"></script>-->
</head>
<body id="tictactoes-body">
<div id="root">
<div class="game">
<div class="gmae-board">
<div>
<div class="board-row">
<button class="square"></button><button class="square"></button
><button class="square"></button>
</div>
<div class="board-row">
<button class="square"></button><button class="square"></button
><button class="square"></button>
</div>
<div class="board-row">
<button class="square"></button><button class="square"></button
><button class="square"></button>
</div>
</div>
</div>
<div class="game-info">
<div>次の手番: X</div>
<div>
<li><button>Go to game start</button></li>
<!-- <li><button>Go to move #1</button></li> -->
</div>
</div>
</div>
</div>
</body>
body
にid
をつけているのは、他画面のbody
のcss設定が効いてしまって背景が青がかった緑色になってしまったので、そのスタイルを上書きするためです。
↑RailsをProgate無料レッスンでやった時のhome.scss
のbody
のスタイルが効いてしまう。
スタイルが全体に効いてしまう理由は以下を参考にさせていただきました。
RailsがJavaScriptやスタイルシートを読み込む仕組み
CSSはassets/stylesheees
配下に自動的に作成されるのでそこに前回のindex.css
をコピーしていきます。
#tictactoes-body {
font: 14px "Century Gothic", Futura, sans-serif;
margin: 20px;
background-color: white;
ol,
ul {
padding-left: 30px;
}
.board-row:after {
clear: both;
content: "";
display: table;
}
.status {
margin-bottom: 10px;
}
.square {
background: #fff;
border: 1px solid #999;
float: left;
font-size: 24px;
font-weight: bold;
line-height: 34px;
height: 34px;
margin-right: -1px;
margin-top: -1px;
padding: 0;
text-align: center;
width: 34px;
}
.square:focus {
outline: none;
}
.kbd-navigation .square:focus {
background: #ddd;
}
.game {
display: flex;
flex-direction: row;
}
.game-info {
margin-left: 20px;
}
}
##ルーティング
routes.rb
にGETリクエストのルーティングを追加します。
get 'tictactoes' => 'tictactoes#index'
ここでブラウザで確認してみたいと思います。
ターミナルから
rails server
を実行した後、http://localhost:3000/tictactoes
にアクセス。
##Model
マス目のクリック状態はDBに持つことにします。
以下のテーブル構成にしました。
テーブル名:squares
カラム:
カラム名 | 型 | 主キー | 説明・備考 |
---|---|---|---|
stepNumber | integer | O | 手順番号。主キー |
squareNumber | integer | O | マス目番号。主キー |
content | string | マス目の中の値(O/X) |
migrationファイルを作っていきます。
以下をターミナルで実行(以下はModelを作って、ついでにmigrationファイルが作られてます。)
rails generate model Square stepNumber:integer:unique squareNumber:integer:unique content:string
以下が作成されます。
class Square < ApplicationRecord
end
class CreateSquares < ActiveRecord::Migration[6.0]
def change
create_table :squares do |t|
t.integer :stepNumber
t.integer :squareNumber
t.string :content
t.timestamps
end
end
end
あれ?できたmigrationファイルに主キーの指定がない・・・
なんでや・・・
理由はわかりませんが、手動で以下を追加を追加すればよいと思います。
※null不可も追加しました。
主キーに指定しただけでは勝手にnull不可とはならないようです。
後で他のSQLクライアントでスキーマ情報を見てみるとわかりますが、id
というカラムが主キー、null不可で自動的に作成されています。
そのカラムがあるからでしょうか・・・。
add_index :squares, [:stepNumber, :squareNumber], :unique => true
change_column :squares, :stepNumber, :integer, null: false
change_column :squares, :squareNumber, :integer, null: false
私は一度テーブルを作ってしまって後から追加しました。
ファイルが用意できたら、マイグレーションファイルを実行
rails db:migrate
フリーのSQLクライアント
A5:SQL Mk-2
を使用してテーブルを確認してみます。
インストール後、起動
「データベース」を右クリック → 「データベースの追加と削除」
左下の「追加」
「SQLite(sqlite3.dll経由)」を選択
db/development.sqlite3
を選択します。
追加すると、左のツリーに合わられるので、展開してテーブル情報を確認します。
ログイン情報を求められたら、空欄のままOKで良いです。
データタブを開き、マス目に未入力の状態のレコードを作成します。
Excel等で作って、コピー&ペーストすればデータが作成されます。
#マス目に数字を表示する
squareテーブルの各レコード.contentに数値を入れておいて、その値を表示させたいと思います。
全件取得します。
class TictactoesController < ApplicationController
def index
@Squares = Square.all
end
end
erbの方ではマス目が3つごとに<div class="board-row">
に囲われる必要があるので、if文で制御してます。
<head>
<!--<link rel="stylesheet" href="css/index.css" />-->
<!--<script src="js/index.js"></script>-->
</head>
<body id="tictactoes-body">
<div id="root">
<div class="game">
<div class="gmae-board">
<div>
<% @Squares.each.with_index do |sq, i| %>
<% if i % 3 == 0%>
<div class="board-row"></div>
<% end %>
<button class="square">
<%= sq.content %>
</button>
<% end %>
</div>
</div>
<div class="game-info">
<div>次の手番: X</div>
<div>
<li><button>Go to game start</button></li>
<!-- <li><button>Go to move #1</button></li> -->
</div>
</div>
</div>
</div>
</body>
#X/Oを入力できるようにする
全然Railsわからなくてめちゃ苦戦しました。
突っ込みどころ満載かもですが
(function() {
window.addEventListener("DOMContentLoaded", () => {
// 全square
document.querySelectorAll(".square").forEach((element, index) => {
// クリックイベント
element.addEventListener("click", squareClick.bind(element, index));
});
let isClick = false;
function squareClick(index) {
// 2度押し防止、既に値が埋まっているか
if (isClick || this.value) {
return;
} else {
isClick = true;
}
document.getElementById("clickNo").value = index;
document.forms[0].submit();
}
});
})();
formのsubmitなのでもっさりしており、クリック連打で重複してリクエストしていたので、防止しました。
また、学習の為、ほとんどサーバー側でやってみよう!と思っていたのですが私には制御が難しくて、「既に埋まっていたら」という判定はクライアントでやっています。
自作jsを何処に置けばよいかググっていたところ、assets/javascripts
らしいのだが、これはRails5までであり、
Rails6では、assets/javascripts
フォルダが無くなっており、jsを何処に置けばいいのかわからない。
→ app/javascript/packs
に配置して、読み込ませたいhtmlのヘッダで以下のように指定すると読み込めた。
<%= javascript_pack_tag 'tictactoes', 'data-turbolinks-track': 'reload' %>
でもこれだと、複数のjsを読み込むことになり、1つに圧縮してリクエストを減らしていた恩恵が受けられなくなる・・・。
ドウシヨウ
erbの方はform_withというものを使ってみます。
<head>
<!--<link rel="stylesheet" href="css/index.css" />-->
<!--<script src="js/index.js"></script>-->
<%= javascript_pack_tag 'tictactoes', 'data-turbolinks-track': 'reload' %>
</head>
<body id="tictactoes-body">
<%= form_with(url: "/tictactoes/sqClick", method: "post") do |f| %>
<div id="root">
<div class="game">
<div class="gmae-board">
<div>
<% @Squares.each.with_index do |sq, i| %>
<% if i % 3 == 0%>
<div class="board-row"></div>
<% end %>
<%= f.text_field '', :name => "item[]", :class => "square", :readonly => true , :value => sq.content%>
<% end %>
</div>
</div>
<div class="game-info">
<div><%= @nextStepMessage %></div>
<div>
<li><button>Go to game start</button></li>
<!-- <li><button>Go to move #1</button></li> -->
</div>
</div>
</div>
</div>
<%= f.hidden_field :clickNo, :value => @clickNo %>
<%= f.hidden_field :xIsNext, :value => @xIsNext %>
<%= f.hidden_field :stepNumber, :value => @stepNumber %>
<% end %>
</body>
うーん。独特すぎて謎汗
サーバー側
class TictactoesController < ApplicationController
#初期画面表示
def index
# 手順1以降を削除して初期化
Square.where.not(stepNumber: 0).delete_all
#手順0を全件取得
@Squares = Square.all
#初期はX
@xIsNext = "true"
#手順の開始は0から
@stepNumber = 0
#次の手番の文字列
@nextStepMessage = "次の手番: X"
end
#マス目クリック
def sqClick
#現在の手順インクリメント
stepNumber = params[:stepNumber].to_i
@stepNumber = stepNumber.succ
#マス目の配列
items = params[:item]
sq = Square.new
#既に勝敗が着いた
if sq.calculateWinner(items)
return
else
#XとOを設定
items[params[:clickNo].to_i] = params[:xIsNext] == "true" ? "X": "O"
isWinner = sq.calculateWinner(items)
#新しい手順をDBに保存
sq.saveSquare(@stepNumber, items)
#フラグの更新
@xIsNext = params[:xIsNext] == "true" ? "false" : "true"
#次の手番の文字列更新
if isWinner
@nextStepMessage = "勝者:" + (params[:xIsNext] == "true" ? "X": "O")
else
@nextStepMessage = params[:xIsNext] == "true" ? "次の手番: O": "次の手番: X"
end
#最新の手順を取り直す
@Squares = Square.where(stepNumber: @stepNumber)
end
#indexのhtmlを使いまわす
render action: :index
end
end
自分でも何をやっているのやら・・・。
@変数 でview側に値を埋め込めます。
render action: :index
でindex.html.erb
を使いまわしています。
判定処理calculateWinner
やレコード追加処理はモデルSquare
でやっています。
class Square < ApplicationRecord
Lines = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6]
]
def saveSquare(stepNumber, items)
#新しい手順のマス目保存処理
for i in 1..9 do
Square.create(stepNumber: stepNumber, squareNumber: i, content: items[i - 1])
end
end
#勝敗判定関数(公式チュートリアルから拝借)
def calculateWinner(items)
for l in Lines do
a, b, c = l
if items[a] != "" && items[a] == items[b] && items[a] == items[c]
return true
end
end
return false
end
end
テーブルデータ
id | stepNumber | squareNumber | content | created_at | updated_at |
---|---|---|---|---|---|
1 | 0 | 1 | 2020/03/29 | 1899/12/30 1:50:48 | |
2 | 0 | 2 | 2020/03/29 | 1899/12/30 1:51:11 | |
3 | 0 | 3 | 2020/03/29 | 1899/12/30 1:51:11 | |
4 | 0 | 4 | 2020/03/29 | 1899/12/30 1:50:48 | |
5 | 0 | 5 | 2020/03/29 | 1899/12/30 1:51:11 | |
6 | 0 | 6 | 2020/03/29 | 1899/12/30 1:51:11 | |
7 | 0 | 7 | 2020/03/29 | 1899/12/30 1:50:48 | |
8 | 0 | 8 | 2020/03/29 | 1899/12/30 1:51:11 | |
9 | 0 | 9 | 2020/03/29 | 1899/12/30 1:51:11 | |
10 | 1 | 1 | 2020/03/31 14:50:05 | 2020/03/31 14:50:05 | |
11 | 1 | 2 | 2020/03/31 14:50:05 | 2020/03/31 14:50:05 | |
12 | 1 | 3 | X | 2020/03/31 14:50:05 | 2020/03/31 14:50:05 |
13 | 1 | 4 | 2020/03/31 14:50:05 | 2020/03/31 14:50:05 | |
14 | 1 | 5 | 2020/03/31 14:50:05 | 2020/03/31 14:50:05 | |
15 | 1 | 6 | 2020/03/31 14:50:05 | 2020/03/31 14:50:05 | |
16 | 1 | 7 | 2020/03/31 14:50:05 | 2020/03/31 14:50:05 | |
17 | 1 | 8 | 2020/03/31 14:50:05 | 2020/03/31 14:50:05 | |
18 | 1 | 9 | 2020/03/31 14:50:05 | 2020/03/31 14:50:05 | |
19 | 2 | 1 | 2020/03/31 14:50:06 | 2020/03/31 14:50:06 | |
20 | 2 | 2 | 2020/03/31 14:50:06 | 2020/03/31 14:50:06 | |
21 | 2 | 3 | X | 2020/03/31 14:50:06 | 2020/03/31 14:50:06 |
22 | 2 | 4 | 2020/03/31 14:50:06 | 2020/03/31 14:50:06 | |
23 | 2 | 5 | 2020/03/31 14:50:06 | 2020/03/31 14:50:06 | |
24 | 2 | 6 | O | 2020/03/31 14:50:06 | 2020/03/31 14:50:06 |
25 | 2 | 7 | 2020/03/31 14:50:06 | 2020/03/31 14:50:06 | |
26 | 2 | 8 | 2020/03/31 14:50:06 | 2020/03/31 14:50:06 | |
27 | 2 | 9 | 2020/03/31 14:50:06 | 2020/03/31 14:50:06 | |
28 | 3 | 1 | 2020/03/31 14:50:07 | 2020/03/31 14:50:07 | |
29 | 3 | 2 | 2020/03/31 14:50:07 | 2020/03/31 14:50:07 | |
30 | 3 | 3 | X | 2020/03/31 14:50:07 | 2020/03/31 14:50:07 |
31 | 3 | 4 | 2020/03/31 14:50:07 | 2020/03/31 14:50:07 | |
32 | 3 | 5 | 2020/03/31 14:50:07 | 2020/03/31 14:50:07 | |
33 | 3 | 6 | O | 2020/03/31 14:50:07 | 2020/03/31 14:50:07 |
34 | 3 | 7 | 2020/03/31 14:50:07 | 2020/03/31 14:50:07 | |
35 | 3 | 8 | 2020/03/31 14:50:07 | 2020/03/31 14:50:07 | |
36 | 3 | 9 | X | 2020/03/31 14:50:07 | 2020/03/31 14:50:07 |
37 | 4 | 1 | 2020/03/31 14:50:10 | 2020/03/31 14:50:10 | |
38 | 4 | 2 | 2020/03/31 14:50:10 | 2020/03/31 14:50:10 | |
39 | 4 | 3 | X | 2020/03/31 14:50:10 | 2020/03/31 14:50:10 |
40 | 4 | 4 | 2020/03/31 14:50:10 | 2020/03/31 14:50:10 | |
41 | 4 | 5 | 2020/03/31 14:50:10 | 2020/03/31 14:50:10 | |
42 | 4 | 6 | O | 2020/03/31 14:50:10 | 2020/03/31 14:50:10 |
43 | 4 | 7 | 2020/03/31 14:50:10 | 2020/03/31 14:50:10 | |
44 | 4 | 8 | O | 2020/03/31 14:50:10 | 2020/03/31 14:50:10 |
45 | 4 | 9 | X | 2020/03/31 14:50:10 | 2020/03/31 14:50:10 |
46 | 5 | 1 | 2020/03/31 14:50:11 | 2020/03/31 14:50:11 | |
47 | 5 | 2 | 2020/03/31 14:50:11 | 2020/03/31 14:50:11 | |
48 | 5 | 3 | X | 2020/03/31 14:50:11 | 2020/03/31 14:50:11 |
49 | 5 | 4 | 2020/03/31 14:50:11 | 2020/03/31 14:50:11 | |
50 | 5 | 5 | X | 2020/03/31 14:50:11 | 2020/03/31 14:50:11 |
51 | 5 | 6 | O | 2020/03/31 14:50:11 | 2020/03/31 14:50:11 |
52 | 5 | 7 | 2020/03/31 14:50:11 | 2020/03/31 14:50:11 | |
53 | 5 | 8 | O | 2020/03/31 14:50:11 | 2020/03/31 14:50:11 |
54 | 5 | 9 | X | 2020/03/31 14:50:11 | 2020/03/31 14:50:11 |
55 | 6 | 1 | 2020/03/31 14:50:12 | 2020/03/31 14:50:12 | |
56 | 6 | 2 | 2020/03/31 14:50:12 | 2020/03/31 14:50:12 | |
57 | 6 | 3 | X | 2020/03/31 14:50:12 | 2020/03/31 14:50:12 |
58 | 6 | 4 | 2020/03/31 14:50:12 | 2020/03/31 14:50:12 | |
59 | 6 | 5 | X | 2020/03/31 14:50:12 | 2020/03/31 14:50:12 |
60 | 6 | 6 | O | 2020/03/31 14:50:12 | 2020/03/31 14:50:12 |
61 | 6 | 7 | O | 2020/03/31 14:50:12 | 2020/03/31 14:50:12 |
62 | 6 | 8 | O | 2020/03/31 14:50:12 | 2020/03/31 14:50:12 |
63 | 6 | 9 | X | 2020/03/31 14:50:13 | 2020/03/31 14:50:13 |
64 | 7 | 1 | X | 2020/03/31 14:50:14 | 2020/03/31 14:50:14 |
65 | 7 | 2 | 2020/03/31 14:50:14 | 2020/03/31 14:50:14 | |
66 | 7 | 3 | X | 2020/03/31 14:50:14 | 2020/03/31 14:50:14 |
67 | 7 | 4 | 2020/03/31 14:50:14 | 2020/03/31 14:50:14 | |
68 | 7 | 5 | X | 2020/03/31 14:50:14 | 2020/03/31 14:50:14 |
69 | 7 | 6 | O | 2020/03/31 14:50:14 | 2020/03/31 14:50:14 |
70 | 7 | 7 | O | 2020/03/31 14:50:14 | 2020/03/31 14:50:14 |
71 | 7 | 8 | O | 2020/03/31 14:50:14 | 2020/03/31 14:50:14 |
72 | 7 | 9 | X | 2020/03/31 14:50:14 | 2020/03/31 14:50:14 |
#感想
Railsの書き方が独特すぎて超苦戦してます。今のところメリット感じない・・・
次回は履歴機能を持たせて完成に持っていこうと思います。
いつかRailsのチュートリアルもやってもっと基礎から理解しよう・・・。
→ 次回