前回のLINE Bot 作成に引き続き、AWS 関連です。Webアプリ側に寄せています。
ky-yk-d.hatenablog.com
前回やったこと
今回やること
LINEボットを作成して、 API Gateway からLambda関数を呼び出す流れはわかりました。
前回は、API Gateway の呼び出しはLINE側で用意されていましたが、今回はWebページからJavaScript で呼び出してみます。また、
フレームワーク を使ったWebアプリを作成した(編集した)経験はあるのですが、クライアントサイドJavaScript を書いた経験はほぼゼロです。自分でWebアプリを作れるようになるには、クライアントサイドJSは避けて通れませんので、今回はその初歩をやってみました。
図示するほどのものではないのですが、憧れがあったので構成図を。素材は以下から。
ちなみに、PowerPoint でペタペタ貼り付けてスクショ撮ったんですけど、色々間違っている気がします。
アーキテクチャ 構成図(書いてみたかった)
API Gateway 〜DynamoDBの設定は下記の書籍(198頁以下)を参照しました。
テーブルを作成します。以上!
・・・
というわけには流石にいきませんが、リレーショナルデータベースと異なりスキーマ を設定する必要がないので、確実に設定しなければならないのはテーブル名とプライマリーキーくらいです。
テーブル名はSAMPLE_TASKS
、プライマリーキーはID
としました。普段(リレーショナルDBでの)テーブル名やカラム名 は大文字表記の場合が多いので、書籍の指示を無視して大文字にしたのですが、画面との連携を考えると小文字の方が良かったと思います(そして見返して思いましたがtaskname
は小文字にしてますね・・・)
DynamoDB(データを入れた後の状態)
AWS Lambda の設定
Lambdaで設定する内容は以下の2点です。
DynamoDBFullAccessを付与したロールを割り当てる
関数を記載する
今回は、LambdaからDynamoDBにアクセスするので、DynamoDBに対する読み書きの権限を有するロールをLambda関数に割り当てる必要があります。そこで、Lambda関数の作成の前にIAMロールを作成します。ロール作成画面で、「信頼されたエンティティの種類」でLambdaを、次の画面の「アクセス権限ポリシー」でDynamoDBFullAccessを選択しましょう。
上記の手順でIAMロールを作成できたら、Lambda関数の作成です。「一から作成」を選択し、先程のロールを割り当てて作成します。今回は、ランタイムにはNode.js 8.10を選択しました。
Lambda関数の中身は今回はこのファイルひとつです。GETとPUTの両方をこの関数で受け付けます。
index.js
const AWS = require('aws-sdk' );
const dynamo = new AWS.DynamoDB.DocumentClient();
exports.handler = (event , context, callback) => {
const operation = event .method;
let params;
switch (operation) {
case 'GET' :
params = {
TableName: "SAMPLE_TASKS" ,
} ;
dynamo.scan(params, callback);
break ;
case 'PUT' :
params = {
TableName: "SAMPLE_TASKS" ,
Item: {
"ID" : event .body.Item.id,
"taskname" : event .body.Item.taskname
}
} ;
dynamo.put(params, callback);
break ;
default :
callback('Unknown operation: ${operation}' );
}
} ;
API Gateway では以下の手順で作業を行います。
API の作成(sample_crud_tasks
)
リソースの作成(sample_tasks
)
GETメソッドとPUTメソッドを作成し、Lambda関数と紐づける
GETメソッドとPUTメソッドの双方の「統合リクエス ト」に本文マッピング テンプレートを指定
ステージにデプロイ
作成したAPI (GETメソッド) ※OPTIONはのちの過程で自動的に作成される
本文マッピング テンプレートを設定するところについては説明が必要と思われます。「統合リクエス ト」をクリックして開く画面の最下部に「本文マッピング テンプレート」という項目があるので、Content-Typeの欄にはapplication/json
と記載して追加します。上記の2番目のような画面になります。
本文マッピング ・テンプレート(GET, PUT共通)
{
"method ": "$context.httpMethod ",
"body ": $input .json ('$' )
}
送られてきたHTTPリクエス トの内容を、受け手側のデータにマッピング して渡すもののようです。
$context.httpMethod
は一見してわかるように使用されたHTTPメソッド名で、画面側のJavaScript のxhr.open()
の第一引数で指定しているものです。Lambda側ではevent.method
として参照されています。どのメソッドを使用したかによってLambda側で処理を変えるためのものですね。
$input.json('$')
は入力されたxhr.send()の引数で渡した内容(=リクエス ト本文)をJSON 形式で読み取っています。こちらも、Lambda側でevent.body
として参照され、DynamoDBに登録するデータの取り出しに使われています。
API Gateway のマッピング テンプレートのリファレンスはこちら
API Gateway のマッピングテンプレートリファレンス - Amazon API Gateway
ここまでで、DynamoDBのためのAPI は完成です。
さて、ここまでは専らAWS のお話(『実践AWS Lambda』 に記載の内容)でした。ここからは作成したAPI をWebページから呼び出します。呼び出しただけではしょうがないので、以下の2つを実装します。
簡単な入力フォームを用いたデータの追加(PUTなので同じIDなら更新になる)
DynamoDBのデータのリスト表示
使用する技術としては、以下のようなものです。
僕は「DOMってなに?モビルスーツ ?」というレベルなので、極めて初歩的な内容となっています。また、jQuery などを用いればAjax はもっと簡潔に実装できるのだと思うのですが、色々なところから「jQuery はもう・・・」という声が聞こえてきています(例:You-Dont-Need-jQuery )。研修のときにjQuery がモダンなのだという話を聴いた気がしたのですが。
今後の開発のなかでjQuery を使いたくなるようなことがあれば学ぶことはやぶさ かではないのですが、今回はJavaScript の勉強をするという観点からも、ピュアJSで記述しました。また、HTMLファイルとJSファイルを別に作成すべきところなのでしょうが、今回はHTMLファイルに<script>
タグで埋め込んでしまいました。どこかのタイミングで切り離します。ソースコード に重複も多いですし、リファクタをちゃんと考えてみたいです。
こんな感じになる(大きさバラバラ・・・)
なお、今回のコードを実装するにあたっては、下記を参照しました。
tomosoft.jp
それでは実際のコードです。
index.html
< html >
< head >
< meta charset = "utf-8" />
< meta http-equiv = "X-UA-Compatible" content = "IE=edge" >
< title > Page Title</ title >
< meta name = "viewport" content = "width=device-width, initial-scale=1" >
</ head >
< body >
< form >
< input id = "ID" type = "text" placeholder= "IDを入力" >
< input id = "taskname" type = "text" placeholder= "タスク名を入力" >
< input id = "submit" type = "button" value = "送信" >
</ form >
< hr />
< div id = "status" ></ div >
< form >
< input id = "btn" type = "button" value = "取得" >
</ form >
< hr />
< div id = "result" ></ div >
< script >
document .addEventListener ('DOMContentLoaded' , function (){
document .getElementById ('submit' ).addEventListener ('click' , function (){
var status = document .getElementById ('status' );
var xhr = new XMLHttpRequest ();
xhr.onreadystatechange = function (){
if (xhr.readyState === 4){
if (xhr. status === 200) {
status .textContent = '送信しました' ;
} else {
status .textContent = 'サーバーエラーが発生しました' ;
}
} else {
status .textContent = '送信中' ;
}
} ;
var obj = {
Item : {
id: document .getElementById ('ID' ).value,
taskname: document .getElementById ('taskname' ).value
}
} ;
var json = JSON.stringify (obj );
xhr.open ('PUT' , 'https://l3uk6hufcf.execute-api.ap-northeast-1.amazonaws.com/prod/sample-tasks' , true );
xhr.setRequestHeader ('Content-Type' , 'application/json' );
console.log (json );
xhr.send (json );
} )
} )
</ script >
< script >
document .addEventListener ('DOMContentLoaded' , function (){
document .getElementById ('btn' ).addEventListener ('click' , function (){
var result = document .getElementById ('result' );
var xhr = new XMLHttpRequest ();
xhr.onreadystatechange = function (){
if (xhr.readyState === 4){
if (xhr. status === 200) {
var data = xhr.response;
if (data === null ) {
result.textContent = 'データが存在しません' ;
} else {
console.log (typeof (data ));
console.log (data )
var items = data.Items;
var ul = document .createElement ('ul' );
for (var i = 0; i < items.length; i++ ){
var li = document .createElement ('li' );
var text = document .createTextNode (items [ i ] .ID + ' ' + items [ i ] .taskname );
li.appendChild (text );
ul.appendChild (li );
}
result.replaceChild (ul, result.firstChild );
}
} else {
result.textContent = 'サーバエラーが発生' ;
}
} else {
result.textContent = '通信中'
}
} ;
xhr.responseType = 'json' ;
xhr.open ('GET' , 'https://l3uk6hufcf.execute-api.ap-northeast-1.amazonaws.com/prod/sample-tasks' , true );
xhr.send (null );
} , false );
} , false );
</ script >
</ body >
</ html >
HTMLの部分はまぁいいとして、JSの部分を丁寧にみてみようと思います。
送信ボタンの処理
document .addEventListener('DOMContentLoaded' , function (){
/ * 中略 */
} )
ページロード時に実行されるイベントリスナーを登録しています。/* 中略 */
の部分に記載した内容が初期化処理になります。次は中身をみてみます。
document .getElementById('submit' ).addEventListener('click' ,function (){
} )
submit
ボタンにイベントリスナーを登録しています。さきほどDOMContentLoaded
を渡していた第一引数は、今回はclick
となっています。イベントリスナーの種類を指定しているのですね。クリックしたときに、第二引数で渡している関数オブジェクトが実行されるわけです。さらに括弧の中をみていきます。
var status = document .getElementById('status' );
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function (){
if (xhr.readyState === 4){
if (xhr.status === 200) {
status .textContent = '送信しました' ;
} else {
status .textContent = 'サーバーエラーが発生しました' ;
}
} else {
status .textContent = '送信中' ;
}
} ;
var obj = {
Item : {
id: document .getElementById('ID' ).value,
taskname: document .getElementById('taskname' ).value
}
} ;
var json = JSON.stringify(obj);
xhr.open('PUT' , 'https://l3uk6hufcf.execute-api.ap-northeast-1.amazonaws.com/prod/sample-tasks' , true );
xhr.setRequestHeader('Content-Type' , 'application/json' );
console.log(json);
xhr.send(json);
非同期通信を管理するXMLHttpRequest が登場しています。onreadystatechange
プロパティには、通信の状態が変化したときに呼び出されるイベントハンドラ を格納します。この例の場合、HTTP通信の状態(readyState
)とHTTPステータスコード (status
)によって通信の状態を判断し、status.textContent
にメッセージを代入することで通信状態を画面に表示しています。
後半では、PUTリクエス トでJSON を送付する処理を記述しています。まず、送信したいJSON データの元となるJavaScript のオブジェクトをobj
に代入し、それをJSON.stringify()
で文字列形式に変換しています。次に、XMLHttpRequest オブジェクトのopen()
メソッドで、HTTPリクエス トの初期化を行います。第一引数がHTTPメソッド、第二引数がアクセス先URLです。第三引数以降はオプションで、ここでは第三引数にtrueを渡すことで非同期通信であることを明示しています(第三引数はデフォルトがtrueなので書いても書かなくても変わらないはずですが)。
重要なのは、xhr.setRequestHeader('Content-Type', 'application/json');
の部分です。リクエス トヘッダーを指定しています。API Gateway で本文マッピング テンプレートを設定した際、「Content-Typeの欄にはapplication/json
と記載して〜〜」と書いたことに対応しています。この処理の記述を省いてしまうと、リクエス トは拒否されます(ステータスコード 415)。この点については下記の記事が参考になるかと思います。
dev.classmethod.jp
リクエス トヘッダーを指定したら、最後にJSON 文字列をリクエス トの本文としてsend()
メソッドに渡して送信します。ここまでの処理が、すべて`「送信」ボタンをクリックした際に実行されます。
取得ボタンの処理
リクエス ト送信時のJSON 関係の処理がないこと以外、基本は送信ボタンの処理と同じです。ただ、こちらでは取得したデータをHTML側に埋め込むための処理が加わっています。
var data = xhr.response;
if (data === null ) {
result.textContent = 'データが存在しません' ;
} else {
console.log(typeof (data));
console.log(data)
var items = data.Items;
var ul = document .createElement('ul' );
for (var i = 0; i < items.length; i++){
var li = document .createElement('li' );
var text = document .createTextNode(items[ i] .ID + ' ' + items[ i] .taskname);
li.appendChild(text);
ul.appendChild(li);
}
result.replaceChild(ul, result.firstChild);
API Gateway (の背後にいるLambda)からはオブジェクト形式でデータが送られてきます。response
プロパティがレスポンスの本体です。それは下記のような形のデータです(ブラウザでAPI のエンドポイントに直接アクセスすると見られます)。
{
"Items ":[
{ "taskname ":"test2 ","ID ":"secondData "} ,
{ "taskname ":"test ","ID ":"firstData "} ,
{ "taskname ":"変更後!!! ","ID ":"aaa "}
] ,
"Count ":3 ,
"ScannedCount ":3
}
(参考)AWS-SDKのscanの仕様(英語)
取得されたデータの個数(Count
)やスキャン対象となったデータの個数(ScannedCount
※今回はフィルター適用していないのでCount
と等しい)も一緒に送られてきていることがわかります。今回欲しいのはそれぞれのデータなので、Items
を順に扱えば足ります。
ローカル変数items
にItems
の内容を代入したあとは、forループを使いながらid="result"
の箇所に挿入する要素を作成しています。古典的なfor文で記載していますが、たぶんもっと近代的な書き方があるのだと思います。
仕上げ(CORSの有効化)
li
、ul
要素を順に作成したあと、最後にresult.replaceChild()
で置き換えて、この無事表示ができました。めでたしめでたし。
と思いきや、この状態でindex.htmlにアクセスして、送信ボタンや取得ボタンを押すとエラーになります。クロスオリジン通信を可能にする必要があります。ブラウザのコンソールに、Access-Control-Allow-Origin
がなんちゃらかんちゃらと出るはずなので、これをコピーしてAPI Gateway のAPI のアクションで「CORSの有効化」を選択し、Access-Control-Allow-Headers
のところに上記のAccess-Control-Allow-Origin
を加えます。この設定をすることで、ようやくAPI をWebページから叩くことができます。
API Gateway リソースの CORS を有効にする - Amazon API Gateway
※XDR(Cross-Domain Request)は、元来セキュリティ上の必要性から制限されたもので、対応手段はCORS(Cross-Origin Resource Sharing)以外にも存在するようです。どの手段が適切であるかは、実践レベルでは要検討でしょう。
dev.classmethod.jp
JavaScript のクロス ドメイン (Cross-Domain) 問題の回避と諸注意 tsmatz.wordpress.com
終わりに
はい。短くまとめるはずだったのに、今回も長くなってしまいました。個々の技術要素については入門的な記事や書籍がいくらでも存在しているので、あえて記事にするとすれば「実現したことベース」にして繋ぎ方を意識したものがいいかなと思ってるのですが、それゆえにこそ色々な要素を詰め込んでしまい、長くなってしまう傾向がありますね。これだけ長くなるとどこに何書いたかも忘れますし、構成や文章もグダグダになりがちです。なんとかしたいですね。技術的には、次はVue.jsとDynamoDBでも繋げてみようかと思っています。乞うご期待。
ソースコード についてはツッコミどころ満載であることは自覚しており、発展させながら直していこうと思っていますが、幾分初心者であるがために気づかないミスや改善点などあると思いますので、pull request・Issue等なげていただけるととても嬉しいです。
github.com