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

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

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

さて、前回やっと掲示板が動くようにできました。

とは言ってもChatGPT先生が教えてくれたのをそのままコピペしただけですが。

そんなコピペしただけのコードを動かすのに必死になっていろいろ修正してたんだから死にたくなり笑えてきます。

さて、そんなChatGPT先生が教えてくれたコードですが、HTMLはまあわかりますので、フロントエンドのJavaScript、すなわちscript.jsを見ていこうと思います。

コードはこちら。

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('/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('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(); // ページ読み込み時に一覧を取得

長げぇ・・・。

(※いきなり追記です。

本当はScript.jsに関して、一つの記事で一気にやってしまおうと思ってたのですが、予想よりかなり多い文字数となってしまいました。

なので、前後編に分けることにします。

キリがいいところで分けたいとおもうので、上のコードのtryと書かれているブロックまでを前編にします。)

もう普通にJavaScriptのややこしいところが出てきていますので、わからない方は右のカラムにあるJavaScriptの常識シリーズをご覧ください。

右カラムに目次がありますので、そこから飛べます。

そこに書いてあるものはここでは省いていきます。

では最初にわからないものは

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

これですね。

いや、ここはやるにはやったんですけど、今までやってきた方法は

if (name === "" || comment === "") {
  alert("名前とコメントを記入してください");
} else {
  // 投稿処理
}

というやり方でした。

でもこれ、こんな風に書かなくても上に書いたようにすれば、見やすいし速く書けるしでいいことづくめなんです。

具体的にはelseとかが要らなくなります。

if文の条件式

これ、めっちゃわかりやすくてif(name)と書いたら、「nameがtrueなら」という意味になります。

で、「!name」と書いたら「nameがtrue以外なら」になります。

!は否定を表します。

true以外ってのはfalseや空欄、0とかundefinedとかnullとか、まあネガティブな感じがするやつです。

!nameが条件式に出てくれば、こういう意味になります。

なのでif(!name)と書くだけで「nameが空欄なら」という意味になります。

これを||、つまり「or」を使うと

if(!name || !comment){};

これで「nameもしくはcommentのどちらかが空欄であった場合」を示すことになります。

で、空欄であった場合はアラート文を出したいので、

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

と書けばよくなります。

ただこの場合だと、alertを出したあとも処理が続いていってしまいます。

せっかくif文で「ちゃんと名前とコメントは書いたんだろうな?」と尋問しても、その答えに関わらずそれ以降の処理が続いてしまうんです。

なのでここはreturnを使って、もし名前かコメントを書いてないのであれば、そこで処理を止めるようにします。

return 値だったら、関数の結果を返すという意味になりますが、returnだけだとそこで処理を止めるという意味になります。

従って上のコードは

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

と書くことになります。

次はここですね。

 try {
    const res = await fetch('/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);
  }
});

try

はい、私は初めて見ました。

tryとは

これは一種の制御構文です。

制御構文というと、また「何それ?」という感想が出てくるのですが、これは知らないうちに普段から使ってるやつです。

functionとかifとかwhileとか。

()(括弧)がつくものとつかないものがありますけど、どれも制御構文です。

だからこれもtryと書くだけで役割を果たすことになります。

具体的にはエラーが起こった時の処理のときに役立ちます。

使い方は以下の通り。

try {
  // ここで処理を実行
} catch (error) {  
  // エラー時の処理
}

最初の//ここで実行と書いてあるところで、本来やりたい処理を実行します。

で、そこでの処理にエラーが出た場合、//エラー時の処理に進みます。

エラーがなかったらcatch(error)は無視してそのまま進みます。

エラー時の処理は、コンソールにエラー表示をさせるとか、今回のようにアラートを出すというように、ユーザーに知らせることが目的です。

いきなりcatch()の括弧に中にerrorという文字が入っていますが、これは変数です。

errorでもeだけでもかまいません。

ここに書き込むことで変数errorにエラーの内容が書き込まれます。

なのでこの変数を使って、console.error(error);alert(error.message);のように書きます。

console.error(error);はエラーの内容をコンソールに目立つように書けという意味です。

console.log();の変形版ですね。

errorlogのところを目立たせろ、という意味になります。

alert(error.message);はそのまんま、エラーの内容を書いたアラートを出せ、という指示ですね。

