エミ眠太の自由帳
 エミ眠太の自由帳
    
 
 eBook      2023-08-16      

Django REST Framework + Next.js (MUI) + Markdownでブログを作る【Part 3】

Language:
 Markdown Node.js JavaScript
Framework/Library:
 React Next.js MUI(Material-UI)
Technology:
 環境構築 ダイナミックルーティング
Platform/Tool:

記事一覧へ

目次


前回までの振り返り

【Part 1】では各種基礎知識について、【Part 2】ではバックエンドとしてDjango REST Framework(以下DRF)部分の開発を進めましたが、【Part 3】ではフロントエンドとしてのNext.js部分の開発をしていきましょう。

環境構築

まずはNext.jsで開発するための環境を作っていきます。なお環境構築についてはNext.js + Tailwind CSSの環境構築 の記事で紹介したことがあるため一部説明を省略しますが、今回は基礎からということでTailwindCSSを利用せずNext.jsのみで環境構築してみたいと思います。

ディレクトリ作成〜Next.jsプロジェクトの作成

こちらも過去記事ではセクションに分けて作成して解説していましたが、DRF同様にサクサクいきましょう。

「一行解説」を見る
  1. (l.3) npm`を使用する形式でNext.jsプロジェクト(blog_front)を作成する。
  2. (l.4-6) Next.jsプロジェクト(blog_front)に移動して、プロジェクト直下にcomponentsディレクトリ、configディレクトリ、utilsディレクトリを、pagesディレクトリ下にentriesディレクトリを作成します。
  3. (l.7) pagesディレクトリ下にデフォルトで作成されているapiディレクトリは使わないので削除します。

ここまで完了すると、Next.jsプロジェクトのディレクトリは上の通りになっていると思います。

その他モジュールインストール

今回、オープンソースのUIコンポーネントライブラリであるMUIを利用します。MUIのInstallationを参考にインストールしてみましょう。また、Material Icons というアイコンを簡単に利用できるライブラリも使用したいので併せてインストールします。他にもMarkdownをHTMLに変換するためのmarkedもこのタイミングでインストールしてしまいましょう。実行後package.jsonファイルを確認すると、下のようなバージョンがインストールされていることが確認ができます。

必要なモジュールがインストールできたら、一旦下のコマンドで開発環境を立ち上げてみましょう。

npm run dev

以上で環境構築は完了です。

コンポーネント作成

ここからNext.jsプロジェクトを本格的に作成していきます。はじめにページ共通のコンポーネント部分を作っていきましょう。コンポーネントはcomponentsディレクトリ下に、コンポーネントごとにJSファイルを新しく用意し作成していきます。なお、それぞれのコンポーネントのスタイルは、それぞれ同じ名前のCSSファイルをstylesディレクトリ下に作成していきます。

ページ共通レイアウト

まずはページ共通の要素として大部分を占めるレイアウトを作成します。レイアウトの要素としては、ヘッダー・フッター・全体レイアウトの3つです。挙げた順番に作っていきましょう。なお、いずれも関数コンポーネントを使用していますが、return内の記述については単純なHTMLと変わらないためほとんど省略しています。

components/Header.js(ヘッダー)+ styles/Header.module.css

ページ共通のヘッダーを作成します。

「一行解説」を見る
  1. ヘッダー内には、ページへのリンクをつけています。特にこだわりはありませんが、タイトルを左寄せ、その他のリンクを右寄せにします。

components/Footer.js(フッター)+ styles/Footer.module.css

ページ共通のフッターを作成します。

「一行解説」を見る
  1. フッター内にも同様にページへのリンクをつけています。
  2. (l.16-20) X(旧:Twitter)のアイコンも入れてみました。アイコンについては、MUIから簡単にインポートできます。
    • (l.17) target="_blank" rel="noopener noreferrer"としているのは、アイコンクリック時に新しくタブを開いてページを開くためです。

components/Layout.js(全体レイアウト)+ styles/Layout.module.css

ページ共通のフッターを作成します。上で作ったヘッダーとフッターを組み込みます。

「一行解説」を見る
  1. (l.5-15) 他のコンポーネントと違い、レイアウトコンポーネントは今回のブログ全体の枠となるコンポーネントとなります。そのため、childrenプロパティを設定することでレイアウトコンポーネントを任意のページで再利用することができます。ページ作成の章ではこのレイアウトコンポーネントが多数使われますが、このファイル内の (l.10) 部分の{children}に各ページの要素が入るというイメージです。今回の (l.8) <Header/>と (l.12) <Footer/>{children}の外側に記載されているので、レイアウトコンポーネントが利用される限り対象のページ内で付随して設定されるため、ページ側で個別に設定する必要はありません。

個別コンポーネント

続いて個別の部品を作成します。

components/EntryListItem.js + styles/EntryListItem.module.css

components/EntryListItem.jsは投稿一覧ページにおいて、投稿タイトルをキーに縦に並べて表示するためのコンポーネントです。このコンポーネントから詳細ページに遷移するようなイメージです。

「一行解説」を見る
  1. (l.5-16) 先ほどのレイアウトコンポーネントと似ていますが、今度はnという値が数箇所で使われています。(※実際にはnでなくともコンポーネント内で統一されていれば問題ありませんが、)今回はこのコンポーネントが利用されるページ内で実行されるgetStaticPropsから取得したPropsがnに入ることを想定して仮置きされているイメージです。このコンポーネントが使われるページにおいては、getStaticPropsはAPIからの全値を取得します。DRFプロジェクト側でモデルを作成した際のフィールドを (l.9) {n.category.f_category}、 (l.11) ${n.slug}、 (l.12) {n.title}の通り設定することで、DBから取得した値を表示することができます。

これでコンポーネントの作成は完了しました。続いて、実際にコンテンツを掲載するページを作成していきましょう。

ページ作成

コンポーネント同様、ページについてもpageディレクトリ下に、ページごとにJSファイルを新しく用意し作成していきます。コンポーネントで作成した部品をページに組み込みつつ作成していくイメージです。

APIからのデータ取得

今回のページ作成は静的データだけではなく、Django REST Framework(DRF)プロジェクトで作成したAPIから取得する動的なデータも組み合わせて作成します。そのため、ページ作成の前にAPIからデータを取得する仕組みを先に構築します。データ取得のためには、下の通りconfig/index.jsファイルとutils/fetchEntry.jsファイルを作成します。

config/index.js(APIのURL設定)

「一行解説」を見る
  1. (l.1) このファイルではNext.jsプロジェクトの環境変数として、どこからでも使えるAPIのURLを設定しています。もしprocess.env.NEXT_PUBLIC_API_URLに値が入っていれば優先的にそちらを参照しますが、今回は設定していないのでDRFプロジェクトが立ち上がっているローカルサーバを参照します。この記述方法は本番環境とローカル環境の使い分けをしたい場合に便利です。

utils/fetchEntry.js(APIでのフェッチ関数作成)

「一行解説」を見る
  1. (l.1) config/index.jsで設定したAPIのURLをここで早速利用します。
  2. (l.3-8) ここではバックエンドのDRFプロジェクトにおけるEntryモデルに当てはまる全てのデータを、Next.jsプロジェクトの任意のページから非同期(async/await)で取得する方法をまとめて記述しておきます。
    • (l.3) 関数のfunction宣言前に、exportと書くことで他のファイルでも当該関数をimportして利用できることを、asyncと書くことで非同期処理を含む(async)関数であることを、宣言しています。
    • (l.4) Next.jsで提供されているfetchを利用します。.fetch()メソッドでAPIサーバにHTTPリクエストをして(今回はGETメソッドしか利用ができないので)GETメソッドで取得したデータを変数resに入れますが、HTTPレスポンス(データの取得完了)までには時間がかかる場合があるため、処理の途中で次行の処理に移行してしまわないために、awaitでデータ取得の実行完了を待機します。
    • (l.5) .json()メソッドで変数res内のレスポンスオブジェクトをJSONで取得し変数entriesに入れますが、その処理の実行完了をawaitで待機します。
    • (l.6) DRFプロジェクトのEntryモデルにおいてis_publickフィールドを作成し、記事の公開・未公開を設定したのを覚えていますでしょうか?その際、0を公開、1を未公開(ドラフト)としましたが、未公開記事は取得しないように.filter() メソッドでフィルタリングを実施しましょう。現在entries(配列)内に全データが格納されているので、ここからis_publickが0となっているもの(n.is_publick===0)を抽出して新しい変数publicEntries(配列)に格納します。.filter()メソッドは配列に対してアロー関数でentries.filter((n) => n.is_publick === 0)として処理を実行します。
    • (l.7) 最後に、フィルタリング済みのpublicEntriesを返り値として渡して完了です。
  3. (l.10-21) 続いて、各データを識別するためのslugを取得します。 (l.23-27) 内の (l.24) でslugを引数にAPIサーバの詳細データにHTTPリクエストしていますが、その引数となるslugを取得するための関数になります。これはpagesディレクトリ部分でも説明しますが、ダイナミックルーティングを実装するために必要な作業です。
    • (l.10-13) ここは (l.3-6) とほぼ同じなので省略します。
    • (l.14) フィルタリング済みのpublicEntriesについて、.map()メソッドで各要素を一度ずつ呼び出します。
    • (l.15-19) paramsキーを持つオブジェクト内で、slugキーを持ったn.slugという値をString()メソッドで文字列化して、返り値とします。
  4. (l.23-27) (l.10-21) でslugを取得したので、それを引数にAPIサーバから詳細データを取得します。
    • (l.24) 詳細データを.fetch()しています。この部分以外は (l.3-6) 、 (l.10-14) と同じなので省略します。

pagesディレクトリ下のページ

APIからのデータ取得の準備ができたところで、実際のページ作成のパートに行きましょう。

pages/index.js(Topページ)+ styles/Home.module.css

今回のブログ作成におけるTopページで、コード下の説明の通り1.と2.のように大きく2つの部分に分かれます。1.がページ部分2.がデータ取得部分です。Next.jsにおいてpagesディレクトリ直下のindex.jsファイルは、通常サブディレクトリの付かない形トップページとしてのURLになりますが、このページではトップページ下の/entriesディレクトリへの遷移と最新の1記事を表示する機能を担っています。

「一行解説」を見る
  1. (l.7-23) Topページを構成する静的な処理を記述するページコンポーネントと呼ばれる部分です。このページではHomeと名付けており、コンポーネントで作成したLayoutコンポーネントとEntryListItemコンポーネントを内部で利用しています。
    • (l.7) export default function Home({})のページコンポーネントでは、非同期関数で取得したallEntriesという値(props)を引数に設定しています。これにより、このページ内でAPIから取得した値を取り扱うことができるようになりました。取得方法は2.の部分で説明します。
    • (l.10) 作成済みのLayoutコンポーネントを埋め込んでいます。
    • (l.11-13) /entriesディレクトリへの遷移リンクを置いています。スタイルについては、下のコードの通りHome.module.cssを別途作成してそこから引っ張ってきています。Next.jsでデフォルトで提供されている<Link>については、また別記事で紹介しようと思います。
    • (l.17-19) APIから取得したallEntriesについて、作成済みの<EntryListItem>コンポーネントを当てて取り出します。なお、2.の部分で紹介しますが、今回は1要素しか取り出しはできません。
  2. (l.25-32) TopページにおいてAPIから取得した値を呼び出す非同期の関数部分です。APIサーバからのデータ取得のために作成したgetAllEntries()メソッドを利用しています。
    • (l.25) Next.jsが提供するgetStaticProps()メソッドを利用し、ビルド時にAPIサーバからデータを取得することを指示します。
    • (l.26) 事前に作成したgetAllEntries()メソッドを呼び出し、全データを取得します。
    • (l.29) 取得したデータはページコンポーネント内で使えるように、propsとして返されます。Topページでは最新記事1記事だけを表示したいので、今回は.slice()メソッドを利用することで、propsのうち1要素のみを取得しています。
    • (l.30) 生成されたページが再生成されるまでの間隔をrevalidateで指定します。これにより今回であれば3秒ごとにHTML再生成され、最新のデータを保つことができます。また、ここでrevalidateが設定されることにより、ISRが有効になります。ここでrevalidateを設定しないと通常のSSGとなります。

pages/about.js(Aboutページ)+ styles/About.module.css

今回作成するブログ自体について説明するページです。ページ数が少なく物寂しかったのでおまけとして作りました。APIサーバからデータを取得しないページなので、とても単純な記述です。Next.jsではいかに簡単にページが生成できるかが分かるかと思います。

pages/entriesディレクトリ下のページ

pages/entries/index.js(投稿一覧ページ)+ styles/AllEntries.module.css

ブログに投稿された記事一覧を参照できるページです。pages/index.js(Topページ)とほとんど同じなので説明は省略しますが、こちらのページでは (l.23) で.slice()メソッドを利用していないので、対象の全データが取得できる形になります。

pages/entries/[slug].js(個別ページ)+ styles/Entry.module.css

ブログの記事ページ(個別ページ)です。ここではダイナミックルーティングを使用して記事ページを作成します。こちらもコード下の説明の通り1.と2.のように2つの部分に分かれます。1.がページコンポーネントの部分で、2.がデータ取得部分です。

「一行解説」を見る
  1. (l.7-l.23) ページコンポーネント部分はpages/index.js(Topページ)pages/entries/index.js(投稿一覧ページ)と大きく変わりませんが、 (l.18) ではMarkdownを表示しています。
    • (l.18) 前半部分ではdangerouslySetInnerHTMLという記述がありますが、Reactにおいて直接HTMLを挿入するためのプロパティです。名前に"dangerously"とつくように、適切な対処がされない場合にセキュリティ上のリスク(XSS / クロスサイトスクリプティング)があるため、信頼できるソースからのデータ取得の場合に利用するようにしましょう。。続く__htmlプロパティには、実際に挿入されるHTMLコードが指定されます。ここではAPIサーバから取得したeachEntryというpropsにおけるbodyフィールドの表示を実行していますが、これをmarked()メソッドの引数とすることで、MarkdownテキストのHTMLへの変換を試みます。
  2. (l.25-41) pages/index.js(Topページ)pages/entries/index.js(投稿一覧ページ)同様にデータを取得する部分ですが、記事ページにおいては詳細データを記事に合わせて個別で取得する必要があります。そのために、getStaticProps()だけでなくgetStaticPaths()という関数を使用します。個別でのデータ取得は、getStaticPaths()で取得したslugをもとにgetStaticProps()が実行され個別ページが生成されるという順序で実行されます。これは全ての個別ページにおいてビルド時に実行されます。
    • (l.26) utils/fetchEntry.jsファイルで作成したgetAllEntriesSlugs()でページの全slugを取得します。
    • (l.28) getAllEntriesSlugs()で取得したpathsを返り値としています。この返り値pathsの中身は、utils/fetchEntry.jsファイルで実装したようにparams: { slug: n.slug, }という形式になっています。
    • (l.29) 2つ目の返り値にはfallbackというプロパティを設定します。これにはダイナミックルーティングに対する動作を制御する機能があり、falsetrueblockingの3つの値のいずれかを取ることができます。falseを設定すると、たとえAPIサーバ側に一致するslugの値とそれに基づくデータがあったとしても、ページがビルド時に生成されていなかった場合には404を返します。一方で、trueにすると、初回アクセス時に動的にページを生成し、2回目以降のアクセス時には静的なページとして返されます。また、blockingを使用すると、初回アクセス時にはSSRと同じ挙動になり、2回目以降はtrueと同じです。
    • (l.33) pages/index.js(Topページ)pages/entries/index.js(投稿一覧ページ)同様にgetStaticProps()を利用しつつ、今度は引数にparamsを設定して当該記事ページのslugを取得します。この引数のparamsは、getStaticPaths()で取得した値になります。
    • (l.34) utils/fetchEntry.jsファイルで作成したgetEachEntry()で詳細データを取得し、eachEntry変数に格納します。引数はgetStaticPaths()で取得したparams内のslugキーの値です。
    • (l.35-40) 返り値の1つ目にはeachEntry変数を設定しますが、 2つ目にはrevalidateを設定します。これはpages/index.js(Topページ)と同じ処理なので省略します。

動作確認

最後に今回作成したプロジェクトが問題なく作動するか確認してみましょう。

フロントエンドの動作確認

改めてnpm run devを実行してローカルサーバーを立ち上げ、フロントエンドでの動作確認をしてみましょう。下の画像の通り、ホーム画面が表示されると思います。 続いて、上の画像の"CHECK ALL ENTRIES→"を押してみると、下のような投稿一覧ページが表示されると思います。DRFプロジェクト側で3レコード作成していましたが、ここでは2レコードしか表示されていません。DRFプロジェクト側では、1レコードのみ非公開にしていたので、それがうまく反映されていることが分かります。 最後に、上の画像の"Pythonの文法"から個別の記事ページに移動してみると、下の画像のようなページが表示されていると思います。URLを見てみると、"python-grammar"と問題なくパスが取得され、ページが表示されたことが分かります。

ビルド実行

npm run buildを実行して、本番環境へのデプロイが可能な形にします。実行後下のようなメッセージがターミナルに出現すれば、ビルド完了です。Vercelでもデプロイできる状態になりました。

まとめ

以上で、Django REST FrameworkとNext.js(+α)を利用して簡単なブログ作成ができました。 こちらのeBookは今後も更新していくようにします。


記事をシェアする




記事一覧へ