# Next.jsのチュートリアル、その3
Next.jsのチュートリアル、その1、Next.jsのチュートリアル、その2に続いて3つ目です。
# Pre-rendering and Data Fetching
今回はブログサイトを作っていくチュートリアルということでは、まずはファイルベースでやっていくけど、そのうちデータベースとかHeadless CMSとかもやってくよ的な感じ。
今回は、Next.jsのpre-rendering機能、Static GenerationとServer-side Rendering、Static Generationのデータがある場合と無い場合、getStaticProps
を使って外部データを取り込んだり、便利な使い方だったり、、みたいなことを学んでいきますよ、と。
# Pre-rendering
ってことでデータのフェッチをしていく前にNext.jsにおてとても大事なコンセプトであるPre-renderingについて。
デフォルトではNext.jsは全てのページをpre-renderします、と。これの意味するところとしては、全部のページについてHTMLを事前に作るということで、クライアント側のJavaScriptで諸々やりくりするのとも違う。Pre-renderingはパフォーマンスも早いし、SEOにも有利、とのこと。
そして、生成された各HTMLは最小限のJavaScriptしか関連させないようになっていつつ、ページがブラウザにロードされればそのページはいい感じにインタラクティブに動く、ということをhydration(ハイドレーション)と呼ぶんだそうです。
# Pre-renderingの詳細
以下の手順でやってみると、
- JavaScriptをブラウザで無効にする
- ページにアクセスしてみる
確かに、JavaScriptが無い状態でもちゃんと画面が表示されています。プレーンなReactアプリだったらこうはいかないのだそうで。
Hydrationというのは、HTMLが最初に表示されてReactのコンポーネントがイニシャライズされると、以前のLinkコンポーネントのようにブラウザに依存せずにインタラクティブに動作する的な振る舞い。そもそも、もし、Pre-renderingでなければ何も画面に表示されまへん、と。
# Pre-renderingの2つの方法
Static GenerationとServer-side Renderingというのがありまして、と。
- Static Generationはビルド時にHTMLを作るヤツ
- Server-side Renderingはリクエスト毎にHTMLを作るヤツ
また、Static Generationであっても開発モード(npm run dev)の場合は全てのページはリクエスト毎にレンダリングされます、と。
# ページ毎に選択できる
Next.jsではページ毎にStatic GenerationとServer-side Renderingを選択できますよ、と。これをhybrid(ハイブリッド)と呼ぶそうですが、ほとんどのページをStatic Generationにして、その他をServer-side Renderingにするとか。
オススメはデータがあってもなくてもStatic GenerationでCDNにスタティックなファイルを置いておくだけで良いので早いし良いよ、と。
- マーケティングなランディングページ
- ブログ記事
- eコマースの商品情報
- ヘルプサイトとかドキュメントサイト
この辺は使い所な感じだそうで。ってか大体Static Generationで良さそうだけど、常にup-to-dateが求められるところだったら、Server-side Renderingを使うか、もしくはpre-renderingをスキップしてクライアント側のJavaScriptで更新データを取りに行くような実装にすると良い。
ってことで、今回のレッスンはStatic Generationにフォーカスしつつ、データの扱いをやっていく感じ。
# Pre-rendering と データのフェッチ
HTMLを生成するのに外部データにアクセスする必要がある時。例えばファイルシステムにアクセスしたり、外部のAPI呼んだり、データベースにクエリしたり。Next.jsはout-of-the-boxでStatic Generation with dataできるよ、と。
ってことで、getStaticProps
を使ったデータの取得。
Next.jsではgetStaticProps
というasyncファンクションを使って以下のようにやりくりできます、と。
export default function Home(props) { ... }
export async function getStaticProps() {
// ファイル、API、データベースなど外部のソースからデータを取得
const data = ...
// `props` のvalueがHomeコンポーネントに渡る
return {
props: ...
}
}
でもって、開発モードの場合はリクエスト毎にgetStaticPropsが実行されますよ的な話があって、実際やってみましょう。
# ブログのデータ
マークダウンファイルに書かれたブログデータを使ってやっていく。
- postsというディレクトリをトップレベルディレクトリに作る(以前作ったpages/postsではなく)
- postsの中は
pre-rendering.md
とssg-ssr.md
という名前のファイルを置く
(なんかチョイチョイSSGっていうのが出てくるというか、残ってるというか、だけど、昔はStatic Site Generationって呼んでた名残なのかなぁ…)
ってことで、posts/pre-rendering.md
の方は↓こんな感じ。
---
title: 'Pre-renderingの2つのやり方'
date: '2021-04-30'
---
Next.js has two forms of pre-rendering: **Static Generation** and **Server-side Rendering**. The difference is in **when** it generates the HTML for a page.
- **Static Generation** is the pre-rendering method that generates the HTML at **build time**. The pre-rendered HTML is then _reused_ on each request.
- **Server-side Rendering** is the pre-rendering method that generates the HTML on **each request**.
Importantly, Next.js lets you **choose** which pre-rendering form to use for each page. You can create a "hybrid" Next.js app by using Static Generation for most pages and using Server-side Rendering for others.
そして、posts/ssg-ssr.md
の方は
---
title: 'Static Generationを使う時 v.s. Server-side Renderingを使う時'
date: '2021-05-01'
---
We recommend using **Static Generation** (with and without data) whenever possible because your page can be built once and served by CDN, which makes it much faster than having a server render the page on every request.
You can use Static Generation for many types of pages, including:
- Marketing pages
- Blog posts
- E-commerce product listings
- Help and documentation
You should ask yourself: "Can I pre-render this page **ahead** of a user's request?" If the answer is yes, then you should choose Static Generation.
On the other hand, Static Generation is **not** a good idea if you cannot pre-render a page ahead of a user's request. Maybe your page shows frequently updated data, and the page content changes on every request.
In that case, you can use **Server-Side Rendering**. It will be slower, but the pre-rendered page will always be up-to-date. Or you can skip pre-rendering and use client-side JavaScript to populate data.
マークダウンのメタデータのtitle
とdate
はYAML Front Matterと呼ばれるやるでgray-matterってライブラリでパース出来ます、と。
ってことで、さっそくこのデータをgetStaticPropsを使って取得してみましょう的な。これにはpages/index.js
にコードを書いていく感じ。やることは
- markdownをパースして
title
とdate
とファイル名(id
として使う)を取得 - 日付順にリスト表示する
ってことで、まずは gray-matter のインストールから
$ npm install gray-matter
added 10 packages, and audited 287 packages in 2s
44 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
でもって、これをつかってファイルからデータを取得するのをライブラリ化する感じにして、トップレベルディレクトリにlibというのを作ってそこにposts.js
というのを作っていきます、と。
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
const postsDirectory = path.join(process.cwd(), 'posts')
export function getSortedPostsData() {
// /posts 配下にあるファイル名を取得
const fileNames = fs.readdirSync(postsDirectory)
const allPostsData = fileNames.map(fileName => {
// ".md" をファイル名から取り除いてidに代入
const id = fileName.replace(/\.md$/, '')
// 文字列としてmarkdownファイルを読み取る
const fullPath = path.join(postsDirectory, fileName)
const fileContents = fs.readFileSync(fullPath, 'utf8')
// gray-matter を使ってメタデータをパース
const matterResult = matter(fileContents)
// idとパースしたデータを返す
return {
id,
...matterResult.data
}
})
// dateでソート
return allPostsData.sort((a, b) => {
if (a.date < b.date) {
return 1
} else {
return -1
}
})
}
(なんか、このdateでソートしてるところのallPostsData.sortっていつ呼ばれるんだろうか…?)
でもって、pages/index.js
の中のgetStaticProps用に上記のgetSortedPostsDataをimportしましょうね、と。
ってことで、pages/index.js
import Head from 'next/head'
import Layout, { siteTitle } from '../components/layout'
import utilStyles from '../styles/utils.module.css'
import { getSortedPostsData } from '../lib/posts'
export async function getStaticProps() {
const allPostsData = getSortedPostsData()
return {
props: {
allPostsData
}
}
}
export default function Home() {
return (
<Layout home>
<Head>
<title>{siteTitle}</title>
</Head>
<section className={utilStyles.headingMd}>
<p>Yo!!</p>
<p>
(This is a sample website - you’ll be building a site like this on{' '}
<a href="https://nextjs.org/learn">our Next.js tutorial</a>.)
</p>
</section>
</Layout>
)
}
(少し長いけど、現時点でのコードのスナップショット)
そして、Homeコンポーネントに新しく<section>
タグを加えて、以下のように。
import Head from 'next/head'
import Layout, { siteTitle } from '../components/layout'
import utilStyles from '../styles/utils.module.css'
import { getSortedPostsData } from '../lib/posts'
export async function getStaticProps() {
const allPostsData = getSortedPostsData()
return {
props: {
allPostsData
}
}
}
export default function Home({ allPostsData }) {
return (
<Layout home>
<Head>
<title>{siteTitle}</title>
</Head>
<section className={utilStyles.headingMd}>
<p>Yo!!</p>
<p>
(This is a sample website - you’ll be building a site like this on{' '}
<a href="https://nextjs.org/learn">our Next.js tutorial</a>.)
</p>
</section>
<section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}>
<h2 className={utilStyles.headingLg}>Blog</h2>
<ul className={utilStyles.list}>
{allPostsData.map(({ id, date, title }) => (
<li className={utilStyles.listItem} key={id}>
{title}
<br />
{id}
<br />
{date}
</li>
))}
</ul>
</section>
</Layout>
)
}
(なんかおじさん的にはさっきのsortもそうだけど {allPostsData.map(({ id, date, title }) => (
これでいい感じに回せちゃうのもなんかしっくりこないんだけど…笑)
と、そんなこんなで↓ちゃんと出ました〜
# getStaticPropsの詳細
上記ではファイルシステムからデータを取得してきたけど↓のようにAPIを使ってデータを取得することもできますよ、と。
export async function getSortedPostsData() {
// ファイルシステムからデータを取得するのではなく、
// 外部のAPIエンドポイントからデータを取得する
const res = await fetch('..')
return res.json()
}
また、Next.jsは自身のfetchファンクションをクライアント側/サーバー側ともに持っているため、importはしなくてもOKとのこと。
データベースについても同様に↓のように。
import someDatabaseSDK from 'someDatabaseSDK'
const databaseClient = someDatabaseSDK.createClient(...)
export async function getSortedPostsData() {
// SQLでデータベースからデータを取得する
return databaseClient.query('SELECT posts...')
}
getStaticProps
はサーバー側でのみ動作し、JavaScriptのバンドルすらクライアントのブラウザには含まれない。なのでここに書かれたSQLはブラウザから参照することはできない。
開発環境とプロダクションっていうところで、今まで使ってきた開発環境のnpm run dev
だと毎回getStaticPropsが呼ばれるけど、プロダクションではビルド時にしか呼ばれません、と。ただし、fallbackを使うとこれをハンスすることができるのだそうで。
また、ビルド時に動くことを前提にしているので、リクエスト時に来るようなクエリパラメータやHTTPのヘッダを使うことはできません。
でもって、getStaticProps
はpageからしかexportすることはできなくて、それはReactがページをレンダリングする時に全てのデータが必要、というところから来るのだそうです。
んじゃ、リクエストの時のデータをフェッチする必要があったら?っていうような場合はServer-side Renderingとかpre-renderingをスキップするとか。んま、その辺の戦略はまた次で。
# リクエスト時のデータ取得
Server-side RenderingにおいてはgetServerSideProps
というファンクションが使えるよ、と。(getStaticProps
はあくまでもStatic Generation)
んま、実際今回は使わないけど、実装としては以下のような感じ。
export async function getServerSideProps(context) {
return {
props: {
// props for your component
}
}
}
getServerSidePropsはリクエスト時に呼ばれるので(context
)にはリクエスト時のパラメータが入ってきますよ、と。そして、至るところに出てくる気がするけど、これやると遅いし、CDNにキャッシュできないからね、、的な話。
ってことで、もし、pre-renderしなくてイイんだったら、Client-side Renderingという戦略を取ることもできますよ、と。
これは、、
- 外部データを使わないところはpre-renderingしておいて
- ページがロードされた時にクライアントのJavaScriptを使って必要な部分を取りに行く、と。
このアプローチはユーザーダッシュボードとか、プライベートでユーザー固有でSEOとか意識する必要がなく、pre-renderしなくてもいい時に有効。そして、データは頻繁に更新されて、最新の情報をリクエスト時に取得する必要がある時。
# SWR
Next.jsはデータを取得する用のReeact hookを作りました、と。クライアント側でデータを取得する時にはこれを使うのがオヌヌメ、と。caching, revalidation, focus tracking, refetching on interval,,,とかっていう多機能な感じなのだそうで。
確かにパッと見ただけで便利そうや↓
import useSWR from 'swr'
function Profile() {
const { data, error } = useSWR('/api/user', fetch)
if (error) return <div>failed to load</div>
if (!data) return <div>loading...</div>
return <div>hello {data.name}!</div>
}
ってことで、Pre-rendering and Data Fetchingは以上で、次回はDynamic Routesよん。