SSG(Server Side Generation)を利用する機会がありそうなのでメモ。
Next.jsには9.3からSSGが導入されました。
やりたいこと
- TOPページ(index.js)に記事一覧
- [id].jsに個別ページ
- index.jsと[id].jsをSSGする
- 試しにcsr.jsも作成して、CSR(Client Side Rendering)の様子も見る
[id].jsとか[name].jsはNextでの動的ルーティングのページになります。
簡易APIの作成(Blog記事APIを想定)
blog記事をSSGすることを想定し、簡単なMock APIを作成してみます。
ここでは静的なJSONを返していますが、実際はDBやHeadlessCMS等から返すことになります。
作業場の作成と必要ファイルの生成
mkdir mock-blog-api
cd mock-blog-api
npm install express cors
touch index.js
実装
index.jsを実装していきます。
ポイントは、
- クライアントとオリジンが異なるので簡易CORS対応をしている
- ポート3001で起動している(Next側が標準で3000なのでぶつからないようにする)
といったところです。
const express = require("express");
const app = express();
//CORS対応(許可)
const cors = require("cors");
app.use(cors());
const data = {
status: "OK",
contents: [
{ id: 1, title: "記事1", body: "記事1の内容。" },
{ id: 2, title: "記事2", body: "記事2の内容。" },
{ id: 3, title: "記事3", body: "記事3の内容。" }
]
};
//記事一覧を返すAPI
app.get("/blog", (req, res) => {
res.json(data);
});
//個別記事を返すAPI
app.get("/blog/:id", (req, res) => {
const id = req.params.id;
res.json(data.contents.find(d => d.id == id));
});
//サーバリッスン開始
app.listen(3001, () => {
console.log("Server start on port 3001.");
});
実行
実行しておきます(Nextの実装後でもいいです)。
node index.js
Next.jsの実装
API側ができたので本丸のNext側の実装です。
プロジェクトの生成
nodeは入ってる前提でnpxでcraete-next-appを呼び出します。
npx create-next-app next-ssg-test
cd next-ssg-tset
必要ファイルの生成
pages以下にblogディレクトリを生成し、その中に[id].jsを生成。
mkdir pages/blog
touch pages/blog/[id].js
いちおうcsrのコードを確認するためcsr.jsも作成し実装してみます。
touch pages/csr.js
index.js(記事一覧)
getStaticPropsがサイト実行前にサーバで実行され内容を反映したページが生成されます。
import Link from "next/link";
const Home = ({ blog }) => {
return (
<div>
<h1>記事一覧(SSG)</h1>
<ul>
{blog.map((blog) => (
<li key={blog.id}>
<Link href={`/blog/${blog.id}`}>
<a>{blog.title}</a>
</Link>
</li>
))}
</ul>
<p><Link href="/csr"><a>CSRページへ</a></Link></p>
</div>
);
}
//SSG処理
export const getStaticProps = async () => {
const res = await fetch("http://localhost:3001/blog");
const json = await res.json();
return {
props: {
blog: json.contents,
}
}
}
export default Home;
[id].js(個別ページ)
getStaticPathsで生成するページ(path)のパターンを定義。
getStaticPropsでそのパターンでの呼び出しを定義します。
import Link from "next/link";
const BlogId = ({ blog }) => {
return (
<div>
<h1>{blog.title}</h1>
<p>{blog.body}</p>
<br />
<p><Link href="/"><a>戻る</a></Link></p>
</div>
);
}
export default BlogId;
//どんなpathがあるか
export const getStaticPaths = async () => {
const res = await fetch("http://localhost:3001/blog");
const json = await res.json();
const paths = json.contents.map((content) => `/blog/${content.id}`);
return { paths, fallback: false };
}
//各ページのレンダリング
export const getStaticProps = async (context) => {
const id = context.params.id;
const res = await fetch(`http://localhost:3001/blog/${id}`)
const json = await res.json();
return {
props: {
blog: json
}
}
}
csr.js(普通の?React的CSR)
useStateとuseEffectを利用した一般的なAPI連携。
import Link from "next/link";
import { useState, useEffect } from "react";
const CsrPage = () => {
//contentの入れ物
const [contents, setContents] = useState([]);
//ページ読み込み時に取得(CSR)
useEffect(() => {
const getContents = async () => {
const res = await fetch("http://localhost:3001/blog");
const json = await res.json();
setContents(json.contents);
}
getContents();
}, []);
return (
<div>
<h1>記事一覧(CSR)</h1>
<ul>
{contents.map((content) => (
<li key={content.id}>
<Link href={`/blog/${content.id}`}>
<a>{content.title}</a>
</Link>
</li>
))}
</ul>
<Link href="/"><a>戻る</a></Link>
</div>
);
}
export default CsrPage;
実行
### 開発モード(開発中)
以下のようにします。
npm run dev
本番モード(SSG処理)
run buildでビルドし、生成ぶつを実行するにはrun startとします。
buildの結果物は.next/server/pages以下に生成されるようです。
npm run build
npm run start
その他
これらをgithub軽油でvercelやgithub pagesなどにデプロイします。
おまけ
ついでにuseSWRを利用してデータを取得してみます。
SWRはRenderingの種類ではない。Stale-While-Revalidateというキャッシュ戦略の略。
touch pages/swr.js
実装。シンプルに書け、かついろいろ便利な機能があるが、ここでは以上とします。
import useSWR from "swr";
import Link from "next/link";
//fetcherの定義
const url = "http://localhost:3001/blog";
const fetcher = () => fetch(url).then(r => r.json());
const SwrPage = () => {
//swr
const { data, error } = useSWR(url, fetcher);
//エラーやローディング中の処理
if (error) return <div>fail to load.</div>
if (!data) return <div>Loading...</div>
return (
<div>
<h1>記事一覧(SWR利用)</h1>
<ul>
{data.contents.map((content) => (
<li key={content.id}>
<Link href={`/blog/${content.id}`}>
<a>{content.title}</a>
</Link>
</li>
))}
</ul>
</div>
);
}
export default SwrPage;