今のところ、catchの中に書くのは、ユーザーにエラーがあるということを知らせるときくらいです。

なので//エラー時の処理の中は基本的にほとんど書くことはありません。

というわけで、ここからはtryの中で何をやっているのかを見ていきます。

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

めんどくさそうなコードになってきましたが、とりあえずはキーと値という形をとっていますので、何とか理解できそうな雰囲気です。

それでもいくつかわからないものがありますね。

まず全体的な形を見ていきます。

ChatGPT先生によれば、これは「このコードは fetch を使って、サーバーにJSONデータをPOST送信する処理 」だそうです。

わかりそうで、一部わかりません。

これまでの勉強のおかげで「サーバーにJSONデータをPOST送信する」という意味はわかります。

問題は「fetchを使って」というとこ。

なのでfetchを見ていくことにします。

fetchとは

意味自体はそんなに難しいものではなく、さっき上で述べた「サーバーにJSONデータをPOST送信する」に加えて「宛先」を含めたものの関数をfetchといいます。

正確には「送信するようにリクエストすること」なんですけど、一般的な理解としては「送信すること」で構いません。

関数ですからfetch();という形で呼び出すことになります。

なんかダーーっと書いてありますけど、分解してみるとfetch(第一引数,第二引数)という形になっているのがわかるかと思います。

で、その第一引数である'http://localhost:3000/api/messages'が宛先になります。

APIを作ったときには受け取り窓口を作りましたが、あれに宛てていることを示しています。

このときはまだどこから受け取るかを決めてなかったので便宜上/submitとしていましたが。

そして第二引数に実際に送るデータを記します。

{
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ name, comment }),
    }

これも一つずつ見ていくとしましょう。

method:'POST'とは

送信の方法ですね。

HTMLの<form>でも<form action="/submit" method="post">と書いていました。

ただこれ、もうJavaScriptfetchで指定するなら、HTMLでは書かないほうがいいです。

何も書かなくても問題はありませんけど、わざわざ<form>で書いて祖語ができてしまったらそっちのほうがややこしいので。

なのでもう「送信方法や送信先fetchでしてしまう」ということでいいでしょう。

さて、次いってみましょう。

headers:とは

headers: { 'Content-Type': 'application/json' },

と書かれている部分ですね。

これは送るデータに関する情報です。

'Content-Type': 'application/json'は送信するデータはJSON形式であると示しています。

こうすることでAPI側はJSON形式で来たデータをNode.jsで処理するためにJavaScript形式に変換すれば良いことになります。

どうせ次の記事で書くのでここでは覚えておく必要はありませんが、server.jsでは

app.use(express.json());

と書けば、送られてきたJSON形式のデータをJavaScript形式に変換できます。

なお以前APIを作ったときはfetchは通してなかったので、データはJSON形式ではなくURLエンコーデッド形式でした。

なのでAPI側では

app.use(express.urlencoded({ extended: true }));

と書いて、JavaScript形式に変換していました。

今回はデータの形式だけを記していますが、他にもいろいろな情報をここに加えていくことになります。

まあそれは必要になったときに付け足していくことにしましょう。

「ほかにもこんなのがあります」とか言っていろんな種類を出したら、絶対に嫌になりますし。

ただ一つだけ知っておくといいのではないかと思うのがあります。

今回「JSON形式で送ります」という情報を記載したのですが、「JSON形式で返してください」というのは書く必要がないということです。

何も書かなければJSON形式で返してくれるからです。

なのでこれは遠慮なく省略します。

次いきます。

body:とは

まあこれも以前、APIを作成したときに出てきたものですが、要は送信するデータそのものです。

掲示板みたいに名前とコメントを送信するなら、名前とコメントそのものです。

なので実際{ name, comment }という文字列も見えています。

となると問題はJSON.stringifyが何者なのかということなのですが、これはそう難しいものではなく、JavaScript形式で書かれているオブジェクトをJSON形式、つまり文字列に変換するものです。

stringは文字列を意味し、それにfyをつけているわけですから「文字列にする」という意味になります。

さて。

やっとこれでfetchの中身は終わりました。

で、忘れていたわけではないのですが、そのfetchの前にawaitってのがありますね。

これも解明しておかないと変数resの意味が分かりません。

というわけでここで見ておきましょう。

awaitとは

これはそのまま「待つ」と言う意味です。

