Next.js
#React
勉強用Githubリポジトリ
ReactについてはReactの勉強を参照
プリレンダリングのことがわかって来て、Next.jsはビルド時に情報が決まる静的ページの多いサイトに長けていそうと感じた。
webpackやんなくて良いのと自動ルーティング、静的生成、コード分割なんかができるのは Nuxtと同様。
Nextの大きな特徴は静的生成を好きなところで使えるため、パフォーマンスチューニングを追い込みやすいこと。
getStaticPropsでAPI通信によるデータをビルド時に行い、静的生成しておける(ビルド時に全て決まっていれば)
getStaticPathsで動的ルーティングを行うページを全てビルド時に静的生成しておける(ビルド時に全て決まっていれば)
一方Nuxtはプロジェクト作成時のベースがしっかりしいて、よりCoCなので、DXと開発の初速に長ける。
あとVercelすごい。早くもNetlifyから乗り換えかな
Nuxtのホスティングもできる
ビルドするディレクトリをGithub上にあっても選べる
Github連携のPreview Deployができる
参考:大幅にリニューアルされた Next.js のチュートリアルをどこよりも早く全編和訳しました - Qiita
ブログアプリを作るNext.jsの新しい公式チュートリアルの和訳されたものを読んでみる
Next.jsとは
Next.jsはReactのフレームワーク
webpackのめんどくささから解放してくれる
自動ルーティング
コード分割の最適化を勝手にやってくれる
ページごとにSSRとCSRを設定できる
CSS in JSライブラリなどのサポート
セットアップ
必要なもの
Node.js
git
$ npm init next-app nextjs-blog --example "https://github.com/zeit/next-learn-starter/tree/master/learn-starter"
このテンプレートでプロジェクトを作成。他にも色々
$ cd nextjs-blog
$ npm run dev もしくは yarn dev
http://localhost:3000/ で動作確認
ファイルに変更があるとリロードなしに変更を画面に適用してくれる。(Hot Module Replacement)
ページルーティング
NextもNuxtと同じようにディレクトリ階層とファイル名を元にルーティングが自動で行われる。
code: pages/posts/first-post.js
export default function FirstPost() {
return <h1>First Post</h1>
}
http://localhost:3000/posts/first-post.js にアクセス -> First Postと表示される
ページタイトルなどのメタタグを直したい場合はHeadコンポーネントを使う。
code: pages/posts/first-post.js
import Head from 'next/head'
export default function FirstPost() {
return <>
<Head>
<title>First Post</title>
</Head>
<h1>First Post</h1>
</>
}
同一サイト内のページ遷移を行うリンクは、Linkコンポーネントを使って行う。
code: pages/index.js
import Link from 'next/link'
...
Read <Link href="/posts/first-post">this page!</Link>
first-post.jsでもhomeに戻るようなルーティングを追加した。(省略)
aではなくLinkでページを遷移すると、ページのDOMが切り替わる形でページ遷移が行われるのでロードが発生しなくて済む。
さらにNext.jsでは本番環境においてコード分割を行なった上で以下のような動作を行うため、高速なUXが期待できる。
トップページに来てもサイト内の全てのページのデータ(CSSなどを含む)は取得しない。
今いるページのLinkコンポーネントの遷移先ページのデータをバックグラウンドで取得する。(プリフェッチ)
静的ファイルの取り扱い
静的ファイルはpublicディレクトリ内に配置する。
例えば画像なら、index.jsから以下のように参照されている。
code: pages/index.js
<img src="/vercel.svg" alt="Vercel Logo" className="logo" />
NextではCSSとSassをサポートしているが、今回はCSSを使う。
Nextではstyled-jsというライブラリをデフォルトでサポートしており、JSXにscopedなスタイルを記述できる。
code: pages/index.js
<style jsx>{`
...
`}</style>
CSSモジュールとLayoutコンポーネントを作成し、Layoutコンポーネントにスタイルを適用
code: components/layout.module.css
.container {
max-width: 36rem;
padding: 0 1rem;
margin: 3rem auto 6rem;
}
code: components/layout.js
import styles from './layout.module.css'
export default function Layout({ children }) {
return <div className={styles.container}>
{children}
</div>
}
first-post.jsで読み込み、利用する。
code: pages/first-post.js
import Layout from '../../components/layout'
...
return <Layout>
...
</Layout>
グローバルCSSの適用はpages/_app.jsでのみ適用できる。
それ以外は上記と同様、〇〇.module.cssで読み込む
code: styles/global.css
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu,
Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
line-height: 1.6;
font-size: 18px;
}
...他にも色々
code: pages/_app.js
import '../styles/global.css'
export default function App({ Component, pageProps }) {
return <Component {...pageProps} />
}
_app.jsを作成した場合はHMRが効かないのでサーバを立ち上げ直す。
基本はこんな感じで、あとはスタイリングをちゃんとやる。(省略)
静的ファイルに関するTips
classnamesライブラリによってクラス名の条件分岐を簡潔にできる
$ npm install classnames
code: alert.module.css
.success {
color: green;
}
.error {
color: red;
}
code: alert.js
import styles from './alert.module.css'
import cn from 'classnames'
export default function Alert({ children, type }) {
return (
<div
className={cn({
styles.success: type === 'success',
styles.error: type === 'error'
})}
{children}
</div>
)
}
JedWatson/classnames: A simple javascript utility for conditionally joining classNames together
Next.jsではCSSのコンパイルをPostCSSによってコンパイルすることでscoped cssなどを実現している
プロジェクト直下にpostcss.config.jsというファイルを作ることでPostCSSの設定をカスタマイズでき、TailwindCSSなどと合わせて使うことができる。
Tailwind css: display: flexを.flexクラスにするなど、BootstrapみたいにクラスでスタイリングできるようにするUtility
TailwindCSS入門 ~ Utility First + デザインシステムの構築 ~ - Qiita
Tailwind CSS を purgecss (使われてない CSS を削除するものです)と合わせて使う例
$ npm install tailwindcss @fullhuman/postcss-purgecss postcss-preset-env
autoprefixer(ブラウザ依存prefixを自動で考慮してくれるやつ?)はデフォルトで Next.js に含まれているため不要
code: postcss.config.js
module.exports = {
plugins: [
'tailwindcss',
...(process.env.NODE_ENV === 'production'
? [
[
'@fullhuman/postcss-purgecss',
{
content: [
'./pages/**/*.{js,jsx,ts,tsx}',
'./components/**/*.{js,jsx,ts,tsx}'
],
defaultExtractor: content =>
content.match(/\w-/:+(?<!:)/g) || []
}
]
]
: []),
'postcss-preset-env'
]
}
Advanced Features: Customizing PostCSS Config | Next.js
sassをインストールすれば拡張子を.module.scssや.module.sassとすることでscss/sassが使える。
$ npm install sass
Basic Features: Built-in CSS Support | Next.js
プリレンダリングとデータフェッチング
プリレンダリング:コンポーネントの描画をJSでやるのではなく、あらかじめHTMLにしておく
Next.jsではデフォルトで全てのページをプリレンダリングする。Reactではそうではない。
HTMLと最小限JSの紐付け -> HTMLページの読み込み -> ハイドレーション(対応するJSの実行によるコンポーネント初期化)
JavaScriptなしで実行されているかを確認:ブラウザコンソールでCmd+Shift+P -> Disable JavaScriptを実行 -> 更新
これをすると、今回のブログアプリがプリレンダリングされていることを確認できる。
プリレンダリングには2つの形式がある。
静的生成(Static Generation):ビルド時にHTMLが生成され、その後のリクエストでは再利用される
SSR (Server Side Rendering):リクエストごとにサーバ側でHTMLを埋め込んでレスポンスを返す
開発モードではSGの設定をしていてもSSRの挙動をする。
Next.jsではページごとにどちらのプリレンダリング形式を使用するか選択できる
基本静的生成で、リクエストごとに表示される内容が異なる場合はSSRを使う。
ページの描画に外部データが必要な場合にも静的生成を行うことはできる
getStaticPropsを使い、props: data を返す関数を定義することで実現する
リクエスト時ではなくbuild時のデータが使われるので注意
ここでは、Markdownのブログ記事内容をgetStaticPropsで読み込んで表示することを行う。
Markdownのメタデータを解析するgray-matterを導入
$ npm install gray-matter
code: lib/posts.js
import fs from 'fs' // file system
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 => {
// id を取得するためにファイル名から ".md" を削除
const id = fileName.replace(/\.md$/, '')
// マークダウンファイルを文字列として読み取る
const fullPath = path.join(postsDirectory, fileName)
const fileContents = fs.readFileSync(fullPath, 'utf8')
// gray-matterで投稿のメタデータ部分をオブジェクトとして取得
const matterResult = matter(fileContents)
return { id, ...matterResult.data }
})
// 投稿を日付でソートして返す
return allPostsData.sort((a, b) => {
if (a.date < b.date) {
return 1
} else {
return -1
}
})
}
code: 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' // postsを読み込む
// getStaticPropsでgetSortedPostsDataの結果をビルド時に静的生成するようexportする。
export async function getStaticProps() {
const allPostsData = getSortedPostsData()
return {
props: {
allPostsData
}
}
}
// getStaticPropsはpropsというキーの中身で同じ名前のものを読み込めるようにしてくれる。
export default function Home({ allPostsData }) {
return (
<Layout home>
<Head>
<title>{siteTitle}</title>
</Head>
<section className={utilStyles.headingMd}>...前のチャプターで作った自己紹介...</section>
<section className={${utilStyles.headingMd} ${utilStyles.padding1px}}>
<h2 className={utilStyles.headingLg}>Blog</h2>
<ul className={utilStyles.list}>
// 取得したallPostsDataを使える
{allPostsData.map(({ id, date, title }) => (
<li className={utilStyles.listItem} key={id}>
{title}<br />
{id}<br />
{date}
</li>
))}
</ul>
</section>
</Layout>
)
}
Can't resolve'fs' と出たら
Module not found: Can't resolve 'fs' (Next.js バージョン9.3と9.4での対処法) - Qiita
上記の例ではgetStaticPropsを使って静的ファイルを取得しているが、外部APIやDBへクエリを投げた結果なども取得できる。行されず、サーバでのみ実行されるためクエリパラメータやHTTPヘッダは使えない。
開発環境ではgetStaticPropsはリクエストの度に実行される。
getStaticPropsはpagesでのみ利用でき、その他componentsからはexportできない。理由の一つは、React では、ページがレンダリングされる前に、必要なデータがすべて揃っている必要があるから。
SSRを行うには、getServerSidePropsを使う。
でも可能な限りgetStaticPropsを使うことを考える。
レンダリングに必要なデータをサーバで揃えるまでの時間が長くなるため、Time To First Byte (TTFB)が長くなってしまう。
そもそもデータの取得はクライアントサイドでやらせて構わないという場合はクライアントサイドレンダリングを行えば良い。
この戦略はユーザーのダッシュボードページなどに有効。
ページがプリレンダリングされる必要はなく、データは頻繁に更新され、リクエスト時のデータ取得を必要とする。
ダッシュボードはプライベートなもので、ユーザーに固有のページであり、SEOは関係ない。
逆にいうと静的生成やSSRを行うのは描画までの時間のためだけでなく、SEOのためでもあると言える。
クライアントサイドレンダリングを行う場合は、SWRというデータフェッチ用のReactフックが便利。
React hooksについてはReact hooksを参照
データ取得に対応する部分にローディングやfetch成功時の挙動などを設定できる。
code: swr.js
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>
}
プリレンダリングについてのより詳しい情報はこちら
動的ルーティング
Nextでは/articles/1のような動的ルーティングによるページもgetStaticPathsによって静的生成を行なってくれる。
getStaticPathsも本番ではビルド時にのみ実行され、開発環境ではリクエストごとに実行される。
動的ルーティングを行うページのファイル名には[id].jsのように角カッコを使う。
$ npm install remark remark-html date-fns
code: lib/posts.js
import remark from 'remark'
import html from 'remark-html'
...
export function getAllPostIds() {
const fileNames = fs.readdirSync(postsDirectory)
// 以下のような配列を返します:
// [
// {
// params: {
// id: 'ssg-ssr'
// }
// },
// {
// params: {
// id: 'pre-rendering'
// }
// }
// ]
return fileNames.map(fileName => {
return {
params: {
id: fileName.replace(/\.md$/, '')
}
}
})
}
// 外部のAPIから取得しても良い。
export async function getPostData(id) {
const fullPath = path.join(postsDirectory, ${id}.md)
const fileContents = fs.readFileSync(fullPath, 'utf8')
// 投稿のメタデータ部分を解析するために gray-matter を使う
const matterResult = matter(fileContents)
// マークダウンを HTML 文字列に変換するために remark を使う
const processedContent = await remark()
.use(html)
.process(matterResult.content)
const contentHtml = processedContent.toString()
// データを id および contentHtml と組み合わせる
return {
id,
contentHtml,
...matterResult.data
}
}
code: pages/posts/id.js
import Layout from '../../components/layout'
import Head from 'next/head'
import Date from '../../components/date'
import { getAllPostIds, getPostData } from '../../lib/posts'
import utilStyles from '../../styles/utils.module.css'
export default function Post({ postData }) {
return (
<Layout>
<Head>
<title>{postData.title}</title>
</Head>
<article>
<h1 className={utilStyles.headingXl}>{postData.title}</h1>
<div className={utilStyles.lightText}>
<Date dateString={postData.date} />
</div>
<div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
</article>
</Layout>
)
}
// 多分これを元にid.jsのページを静的生成する
export async function getStaticPaths() {
const paths = getAllPostIds()
return {
paths,
// fallback: falseを返すと存在しないidにアクセスされた時404を表示。trueだとカスタムできる。
// 詳細: https://nextjs.org/docs/basic-features/data-fetching#fallback-pages
fallback: false
}
}
// 外部のAPIから取得しても良い。
export async function getStaticProps({ params }) {
const postData = await getPostData(params.id)
return {
props: {
postData
}
}
}
code: components/date.js
import { parseISO, format } from 'date-fns'
export default function Date({ dateString }) {
const date = parseISO(dateString)
return <time dateTime={dateString}>{format(date, 'LLLL d, yyyy')}</time>
}
code: pages/index.js
import Link from 'next/link'
import Date from '../components/date'
...
<li className={utilStyles.listItem} key={id}>
<Link href={/posts/${id}}>
<a>{title}</a>
</Link>
<br />
<small className={utilStyles.lightText}>
<Date dateString={date} />
</small>
</li>
[...id].jsというファイル名にした場合は、id以下の全てのパスを取得するようになる。
code: pages/posts/...id.js
export async function getStaticPaths() {
return [
{
params: {
// /posts/a/b/c を静的に生成する
id: 'a', 'b', 'c'
}
}
//...
]
}
export async function getStaticProps({ params }) {
const data = await getData(params.id) // 'a', 'b', 'c'
return { props: { data } }
}
Next.js のルーターにアクセスしたければ、useRouterフックを next/router からインポートする
404ページはpages/404.jsという名前で作成するとカスタマイズできる
code: pages/404.js
export default function Custom404() {
return <h1>404 - Page Not Found</h1>
}
APIルート
Next.js アプリの中に API エンドポイントを作成できる。
pages/api ディレクトリの中に、以下のフォーマットを持つ関数を作成する。
code: pages/api/hello.js
export default (req, res) => {
res.status(200).json({ text: 'Hello' })
}
http://localhost:3000/api/hello にアクセス
req は http.IncomingMessage のインスタンス。ビルド済みのミドルウェアもある
res は http.ServerResponse のインスタンス。ヘルパー関数もある。
getStaticProps、getStaticPathsからは APIルートにアクセスしない。
API ルートのコードはクライアント側のバンドルには含まれないので、フォーム情報などを安全に扱える。
プレビューモードなるものにこのAPIルートが使われるらしい。ヘッドレス CMS からデータを取得する場合に便利とのこと
動的APIルートも可能
デプロイ
Githubへpush
Vercelアカウントを作成
静的 & JAMstack 開発とサーバレス関数をサポートするグローバル CDN を備えたオールインワンのプラットフォーム
Next.jsへのサポートは最高水準
静的ファイルの提供が高速
SSRを使用するページとAPI ルートは自動的に他とは分離されたサーバレス関数になり、スケーラブル。
カスタムドメイン
環境変数
自動 HTTPS
Vercelのドキュメント
Import Git Repository > Select > Add Github Org or Account > Only Select Repositories > nextjs-blog選択 > Install > Import
自分のアカウントを選択 > 全てデフォルト値でDeploy > 数分でDeployが完了 > Visit
VercelをImportしたGithubリポジトリはPRでプレビューができる
ローカルで新しいブランチを作成する
新しいブランチをpushする
そのブランチからmainへPRを作成する
変更を加えて、GitHub に push する
(変更を加える > Push > PR作成だとデプロイが行われない?すでにあるPRのブランチにPushされたら走った)
それでもPush > PR作成が普通のやり方だと思うので、強制的にPreview Deployしたいときはある
Vercel CLIをインストール $ npm i -g vercel
Preview Deployしたいローカルブランチにいる状態で$ vercel
ログインが必要な場合はgithubのemailを入力 > 届くメールでverify > もう一度 $ vercel
InspectのURL > Visit でPreview Deployを確認できる
Githubの方の表示は変わらない。あくまでVercel側でのPreview Deploy
https://gyazo.com/e06a580a9b5e5bcba01b56d658f6292d
https://github.com/fuurin/nextjs-blog/deployments から他のブランチのデプロイ状況も確認できる
PRをmergeしても自動的にDeployされる
上記のような工程をDPSワークフローと呼び、推奨されている。
Development
Preview
Ship
TypeScript
tsconfig.jsonを作成。(この状態でyarn devするとTypeScriptのモジュールを落としてくるようインストラクションが出る)
$ touch tsconfig.json
TypeScriptのモジュールを追加
$ yarn add --dev typescript @types/react @types/node
$ yarn dev
tsconfig.json ファイルに必要な記述を埋めてくれる。このファイルはカスタマイズ可能。
next-env.d.ts ファイルを作成する。これによって Next.js の型が TypeScript のコンパイラによって拾われる。
このファイルはいじらない。
TypeScript対応完了
Next.js固有の型
getStaticProps、getStaticPaths、getServerSideProps => GetStaticProps、GetStaticPaths、GetServerSideProps
code: static_types.ts
import { GetStaticProps, GetStaticPaths, GetServerSideProps } from 'next'
export const getStaticProps: GetStaticProps = async context => { /* ... */ }
export const getStaticPaths: GetStaticPaths = async () => { /* ... */ }
export const getServerSideProps: GetServerSideProps = async context => { /* ... */ }
APIルート
code: api_types.ts
import { NextApiRequest, NextApiResponse } from 'next'
export default (req: NextApiRequest, res: NextApiResponse) => { /* ... */ }
AppProps
pages/_app.js を pages/_app.tsx にして、AppProps というビルトインの型が使える。
code: pages/_app.tsx
import { AppProps } from 'next/app'
function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}
export default App
Next.jsとTypeScriptについてのドキュメント
あとは上記のブログアプリをTypeScriptに書き換える
ファイル名は.jsから.tsx(lib配下などは.ts)にするとVSCodeがちゃんと認識してくれる
基本的にfunctionによる関数の定義に型をつけるだけ
主要なTSの型定義ファイルはDefinitelyTypedにあるが、ない場合は直下にglobal.d.tsを作成する
code: global.d.ts
// remark-htmlの型定義ファイルがないので作成
declare module 'remark-html' {
const html: any
export default html
}