Ruby on Rails へ Fetch API で非同期通信を行う
Turbo とか使えばわざわざ自前で Fetch API 使って非同期通信実装することなんてあんまりないのかなと思ったのですが書きました
TL;DR
- 大体この辺に書いてあることと同じです
- Fetch の使用 | Web API | MDN
- Fetch APIを使ってRailsのAPIを叩く | The Pragmatic Ball boy
- Basic fetch requests with JS + Rails | DEV Community 👩💻👨💻
この記事でやること、やらないこと
- この記事でやること
- Rails サーバーに JavaScript の Fetch API を使って非同期通信で GET / POST
- Rails の CSRF対策用 Token を使った POST
- POST の Request Body に JSON を指定して Rails サーバー側で受け取った値を確認する方法
- この記事でやらないこと
- HTTP Request についての詳しい話
- GET とは何か / POST とは何か / Request Body とは / Request Header とは
- Fetch API についての詳しい話
- Promise の使い方 / 書き方など
- Rails を API モードとして使って外部 HTML から API を叩けるようにする方法
- GET / POST 以外の非同期通信
- XMLHTTPRequest を使った非同期通信
- HTTP Request についての詳しい話
GET/POST Method を受け付けるAPIを作成
GET と POST をそれぞれ受け付けるコントローラーを作っても良いのですが
今回は GET と POST 両方を受け付けるコントローラーを用意しましょう。
どこに用意しても良いのですが今回は home#get_post
に用意します。
class HomeController < ApplicationController
def index
end
def get_post
end
end
config/routes.rb
にこんな感じで書くことで
http://localhost:3000/get_post
で GET も POST も受け付けることができます。
Rails.application.routes.draw do
root to: 'home#index' # これは home#index を root に割り当てる定義
match 'get_post', to: 'home#get_post', via: [:get, :post]
end
- 3.7 HTTP動詞を制限する | Rails のルーティング | Railsガイド
で、ブラウザからアクセスして GET した時用にviewも用意しておきましょう。
<h1>Home#get_post</h1>
<p>Find me in app/views/home/get_post.html.erb</p>
OKですね。試しにブラウザからアクセスしてみましょう。
いいですね。
Fetch API で GET Request してみる
home#index
に JavaScript を追加して
http://localhost:3000/get_post
に Fetch API で GET Request してみます。
<h1>Home#index</h1>
<p>Find me in app/views/home/index.html.erb</p>
<button type="button" id="button_get">Fetch API : GET</button>
<script>
// -------------------------------------------
// button: Fetch API : GET
// -------------------------------------------
const button_get = document.getElementById('button_get');
button_get.addEventListener('click',function(){
fetch('/get_post', {
method: 'GET'
})
.then(function(response){
const response_message = response.status + ':' + response.statusText
console.log(response_message);
window.alert('response_message=' + response_message);
});
});
</script>
ボタンの onClick イベントで Fetch API を実行しているだけです。
Fetch API は method を省略して実行することで default GET となりますが
ここではわかりやすさのために method: 'GET'
を明示しています。
Fetch API が成功したら response オブジェクトの情報を alert
で表示しています。
丁寧にやるのであれば response が成功したかどうかを response.ok
でチェックしたり
ネットワークエラーで Fetch API そのものの失敗を catch
したりの記述も必要ですが
今回は省略しています。
- response オブジェクトについてはこちら
- Response | Web API | MDN
Fetch API で POST Request してみる
GET ができたので POST もしてみましょう。
まずは同じ感じで書いてみます。
<h1>Home#index</h1>
<p>Find me in app/views/home/index.html.erb</p>
<button type="button" id="button_get">Fetch API : GET</button> |
<button type="button" id="button_post">Fetch API : POST</button>
<script>
// -------------------------------------------
// button: Fetch API : GET
// -------------------------------------------
const button_get = document.getElementById('button_get');
button_get.addEventListener('click',function(){
fetch('/get_post', {
method: 'GET'
})
.then(function(response){
const response_message = response.status + ':' + response.statusText
console.log(response_message);
window.alert('response_message=' + response_message);
});
});
// -------------------------------------------
// button: Fetch API : POST
// -------------------------------------------
const button_post = document.getElementById('button_post');
button_post.addEventListener('click',function(){
fetch('/get_post', {
method: 'POST'
})
.then(function(response){
const response_message = response.status + ':' + response.statusText
console.log(response_message);
window.alert('response_message=' + response_message);
});
});
</script>
……あらら、HTTP 422 Unprocessable Entity ERROR になってますね。
これは、Rails の default の仕様で、
app/views/layouts/application.html.erb に最初から書かれている
<%= csrf_meta_tags %>
などによって
GET以外のあらゆる非同期通信Requestでは正しい X-CSRF-Token
を Request Header に含めないと
サーバー側は Request を弾くようにしているためです。
クロスサイトリクエストフォージェリ(CSRF)というサイバー攻撃対策用のTokenを用いた仕組みですね。
- 3.1 CSRFへの対応策 | Rails セキュリティガイド | Railsガイド
Request Header に X-CSRF-Token を含めるようにする
ではどうやって POST Request を送れるようにするのかというと、
app/views/layouts/application.html.erb にある <%= csrf_meta_tags %>
すなわちレンダリングされたページの <head>
タグ内にある
<meta name="csrf-token" content="XXXXXXX">
を読み取って
POST Request の Request Header に含めてやります。
また、Google で検索すれば色々とやり方には辿り着けますね。
- Fetch APIを使ってRailsのAPIを叩く | The Pragmatic Ball boy
- 今回はここの方法を参考にしました
- https://yanamura.hatenablog.com/entry/2017/05/12/094103
- Basic fetch requests with JS + Rails | DEV Community 👩💻👨💻
- railsのCSRF対策について | Qiita
- 外部からPOSTできない?RailsのCSRF対策をまとめてみた | Qiita
CSRF-TOKEN で session の確認もできるので、
例えば devise でログイン機能を作ってあった時でもログイン済みのPOSTとして機能するようになります。
- RailsのCSRF保護を詳しく調べてみた(翻訳)|TechRacho(テックラッチョ)|BPS株式会社
というわけで実装してみましょう。
<h1>Home#index</h1>
<p>Find me in app/views/home/index.html.erb</p>
<button type="button" id="button_get">Fetch API : GET</button> |
<button type="button" id="button_post">Fetch API : POST</button>
<script>
// -------------------------------------------
// button: Fetch API : GET
// -------------------------------------------
const button_get = document.getElementById('button_get');
button_get.addEventListener('click',function(){
fetch('/get_post', {
method: 'GET'
})
.then(function(response){
const response_message = response.status + ':' + response.statusText
console.log(response_message);
window.alert('response_message=' + response_message);
});
});
// -------------------------------------------
// button: Fetch API : POST
// -------------------------------------------
const button_post = document.getElementById('button_post');
button_post.addEventListener('click',function(){
fetch('/get_post', {
method: 'POST',
credentials: 'same-origin',
headers: {
'X-CSRF-Token': getCsrfToken()
},
})
.then(function(response){
const response_message = response.status + ':' + response.statusText
console.log(response_message);
window.alert('response_message=' + response_message);
});
});
// -------------------------------------------
// app/views/layouts/application.html.erb に csrf_meta_tags で設定されている csrf-token' の取得
// -------------------------------------------
const getCsrfToken = () => {
const metas = document.getElementsByTagName('meta');
for (let meta of metas) {
if (meta.getAttribute('name') === 'csrf-token') {
console.log('csrf-token:', meta.getAttribute('content'));
return meta.getAttribute('content');
}
}
return '';
}
</script>
レンダリングされたページの <head>
タグ内にある
<meta name="csrf-token" content="XXXXXXX">
を読み取るメソッドを作成し、
POST Request を行う Fetch API に以下を追記しました。
credentials: 'same-origin',
headers: {
'X-CSRF-Token': getCsrfToken()
},
POST が 200 OK になりましたね!
JSON 形式で Request Body を送ってみる
せっかく POST できたので Request Body も送ってみましょう。
まずはJavaScript オブジェクトをハードコーディングで記述して Request Body に指定してみます。
JavaScript オブジェクトを JSON.stringify
するだけですね。
Request Header に 'Content-Type': 'application/json'
を追記するのも忘れないようにしましょう。
こんな感じです。
<h1>Home#index</h1>
<p>Find me in app/views/home/index.html.erb</p>
<button type="button" id="button_get">Fetch API : GET</button> |
<button type="button" id="button_post">Fetch API : POST</button>
<script>
// -------------------------------------------
// button: Fetch API : GET
// -------------------------------------------
const button_get = document.getElementById('button_get');
button_get.addEventListener('click',function(){
fetch('/get_post', {
method: 'GET'
})
.then(function(response){
const response_message = response.status + ':' + response.statusText
console.log(response_message);
window.alert('response_message=' + response_message);
});
});
// -------------------------------------------
// button: Fetch API : POST
// -------------------------------------------
const button_post = document.getElementById('button_post');
button_post.addEventListener('click',function(){
const post_data = {
key1: "data1",
key2: "data2",
key3: 3
};
fetch('/get_post', {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCsrfToken()
},
body: JSON.stringify(post_data),
})
.then(function(response){
const response_message = response.status + ':' + response.statusText
console.log(response_message);
window.alert('response_message=' + response_message);
});
});
// -------------------------------------------
// app/views/layouts/application.html.erb に csrf_meta_tags で設定されている csrf-token' の取得
// -------------------------------------------
const getCsrfToken = () => {
const metas = document.getElementsByTagName('meta');
for (let meta of metas) {
if (meta.getAttribute('name') === 'csrf-token') {
console.log('csrf-token:', meta.getAttribute('content'));
return meta.getAttribute('content');
}
}
return '';
}
</script>
- JSON.stringify() | JavaScript | MDN
POST されたデータがちゃんとRails側で受け取れているかは
Controller 側で params
を確認すれば良いです。
binding.pry して params
を確認してやるのも良いですね。
class HomeController < ApplicationController
def index
end
def get_post
# binding.pry
pp params
end
end
テキストボックスの値を POST する
ここまでくるともう Rails + Fetch API の範疇を超えてきますが、
せっかくなのでよくやりたいことである 画面上のテキストボックスの値をPOSTするのも
やってみましょう。
特に難しいことはないですね。
JavaScript オブジェクトを動的に作成するには
data[key] = hoge
の形で指定してやればいいってことぐらいです。
(もちろん、key が固定値で良いなら data["Fuga"] = hoge
でもOKです)
- JavaScriptでJSONデータを作る方法 | 小粋空間
<h1>Home#index</h1>
<p>Find me in app/views/home/index.html.erb</p>
<input type="text" id="input_text" name="name_input_text" maxlength="8" size="10">
<br>
<button type="button" id="button_get">Fetch API : GET</button> |
<button type="button" id="button_post">Fetch API : POST</button>
<script>
// -------------------------------------------
// button: Fetch API : GET
// -------------------------------------------
const button_get = document.getElementById('button_get');
button_get.addEventListener('click',function(){
fetch('/get_post', {
method: 'GET'
})
.then(function(response){
const response_message = response.status + ':' + response.statusText
console.log(response_message);
window.alert('response_message=' + response_message);
});
});
// -------------------------------------------
// button: Fetch API : POST
// -------------------------------------------
const button_post = document.getElementById('button_post');
button_post.addEventListener('click',function(){
const post_data = {
key1: "data1",
key2: "data2",
key3: 3
};
const input_text = document.getElementById('input_text');
post_data[input_text.name] = input_text.value;
fetch('/get_post', {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCsrfToken()
},
body: JSON.stringify(post_data),
})
.then(function(response){
const response_message = response.status + ':' + response.statusText
console.log(response_message);
window.alert('response_message=' + response_message);
});
});
// -------------------------------------------
// app/views/layouts/application.html.erb に csrf_meta_tags で設定されている csrf-token' の取得
// -------------------------------------------
const getCsrfToken = () => {
const metas = document.getElementsByTagName('meta');
for (let meta of metas) {
if (meta.getAttribute('name') === 'csrf-token') {
console.log('csrf-token:', meta.getAttribute('content'));
return meta.getAttribute('content');
}
}
return '';
}
</script>
Rails 側のログをみると……
Started POST "/get_post" for XXX.XXX.XXX.XX at 2021-03-25 00:00:00
Processing by HomeController#get_post as */*
Parameters: {"key1"=>"data1", "key2"=>"data2", "key3"=>3, "name_input_text"=>"ttt", "home"=>{"key1"=>"data1", "key2"=>"data2", "key3"=>3, "name_input_text"=>"ttt"}}
#<ActionController::Parameters {"key1"=>"data1", "key2"=>"data2", "key3"=>3, "name_input_text"=>"ttt", "controller"=>"home", "action"=>"get_post", "home"=>{"key1"=>"data1", "key2"=>"data2", "key3"=>3, "name_input_text"=>"ttt"}} permitted: false>
"get_post"
Rendering layout layouts/application.html.erb
Rendering home/get_post.html.erb within layouts/application
Rendered home/get_post.html.erb within layouts/application (Duration: 0.2ms | Allocations: 33)
Rendered layout layouts/application.html.erb (Duration: 4.6ms | Allocations: 155)
Completed 200 OK in 9ms (Views: 7.9ms | Allocations: 639)
OK ですね! ちゃんと送信されてきています。
というか別に pp params
しなくてもパラメータはログに出ていますね……笑
テキストボックスの値をまとめて取得してみる
これも Rails + Fetch API の範疇を超えてきますが、
テキストボックスの値をまとめて取得してPOSTしてみましょう。
document.querySelectorAll
を使ってみたかっただけです。
こんな感じの画面で、
ここを
{
keywords: [
{keyword: "aaa"},
{keyword: "bbb"},
{keyword: "ccc"},
]
}
こんな感じの JSON として送信して Rails 側で受け取ってみようと思います。
<h1>Home#index</h1>
<p>Find me in app/views/home/index.html.erb</p>
input_text: <input type="text" id="input_text" name="name_input_text" maxlength="8" size="10">
<br>
keywords: <input type="text" group='keyword' maxlength="8" size="10">
<input type="text" group='keyword' maxlength="8" size="10">
<input type="text" group='keyword' maxlength="8" size="10">
<br>
<button type="button" id="button_get">Fetch API : GET</button> |
<button type="button" id="button_post">Fetch API : POST</button>
<script>
// -------------------------------------------
// button: Fetch API : GET
// -------------------------------------------
const button_get = document.getElementById('button_get');
button_get.addEventListener('click',function(){
fetch('/get_post', {
method: 'GET'
})
.then(function(response){
const response_message = response.status + ':' + response.statusText
console.log(response_message);
window.alert('response_message=' + response_message);
});
});
// -------------------------------------------
// button: Fetch API : POST
// -------------------------------------------
const button_post = document.getElementById('button_post');
button_post.addEventListener('click',function(){
// 固定値
const post_data = {
key1: "data1",
key2: "data2",
key3: 3
};
// id で取得
const input_text = document.getElementById('input_text');
post_data[input_text.name] = input_text.value;
// attribute で取得
const keyword_nodes = document.querySelectorAll("input[group='keyword']");
const keyword_data = [];
for (let keyword_node of keyword_nodes) {
const keyword = {keyword: keyword_node.value};
keyword_data.push(keyword);
}
post_data['keywords'] = keyword_data;
fetch('/get_post', {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCsrfToken()
},
body: JSON.stringify(post_data),
})
.then(function(response){
const response_message = response.status + ':' + response.statusText
console.log(response_message);
window.alert('response_message=' + response_message);
});
});
// -------------------------------------------
// app/views/layouts/application.html.erb に csrf_meta_tags で設定されている csrf-token' の取得
// -------------------------------------------
const getCsrfToken = () => {
const metas = document.getElementsByTagName('meta');
for (let meta of metas) {
if (meta.getAttribute('name') === 'csrf-token') {
console.log('csrf-token:', meta.getAttribute('content'));
return meta.getAttribute('content');
}
}
return '';
}
</script>
class HomeController < ApplicationController
def index
end
def get_post
# String Parameters
permit_parameters = params.permit(:key1, :key2, :key3, :name_input_text, keywords: [:keyword]).to_h
pp permit_parameters[:key1]
pp permit_parameters[:key2]
pp permit_parameters[:key3]
pp permit_parameters[:name_input_text]
permit_parameters[:keywords].each do |keyword|
pp keyword[:keyword]
end
end
end
こんな感じですね。
POST すると、Rails 側の出力はこんな感じです。
Parameters: {"key1"=>"data1", "key2"=>"data2", "key3"=>3, "name_input_text"=>"111", "keywords"=>[{"keyword"=>"aaa"}, {"keyword"=>"bbb"}, {"keyword"=>"ccc"}], "home"=>{"key1"=>"data1", "key2"=>"data2", "key3"=>3, "name_input_text"=>"111", "keywords"=>[{"keyword"=>"aaa"}, {"keyword"=>"bbb"}, {"keyword"=>"ccc"}]}}
Unpermitted parameter: :home
"data1"
"data2"
3
"111"
"aaa"
"bbb"
"ccc"
JavaScript としてのポイントとしてはこの辺り、
- Document.querySelectorAll() - Web API | MDN
- JavaScript 属性値を取得/設定/削除する(getAttribute) | ITSakura
Rails 側のポイントとしては StrongParameters を通してハッシュ化することで
受け取った params をコントローラー側で扱えるようにするあたりがポイントとなります。
- ネストするStrong Parametersの書きかた | Qiita
- RailsのStrongParametersと友だちになった | Qiita
以上です。おつかれさまでした。
今回のコードは以下リポジトリに含めています。
Discussion