fetchは非同期処理なので普通ならすぐに次に進んでしまいます。

ここで同期とは「順番通りに処理していく」、非同期とは「順番を無視して処理できるところから処理していく」という意味です。

ですので、再度言いますが、fetchは非同期処理なので、本来ならこれが処理されなくても、順番を追い越して次の処理に進んでしまうんです。

しかしfetchはデータをサーバーに届けるという処理です。

これをすっ飛ばして次の処理に移ったとしても、「データも届いてないのに何をせえっちゅうねん」という話になります。

なので「データが届くまではここで待て」と指示しておく必要があるわけです。

awaitasyncという関数の中で書く必要があります。

ここでも最初の行で書いています。

document.getElementById('form').addEventListener('submit', async (e) => {
 //いろいろな処理
}

asyncとは「非同期」という‘‘意味です。

イメージとしては、asyncと書いておけば、その中のどこかで非同期に関する命令を出すぞ、という宣言になります。

ただ考え方の順番としては逆で「awaitを使いたい」→「よっしゃasyncを書いて宣言したことにしとこ」という具合です。

なお、await演算子に分類されます。

+とか-とかと同じです。

プラスは「足せ」、マイナスは「引け」、そしてawaitは「待て」、という具合です。

awaitってなに?と聞かれたら「演算子さ」と答えればOKです。

asyncの説明をさらにすると「Promiseを返す」とかいろいろあるんですけど、今は面倒なのでスルーします。

学校や受験のテストでもそうですけど、面倒そうな問題はスルーして、あとからやるのが定石なのです。

さて、次です。

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

たかがif文の分際で、結構ややこしく見えます。

一つずつ見ていきます。

res.okとは

resとはレスポンスです。

レスポンスですからサーバーからのデータということになります。

ユーザーからだとリクエストです。

で、res.okですから、なんとなくレスポンスがOKなんだな、という感じがしますが、その感じのままでいいです。

res.okだと、HTTPステータスコードが200~299で返ってきて、これは「成功した」ということを示します。

HTTPステータスコードっていろいろあるんですけど、一番なじみがあるのは「404」ではないでしょうか。

まあこんな感じで200番、300番、400番、500番といろいろあります。

リクエストがおかしいとかサーバーがおかしいとか、番号を見ればだいたい何がおかしいかがわかるようになっています。

200番台は基本成功を意味します。

今、コードに出てるのはif(!res.ok)ということですから、「レスポンスがOKでない場合」を見ています。

次に出てくるのはthrowですね。

throwとは

throwとは、と言っておいてなんですけど、この場合throw new Error('失敗')のかたまりで覚えておいたほうがいいとおもうので、その前提で話していきます。

throwは、そのまんま「投げる」です。

どこに投げるのかというと、最初はcatchのように「エラーが出たらどうするかを書いてある場所」に、です。

もしそういう場所もなければ無条件で強制終了です。

throw new Error('失敗')ですからnew Error('失敗')を投げるということになります。

ではnew Error('失敗')とはなんなのかってことになるんですけど、これはエラーオブジェクトの実体を作り出しています。

クラスやらコンストラクタやらいうアレです。

クラスが設計図でnewで実体化するとか、っていう。

これ、いろんな教科書やら動画やらで説明されてるんですけど、私、全然あまりわからなくて。

なので、私がわかりやすく書きます。

クラスとは

クラスってのは「あたらしい関数を含むもの」です。

この新しい関数がコンストラクタ関数です。

で、クラスは、さも自分がコンストラクタ関数であるかのように振る舞います。

具体的に書いてみる
class Person { //クラス
    constructor(name, power) { //コンストラクタ関数
        this.name = name;
        this.power = power;
    }
}


const choJin = new Person("ザ・マン", 9999);
  console.log(`グロロロ…私は${choJin.name}。超人強度は${choJin.power}万パワーだ。`);
分解してみる

①クラスの中でconstructor関数を定義しています。

class Person {
    constructor(name, power) {
        this.name = name;
        this.power = power;
    }

②クラスがconstructor関数のフリをして自分が関数みたいになってます。

const choJin = new Person("ザ・マン", 9999);
  console.log(`グロロロ…私は${choJin.name}。超人強度は${choJin.power}万パワーだ。`);

このとき、クラスはnewを付けます。

このnewをつけることで、コンストラクタ関数の「フリ」をしてるわけです。

あくまで「フリ」です。

クラス自体が関数になるわけではなく、newを付けることでコンストラクタ関数が呼ばれて実行されるということです。

で、結果は

そうして実行されたものはオブジェクト化します。

いわゆる「設計図に対して、実体化する」と言われてるやつですが、私にとってはオブジェクト化と言われたほうが数倍わかりやすいです。

しかしながら世間ではこのオブジェクト化のことをインスタンス化」、オブジェクトのことをインスタンスと呼ばれているので、これらの言葉も覚えておきましょう。

上の例でいうと

const choJin = new Person("ザ・マン", 9999);

このchoJinはオブジェクトです。

注意点

そしてこのnewですが、コンストラクタをconstructorと書かないとちゃんと呼び出してくれません。

必ずconstructorと書くようにしましょう。

まとめ

①クラスはコンストラクタ関数を内部に持っている。

②突如現れたnewがコンストラクタ関数を呼び出して実行するため、クラスがコンストラクタ関数のフリをしてるみたいになる。

new コンストラクタ関数`が実行されるとオブジェクト化する。

④ただし、コンストラクタはconstructorと書かなければnewが呼び出してくれない。

これを踏まえてもう一度見ていきます。

new Error('送信に失敗しました');

いきなりnew Error();と出てきています。

クラスはどこで作られた!?という謎が浮上しますが、これはJavaScriptがあらかじめ用意してくれているクラスです。

誰に断る必要もなく勝手に使って構いません。

newを使って、勝手にコンストラクタ関数を呼び出してError();を実行してしまいましょう。

これでエラーオブジェクトができあがりました。

このエラーオブジェクトの意味は「エラーであること」です。

そのまんまやんけ、と思うかもしれませんけど、そのまんまです。

エラーの番号であったり、内容であったり、いろんな情報を含んでいるものです。

メソッドを付け足していけば、いろいろなエラーに関する操作ができます。

さて、その出来上がったエラーオブジェクトを!res.okの結果に従ってthrow(放り投げて)います。

この結果はtrueつまりresokじゃなかったことを意味しています。

つまりレスポンスが正常じゃなかったので、エラーオブジェクトをぶん投げて強制終了させるというわけです。

・・・かなり疲れてきましたね。

もうすこしで前編は終わりですので、頑張りましょう。

というか、別に一気に読まないといけないわけでもないので、適宜コーヒーブレイクを取るなり寝てしまうなりしてもかまわないと思いますが。

閑話休題

話を戻します。

const data = await res.json();
    showPostedMessage(data);
    document.getElementById("comment").value = "";//コメント内容をクリア
    fetchMessages(); // 任意:投稿一覧を取得して再表示

もうこの辺は大体見ればわかる感じもしますので、さらっと流す感じでいきましょう。

まず

const data = await res.json();

res.json();は以前にも出てきましたね。

サーバーからのレスポンスの内容をJSON形式にする、というものです。

で、awaitは上述したとおり「待つ」という意味ですから、「レスポンスの内容をすべてJSON形式にするまで待て」ということになります。

で、変換し終えたらdataに放り込みます。

それまで処理が処理が先に進むことはありません。

上でも言いましたけど、「肝心のデータの処理も完了してないのに先に進んで何をするんよ」って話です。

さて、次です。

というかこのあとの関数って全部あとから定義したものばかりですので、今回はここまででいいですね。

一応書いておくと

 showPostedMessage(data);
 document.getElementById("comment").value = "";//コメント内容をクリア
 fetchMessages(); // 任意:投稿一覧を取得して再表示

このうちshowPostedMessagefetchMessagesはこの後で定義された関数です。

で、

 document.getElementById("comment").value = "";//コメント内容をクリア

は、もはや説明の必要もないと思いますが、IDがcommentの要素、つまりHTMLのtextareaである入力フォームの値(value)を空欄("")にする、という意味です。

おわりに

「かなり疲れた」と言ったところから、もう少しかかるかと思いましたけど、ほとんどスルーレベルで終われました。

次の後編では、

 showPostedMessage(data);
fetchMessages();

の定義の内容などを見ていきます。

見たところ、そこまで難しそうなものはなさそうなのでサクッと終わらせたいところですが、こういうとまたどっかで泥沼にハマって理解するのに苦労するフラグになりそうです。

Image