【Part 1】では各種基礎知識について、【Part 2】ではバックエンドとしてDjango REST Framework(以下DRF)部分の開発を進めましたが、【Part 3】ではフロントエンドとしてのNext.js部分の開発をしていきましょう。
まずはNext.jsで開発するための環境を作っていきます。なお環境構築についてはNext.js + Tailwind CSSの環境構築 の記事で紹介したことがあるため一部説明を省略しますが、今回は基礎からということでTailwindCSSを利用せずNext.jsのみで環境構築してみたいと思います。
こちらも過去記事ではセクションに分けて作成して解説していましたが、DRF同様にサクサクいきましょう。
npm`を使用する形式でNext.jsプロジェクト(blog_front
)を作成する。
blog_front
)に移動して、プロジェクト直下にcomponents
ディレクトリ、config
ディレクトリ、utils
ディレクトリを、pages
ディレクトリ下にentries
ディレクトリを作成します。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と変わらないためほとんど省略しています。
ページ共通のヘッダーを作成します。
ページ共通のフッターを作成します。
target="_blank" rel="noopener noreferrer"
としているのは、アイコンクリック時に新しくタブを開いてページを開くためです。ページ共通のフッターを作成します。上で作ったヘッダーとフッターを組み込みます。
children
プロパティを設定することでレイアウトコンポーネントを任意のページで再利用することができます。ページ作成の章ではこのレイアウトコンポーネントが多数使われますが、このファイル内の (l.10) 部分の{children}
に各ページの要素が入るというイメージです。今回の (l.8) <Header/>
と (l.12) <Footer/>
は{children}
の外側に記載されているので、レイアウトコンポーネントが利用される限り対象のページ内で付随して設定されるため、ページ側で個別に設定する必要はありません。続いて個別の部品を作成します。
components/EntryListItem.js
は投稿一覧ページにおいて、投稿タイトルをキーに縦に並べて表示するためのコンポーネントです。このコンポーネントから詳細ページに遷移するようなイメージです。
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ファイルを新しく用意し作成していきます。コンポーネントで作成した部品をページに組み込みつつ作成していくイメージです。
今回のページ作成は静的データだけではなく、Django REST Framework(DRF)プロジェクトで作成したAPIから取得する動的なデータも組み合わせて作成します。そのため、ページ作成の前にAPIからデータを取得する仕組みを先に構築します。データ取得のためには、下の通りconfig/index.js
ファイルとutils/fetchEntry.js
ファイルを作成します。
process.env.NEXT_PUBLIC_API_URL
に値が入っていれば優先的にそちらを参照しますが、今回は設定していないのでDRFプロジェクトが立ち上がっているローカルサーバを参照します。この記述方法は本番環境とローカル環境の使い分けをしたい場合に便利です。config/index.js
で設定したAPIのURLをここで早速利用します。Entry
モデルに当てはまる全てのデータを、Next.jsプロジェクトの任意のページから非同期(async/await
)で取得する方法をまとめて記述しておきます。
function
宣言前に、export
と書くことで他のファイルでも当該関数をimport
して利用できることを、async
と書くことで非同期処理を含む(async)関数であることを、宣言しています。.fetch()
メソッドでAPIサーバにHTTPリクエストをして(今回はGETメソッドしか利用ができないので)GETメソッドで取得したデータを変数res
に入れますが、HTTPレスポンス(データの取得完了)までには時間がかかる場合があるため、処理の途中で次行の処理に移行してしまわないために、await
でデータ取得の実行完了を待機します。.json()
メソッドで変数res
内のレスポンスオブジェクトをJSONで取得し変数entries
に入れますが、その処理の実行完了をawait
で待機します。Entry
モデルにおいてis_publick
フィールドを作成し、記事の公開・未公開を設定したのを覚えていますでしょうか?その際、0を公開、1を未公開(ドラフト)としましたが、未公開記事は取得しないように.filter()
メソッドでフィルタリングを実施しましょう。現在entries
(配列)内に全データが格納されているので、ここからis_publickが0となっているもの(n.is_publick===0
)を抽出して新しい変数publicEntries
(配列)に格納します。.filter()
メソッドは配列に対してアロー関数でentries.filter((n) => n.is_publick === 0)
として処理を実行します。publicEntries
を返り値として渡して完了です。slug
を取得します。 (l.23-27) 内の (l.24) でslug
を引数にAPIサーバの詳細データにHTTPリクエストしていますが、その引数となるslugを取得するための関数になります。これはpages
ディレクトリ部分でも説明しますが、ダイナミックルーティングを実装するために必要な作業です。
publicEntries
について、.map()
メソッドで各要素を一度ずつ呼び出します。params
キーを持つオブジェクト内で、slug
キーを持ったn.slug
という値をString()
メソッドで文字列化して、返り値とします。.fetch()
しています。この部分以外は (l.3-6) 、 (l.10-14) と同じなので省略します。APIからのデータ取得の準備ができたところで、実際のページ作成のパートに行きましょう。
今回のブログ作成におけるTopページで、コード下の説明の通り1.と2.のように大きく2つの部分に分かれます。1.がページ部分2.がデータ取得部分です。Next.jsにおいてpages
ディレクトリ直下のindex.js
ファイルは、通常サブディレクトリの付かない形トップページとしてのURLになりますが、このページではトップページ下の/entries
ディレクトリへの遷移と最新の1記事を表示する機能を担っています。
Layout
コンポーネントとEntryListItem
コンポーネントを内部で利用しています。
export default function Home({})
のページコンポーネントでは、非同期関数で取得したallEntries
という値(props
)を引数に設定しています。これにより、このページ内でAPIから取得した値を取り扱うことができるようになりました。取得方法は2.の部分で説明します。Layout
コンポーネントを埋め込んでいます。/entries
ディレクトリへの遷移リンクを置いています。スタイルについては、下のコードの通りHome.module.css
を別途作成してそこから引っ張ってきています。Next.jsでデフォルトで提供されている<Link>
については、また別記事で紹介しようと思います。allEntries
について、作成済みの<EntryListItem>
コンポーネントを当てて取り出します。なお、2.の部分で紹介しますが、今回は1要素しか取り出しはできません。getAllEntries()
メソッドを利用しています。
getStaticProps()
メソッドを利用し、ビルド時にAPIサーバからデータを取得することを指示します。getAllEntries()
メソッドを呼び出し、全データを取得します。props
として返されます。Topページでは最新記事1記事だけを表示したいので、今回は.slice()
メソッドを利用することで、propsのうち1要素のみを取得しています。revalidate
で指定します。これにより今回であれば3秒ごとにHTML再生成され、最新のデータを保つことができます。また、ここでrevalidate
が設定されることにより、ISRが有効になります。ここでrevalidate
を設定しないと通常のSSGとなります。今回作成するブログ自体について説明するページです。ページ数が少なく物寂しかったのでおまけとして作りました。APIサーバからデータを取得しないページなので、とても単純な記述です。Next.jsではいかに簡単にページが生成できるかが分かるかと思います。
ブログに投稿された記事一覧を参照できるページです。pages/index.js(Topページ)
とほとんど同じなので説明は省略しますが、こちらのページでは (l.23) で.slice()
メソッドを利用していないので、対象の全データが取得できる形になります。
ブログの記事ページ(個別ページ)です。ここではダイナミックルーティングを使用して記事ページを作成します。こちらもコード下の説明の通り1.と2.のように2つの部分に分かれます。1.がページコンポーネントの部分で、2.がデータ取得部分です。
pages/index.js(Topページ)
やpages/entries/index.js(投稿一覧ページ)
と大きく変わりませんが、 (l.18) ではMarkdownを表示しています。
dangerouslySetInnerHTML
という記述がありますが、Reactにおいて直接HTMLを挿入するためのプロパティです。名前に"dangerously"とつくように、適切な対処がされない場合にセキュリティ上のリスク(XSS / クロスサイトスクリプティング)があるため、信頼できるソースからのデータ取得の場合に利用するようにしましょう。。続く__htmlプロパティ
には、実際に挿入されるHTMLコードが指定されます。ここではAPIサーバから取得したeachEntry
というprops
におけるbody
フィールドの表示を実行していますが、これをmarked()
メソッドの引数とすることで、MarkdownテキストのHTMLへの変換を試みます。pages/index.js(Topページ)
やpages/entries/index.js(投稿一覧ページ)
同様にデータを取得する部分ですが、記事ページにおいては詳細データを記事に合わせて個別で取得する必要があります。そのために、getStaticProps()
だけでなくgetStaticPaths()
という関数を使用します。個別でのデータ取得は、getStaticPaths()
で取得したslug
をもとにgetStaticProps()
が実行され個別ページが生成されるという順序で実行されます。これは全ての個別ページにおいてビルド時に実行されます。
utils/fetchEntry.js
ファイルで作成したgetAllEntriesSlugs()
でページの全slug
を取得します。getAllEntriesSlugs()
で取得したpaths
を返り値としています。この返り値paths
の中身は、utils/fetchEntry.js
ファイルで実装したようにparams: { slug: n.slug, }
という形式になっています。fallback
というプロパティを設定します。これにはダイナミックルーティングに対する動作を制御する機能があり、false
・true
・blocking
の3つの値のいずれかを取ることができます。false
を設定すると、たとえAPIサーバ側に一致するslug
の値とそれに基づくデータがあったとしても、ページがビルド時に生成されていなかった場合には404を返します。一方で、true
にすると、初回アクセス時に動的にページを生成し、2回目以降のアクセス時には静的なページとして返されます。また、blocking
を使用すると、初回アクセス時にはSSRと同じ挙動になり、2回目以降はtrue
と同じです。pages/index.js(Topページ)
やpages/entries/index.js(投稿一覧ページ)
同様にgetStaticProps()
を利用しつつ、今度は引数にparams
を設定して当該記事ページのslug
を取得します。この引数のparams
は、getStaticPaths()
で取得した値になります。utils/fetchEntry.js
ファイルで作成したgetEachEntry()
で詳細データを取得し、eachEntry
変数に格納します。引数はgetStaticPaths()
で取得したparams
内のslug
キーの値です。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は今後も更新していくようにします。