【JavaScript入門】過去投稿のタイムライン表示と「いいね」(postListArea.js)

 postListArea.jsサーバーから取得した投稿一覧を描画 し、ユーザーが ❤️ 「いいね!」ボタンを押すたびに リアルタイムでカウントを更新 するモジュールです。
 ページ読み込み直後と投稿完了後に updatePostList() を呼び出すことで、画面遷移なしで最新タイムラインを再表示できます。いいね数は「楽観的 UI 更新 → サーバー確定値で上書き」という 2 段階方式を採用し、スナッピーな操作感と整合性を両立しています。

フォルダ構成(該当部分)

node-js/
 └─ app/
     ├─ index.html
     ├─ style.css
     └─ js/
          ├─ common.js
          ├─ storage.js
          ├─ imageArea.js
          ├─ imageEffects.js
          ├─ controlArea.js
          ├─ commentArea.js
          ├─ sendArea.js
          ├─ dateFormat.js
          └─ postListArea.js   ← ★今回実装


postListArea.js ― フルコード & 詳細コメント

/* ---------------------------------------------------
   postListArea.js  (フロントのみで完結)
   - 過去投稿タイムラインを Ajax で取得して描画
   - ❤️ いいね!をクリックすると即座に +1
   - サーバー戻り値が文字列でも安全に数値化
   - 連打防止に disabled 制御
--------------------------------------------------- */
window.addEventListener('DOMContentLoaded', () => {
  updatePostList();                                    // 初回ロード
  document.querySelector('#postListArea')
          .addEventListener('click', onLikeClicked);   // いいね!はイベント委任
});

/* ========== タイムライン取得 ========== */
function updatePostList(){
  const root = document.querySelector('#postListArea');

  fetch('/get')                                        // ← API 例: /get
    .then(r => {
      if (!r.ok) throw new Error(`HTTP ${r.status}`);
      return r.json();                                 // { list:[…] } を想定
    })
    .then(({ list }) => {
      root.innerHTML = '';                             // 旧 DOM を一掃
      list.forEach(post => root.appendChild(buildItem(post)));
    })
    .catch(console.error);
}

/* ========== 投稿 1 件を DOM 化 ========== */
function buildItem(post){
  const wrap = document.createElement('div');
  wrap.className = 'postListItem';

  const likeCnt = parseInt(post.likes, 10) || 0;       // 文字列対策 (例 "5")

  wrap.innerHTML = `
    <div class="plImage">
      <img src="/${post.dir}${post.image}" alt="投稿画像">
    </div>
    <div class="plBody">
      <p class="plComment">${post.comment}</p>
      <div style="display:flex;justify-content:space-between;align-items:center;">
        <time class="plDate">
          ${dateFormat('YYYY/MM/DD hh:mm:ss', new Date(post.date))}
        </time>
        <button type="button"
                class="likeBtn"
                data-id="${post.id}">
          ❤️ <span>${likeCnt}</span>
        </button>
      </div>
    </div>`;
  return wrap;
}

/* ========== いいね!処理 (イベント委任) ========== */
function onLikeClicked(e){
  const btn = e.target.closest('button.likeBtn');      // img や span クリック対策
  if (!btn) return;

  e.preventDefault();
  const id   = btn.dataset.id;                         // 投稿 ID
  const span = btn.querySelector('span');
  let   cnt  = parseInt(span.textContent, 10) || 0;

  /* ➀ 楽観的 UI 更新 (即時 +1) */
  btn.disabled = true;                                 // 連打防止
  span.textContent = ++cnt;                            // 1 行でカウントアップ

  /* ➁ サーバーへ POST */
  fetch(`/like/${encodeURIComponent(id)}`, {
    method:      'POST',
    credentials: 'same-origin',
    headers:     { 'Accept': 'application/json' }
  })
    .then(res => {
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      const ct = res.headers.get('Content-Type') || '';
      return ct.includes('application/json') ? res.json() : null;
    })
    .then(data => {
      // サーバーが正式な likes を返せば UI を上書き
      if (data && data.likes != null) {
        const serverCnt = parseInt(data.likes, 10);
        if (!Number.isNaN(serverCnt)) span.textContent = serverCnt;
      }
    })
    .catch(err => {
      console.error('Like リクエストに失敗:', err);
      // エラー時は楽観的更新を維持 (必要ならリトライ UI を実装)
    })
    .finally(() => { btn.disabled = false; });
}

主要命令まとめ

命令 / API働き主な使い方
fetch('/get')投稿リストを取得 (GET)JSON で { list:[…] } を返す API を想定
Element.innerHTML = ''子要素を一掃タイムラインを丸ごとリフレッシュ
`parseInt(str,10)0`
.closest('button.likeBtn')イベント委任先のボタン探索画像クリックなどでも正しく拾う
btn.disabled = trueクリック一時無効化二重送信・連打対策
encodeURIComponent(id)URL 埋め込み時のエスケープ/like/{id} を安全生成
credentials:'same-origin'Cookie 同送セッション認証用
Content-Type 判定JSON 応答だけを res.json()HTML 返しなどでも落ちない

関数別ロジック解説

関数処理フロー
updatePostList()1. /get から JSON 取得 → 2. 既存 DOM 削除 → 3. buildItem() で投稿を順に生成し挿入
buildItem(post)投稿 1 件を HTML 文字列で組み立て → 包装 div を返す。data-id に投稿 ID を埋めて “いいね!” の識別に利用
onLikeClicked(e)① 楽観的 UI 更新(+1 & disabled)→ ② /like/{id} へ POST → ③ サーバー戻り値があれば確定値で上書き → ④ 失敗時はログのみ残して UI 維持

実装ポイント & ベストプラクティス

  1. 楽観的 UI 更新
    ネットワーク待ちを感じさせないため、まずクライアント側で +1 表示し、後でサーバー確定値で調整します。
  2. イベント委任
    タイムラインは再描画で DOM が入れ替わるため、親要素 (#postListArea) にクリックハンドラを 1 本だけ付与し、.closest() でボタンを特定します。
  3. 文字列数値化の徹底
    JSON フィールドが "7" のような文字列で来ても安全に parseInt|| 0 で扱い、NaN を防ぎます。
  4. 障害時の UX
    送信失敗でも「見た目は +1 のまま」ですが、次に updatePostList() が走るとサーバー側の正しい値に戻るため整合性は保てます。必要に応じてリトライ UI を追加してください。

これで 「みんなのつぶやき」アプリの主要フロントエンド実装 が一巡しました。
次回は ローカル + サーバーでの動作確認手順 をまとめ、デバッグのコツやよくあるエラー対処を解説します。