つくる ポートフォリオアプリを仕上げる
このレッスンで分かること
- 第2章から第9章までを 1 枚の
app.jsに集める順番- 状態を 1 か所に置き、描き直しを 1 本の関数にまとめる組み立てかた
fetchが失敗したときに、利用者へ何をどこに出すか- しぼり込みと並び替えを、元の配列を壊さずに掛け合わせる書きかた
つくる ポートフォリオアプリを仕上げる とは
API から作品を取り、公開ずみのものだけを選び、タグでしぼり、並び替え、問い合わせフォームの入力を検証するところまでを 1 枚のファイルで完成させる、このコースの総仕上げです。
ここまでで何ができるようになったか
入門の最終回で作ったのは、works 配列を手で書き換えると一覧の見た目が変わるページでした。そこから 9 章ぶん進んだいま、手元にある道具はこれだけあります。
| 章 | 覚えたこと | この課題で使うところ |
|---|---|---|
| 第2章 | map filter sort | 公開ずみを選び、タグでしぼり、表示回数で並べる |
| 第3章 | 純粋関数 | visibleWorks を「読むだけ」の関数にする |
| 第4章 | 分割代入とスプレッド | sort の前に写しを作る |
| 第5章 | クラス | 作品 1 件の扱いを型としてそろえる |
| 第6章 | try catch と画面への表示 | 通信の失敗を利用者に見せる |
| 第7章 | 正規表現 | メールアドレスの形を確かめる |
| 第8章 | async と await | fetch の返事を待つ |
| 第9章 | response.ok とステータス | 200 以外を失敗として扱う |
この回でやるのは、新しい構文を覚えることではありません。ばらばらに練習したものを 1 枚のファイルに同居させても壊れない形に置くことが主題です。
状態は 1 か所、描き直しは 1 本
このアプリが覚えておく必要のあるものは、次の 3 つだけです。
- API から届いた作品の全部 (
allWorks) - いま選ばれているタグ (
#tag-filterの value) - いま選ばれている並び順 (
#sort-orderの value)
後ろの 2 つは select 要素そのものが覚えているので、別の変数にうつす必要はありません。同じことを 2 か所が覚えると、必ずどちらかが古くなります。
let allWorks = [];
function visibleWorks() {
const tag = tagFilter.value;
const picked = allWorks
.filter((work) => work.published)
.filter((work) => tag === "all" || work.tags.includes(tag));
// sort は元の配列を並べ替えてしまうので、写しを作ってから掛ける
const sorted = [...picked];
if (sortOrder.value === "views-asc") {
sorted.sort((a, b) => a.views - b.views);
} else {
sorted.sort((a, b) => b.views - a.views);
}
return sorted;
}visibleWorks は画面を触りません。allWorks と 2 つの select を読んで、配列を返すだけです。画面を書き換えるのは render 1 本に閉じます。
function render() {
const list = visibleWorks();
workList.textContent = "";
list.forEach((work) => workList.append(createWorkCard(work)));
worksCount.textContent = list.length + " 件を表示中";
}
tagFilter.addEventListener("change", render);
sortOrder.addEventListener("change", render);しぼり込みと並び替えを別々のイベントで別々に画面へ反映すると、「タグを変えたら並び順が既定に戻った」という組み合わせの不具合が出ます。どちらが変わっても
renderを呼ぶだけにすると、この種の不具合はそもそも起きません。
失敗したときに何を出すか
第9章でやったとおり、fetch は 404 でも 500 でも catch に入りません。response.ok を自分で見て、falsy なら投げます。
async function loadWorks() {
worksStatus.textContent = "読み込み中です";
try {
const response = await fetch(API_URL);
if (!response.ok) {
throw new Error("サーバーが " + response.status + " を返しました");
}
const data = await response.json();
allWorks = data.works;
worksStatus.textContent = allWorks.length + " 件を読み込みました";
render();
} catch (error) {
allWorks = [];
workList.textContent = "";
worksCount.textContent = "0 件を表示中";
worksStatus.classList.add("error");
worksStatus.textContent = "作品を読み込めませんでした。" + error.message;
}
}catch の中で画面を空に戻しているのが要点です。前回の表示が残っていると、利用者には古いデータが最新に見えます。失敗したときは、失敗したと分かる状態にしてから理由を書きます。
手元で失敗を試したいときは、
API_URLの末尾に?status=500を足してください。?delay=2000にすれば読み込み中の表示も確かめられます。?empty=1なら 0 件のときの見え方が出ます。
入力の検証は送信の直前に 1 回
第7章の正規表現をここで使います。type="email" の検証はブラウザ任せになってしまい、この課題では手で確かめます。
const EMAIL_PATTERN = /^[\w.+-]+@[\w-]+\.[\w.-]+$/;
contactForm.addEventListener("submit", (event) => {
event.preventDefault();
const name = document.querySelector("#name").value.trim();
const email = document.querySelector("#email").value.trim();
const errors = [];
if (name === "") errors.push("お名前を入力してください");
if (!EMAIL_PATTERN.test(email)) errors.push("メールアドレスの形が正しくありません");
if (errors.length > 0) {
formError.textContent = errors.join(" / ");
formMessage.textContent = "";
contactForm.classList.add("has-error");
return;
}
formError.textContent = "";
contactForm.classList.remove("has-error");
formMessage.textContent = name + " さん、送信を受け付けました";
});エラーを配列にためてから 1 回で出すと、「名前も空でメールも変」という場合に両方を伝えられます。1 つ見つけた時点で return すると、直すたびに次のエラーが出てくる形になります。
よくある間違い
描き直しの前に空にし忘れる。
function render() {
const list = visibleWorks();
// workList.textContent = ""; を書き忘れると
list.forEach((work) => workList.append(createWorkCard(work)));
}しぼり込みを切り替えるたびにカードが下へ積み上がります。件数の表示だけが正しく、画面と食い違います。
sort をそのまま掛ける。
// allWorks そのものが並べ替わる。もとの順番は二度と戻らない
const sorted = allWorks.filter((w) => w.published).sort((a, b) => b.views - a.views);filter が新しい配列を返すのでこの行は無事ですが、allWorks.sort(...) と書くと元が壊れます。写しを作る癖を付けておくほうが安全です。
await を書かずに response を使う。
const response = fetch(API_URL);
if (!response.ok) { /* response は Promise なので ok は undefined */ }undefined は falsy なので、通信が成功していても必ずエラー側に入ります。 「いつも読み込めませんでしたと出る」ときは、まずここを疑ってください。
課題
- createWorkCard で article.work-card を作る。中に h3 でタイトル、p.work-meta で「2026 年 / 120 回表示」の形、p.work-tags でタグを入れる
- visibleWorks で allWorks から published が true のものだけを選び、#tag-filter の値でしぼり、#sort-order の値で並べ替えた配列を返す。all のときはしぼらない。sort の前に配列の写しを作る
- 並び順は views-desc が表示回数の多い順、views-asc が少ない順、title がタイトル順。既定は views-desc
- render で #work-list を空にしてから描き直し、#works-count に「4 件を表示中」の形で件数を出す。#tag-filter と #sort-order の change でこれを呼ぶ
- loadWorks で https://yumesaku.tech/course-api/works.json を await fetch する。response.ok が false なら throw し、成功したら #works-status に「5 件を読み込みました」の形で出して render する
- 失敗したときは catch で #work-list を空にし、#works-count を「0 件を表示中」にして、#works-status に読み込めなかったことと理由を出す
- 送信では preventDefault してから #name が空でないことと、#email が正規表現に合うことを確かめる。だめなら #form-error に理由を出して contact-form に has-error を付け、よければ #form-error を空にし has-error を外して #form-message に「送信を受け付けました」を含む文を出す