はじめに
ネイティブ JavaScript で Ajax を実装するには、XMLHttpRequest よりも、Fetch の方が一般的なようです。
学習がてら、Fetch による Ajax 処理の基本的なところをまとめておきます。
POSTメソッドの動作確認は、Rails 6 を使用していますが、どのフレームワークでも基本は変わらないと思います。
1. GET メソッド
GET メソッドは、HTML ファイルと Javascript ファイルだけで簡単に動作確認ができます。
サンプルコードはコピペで動くようにしています。
1-1. fetch を実装する(then - catch - finally の形式)
気象庁の天気予報データ(JSON形式)を取得するだけであれば、次のとおりです。
fetch メソッドの第1引数には URL
、第2引数には オプション
を指定しますが、第2引数は省略することができます(デフォルトで GET メソッドとなります)。
window.addEventListener('load', function() {
fetch('https://www.jma.go.jp/bosai/forecast/data/forecast/130000.json')
.then(function(response) {
return response.json();
})
.then(function(data) {
return console.log(data);
});
});
アロー演算子を使って書くと、次のように、よりシンプルになります。
window.addEventListener('load', function() {
fetch('https://www.jma.go.jp/bosai/forecast/data/forecast/130000.json')
.then(response => response.json())
.then(data => console.log(data));
});
コンソール画面を見ると、JSON データが取得できているのがわかります。
ソースコードの説明
タイトルに「then - catch - finally」と書いていますが、この例では、catch メソッド、finally メソッドは使用していません。
fetch メソッドは返り値として、Promise というものを返します(Promise は、非同期処理の完了を待って結果を返すオブジェクトです)。
正確性は置いておいて、コードの内容を順に見ると、おおよそ次のようなイメージです。
① fetch(url)
第1引数に URL を指定。
第2引数を省略すると、GET メソッドで HTTP リクエストが送信されます。
② .then(function(response) { return response.json() })
thenメソッドにより、①の処理が終了するまで待機。
引数の response
に、①の fetch メソッドから返されたデータ(Promise)が入る。
response.json()
でレスポンスデータを JSON 形式で読み取って返却する。
③ .then(function(data) { return console.log(data) });
then メソッドにより、②の処理が終了するまで待機。
引数の data
に、②から返されたデータ(Promise)が入る。
console.log(data)
でコンソール画面に表示する。
1-2. async / await で fetch を実装する
async / await で書くと次のようになります。
window.addEventListener('load', function() {
(async () => {
const response = await fetch('https://www.jma.go.jp/bosai/forecast/data/forecast/130000.json');
const data = await response.json();
console.log(data);
})();
});
<参考サイト>
・How to Use Fetch with async/await
ソースコードの説明
コードの内容を順に見ると、おおよそ次のようなイメージです。
① const response = await fetch(url);
await により、fetch メソッドによる処理が終わるまで次の処理に進まない。
② const data = await response.json();
await により、response.json()
の処理が終わるまで次の処理に進まない。
③ console.log(data);
①、②の処理が終わるのを待って、console.log(data)
でコンソール画面に表示する。
1-3. 例外処理を追加する
fetch メソッドが成功したかどうかを明確に判定するには Response.ok プロパティを使用します。
Response.ok プロパティは、レスポンスが成功したか否か(200–299の範囲のHTTPステータスか否か)を示す論理値(true / false)を返します。
1-3-1. 例外処理(then - catch - finally の形式)
例外処理(エラー処理)を行うには、then - catch - finally
を使用して、次のように記述します。
window.addEventListener('load', function() {
fetch('https://www.jma.go.jp/bosai/forecast/data/forecast/130000.json')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok'); // fetchが成功したかどうかの判定
}
return response.json()
})
.then(data => console.log(data))
.catch(error => {
alert(error); // 例外(エラー)が発生した場合に実行
})
.finally(() => {
console.log('finally'); // 処理結果の成否に関わらず実行
})
});
1-3-2. async / await の例外処理
async/await で例外処理(エラー処理)を行うには、try - catch - finally
を使用して、次のように記述します。
window.addEventListener('load', function() {
(async () => {
try {
const response = await fetch('https://www.jma.go.jp/bosai/forecast/data/forecast/130000.json');
if (!response.ok) {
throw new Error('Network response was not ok'); // fetchが成功したかどうかの判定
}
const data = await response.json();
console.log(data);
} catch(e) {
alert(e); // 例外(エラー)が発生した場合に実行
} finally {
console.log('finally'); // 処理結果の成否に関わらず実行
}
})();
});
2. POSTメソッド(基本のサンプル)
次に、POST メソッドについてです。
まずは、基本的な骨組みだけのサンプルです。
2-1. 単純なサンプル(then - catch - finally の形式)
POST メソッドの Ajax を骨組みだけで書くと次のような感じです。
GET メソッドとの違いは、fetch の第2引数にオプションとして、HTTPメソッド
、リクエストヘッダ
、リクエストボディ
などの情報を入れるところです。
const options = {
method: 'POST', // HTTPメソッドを指定
headers: { // リクエストヘッダを追加
'Content-Type': 'application/json', // リクエストデータをJSON形式と指定
'Accept': 'application/json' // レスポンスデータをJSON形式と指定
},
body: JSON.stringify(data) // リクエストボディ(送信データ)を指定
}
fetch(url, options) // サーバにリクエストを送信
.then(response => response.json()) // レスポンスデータをJSON形式に
.then(data => console.log(data)); // 取得したデータをコンソール画面に表示
上記の例は、クライアントから JSON 形式のデータを送信して、サーバからも JSON 形式のデータを受け取る場合になります。
送受信するデータの形式に応じて、リクエストヘッダに追加する情報なども異なってきます。
ここで使用している method
、headers
、body
以外のオプションについては「リクエストにオプションを適用する」を参照してください。
2-2. async / await を使用した単純なサンプル
async / await を使って書くと次のような感じです。
(async () => {
const options = {
method: 'POST', // HTTPメソッドを指定
headers: { // リクエストヘッダを追加
'Content-Type': 'application/json', // リクエストデータをJSON形式と指定
'Accept': 'application/json' // レスポンスデータをJSON形式と指定
},
body: JSON.stringify(data) // リクエストボディ(送信データ)を指定
}
const response = await fetch(url, options); // サーバにリクエストを送信
const data = await response.json(); // レスポンスデータをJSON形式に
console.log(data); // 取得したデータをコンソール画面に表示
})()
<参考記事>
・Proper Way to Make API Fetch 'POST' with Async/Await
これだけでは実際的ではないので、以下、具体的に動作するサンプルコードを書いておきます。
3. POST メソッド(フレームワークを使用した例)
POST メソッドの動作確認には、Rails6 を使用しました。
<実行環境>
Rails 6.1.4
ruby 3.0.3
3-1. Rails6 の準備
Rails アプリケーション作成方法は「RubyとRailsを最新版にして新規アプリケーションを作成する」を参照してください。
Rails6 では、Rails5 から変更になっている部分があったので、その点を簡単に触れておきます。
3-1-1 . Rails6 で JavaScript を使用する
Rails6 では JavaScript ファイルの取扱いが変更になっていましたので備忘として書いておきます。
本記事の趣旨とは離れるので折りたたんでおきます。
詳細を表示
rails new
を実行すると、app
ディレクトリ直下に javascript
というフォルダが作成されています。
このフォルダの直下に、main.js
ファイルを作成します。
// 動作確認用のコード
window.addEventListener('load', function() {
console.log('Hello!');
});
このままでは、JavaScript ファイルを読み込みません。
次のように、javascript フォルダの packs
フォルダ内にある application.js
ファイルに、import "main.js"
と追加します。
import Rails from "@rails/ujs"
import Turbolinks from "turbolinks"
import * as ActiveStorage from "@rails/activestorage"
import "channels"
import "main.js" // ここに追加
Rails.start()
Turbolinks.start()
ActiveStorage.start()
一応、以上で JavaScript ファイルを読み込ませることができます。
詳細は、以下の記事を参考にしてください(Rails6 のバージョンにより若干書き方が異なりますので、うまく読み替えてください)。
<参考記事>
・Ruby on RailsにおけるJavascriptファイルの取り扱い(Rails6)
・How to require custom JS files in Rails 6
3-1-2. Rails6 で Turbolinks を無効にする
Turbolinks は、画面遷移を高速化するライブラリで、自動で非同期通信(Ajax)まで行ってくれます。
ここでは、動作確認の邪魔になってしまうので、この Turbolinks を無効化しておきます。
この内容も、本記事の趣旨とは離れるので折りたたんでおきます。
詳細を表示
① gemを削除する
Gemfile から turbolinks
の gem を探して、削除(コメントアウト)します。
手元の環境では、gem 'turbolinks', '~> 5'
の部分をコメントアウトしています(以下)。
# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
# gem 'turbolinks', '~> 5' # ここを削除(コメントアウト)
コメントアウトしたら、ターミナルで、 bundle install
(または、bundle update
)で gem を更新して turbolinks を削除します。
$ bundle install
② JavaScript の設定ファイルから関係部分を削除する
app/javascript/packs フォルダにあるapplication.js
ファイルから、Turbolinks を読み込んでいる部分を削除(コメントアウト)します。以下の2行分の削除が必要です。
import Rails from "@rails/ujs"
// import Turbolinks from "turbolinks" // ここを削除(コメントアウト)
import * as ActiveStorage from "@rails/activestorage"
import "channels"
import "main.js"
Rails.start()
// Turbolinks.start() // ここを削除(コメントアウト)
ActiveStorage.start()
③ View の設定ファイルから関係部分を削除する
app/views/layouts フォルダにある application.html.erb
ファイルから、Turbolinks を読み込んでいる部分を削除(コメントアウト)します。
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
の1行です。
<!DOCTYPE html>
<html>
<head>
<title>AjaxSample</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%# <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
</head>
<body>
<%= yield %>
</body>
</html>
④ 個別の View ファイルでの記載
あとは、個別の View ファイルの form_with
タグのオプションで、local: true
と記載すれば OK です。
<%= form_with(model: @sample, local: true) do |form| %>
<%# 省略 %>
<% end %>
3-2. 関連ファイルの記載
動作確認に使用する、ビュー(HTML)、コントローラーのサンプルコードも載せておきます。
本記事の趣旨とは離れるので、これも折りたたんでおきます。
詳細を表示
① View ファイル
<h2>Sample</h2>
<%= form_with(model: @sample, id: "sample_input", local: true) do |form| %>
<%= form.text_field :memo, class: "sample_form-text" %>
<%= form.submit "登録", class: "sample_form-btn" %>
<% end %>
<div class="samples">
<% @samples.each do |sample| %>
<div>
<span><%= sample.memo %></span>
</div>
<% end %>
</div>
② Controller ファイル
class SamplesController < ApplicationController
# トップページ
def index
@samples = Sample.all # Sampleテーブルのデータ全てを取得したインスタンス
@sample = Sample.new # Sampleテーブルの新しいレコード格納用インスタンス
end
# データの登録
def create
@sample = Sample.new(sample_params)
if @sample.save
respond_to do |format|
format.html { redirect_to root_path } # 通常のリクエストの場合
format.json { render json: { memo: @sample.memo, id: @sample.id } } # Ajaxによるリクエストの場合
end
end
end
# パラメータの受取り
private
def sample_params
params.require(:sample).permit(:memo)
end
end
③ ルーティング
Rails.application.routes.draw do
root to: 'samples#index' # ルートパスに"/samples"を指定
resources :samples # samples関連のルーティングを設定
end
3-3. 実装する画面
サンプルとして実装するのは、下図のように文字列を入力して送信すると、テキストボックスの下に文字列が表示されるだけの簡単な内容です。
送信ボタンを押すと、ページがリロードせずに文字列が表示されます。
3-4. POSTメソッドのサンプルコード(then - catch - finally の形式)
CSRF 対策がされている場合は、リクエスト時に CSRF トークンを送信しないと、認証エラーが生じてしまいます。
下記は、リクエストヘッダから CSRF トークンを送信する例です。
window.addEventListener('load', function() {
let token = document.getElementsByName('csrf-token')[0].content; //HTMLのヘッダからセキュリティトークンの取得
let url = document.getElementById('sample_input').getAttribute('action'); // formのaction属性から必要なURLを取得
document.getElementById('sample_input').addEventListener('submit', function(e) {
e.preventDefault(); // デフォルトのイベントを無効にする
let inputText = document.getElementsByClassName('sample_form-text')[0].value; // 入力値を取得
let data = { sample: {memo: inputText} }; // オブジェクト形式で送信データを格納
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json', // リクエストデータがJSON形式と指定
'Accept': 'application/json', // レスポンスデータをJSON形式と指定
'X-CSRF-Token': token // CSRFトークンをヘッダに追加
},
body: JSON.stringify(data)
};
fetch(url, options)
.then(function(response) {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(function(data) {
let divElm = document.createElement('div'); // divタグを生成
divElm.appendChild(document.createTextNode(data.memo)); // divタグにテキストノードを追加
document.getElementsByClassName('samples')[0].appendChild(divElm); // 作成したHTMLをドキュメントに追加
document.getElementsByClassName('sample_form-text')[0].value = ''; // テキストエリアを空白に戻す
})
.catch(function(error) {
alert(error);
})
.finally(function() {
document.getElementsByClassName('sample_form-btn')[0].disabled = false; // submitボタンのdisableを解除
document.getElementsByClassName('sample_form-btn')[0].removeAttribute('data-disable-with'); // submitボタンのdisableを解除
})
}, false);
});
3-5. async / await を使用したPOSTメソッドのサンプルコード
async / await で書くと次のようになります。
window.addEventListener('load', function() {
let token = document.getElementsByName('csrf-token')[0].content; //HTMLのヘッダからセキュリティトークンの取得
let url = document.getElementById('sample_input').getAttribute('action'); // formのaction属性から必要なURLを取得
document.getElementById('sample_input').addEventListener('submit', function(e) {
e.preventDefault(); // デフォルトのイベントを無効にする
let inputText = document.getElementsByClassName('sample_form-text')[0].value; // 入力値を取得
let sampleData = { sample: {memo: inputText} }; // オブジェクト形式で送信データを格納
(async () => {
try {
const options = {
method: 'POST', // HTTPメソッドを指定
headers: {
'Content-Type': 'application/json', // リクエストデータがJSON形式と指定
'Accept': 'application/json', // レスポンスデータをJSON形式と指定
'X-CSRF-Token': token // CSRFトークンをヘッダに追加
},
body: JSON.stringify(sampleData)
};
const response = await fetch(url, options); // サーバにリクエストを送信
if (!response.ok) {
throw new Error('Network response was not ok'); // fetchが成功したかどうかの判定
}
let data = await response.json();
let divElm = document.createElement('div'); // divタグを生成
divElm.appendChild(document.createTextNode(data.memo)); // divタグにテキストノードを追加
document.getElementsByClassName('samples')[0].appendChild(divElm); // 作成したHTMLをドキュメントに追加
document.getElementsByClassName('sample_form-text')[0].value = ''; // テキストエリアを空白に戻す
} catch (e) {
alert(e);
} finally {
document.getElementsByClassName('sample_form-btn')[0].disabled = false; // submitボタンのdisableを解除
document.getElementsByClassName('sample_form-btn')[0].removeAttribute('data-disable-with'); // submitボタンのdisableを解除
}
})()
}, false);
});
3-6. FormData を使用したPOSTメソッドのサンプルコード
Rails の場合、FormData オブジェクトを使用すると、CSRF トークンをリクエストボディに含めて送信できるので、記載がシンプルになります。
CSRF トークンをリクエストボディから送れるか否かはフレームワークによって異なると思われます。
window.addEventListener('load', function() {
let url = document.getElementById('sample_input').getAttribute('action'); // formのaction属性から必要なURLを取得
document.getElementById('sample_input').addEventListener('submit', function(e) {
e.preventDefault(); // デフォルトのイベントを無効にする
const form = new FormData(document.getElementById('sample_input')); // FormDataを取得
(async () => {
try {
const options = {
method: 'POST', // HTTPメソッドを指定
headers: {
'Accept': 'application/json' // レスポンスデータをJSON形式と指定
},
body: form
};
const response = await fetch(url, options); // サーバにリクエストを送信
if (!response.ok) {
throw new Error('Network response was not ok'); // fetchが成功したかどうかの判定
}
let data = await response.json();
let divElm = document.createElement('div'); // divタグを生成
divElm.appendChild(document.createTextNode(data.memo)); // divタグにテキストノードを追加
document.getElementsByClassName('samples')[0].appendChild(divElm); // 作成したHTMLをドキュメントに追加
document.getElementsByClassName('sample_form-text')[0].value = ''; // テキストエリアを空白に戻す
} catch (e) {
alert(e);
} finally {
document.getElementsByClassName('sample_form-btn')[0].disabled = false; // submitボタンのdisableを解除
document.getElementsByClassName('sample_form-btn')[0].removeAttribute('data-disable-with'); // submitボタンのdisableを解除
}
})()
}, false);
});
さいごに
Fetch メソッドは、まだ、あまり試していませんが、XMLHttpRequest オブジェクトと比べると随分と書きやすい印象です。
本記事は学習の記録に過ぎませんが、何らかの参考となれば幸いです。