プログラミングの理解が遅すぎる初心者がJavaScript、Node.jsで投票型掲示板を作ろうとしてます

トップページでは記事の順番がごちゃごちゃなので、記事もくじをご覧いただければと思います。

掲示板のサーバーを作る⑥書き込みを反映する(フロントエンド後編)

前編はこちら。

この記事では前回の記事をよく出しますので、このリンク先はタブを消さないで適宜参照するとよいかと思います。

さて、前置きは置いといて、とっとと進めましょう。

まずフロントエンド側の全体のJavaScriptはこちら。

document.getElementById('form').addEventListener('submit', async (e) => {
  e.preventDefault();

  const name = document.getElementById('name').value.trim();
  const comment = document.getElementById('comment').value.trim();

  if (!name || !comment) {
    alert('名前とコメントを入力してください');
    return;
  }

  try {
    const res = await fetch('http://localhost:3000/api/messages', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ name, comment }),
    });

    if (!res.ok) throw new Error('送信に失敗しました');

    const data = await res.json();
    showPostedMessage(data);
    document.getElementById("comment").value = "";//コメント内容をクリア
    fetchMessages(); // 任意:投稿一覧を取得して再表示
  } catch (error) {
    alert(error.message);
  }
});
//ここまで前編

//ここから後編
function showPostedMessage(message) {
  const postedDiv = document.getElementById('posted');
  const newMessage = document.createElement('div');
  newMessage.innerHTML = `
    <p><strong>${escapeHTML(message.name)}</strong> さんのコメント:</p>
    <p>${escapeHTML(message.comment)}</p>
    <hr>
  `;
  postedDiv.prepend(newMessage);
}

async function fetchMessages() {
  try {
    const res = await fetch('http://localhost:3000/api/messages');
    if (!res.ok) throw new Error('投稿一覧の取得に失敗しました');
    const messages = await res.json();
    const postedDiv = document.getElementById('posted');
    postedDiv.innerHTML = '';
    messages.forEach(showPostedMessage);
  } catch (error) {
    console.error(error);
  }
}

function escapeHTML(text) {
  const div = document.createElement('div');
  div.textContent = text;
  return div.innerHTML;
}

fetchMessages(); // ページ読み込み時に一覧を取得

コードブロックに書いた、「ここから後編」をやっていきます。

まずfunction showPostedMessage(message)の定義からですね。

showPostedMessage(message)の定義

わかるところはどんどん進めていきましょう。

  const postedDiv = document.getElementById('posted');
  const newMessage = document.createElement('div');

まあそのままという感じ。

一行目は、HTMLのID属性がpostedである要素を取得し、postedDivに代入する、二行目は、divタグを用意してnewMessageに代入するという意味です。

この時点ではまだHTMLにはdivは書き加えられていません。

divタグを用意して、その中に何を入れるかを指定したのち、appendChildで実際にHTMLに加えるというイメージです。

次行きます。

  newMessage.innerHTML = `
    <p><strong>${escapeHTML(message.name)}</strong> さんのコメント:</p>
    <p>${escapeHTML(message.comment)}</p>
    <hr>
  `;
  postedDiv.prepend(newMessage);
newMessage.innerHTML

newMessage.innerHTMLnewMessageのタグの中身すべてという意味です。

イメージでいうと、<div></div>の間に書かれてあるすべてです。

なので上のコードは、「newMessageで作ったdivの中に右辺を代入しろ」ということです。

で、右辺がなかなか大変ややこしいことになっているように見えますが、これはテンプレートリテラルってやつですね。

テンプレートリテラル

文字列全体をバッククォートで囲い、以下のような形で使います。

`${name}`

バッククォートはキーボードの右のほうにある「@」のボタンを、shiftキーと同時に押せば出てきます。

(キーボードによっては異なる場合もあります。)

普通の書き方では、変数を入れるときにはいちいち+でつないでいかないといけないんですけど、その必要がなくなります。

ちなみにリテラルというのは「そのまんま」「文字通りの」という意味です。

つまり、+を省いて、表示させたい文字列と同じ形で書くことができる便利な書き方です。

普通だったら、変数nameを使うときは

  "うわあっ!!" + name + "だ!逃げろー!"

というように普通の文字列とは性質が違うので区別して+でつなげて書かないといけないのですが、テンプレートリテラルを使うと、

`うわあっ!${name}だ!逃げろー!`

と、文字列の一つのように書くことができます。

