【もりけん塾】JS課題36 – Vanilla JS/ 無限スクロール –

JavaScript

無限スクロールで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はこちら