Next.js 9.3
本日(2020/03/10)リリースされたNext.js 9.3では、主に静的サイト生成(Static Site Generation)の機能が追加されました。
これにより、今までgetInitialProps
でSSRしていたものも、静的なHTML + JSONとしてビルド時に吐き出されるようになりました。
早速Next.jsをupgradeし実装してみたところ、
(自作のmarkdownブログでは少しですが)高速化!
getInitialPropsで書いていた部分を置き換えるだけで実装できたので、コードとともに紹介します。
サイトはこちら -> ragnar blog
4/17追記) 結局MarkdownをやめてGraphQLバックエンドになりました
新機能
SSG用のメソッドとしては、
- getStaticProps
- getStaticPaths
の2つが追加されました。
getStaticProps
getStaticProps
は、getInitialProps
で行っていた外部リソースの取得を行うメソッドです。
getInitialProps
との違いは、常にリソースを取得するわけではなく、ビルド時に一度だけ実行されます。
export async function getStaticProps() {
const res = await fetch('https://.../posts')
const posts = await res.json()
return {
props: {
posts,
},
}
}
この場合、postsはビルド時一度だけ実行され、更新されません。
参考: 公式Doc
getStaticPaths
getStaticPaths
は、名前の通りパスを取得するメソッドです。
pages/post/[postId].tsx
というファイルを作れば、/post/1, /post/2...と動的なルーティングが可能です。
[postId].tsx
にgetStaticPaths
を記述し、生成されるパスを取得するようにしておけば、Next.jsがビルド時にルーティング先を静的に事前レンダリングしてくれます。
export async function getStaticPaths() {
const res = await fetch('https://.../posts')
const posts = await res.json()
const paths = posts.map(post => ({
params: { id: post.id },
}))
return { paths, fallback: false }
}
fallback
は、事前生成したルーティングに一致しないURLへのリクエストが来た際の挙動を示します。
fallback = true
の場合、事前ビルドされていないパスに対して404を返却せず、getStaticProps
を呼び出しそのパスに対する応答を返すようです。
公式Docでは高速なビルドをしつつ、データに依存する非常に多数の静的ページがある場合に便利とのこと。
静的ページとして吐き出す際、[slag].tsx
ファイルにはgetStaticPaths
およびgetStaticProps
を実装している必要があるので注意が必要です。
参考: 公式Doc
従来のコード
Next.jsでのmarkdownブログは、Page.getInitialProps
でmarkdownを読み出していました。
- 自作サイトのIndexページの例
import fs from 'fs';
import { NextPage } from 'next';
type CodeProps = {
code: string;
language: string;
};
type InitialProps = {
profile: CodeProps;
site: CodeProps;
};
// InitialPropsはgetInitialPropsの返り値型
const Index: NextPage<InitialProps> = props => {
return (
<>
...略
</>
)
};
Index.getInitialProps = async () => {
const profile = fs.readFileSync("./src/codes/profileCode.py");
const site = fs.readFileSync("./src/codes/thisSite.ts");
return {
profile: { code: profile.toString(), language: "python" },
site: { code: site.toString(), language: "typescript" },
};
};
export default Index
このコードでは、リクエストの度に毎回fs.readFileSync
を実行しファイルを読み出していました。
これをgetStaticProps
に書き換えることで、毎回取得する必要がなくなります。
import { GetStaticProps, NextPage } from 'next';
// Index.getInitialPropsのところを書き換える
export const getStaticProps: GetStaticProps = async () => {
const profile = fs.readFileSync("./src/codes/profileCode.py");
const site = fs.readFileSync("./src/codes/thisSite.ts");
return {
props: {
profile: { code: profile.toString(), language: "python" },
site: { code: site.toString(), language: "typescript" },
},
};
};
getInitialProps
とは異なり、ファイル内にexport async function getStaticProps
としておくだけで良いようです。
- 型定義
GetStaticProps
の型定義はこんな感じ。
今までgetInitialProps
で返却していたオブジェクトを、{props: Object}
とするだけです。
動的ルーティング
先の例はパスに依存しない、静的なページでの外部リソース読み込みの例でしたが、次は動的ルーティングを行っていたページを書き換えます。
markdownブログでは、/post
では特定のフォルダ内のファイルを読み込み、その数だけファイル名でリンクを作成し、/post/[postId]
ページで対象のmdファイルを読み込み、renderしていました。
省略していますが、以前のコードは大体こんな感じ。
import matter from 'gray-matter';
import { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
type Props = {
posts: {
fileName: string;
md: matter.GrayMatterFile<any>;
}[];
};
export const PostListPage: NextPage<Props> = props => {
const posts = props.posts;
return (
<>
<Head>
<title key="title">記事一覧 - Ragnar Blog</title>
</Head>
<Title>記事一覧</Title>
{posts.map((item, key) => (
<PostCard key={key} fileName={item.fileName} md={item.md} />
))}
</>
);
};
PostListPage.getInitialProps = async function() {
// get all .md files from the src/posts dir
const contexts = require.context("../../md", true, /\.md$/);
const posts = contexts.keys().map(path => {
// 拡張子を省いたファイル名
const fileName = path.match(/([^/]*)(?:\.([^.]+$))/)[1];
const mdData = matter(contexts(path).default);
return {
fileName: fileName,
md: mdData,
};
});
// postを日付でソート
posts.sort((a, b) => {
if (a.md.data.date > b.md.data.date) return -1;
if (a.md.data.date < b.md.data.date) return 1;
return 0;
});
return {
posts: posts,
};
};
export default PostListPage;
import matter from 'gray-matter';
import { NextPage, NextPageContext } from 'next';
import Head from 'next/head';
import React from 'react';
type Props = {
md?: matter.GrayMatterFile<any>;
};
const PostDetailPage: NextPage<Props> = props => {
const meta = props.md.data;
const formattedDate = meta.date ? dateFormat(meta.date) : "2000-01-01";
return (
<>
<Head>
<title key="title">{meta.title} - Ragnar Blog</title>
</Head>
<TitleHeader title={meta.title} date={formattedDate} />
<MarkDownViewer md={props.md.content} />
</>
);
};
// postIDから同じファイル名のファイルを読み出してPropsとして渡す
PostDetailPage.getInitialProps = async (context: NextPageContext): Promise<any> => {
const { postId } = context.query;
const contexts = require.context("../../md", true, /\.md$/);
const content = contexts.keys().filter(path => {
const fileName = path.match(/([^/]*)(?:\.([^.]+$))/)[1];
return fileName === postId;
});
const data = matter(contexts(content[0]).default);
return { md: data };
};
export default PostDetailPage;
書き換え
[postId].tsx
から書き換えて行きます。
getStaticPaths
およびgetStaticProps
を記述します。
- 型定義
GetStaticPaths
の型定義。
pathsにルーティングするパスの配列を入れるだけです。
今回は存在するファイルの数しかルーティングは出来ないので、fallbackはfalseです。
getInitialProps
の場合context.queryからslugを取得していましたが、
getStaticProps
ではcontext.params
に変わりました。
import { GetStaticPaths, GetStaticProps, NextPage } from 'next';
const PostDetailPage: NextPage<Props> = props => {
return <>...</>
}
export const getStaticPaths: GetStaticPaths = async function() {
const contexts = require.context("../../md", true, /\.md$/);
const allPosts = contexts.keys().map(path => {
const fileName = path.match(/([^/]*)(?:\.([^.]+$))/)[1];
return { params: fileName };
});
// /post/fileName がパス
return {
paths: allPosts.map(post => `/post/${post.params}`) || [],
fallback: false,
};
};
export const getStaticProps: GetStaticProps = async context => {
// slugはcontext.paramsから取得
const { postId } = context.params;
const contexts = require.context("../../md", true, /\.md$/);
const content = contexts.keys().filter(path => {
const fileName = path.match(/([^/]*)(?:\.([^.]+$))/)[1];
return fileName === postId;
});
const matterData = matter(contexts(content[0]).default);
delete matterData.orig;
if (matterData.data.date) {
matterData.data.date = new Date(matterData.data.date).toISOString();
}
// { props: Props }にする
return { props: { md: matterData } };
};
デプロイして確認
Nowにデプロイしてみると、/post/[postId]
以下のページが生成されており、それらが○ (SSG)
となっていることがわかります。
同時に、ビルド時間が伸びていることもわかります。
サイトで確認
サイトでも確認してみます。
投稿一覧ページから、投稿詳細ページへの遷移は体感半分程度になった気がします。
もともとReactMarkDown
のレンダリングがボトルネックになっているところもあり、差は小さいのですが・・・
終わりに
getInitialProps
を利用していた部分の書き換えは思っていた以上にそのままでした。
getInitialProps
はビルド、サーバーサイド、クライアントサイドで実行されうるメソッドだったので、今回からはより挙動も把握しやすくなったため、
9.3以降では主にgetStaticProps
等が利用されていくことになると思います。
Next + NowのDXは素晴らしいですが、SSGも強化されてさらにNext推しになってしまいそうですね。
ブログも随時更新中ですのでよければ。 -> ragnar blog