let userName = "アクシズ教徒";
const nigero = `うわあっ!${userName}だ!逃げろー!`;
console.log(nigero);

これで出力は「うわあっ!アクシズ教徒だ!逃げろー!」となります。

これを踏まえて右辺を見ていきましょう。

  <p><strong>${escapeHTML(message.name)}</strong> さんのコメント:</p>
    <p>${escapeHTML(message.comment)}</p>
    <hr>
  `;

escapeHTMLの定義は後にしてあるので、内容はそれを見てから確認するとして、出力は「(名前)さんのコメント:(コメント)」という形になることがわかります。

<hr>はHTMLのページに線を引くという意味です。

これも付けてありますので、コメントごとに線を引いていることがわかりますね。

さて、次です。

postedDiv.prepend(newMessage);
prependとappend

prependは指定した要素を親要素の中の一番上に追加するというものです。

逆はappendで一番下に追加します。

書き込んだ内容を上に付け加えていきたい場合は前者、下に付け加えていきたい場合は後者になります。

postedDivは上で定義したように、HTMLの中の書き込んだ内容を表示する場所を指定した'div'タグの要素です。

つまりここでやったことは、

  1. HTMLの、書き込みを反映させる場所の要素を取得
  2. あたらしいdiv要素を作成
  3. あたらしいdiv要素の中身を作成
  4. 作成した要素を一番上に追加する

以上となります。

この処理全体をshowPostedMessageと定義しました。

次です。

fetchMessages()の定義

ここは

try{
 }catch{
};

の形式です。

tryの中身を処理して、エラーが出たらcatchの処理へ進むというものですね。

ここでは「error」と表示するようになっています。

これについては前回で解説していますので、そこを参照していただければと思います。

なのでtryの中身を見ていきましょう。

const res = await fetch('http://localhost:3000/api/messages');

fetchについては前回にやりました。

が、今回は前のように第二引数がありません。

前は第二引数にmethodPOSTを指定していたのでPOST扱いでしたが、デフォルトではGETになります。

GETですから、「データを取得する」という意味になります。

POSTのときも言いましたけど、本当は「データをよこせとリクエストする」ことですが「取得する」と理解して問題ありません。

なので、今回は'http://localhost:3000/api/messages'というアドレスからのデータを取得するという意味になります。

awaitは、この取得が終わるまで次には進まないということです。

これについても前編で解説しています。

そろそろリンクを貼るのもしつこいですかね。

今後はやめときます。

さて、この後はほとんどすべてやったものですね。

いっきにやっていきます。

  if (!res.ok) throw new Error('投稿一覧の取得に失敗しました');
    const messages = await res.json(); 
    const postedDiv = document.getElementById('posted');
    postedDiv.innerHTML = '';
 messages.forEach(showPostedMessage);

これらはすべて前編でもやったところですので、前回記事参照です。

一応意味だけ書いておきます。

上から一行ずつ見ていきましょう。

  1. レスポンスが正常でなかった場合、エラーオブジェクトを作成、catchに投げる
  2. messagesJSON形式にしたレスポンスを代入(代入が終了するまでは待つ)
  3. HTMLの、IDがpostedである要素を取得し、postedDivに代入
  4. postedDivの中身を空欄にする
  5. 取得したデータを一つずつ見ていって、すべて表示する

さて、ここまででshowPostedMessagefetchMessagesの定義がわかりました。

ここで、疑問に思いませんでした?

showPostedMessageは投稿した内容をそのままHTMLに反映させて、fetchMessagesは投稿した内容をサーバーに保存してから、そのデータを取得してHTMLに反映させてるんです。

投稿→そのまま反映。

投稿→サーバーに保存→反映。

結局これ、どちらもHTMLに反映させるという関数なんです。

せっかくshowPostedMessageで投稿した内容をHTMLに反映させたにもかかわらず、fetchMessagesのほうでpostedDiv.innerHTML = ''<div>の中を全部消してから、messages.forEach(showPostedMessage);<div>の中を書き直して、改めて反映させてるんです。

なんじゃこりゃ。

ぶっちゃけていうと、showPostedMessageのほうは実は絶対に必要というわけではありません。

ではなんで付けたのかというと、「書き込んだことをすぐに確認できるようにするため」です。

つまりUX(使いやすさ)向上のためです。

皆さん掲示板に書き込んでるときとかに、よくありません?

「送信」を押したものの、ずっと画面が止まってて書き込めたのかどうかわからなくて、送信を何度も押してしまうようなとき。

待つだけ待って、結局「送信に失敗しました」とか出てくるとブチ切れそうに悲しくなりますが、あれをなくすためです。

本来なら、サーバーに保存されている内容こそがその時点で最新の情報なので、それを反映させなくてはなりません。

それがfetchMessagesにあたります。

実際、データを取得してからその内容をforEachですべて参照し、反映させています。

しかしその場合、サーバーが込み合っていた場合なかなか先に進みません。

なので「まずは投稿できたことはわかるようにする」ということを目的として、サーバーを通さずHTMLに反映させるルートも作りました。

掲示板作成を勉強しているこの段階から「アクセスがいっぱい来すぎたらどうしよう」という大変厚かましいことを考えて付けくわえたものです。

クロスサイトスクリプティングXSS
function escapeHTML(text) {
  const div = document.createElement('div');
  div.textContent = text;
  return div.innerHTML;
}

fetchMessages(); // ページ読み込み時に一覧を取得

とくに難しいところはありませんね。

一応一つずつ見ていきます。

const div = document.createElement('div');

これはdivタグの要素を作っています。

作っただけで何もしていない状態です。

div.textContent = text;

これはtextを「まんま文字列だけにする」という意味です。

たとえば<とか>とかといったHTMLに命令するような記号も、すべて文字列にします。

もし、これを書いておかなかったら、書き込んだ内容にこういった記号が含まれていた場合、HTMLのページ自体を書き換えることができてしまいます。

<script>とか書き込まれたらアクセスした人に嫌がらせどころじゃないレベルの悪意ある動作をさせられてしまいます。

Imageででかでかとエ〇画像とか貼られた日には目も当てられません。

こういうのをクロスサイトスクリプティングXSS)といいます。

なのでこのようにただの文字列扱いにして、それを防ぐわけです。

すこしこれだけだとわかりにくいので、追ってみていくことにします。

textの内容を追ってみる

escapeHTMLが使われているのはどこかと言いますと、

newMessage.innerHTML = 
  `<p><strong>${escapeHTML(message.name)}</strong> さんのコメント:</p>
  <p>${escapeHTML(message.comment)}</p>`

ここになります。

で、この引数、message.namemessage.commentはといいますと、showPostedMessageの引数ですので、ここを見ることになります。

 const data = await res.json();
    showPostedMessage(data);

つまりescapeHTML(text)textに入るのは、サーバーからJSON形式で送られてきたデータの、namecommentということになります。

まあ普通に、入力フォームに書き込んで、内容が保存されたあと返ってきたデータの名前とコメント、ということですね。

return div.innerHTML;

これは完全に文字列化したdivの内容を、タグの中身として使えるようにしています。

もともとはタグなどの意味を含める記号、<``>``&といったものを、無害化(?)したことになります。

これで上にある

newMessage.innerHTML = `
    <p><strong>${escapeHTML(message.name)}</strong> さんのコメント:</p>
    <p>${escapeHTML(message.comment)}</p>

