【Next.js】チュートリアルの実施+周辺知識のキャッチアップ
![Yuki Yuki](https://res.cloudinary.com/zenn/image/fetch/s--McGXmmVW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/31548d082c.jpeg)
Create a Next.js App
Create a Next.js app
- Next.jsプロジェクトの作成コマンド
※チュートリアルではプロジェクト名:nextjs-blog
になっており、また--example
フラグでチュートリアル用にカスタマイズされたテンプレートを使用している。
npx create-next-app プロジェクト名 --use-npm "https://github.com/vercel/next-learn-starter/tree/master/learn-starter"
- 実行方法
npm run dev
http://localhost:3000
へアクセスすると下記画面がが表示される。
Navigate Between Pages
Pages in Next.js
Reactはreact-router-dom
などのライブラリを使って、疑似的にURLを書き換えて見かけ上のページ遷移を実現している。
Next.jsは下記のようにpages
ディレクトリ配下にjs/ts
ファイルを配置することで、URLのマッピングを自動で行う。
--project
--pages
-- index.js -> "/"でアクセス可能
-- posts
-- first-post.js -> "/posts/first-post"でアクセス可能
Link Component
通常HTMLではページのリンクに<a>
タグを使うが、Next.jsでは<Link>
コンポーネントを使用する。
import Link from 'next/link'
使い方
-
<a>
タグの場合
<h1 className="title">
Learn <a href="https://nextjs.org">Next.js!</a>
</h1>
-
<Link>
コンポーネントの場合
<a>
タグを<Link>
コンポーネントで囲む
<h1 className="title">
Read{' '}
<Link href="/posts/first-post">
<a>this page!</a>
</Link>
</h1>
Client-Side Navigation
<a>
タグと<Link>
コンポーネントの違い
<Link>
コンポーネントはclient-side navigation
を可能にする。
client-side navigation
とはJavaScriptを使って画面遷移することで、ローディングなしに画面遷移を可能とする。
そのため、ブラウザから <a>
タグを使って画面遷移するよりも素早い画面遷移が可能となる。
Code splitting and prefetching
Next.jsは必要な時にだけ、必要なファイルをサーバーから持ってくる。
例えば、下記のような構成の場合、"/"へアクセスする時にindex.js
のみファイルを取得する。
--pages
-- index.js
-- about.js
-- plan.js
ただし、下記のようにindex.js
でabout.js
を<Link>指定している場合は、ローディングなしに画面遷移するために、リンク先のページもバックグラウンドで自動的に事前取得する。
--pages
-- index.js
-- about.js <- indexで<Link>コンポーネントのリンク先に指定されている
-- plan.js
![Yuki Yuki](https://res.cloudinary.com/zenn/image/fetch/s--McGXmmVW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/31548d082c.jpeg)
Assets, Metadata, and CSS
Assets
Next.jsではstatic assets(画像、アイコン、静的なhtmlファイル)などをpublic
ディレクトリ配下に配置する。
--project
--public
-- hoge.jpg
-- huga.svg
Image Component and Image Optimization
Next.jsではHTMLの<img>
タグを拡張した、<Image>
コンポーネントを使用する。
このコンポーネントを使用すれば画像の最適化をNext.jsが自動的に行う。
最適化はCMSなどの他のサーバーで管理している画像に関しても最適化を行う。
【最適化の例】
- 画像のリサイズを行う
- jpgファイルを
WebP
などの軽量なフォーマットに変換する
Using the Image Component
画像の最適化はビルド時に一括で行うのではなく、ユーザーがRequestするたびに適宜行っていく。
なので、画像が大量にあったとしてもBuild時間が大幅にかかるわけではない。
読み込みに関してもviewportスクロールされた時に初めて画像が読み込まれる。
下記のようにあらかじめwidth
とheight
を指定しておくと、そのサイズまであらかじめ画像を圧縮する。
また、レスポンス表示で幅が小さくなった場合も自動でそのサイズにトリミングした画像を生成する。
import Image from 'next/image'
const YourComponent = () => (
<Image
src="/images/profile.jpg"
height={144}
width={144}
alt="Your Name"
/>
)
Metadata
Next.jsでは<head>
タグではなく<Head>
コンポーネントを使用する。
これにより、ページごとに動的にMetadataを変更することが可能。
import Head from 'next/head'
export default function FirstPost() {
return (
<>
<Head>
<title>First Post</title>
</Head>
<h1>First Post</h1>
<h2>
<Link href="/">
<a>Back to home</a>
</Link>
</h2>
</>
)
}
CSS Styling
Next.jsはCSS-in-JS
ライブラリとしてstyled-jsxを公式でサポートしている。
公式サポートではないが、styled-componentsやemotionの使用も可能。
また、素のCSSファイルを書くことも可能。
素のCSSの書き方
ここではCSSをコンポーネントレベル(各ページごと)で制御して使用するためにCSS Modulesを使用する。
※CSS Modulesを使用する際はファイル名をxxx.module.css
にする必要がある
CSSファイルを定義
.container {
max-width: 36rem;
padding: 0 1rem;
margin: 3rem auto 6rem;
}
定義したCSSファイルを使用
// ① CSSファイルをimportし名前を割り当てる
import styles from './layout.module.css'
export default function Layout({ children }) {
// ② ①で割り当てた名前.containerで使用
return <div className={styles.container}>{children}</div>
}
Automatically Generates Unique Class Names
上記のようにCSSファイルを使用するとCSS Modules
が自動的にユニークなクラス名の生成を行う。
また、ルーティングや画像同様にCSSに関してもNext.jsがそれぞれに必要なCSSファイルだけをロードする。
Global Styles
Next.jsではプロジェクト全体に共通したCSSを適応させることも可能(global CSS)。
global CSSを適応させるためには、pages
配下に_app.js
ファイルを作る必要がある。
--project
--pages
-- _app.js
_app.jsとは?
前提として、Next.jsでは各ページが初期化する際にNext.js内部に組み込まれたApp
コンポーネントを使用して全てのページの初期化処理を行っている。
そのApp
コンポーネントをカスタマイズ(上書き)するためのファイルが_app.js
ファイルになる。
つまりは全ページで共通的に行いたい処理(ページ間で共通のレイアウトをもたせたりする)を_app.js
ファイルに定義することができる。
公式によると下記のような処理を_app.js
ファイルに書くことができる
- ページ間で共通のレイアウトをもたせたりする
- 共通のstateを持つことができる
- componentDidCatchを利用したカスタムエラー処理
- UIページに共通のデータをインジェクションさせる
- global CSSの定義
※_app.js
ファイルを追加した場合はサーバーを再起動する必要がある
Adding Global CSS
Global CSSファイルを定義
名前や配置場所はどこでも良い。
チュートリアルではproject配下に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;
}
* {
box-sizing: border-box;
}
a {
color: #0070f3;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
img {
max-width: 100%;
display: block;
}
定義したGlobal CSSファイルを使用
// 下記のようにimportするだけでCSSが適用される
import '../styles/global.css'
export default function App({ Component, pageProps }) {
return <Component {...pageProps} />
}
Styling Tips
Using classnames library to toggle classes
classnames
ライブラリを使うと簡単にclass namesをスイッチできる。
install方法
npm install classnames
使用例
- CSSファイルを作成する
.success {
color: green;
}
.error {
color: red;
}
- UIコンポーネントで使用する
// 定義したCSSとclassnamesライブラリをinportする
import styles from './alert.module.css'
import cn from 'classnames'
export default function Alert({ children, type }) {
return (
<div
// 引数のtypeに応じてclass namesを変更する
className={cn({
[styles.success]: type === 'success',
[styles.error]: type === 'error'
})}
>
{children}
</div>
)
}
Customizing PostCSS Config
TODO
Using Sass
Sass
は下記をInstallすると、ファイル名を.scss or .saas
にすることで使えるようになる。
また、CSS Moduleも使用可能で、その際はファイル名を.module.scss or .module.saas
にする
npm install sass
![Yuki Yuki](https://res.cloudinary.com/zenn/image/fetch/s--McGXmmVW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/31548d082c.jpeg)
Pre-rendering and Data Fetching
Pre-rendering
Next.jsでは全てのページをPre-renderingしている。
つまり、Next.jsは前もって全てのページのHTMLを生成している。
SPAのようにブラウザなどがJavaScriptを用いて、HTMLを生成しているのではない。
Two Forms of Pre-rendering
Pre-renderingの方法は2つある
- Static Generation(SSG): Build時にHMTLファイルを生成する
- Server-side Rendering(SSR): RequestごとにHTMLファイルを生成する。
※SPA/SSG/SSRの違い(下記、「代表的なフロントエンドの構成のおさらい」参照)
フロントエンドエンジニアのためのAWSアーキテクチャ
※開発モード(npm run dev
or yarn dev
)ではSSGの方法をとっていたとしても全てRequestごとにPre-renderingを行う(SSR)。
Per-page Basis
Next.jsではページごとに上記2つのpre-rendering方法を選ぶことができる。
公式サイト https://nextjs.org/learn/basics/data-fetching/two-forms
When to Use Static Generation v.s. Server-side Rendering
可能なかぎり、Static Generation(SSG)を使用する。
理由としては、Build時にHTMLを一括でCDNサーバーに作成・配置できるため、毎回RequestによってHTMLを作成するServer-side Rendering(SSR)よりも表示速度が早くなるため。
ユーザーのRequestよりも前にHTMLを作成できるのであれば、SSGを使用する。
(例としてはBlogやECサイトのような前もって表示できるデータが決まっているページなど)
逆に、頻繁に表示できるデータが変わったり、ユーザーの動作がトリガーとなって表示が変わるものなどはSSRを利用する。
Static Generation with and without Data
HTMLを作成する際に、外部データの取得(DBから値を取得するなど)がある場合とない場合が想定される。
外部データの取得が必要ない場合
- Build時に静的なHTMLファイルが生成される
公式サイト https://nextjs.org/learn/basics/data-fetching/with-data
外部データの取得が必要な場合
- Build時にNext.jsがDBにアクセスし必要な外部データを取得する
- その取得データを元にHTMLファイルを生成する
公式サイト https://nextjs.org/learn/basics/data-fetching/with-data
getStaticProps
Static Generation with Data using 外部データの取得が必要な場合のデータ取得にはgetStaticProps
メソッドを使用する。
Build時にgetStaticProps
をみてNext.jsが外部データにアクセスする。
※getStaticProps
メソッドはサーバーサイドでしか動作しない。
// getStaticPropsでreturenしたpropsを引数として受け取れる
export default function Home({ posts }) { ... }
// getStaticPropsの中身は自分で実装する
export async function getStaticProps() {
// DBやAPIから値を取得する.
const data = await fetch('https://.../posts')
const posts = await data.json()
// 取得した値をHomeコンポーネントにprops経由で渡す
return {
props: posts
}
}
Fetch External API or Query Database
- APIからのデータ取得には
fetch()
メソッドを使用する
export async function getSortedPostsData() {
// Instead of the file system,
// fetch post data from an external API endpoint
const res = await fetch('..')
return res.json()
}
- DBからのデータ取得には
query()
メソッドを使用する
import someDatabaseSDK from 'someDatabaseSDK'
const databaseClient = someDatabaseSDK.createClient(...)
export async function getSortedPostsData() {
// Instead of the file system,
// fetch post data from a database
return databaseClient.query('SELECT posts...')
}
Only Allowed in a Page
getStaticProps
メソッドはpages
配下の.js
ファイル、 .jsx
ファイル 、.ts
ファイル、 .tsx
ファイル からのみ呼び出すことが可能。
Fetching Data at Request Time
リクエスト時に外部データを取得する際にはSSRを利用する。
getServerSideProps
メソッドを利用する。
// contextを引数に含めることで、リクエストの特定のパラメータが含まれる
export async function getServerSideProps(context) {
return {
props: {
// props for your component
}
}
}
Client-side Rendering(CSR)
Pre-renderingを必要としない場合、Client-side Rendering使用することも一つの選択肢になる。
クライアント側でJavaScriptを実行し、データを取得する。
例えば、認証が必要なページ(個々人がみるprivateなページ:チュートリアルではダッシュボードがその例)は、SEOの対策が必要なく、Pre-renderingする必要がない。
そのようなページでデータの更新を頻繁に行う場合はCSRを用いて、ブラウザから直接APIコールをCallしてデータを取得すれば良い。
公式サイト https://nextjs.org/learn/basics/data-fetching/request-time
SWR
データ取得のためのReact Hooksライブラリ。
Client-side Renderingする際にはSWRライブラリを使用するのが良い。
※Next.jsを作っているVercel製ライブラリ
公式サイト
特徴としては
- Fetchデータの状態を管理する
- Fetchデータはキャッシュに保持される
- ポーリングによるデータの自動再フェッチ
import useSWR from 'swr'
function Profile() {
// 取得結果とエラーを返す。
// 取得結果が完了していない、もしくはエラーの場合dataはundefined。
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>
}
![Yuki Yuki](https://res.cloudinary.com/zenn/image/fetch/s--McGXmmVW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/31548d082c.jpeg)
Dynamic Routes
Page Path Depends on External Data
Dynamic Routesとは取得したデータによってルーティングのパスが動的に変化すること。
例えば、下記のようにIDによってルーティングを変えたい場合に利用する。
{ id: "foo", ... } -> /posts/foo
{ id: "bar", ... } -> /posts/bar
Overview of the Steps
※チュートリアルではlib/posts.js
ファイルで処理を分割していますが、説明上一つのファイルで処理が行えるように加工している。
- Dynamic Routesするためのファイルを作成する
動的に変更される部分を[]
で囲むようなファイル名にする。
--project
--pages
-- posts
-- [id].js -> このようなファイル名にすることで`/posts/パスパラメータ`のような意味合いになる
- [id].jsの実装
getStaticPaths
でDynamic Routesが可能となる。
getStaticPaths
はBuild時にルーティングとして有効なパスを予め定義するイメージ。
※ここではgetStaticPaths
の処理に焦点を当てる
import Layout from '../../components/layout'
export default function Post() {
return <Layout>...</Layout>
}
export async function getStaticPaths() {
return {
// pathのidに入る可能性があるものをidをKeyにしたObjectのListで定義する
paths: [
{
params: {
// `/posts/ssg-ssr`へのルーティングが可能
id: "ssg-ssr",
},
},
{
params: {
// `/posts/pre-rendering`へのルーティングが可能
id: "pre-rendering",
},
},
],
// fallbackに関しては後述
fallback: false,
};
}
// 上記で定義したをparamsObjectをgetStaticPropsの引数で受け取る。
// `/posts/ssg-ssr`へのルーティングの場合、params = {id: 'ssg-ssr'}
export async function getStaticProps({ params }) {
// 以下の処理は省略
}
Fallback
上記で説明したgetStaticPaths
にはreturnにpaths
とfallback
を必ず返さなければならない。
- paths
Build時に定義したルーティング対象パスの一覧。
※上記でいうssg-ssr
とpre-rendering
- fallback
Build時に定義したパス以外にアクセスしたときの動作を決めるもの。
fallback: false
の場合
getStaticPaths
で定義されていないパスにアクセスすると、404ページに遷移される。
【使い所】
動的に変更されるパスの数が小さなアプリケーション。
予め定義したルーティング対象パス以外のアクセスは全て404ページに遷移したい時に使用。
fallback:true
の場合
getStaticPaths
で定義されていないパスにアクセスしても404ページにならない。
【使い所】
動的に変更されるパスの数が多い場合。
この場合、全ての大量のPathをBuild時に定義するとBuild時間がかかってしまうので、Build時に全てのルーティング対象パスを予め定義したくない。
しかし、予め定義したもの以外のパスにアクセスがあったとしても404ページを返却したくない(=予め定義したもの以外のパスを許容したい)。
上記のような場合に、fallback:true
が使える。
【fallback:true
の流れ】
例えば、予めgetStaticPaths
のpaths
にid=1,2が登録されている場合に、id=3のRequestが来たとする。
-
/posts/3
のパスにRequestが来る。
※paths
に定義されていないidであるが、fallback:true
なので、404ページを返却しない - 裏側でサーバー側が
getStaticProps
を実行し、id=3に紐づく静的なファイルを生成する - 生成が完了したら作成した静的ファイルを返す
- これ以降の
/posts/3
は2で作成した静的ファイルを返す
※2で生成された静的ファイルは、内容の更新があったとしても静的ファイル自体の更新はされない。
つまり、2で一度静的ファイルが作成されたら4以降ではその静的ファイルをずっと参照する。
更新した静的ファイルを参照するためにはIncremental Static Regeneration(ISR)(後述)を使用する。
以下、サンプルコードにて説明
import { useRouter } from 'next/router'
function Post({ post }) {
// fallbackの状態を監視できるuseRouter()を使用
const router = useRouter()
// 上記でいう2が完了するまで、router.isFallback = trueとなり、ローディング画面をを表示
if (router.isFallback) {
return <div>Loading...</div>
}
// Render post...
}
// ビルド時のみに実行する
export async function getStaticPaths() {
return {
// `paths`にid=1,2のみを定義。これらのパスはBuild時に生成される
paths: [{ params: { id: '1' } }, { params: { id: '2' } }],
// id=3を許容できるようにfallback: trueを設定する
fallback: true,
}
}
// Build時ににはparams.id=1,2で実行
// もし、`/posts/3`が来た場合、裏側で非同期的にサーバー側が`getStaticProps`を実行
export async function getStaticProps({ params }) {
// params.id=3
const res = await fetch(`https://.../posts/${params.id}`)
const post = await res.json()
return {
props: posts
}
}
export default Post
Incremental Static Regeneration(ISR)
ISRは動的コンテンツを事前Buildせずに(全てのページを生成するのではなく)、ページにアクセスしたときに初めてBuildするというもの。
ISRでBuildした内容には有効期限(revalidate)を設けることができ、有効期限を過ぎたページにアクセスされた場合は、前回ビルドされたコンテンツを返しつつも、裏側で再Buildされる。
この機能が生まれた背景
ISRはNext.js 9.4から実装されたもの。
これ以前のSSGでのBuild方法は下記のようなデメリットがあり、それを解消するために生まれた。
- 膨大な数(数億、数兆規模)の静的ページをSSG一度にBuildする場合、データの取得やHTMLの作成が膨大になり、Buildに時間がかかる
- ページが更新された際は再度全てのページをビルドしなおさないといけない
ISRは上記で説明したように、ページにアクセスしたときに初めてBuildするため、事前Buildでのページ数を減らすことができ、また、有効期限を過ぎたものは裏側で再Buildされるため、ページの更新を随時行うこともできる。
実装方法
revalidate
を設定すればISRが使えるようになる。
function Blog({ posts }) {
return (
<ul>
{posts.map((post) => (
<li>{post.title}</li>
))}
</ul>
)
}
// 初回Build時にサーバー側で実行
// revalidateで設定した時間を過ぎ、かつRequestが来た時にサーバー側で再実行される
export async function getStaticProps() {
const res = await fetch('https://.../posts')
const posts = await res.json()
return {
props: {
posts,
},
// revalidateを設定すればISRが使えるようになる。(単位は秒)
revalidate: 1,
}
}
export default Blog
Catch-all Routes
Dynamic Routesは階層構造も...
を使って表現できる。
例えば、pages/posts/[...id].js
というファイルを作成すると、下記ファイルがパスとして定義される。
- /posts/a
- /posts/a/b
- /posts/a/b/c
return [
{
params: {
// `/posts/a`のパスが定義される
id: ['a']
},
params: {
// `/posts/a/b`のパスが定義される
id: ['a', 'b']
},
params: {
// `/posts/a/b/c`のパスが定義される
id: ['a', 'b', 'c']
}
}
//...
]
この場合params
にはid配列のが渡ることになる。
404 Pages
404ページをカスタムする場合は、pages/404.js
ファイルを作成する。
このファイルはBuild時に静的ページとして作成される。
export default function Custom404() {
return <h1>404 - Page Not Found</h1>
}
![Yuki Yuki](https://res.cloudinary.com/zenn/image/fetch/s--McGXmmVW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/31548d082c.jpeg)
API Routes
Creating API Routes
API Routesを使えば、Next.js内でNode.jsのWeb APIサーバを簡単に作成できる。
この機能により、WebフロントからサーバまですべてNext.js内で完結させることができるようになる。
作成する際はpages/api
配下にJavaScriptファイルを作成する。
// { text: 'Hello' }をResponseとして返すAPIを作成できる。
export default function handler(req, res) {
res.status(200).json({ text: 'Hello' })
}
A Good Use Case: Handling Form Input
TODO
![Yuki Yuki](https://res.cloudinary.com/zenn/image/fetch/s--McGXmmVW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/31548d082c.jpeg)
Deply周りと、多少飛ばした部分はあるが、大まかな流れがわかったので、クローズ