無限スクロールでAPIから記事データをfetchしていく挙動を実装しました!
こんにちは。はるです。
現在、所属している「もりけん塾」でJavaScriptのハンズオン課題 に取り組み、レビューをいただいています。
今日は、課題36 についてアウトプットをしていきます。
(前回までのアウトプットは、こちらです。)
実装する過程で学んだことを、学習ノートとして記録していきます。
認識の間違えている箇所がありましたら、お問い合わせからご指摘いただけるとうれしいです。
JavaScript課題36の仕様
仕様
無限スクロールを作成する課題です。
yahoo風個別記事ページの記事を無限スクロール化してください
- limit: 一つのfetchでどのくらいの文章を取得するか。初期値は10
- page: 何ページ目か。初期値は1
- currentPage: 現在のページ。カレントページに対して+1する値がパラメータとして渡すpageに代入されます
- スクロールする度にfetchされることを避けるために、fetchするための判断するif分が必要という認識です。スクロールが画面一番下より少し手前まできたか、等です
このAPIを使って https://api.javascripttutorial.net/v1/quotes/?page=1&limit=10
パラメータ、スクロールで一番下まで言ったら、ローディングを出し、その間にapiにpageをプラス1して 取得します。 取得したら今ある他のDOMの最後に取得した記事を追加します
そのapiのリクエストがすぐに返ってきてしまい、ローディングがすぐ終わってしまうので体験が得られないので ローディングを出してから0.5秒後にリクエストしてください
(もりけんさんのJavaScriptハンズオン課題より)
JavaScript課題36の学習記録
最初のfetch
最初に記事を表示したとき、APIからデータをfetchして内容を1回分表示させます。
汎用的なfetchの関数
const fetchData = async(api) => {
addLoading(articleWrapper);
try {
const response = await fetch(api);
if (response.ok) {
return await response.json();
} else {
console.error(`${response.status}:${response.statusText}`);
displayErrorStatus(articleWrapper, response);
}
} catch (error) {
displayInfo(articleWrapper, error);
} finally {
removeLoading();
}
};
上記の関数の引数に、APIのURLを入れて実行します。
今回のAPIのURLは「デフォルトのURL + URLパラメータ」の構造で、URLパラメータで
- limit:1度にfetchする記事の数
- page:現在のページ数
を指定して、記事内容のデータを取得します。
パラメータを含めたURLを作成する関数を作りました。
const contentsUrl = 'https://api.javascripttutorial.net/v1/quotes/';
const apiOptions = {
currentPage : 0,
limit : 10,
};
const createApiUrl = (url, page, limit) => {
const params = new URLSearchParams();
params.append('page', page);
params.append('limit', limit);
return `${url}?${params}`;
};
URLSearchParams
オブジェクトを作成して、appendメソッドを使用することで指定されたキーと値のペアを検索パラメータに追加することができます。
今回の課題のAPIでは、
1回目のURLは、https://api.javascripttutorial.net/v1/quotes/?page=1&limit=10。
2回目は https://api.javascripttutorial.net/v1/quotes/?page=2&limit=10。
というように、パラメータの’page’ 数を増やして10記事ずつ内容をfetchすることができます。
下記の関数で最初のデータをfetchします。
const initContents = async() => {
let url = createApiUrl(contentsUrl, apiOptions.currentPage, apiOptions.limit);
const contentsData = await fetchData(url);
const data = contentsData.data;
if (!data) return;
if (!data.length) {
displayInfo(articleWrapper, "no data");
} else {
const articleList = createElementWithClassName('ul', 'article__list js-article-list');
articleWrapper.appendChild(articleList);
renderContents(data);
observer.observe(document.querySelector('.js-article-list').lastElementChild);
}
};
renderContents()関数、observerについては追って説明します。
データからコンテンツを作成する
fetchしたデータを引数で渡して、そのデータを元にコンテンツを作成する関数です。
const createContents = (data) => {
const fragment = document.createDocumentFragment();
if(data){
data.forEach(article => {
const articleItem = createElementWithClassName('li', 'article__item');
const authorArea = createElementWithClassName('p', 'article__author');
const quoteArea = createElementWithClassName('p', 'article__quote');
authorArea.textContent = article.author;
quoteArea.textContent = article.quote;
fragment.appendChild(articleItem).appendChild(authorArea).after(quoteArea);
})
return fragment;
}
}
const renderContents = (data) => document.querySelector('.js-article-list').appendChild(createContents(data));
記事のwrapperとしてulがひとつあり、その中に記事の数だけ liを追加していきます。
IntersectionObserverで下部までスクロールするかを監視する
IntersectionObserverを使用して、一番最後の記事まで表示されたら次のfetchを行うようにします。
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
addLoading(articleWrapper);
observer.unobserve(entry.target);
setTimeout(() => {
apiOptions.currentPage++;
if(apiOptions.currentPage <= apiOptions.limit) updateContents();
removeLoading(articleWrapper);
}, 500);
}
});
}, observerOptions);
IntersectionObserverはビューポートが基準になっていて、ビューポートの端とターゲットが交差した時に指定のcallbackを行うことができます。
今回は、threshould(=交差率)を1.0にしてターゲットが全て交差した時に発火するようにしました。(デフォルトは0.0 。0.0 – 1.0で指定)
const observerOptions = {
threshold: 1.0,
};
交差したら、ローディングを出して、前回の監視(observer)をunobserveします。
if (entry.isIntersecting) {
addLoading(articleWrapper);
observer.unobserve(entry.target);
ターゲットは一番最後の記事に指定しており、fetchするたびにターゲットも新しいものに更新されます。よって、古いターゲットはもう使わないのでunobserveしておきます。誤作動や、不要なメモリの消費を防ぐことができます。
すぐにfetch→表示される無限スクロールの体験が得られないため、0.5秒後にfetchされるようにしました。
setTimeout(() => {
apiOptions.currentPage++;
if(apiOptions.currentPage <= apiOptions.limit) updateContents();
removeLoading(articleWrapper);
}, 500);
}
ここでupdateContents()関数を実行します。
updateContents()関数
2回目以降のデータをfetchをする関数です。
const updateContents = async() => {
let url = createApiUrl(contentsUrl, apiOptions.currentPage, apiOptions.limit);
const contentsData = await fetchData(url);
const data = contentsData.data;
if (!data) return;
if (!data.length) {
displayInfo(articleWrapper, "no data");
} else {
renderContents(data);
observer.observe(document.querySelector('.js-article-list').lastElementChild);
}
}
最初に書いた URLパラメータをセットしてAPIのURLを作る関数(createApiUrl()
)でAPIのURLを作成します。
createApiUrl()
の引数の一つにapiOptions.currentPageとして現在のページ数が渡ります。
これは、先ほどのIntersectionObserverのコード内で、ターゲットと交差した時に+1されていく変数です。setTimeout(() => {
apiOptions.currentPage++; // こちら
if(apiOptions.currentPage <= apiOptions.limit) updateContents();
removeLoading(articleWrapper);
}, 500);
}
作成したAPIのURLを元にデータをfetchして、renderContents(data)にデータを渡して
追加コンテンツをrender。その際に最後の記事を新しくIntersection ObserverのObserverとして登録します。これで、一番最後の記事までスクロールすると再度fetchが行われる(= 無限スクロール)ようになりました。
renderContents(data);
observer.observe(document.querySelector('.js-article-list').lastElementChild);
コード
Lesson36のリンクから実際の挙動を確認できます。下記のjsonデータ情報を入力するとログインすることができ、お好きなニュースタイトルから今回の記事ページに飛ぶことができます。
jsonデータ
{
"name": "Dicki",
"email": "Demario_Hoppe16@example.org",
"password": "fLDg1A3Tj2qeNsH",
"userId": "259c2dfaca8f1eedcc5dbfb0",
"id": "5"
}
//
学習に使用している本は、JavaScript本格入門・独習JavaScriptです。
あとがき
さえさん(@sae_prog)、hasegawaさん(@starsurferz)takakoさん(@wattwattwatt) 、お忙しい中レビューや動作確認をいただきありがとうございました!
ついにJavaScript課題の最後の機能追加を終えました。
最初にJavaScript課題を始めたばかりの頃は、constやletの違いもわからない超初心者でしたが、1から無限スクロールのコードを書けるようになったのは本当に感激です…
ここまでご指導いただいたもりけんさん(@terrace_tec)をはじめ塾生の皆さんに感謝申し上げます ! 最後に課題37として今まで作成した機能をまとめ、ブラッシュアップして完走したいと思います !
今日は以上です。
//
【もりけん塾で勉強しています】
もりけんさん(@terrace_tec)のHPはこちら