message.name``message.nameescapeHTMLを通しているからどんな記号を使っても安心ということになりました。

ラスト。

最後の一行です。

fetchMessages(); // ページ読み込み時に一覧を取得

これは上で定義したものをそのまま呼び出しています。

現在のサーバーに保存されている内容を一番最初に表示するものです。

掲示板を開いたときに、なにもログが表示されてないのでは「掲示板、削除された?」って話になりますので、まずはこれを呼び出して現在のサーバーに保存されているデータをHTMLに表示させる必要があります。

プログラム上の動作は通常通りコードの上から処理されていきますが、目に見える結果としてはこれが一番最初ということになります。

おわりに

やっとScript.js側のコードが終わりました。

ほんとに長かったです。

おそらく手慣れた人には「これくらいで長いとか言ってたらこの先やっていけない」とかそういうことを言われるんでしょうけど、長いものは長いんです。

私も慣れていって、初心者に向けて「これくらいで長いとか言ってたらこの先やっていけないぞ」と偉そうに言えるように頑張ります。

次はサーバー側のコードを書いていきます。

JavaScriptと同じ書き方ですが、こちらはNode.jsになります。

これが終われば、とりあえず掲示板の作り方の基本は終了です。

そのあとはセキュリティ強化とか、CSSでページの見栄えをよくしたりとか、という作業に入っていきます。

Image