#概要
問い合わせフォーム(javascript) ⇒ APIGateway ⇒ Lambda(Node.js) ⇒ Lambda(Node.js) の流れで簡単な問い合わせサイトを作ります。
一応レスポンシブにします。
バリデーションもあります。
#index.htmlのコーディング
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<link rel="stylesheet" href="style.css" />
<title>問い合わせ</title>
</head>
<body>
<div class="header">
<a href="#" class="logo">Hoge Hoge Company</a>
<a class="active" href="#">Home</a>
</div>
<main class="main-container">
<h2>問い合わせフォーム</h2>
<div class="container">
<form id="form">
<div class="row">
<div class="col-20">
<label for="name">名前</label>
</div>
<div class="col-80">
<input type="text" id="name" name="name" placeholder="Your name.." />
</div>
</div>
<div id="name-alert" class="alert-message" hidden>入力値が不正です。</div>
<div class="row">
<div class="col-20">
<label for="email">E-Mail</label>
</div>
<div class="col-80">
<input type="email" id="email" name="email" placeholder="Your E-Mail.." />
</div>
</div>
<div id="email-alert" class="alert-message" hidden>入力値が不正です。</div>
<div class="row">
<div class="col-20">
<label for="dept">所属</label>
</div>
<div class="col-80">
<input type="text" id="dept" name="dept" placeholder="Ex: Example Co., Ltd." />
</div>
</div>
<div id="dept-alert" class="alert-message" hidden>入力値が不正です。</div>
<div class="row">
<div class="col-20">
<label for="body">内容</label>
</div>
<div class="col-80">
<textarea id="body" name="body" placeholder="お気軽にお問い合わせください。" style="height:200px"></textarea>
</div>
</div>
<div id="body-alert" class="alert-message" hidden>入力値が不正です。</div>
<div class="row">
<button id="submitBtn">送信</button>
</div>
</form>
</div>
<!-- The Modal -->
<div id="myModal" class="modal">
<!-- Modal content -->
<div id="modal-content">
<span class="close">×</span>
<p id="result"></p>
<p id="detail" style="font-size: small;"></p>
</div>
</div>
</main>
<script src="main.js" defer></script>
</body>
</html>
#CSSのコーディング
* {
margin: 0;
padding: 0;
font-family: "Hiragino Kaku Gothic Pro", "ヒラギノ角ゴ Pro W3", "メイリオ",
Meiryo, "MS Pゴシック", sans-serif;
}
/* Style the header with a grey background and some padding */
.header {
display: flex;
flex-flow: row wrap;
align-items: center;
justify-content: space-between;
background-color: #000000;
padding: 20px 10px;
}
/* Style the header links */
.header a {
color: #f2f2f2;
text-align: center;
padding: 12px;
text-decoration: none;
font-size: 18px;
line-height: 25px;
border-radius: 4px;
}
/* Style the logo link (notice that we set the same value of line-height and font-size to prevent the header to increase when the font gets bigger */
.header a.logo {
font-size: 25px;
font-weight: bold;
}
/* Change the background color on mouse-over */
.header a:hover {
background-color: #ddd;
color: black;
}
/* Style the active/current link*/
.header a.active {
background-color: dodgerblue;
color: white;
}
/* Add media queries for responsiveness - when the screen is 500px wide or less, stack the links on top of each other */
@media screen and (max-width: 500px) {
.header {
justify-content: center;
}
.header a {
padding: 12px;
}
}
.main-container {
width: 90%;
margin: auto;
}
.main-container h2 {
margin-top: 12px;
}
/* Style inputs, select elements and textareas */
input,
textarea {
width: 100%;
padding: 12px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
resize: vertical;
}
.alert-red {
border: #ff4500 2px solid;
}
.alert-green {
border: #00ff7f 2px solid;
}
.alert-message {
color: #ff4500;
font-size: small;
text-align: end;
margin-bottom: 10px;
}
/* Style the label to display next to the inputs */
label {
padding: 12px 12px 12px 0;
display: inline-block;
}
/* Style the submit button */
#submitBtn {
background-color: #4caf50;
color: white;
padding: 10px 18px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 15px;
float: right;
}
/* Style the container */
.container {
border-radius: 5px;
background-color: #f2f2f2;
padding: 20px;
}
/* Floating column for labels: 25% width */
.col-20 {
float: left;
width: 20%;
margin-top: 6px;
}
/* Floating column for inputs: 75% width */
.col-80 {
float: left;
width: 80%;
margin-top: 6px;
}
/* Clear floats after the columns */
.row:after {
content: "";
display: table;
clear: both;
}
/* Responsive layout - when the screen is less than 600px wide, make the two columns stack on top of each other instead of next to each other */
@media screen and (max-width: 600px) {
.col-20,
.col-80,
#submitBtn {
width: 100%;
margin-top: 0;
}
}
/* The Modal (background) */
.modal {
display: none; /* Hidden by default */
position: fixed; /* Stay in place */
z-index: 1; /* Sit on top */
left: 0;
top: 0;
width: 100%; /* Full width */
height: 100%; /* Full height */
overflow: auto; /* Enable scroll if needed */
background-color: rgb(0, 0, 0); /* Fallback color */
background-color: rgba(0, 0, 0, 0.4); /* Black w/ opacity */
}
/* Modal Content/Box */
#modal-content {
background-color: #fefefe;
margin: 15% auto; /* 15% from the top and centered */
padding: 20px;
width: 80%; /* Could be more or less, depending on screen size */
}
.request-loading {
border: 1px solid #888;
}
.request-success {
border: 1px solid #00ff7f;
}
.request-fail {
border: 1px solid #ff4500;
}
/* The Close Button */
.close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
}
.close:hover,
.close:focus {
color: black;
text-decoration: none;
cursor: pointer;
}
#javascriptのコーディング
変数URLはAPIGatewayで発行してから実装します。
const URL = 'https://'; // APIGatewayで作成したURL
const LOADING = '処理中...';
const RESULT_OK = 'リクエストを受け付けました。';
const RESULT_OK_DETAIL =
"正常に処理が完了すると 'support@hogehoge.com' からメールが配信されます。";
const RESULT_NG = 'リクエストの受付に失敗しました';
const RESULT_NG_DETAIL =
'大変申し訳ありません。担当者へ直接お問い合わせください。';
const DOMAIN = '@sample.com';
document.getElementById('email').value = DOMAIN;
(() => {
const modal = document.getElementById('myModal');
const modalContent = document.getElementById('modal-content');
const sendBtn = document.getElementById('submitBtn');
const span = document.getElementsByClassName('close')[0];
const result = document.getElementById('result');
const form = document.getElementById('form');
const nameAlert = document.getElementById('name-alert');
const emailAlert = document.getElementById('email-alert');
const deptAlert = document.getElementById('dept-alert');
const bodyAlert = document.getElementById('body-alert');
const resultDetail = document.getElementById('detail');
const inputValueClear = () => {
form.name.value = '';
form.name.setAttribute('class', '');
form.email.value = DOMAIN;
form.email.setAttribute('class', '');
form.dept.value = '';
form.dept.setAttribute('class', '');
form.body.value = '';
form.body.setAttribute('class', '');
result.innerHTML = '';
};
// When the user clicks on <span> (x), close the modal
span.onclick = () => {
if (result.innerHTML !== LOADING) {
modal.style.display = 'none';
if (result.innerHTML === RESULT_OK) inputValueClear();
}
};
// When the user clicks anywhere outside of the modal, close it
window.onclick = event => {
if (event.target === modal) {
if (result.innerHTML !== LOADING) {
modal.style.display = 'none';
if (result.innerHTML === RESULT_OK) inputValueClear();
}
}
};
sendBtn.addEventListener('click', async event => {
event.preventDefault();
const name = form.name.value.trim();
form.name.value = name;
const email = form.email.value.trim();
form.email.value = email;
const dept = form.dept.value.trim();
form.dept.value = dept;
const body = form.body.value.trim();
form.body.value = body;
// validation check
if (validation(name, email, dept, body)) return;
result.innerHTML = LOADING;
modalContent.setAttribute('class', 'request-loading');
modal.style.display = 'block';
const jsonData = JSON.stringify({ name, email, dept, body });
try {
const res = await fetch(URL, {
method: 'POST',
mode: 'cors',
cache: 'no-cache',
body: jsonData,
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
});
console.log('Response!!', res.status);
modalContent.setAttribute('class', 'request-success');
result.innerHTML = RESULT_OK;
resultDetail.innerHTML = RESULT_OK_DETAIL;
} catch (error) {
console.log(error);
modalContent.setAttribute('class', 'request-fail');
result.innerHTML = RESULT_NG;
resultDetail.innerHTML = RESULT_NG_DETAIL;
}
});
const validation = (name, email, dept, body) => {
let validationResult = false;
if (!name.length) {
form.name.setAttribute('class', 'alert-red');
nameAlert.hidden = false;
validationResult = true;
} else {
form.name.setAttribute('class', 'alert-green');
nameAlert.hidden = true;
}
if (!email.length || !/^[^@]+@sample.com$/.test(email)) {
form.email.setAttribute('class', 'alert-red');
emailAlert.hidden = false;
validationResult = true;
} else {
form.email.setAttribute('class', 'alert-green');
emailAlert.hidden = true;
}
if (!dept.length) {
form.dept.setAttribute('class', 'alert-red');
deptAlert.hidden = false;
validationResult = true;
} else {
form.dept.setAttribute('class', 'alert-green');
deptAlert.hidden = true;
}
if (!body.length) {
form.body.setAttribute('class', 'alert-red');
bodyAlert.hidden = false;
validationResult = true;
} else {
form.body.setAttribute('class', 'alert-green');
bodyAlert.hidden = true;
}
return validationResult;
};
form.name.addEventListener('input', event => {
const name = event.target.value.trim();
if (!name) {
event.target.setAttribute('class', 'alert-red');
nameAlert.hidden = false;
} else {
event.target.setAttribute('class', 'alert-green');
nameAlert.hidden = true;
}
});
form.email.addEventListener('input', event => {
const email = event.target.value.trim();
event.target.value = email;
if (!email || !/^[^@]+@sample.com$/.test(email)) {
event.target.setAttribute('class', 'alert-red');
emailAlert.hidden = false;
} else {
event.target.setAttribute('class', 'alert-green');
emailAlert.hidden = true;
}
});
form.dept.addEventListener('input', event => {
const dept = event.target.value.trim();
event.target.value = dept;
if (!dept) {
event.target.setAttribute('class', 'alert-red');
deptAlert.hidden = false;
} else {
event.target.setAttribute('class', 'alert-green');
deptAlert.hidden = true;
}
});
form.body.addEventListener('input', event => {
const body = event.target.value.trim();
if (!body) {
event.target.setAttribute('class', 'alert-red');
bodyAlert.hidden = false;
} else {
event.target.setAttribute('class', 'alert-green');
bodyAlert.hidden = true;
}
});
})();
#AWS SES(Simple Email Service)を設定
※ドメインはRoute53で取得済みの前提
※併せてACM(AWS Certificate Manager)で証明書を取得済み
※SESは送信のみの設定です。
- AWSへログイン
- SESのコンソールへ移動
- リージョンはバージニア北部を選択
- 左のナビゲーションから
Domains
を選択
- をクリック
- ↓のように入力。自分のドメインとDKIM設定をチェックして、をクリック
- 次のモーダルではCNAMEやTXTが表示され、登録しろと指示が出る。Route53を利用している場合は、このモーダル上でDNSの登録がすべて完了する。
- こんな感じになればOK!(※現時点ではサンドボックス上での制限された利用が可能)
- 左のナビゲーションで
Email Addresses
を選択
-
Veryfy a New Email Address
でサンドボックス上で利用できるメールアドレスを登録する。 - 登録したメールアドレスを
Send a Test Email
で登録したドメインからメールが送られるかテストして正常な動作を確認
#最後に起動するLambdaを作成
- Lambdaのコンソールページへ移動
- をクリック
- を選択
- 適当な関数名を入力
- ランタイムは
Node.js 12.x
を選択 - そのほかはデフォルトのままでOK
- をクリック
- 環境変数へ配信元のアドレスを設定
- 実行ロールにはSESの権限を許可
- 関数コードへ↓のコードを実装
'use strict'
const SES = require("aws-sdk/clients/ses");
const ses = new SES({ region: "us-east-1" }); // 米国東部(バージニア北部)
const FROM = process.env.FROM; // 環境変数から取得
exports.handler = async (event) => {
console.log(event);
const TO = [event.email];
const params = {
Destination: {
ToAddresses: TO
},
Message: {
Body: {
Text: {
Data: [
event.dept + ' ' + event.name + ' 様',
' ',
'Hoge Hoge Companyです。',
'お問い合わせしていただきありがとうございます。',
'下記の内容で承りました。',
' ',
'[お問い合わせ内容]' + "\n" + event.body,
].join("\n"),
Charset: "utf-8"
}
},
Subject: {
Data: '受付完了:Webからの問い合わせ',
Charset: "utf-8"
}
},
// From
Source: FROM
};
const result = {statusCode: 200};
try {
const response = await ses.sendEmail(params).promise();
console.log('Response: ', response);
result.body = 'OK!!!!!';
} catch (error) {
console.log('Error: ', error);
result.statusCode = error.code;
result.body = error.message;
}
return result;
};
※process.env
では環境変数で設定したキーと値が利用可能(暗号化も可能)
#最後に起動するLambdaのテスト
- テストイベントの選択をクリック
- ↓のようにテストを作成(※emailキーにはSESで登録したアドレスを記載すること)
- をクリックしてテスト実行
- 成功となればOK。失敗ならログを確認。
- テストでemailキーに指定したアドレスにメールが配信されていればOK
#最初に起動するLambdaを作成
- Lambdaのコンソールページへ移動
- をクリック
- を選択
- 適当な関数名を入力
- ランタイムは
Node.js 12.x
を選択 - そのほかはデフォルトのままでOK
- をクリック
- 環境変数へ配信元のアドレスと受付先のアドレスを入力
- 実行ロールはSESとLambdaの権限を追加
- 関数コードへ↓のコードを実装
'use strict'
const SES = require("aws-sdk/clients/ses");
const ses = new SES({ region: "us-east-1" }); // 米国東部(バージニア北部)
const TO = [process.env.TO]; // 環境変数からの値を取得
const FROM = process.env.FROM;
// 次のLambdaを起動するための設定
const Lambda = require("aws-sdk/clients/lambda");
const lambda = new Lambda({ region: "ap-northeast-1" });
exports.handler = async (event) => {
const name = event.form.name;
const email = event.form.email;
const dept = event.form.dept;
const body = event.form.body;
const sesParams = {
Destination: {
ToAddresses: TO
},
Message: {
Body: {
Text: {
Data: [
'[名前] : ' + name,
'[メールアドレス] : ' + email,
'[所属部署] : ' + dept,
'[お問い合わせ] : ' + "\n" + body,
].join("\n"),
Charset: "utf-8"
}
},
Subject: {
Data: 'Webからの問い合わせ',
Charset: "utf-8"
}
},
// From
Source: FROM
};
// 次のLambdaに送るデータ
const payload = { name, email, dept, body };
console.log('payload: ', payload);
const lambdaParams = {
FunctionName: "inpuirySendMailForm_2nd", // 最後に起動するLambdaの名称
InvocationType: "Event",
Payload: JSON.stringify(payload)
};
const result = {statusCode: 200};
try {
const response = await ses.sendEmail(sesParams).promise();
console.log('Response: ', response);
const callLambda = await lambda.invoke(lambdaParams).promise();
console.log("Lambda Response: ", callLambda);
result.body = 'OK!!!!!';
} catch (error) {
console.log('Error: ', error);
result.statusCode = error.code;
result.body = error.message;
}
return result;
};
#最初に起動するLambdaのテスト(次のLambdaも起動する)
- テストイベントの選択をクリック
- ↓のようにテストを作成
- をクリックしてテスト実行
- 成功となればOK。失敗ならログを確認。
- テストでemailキーに指定したアドレスに受付完了メールが配信され、環境変数でTOに指定した受付先アドレスへ問い合わせ内容が配信されていればOK
#APIGatewayの設定
- をクリック
-
REST API
の構築
を選択
-
API名
と説明
を入力してAPIの作成
をクリック(他はデフォルト)
- アクション⇒リソースの作成を選択⇒リソース名を入力(リソースパスは自動入力)⇒CORSを有効化⇒リソースの作成をクリック
- アクション⇒メソッドの作成⇒POSTを選択⇒セットアップで最初に起動するLambda関数名を入力⇒保存をクリック
- 統合リクエストをクリック⇒マッピングテンプレートを追加⇒Content-Typeは
application/json
⇒テンプレートには↓を入力⇒保存をクリック
{
"form": {
"name": "$util.escapeJavaScript($input.path('$.name'))",
"email": "$util.escapeJavaScript($input.path('$.email'))",
"dept": "$util.escapeJavaScript($input.path('$.dept'))",
"body": "$util.escapeJavaScript($input.path('$.body'))"
}
}
##APIGatewayのテスト
##APIGatewayのステージを作成
今回はv1というステージを作成
##APIGatewayのデプロイ
リソース⇒アクション⇒APIのデプロイを選択⇒v1ステージを選択して⇒デプロイをクリック
##発行されたURLをjavascriptのURL変数へ割り当て
#全体の動作確認
問い合わせフォームへSESで登録したアドレスと必要事項を入力し、送信ボタンをクリックして動作を確認
#実用化に向けて
- LambdaのSDK呼び出しは動作確認ができているバージョンが呼び出されるようにする必要あり。
- SESの上限緩和申請が必要。
- APIGatewayでリソースポリシーを指定して不要なアクセスを防ぐ
#最後に
かなり大雑把に書きました。
小規模利用なら十分かと思います。
役に立つかな?
計画性なく作ってしまったのでここからブラッシュアップ