Open9

Next.js チュートリアルまとめ

セレナーデセレナーデ

Create a Next.js App

Next.js のメリット

Next.js は React のフレームワークである。
React で Web アプリを構築するには以下のことを考慮する必要があるが、Next.js ではそれらを全て解決できる(らしい)。

  • Babel や Webpack などの環境設定
    • Babel:最新の構文を古いブラウザでも動くように変換するツール
    • Webpack:ファイル同士の依存関係を1つにまとめてくれるツール
  • コード分割など、プロダクションの最適化
    • コード分割:バンドルしたファイルを自動的に分割して、パフォーマンスを最適化する
  • パフォーマンスや SEO のための Pre-render 設定
    • Pre-rendering:事前にコードを読み込んでおくこと
    • ↑により、表示を高速化できる
  • Rendering のタイミング(ページによって変える)
    • Server-Side Rendering(SSR):サーバー側でレンダリングしておく
      • レンダリングしたページをクローラーに見せられるため、SEO 的に強くなる
      • クローラー:検索エンジンにおける検索の順位づけに必要な要素をアプリを見て、収集してくるロボット
    • Client-Side Rendering(CSR):ブラウザ側でレンダリングする
      • 通常の React ではこちらのみ
      • クローラーが CSR のページを見るときは、空のページなので SEO 的には弱い
    • (Static Generation(SG))
      • 詳しくは後ほど
  • サーバーサイドの処理
    • API を簡単に作れる

環境構築

前提

  • Node.js のバージョン:10.13 以降
  • 以下のコマンドで調べられる
node -v

Create a Next.js app

自分の作業場所にて、以下のコマンドを実行する。

npx create-next-app@latest `アプリ名` --use-npm --example "https://github.com/vercel/next-learn/tree/master/basics/learn-starter"

開発用サーバーを起動

  • 新しく作成されたディレクトリに移動
cd `アプリ名`
  • 開発用サーバーを立ち上げる
npm run dev

以下のページが表示されれば成功!

セレナーデセレナーデ

ページ遷移

概要

  • pages ディレクトリ内に作られたファイルが Web サイトの1ページとなり、そのファイル名が URL に関連づけられる
  • Link コンポーネントを使用すると、ページ間でクライアントサイドナビゲーション[1]を可能にする
  • Next.js はコードを自動的に分割するため、各ページはそのページに必要なものだけが読み込まれる

Next.js内のページ

  • Next.js では、pages ディレクトリ内にファイルを作れば、Webサイトの1ページとなる
  • ファイル名に基づいて、 URL も関連づけられる
    • 「/」:pages/index.js
    • 「posts/first-post.js」:pages/posts/first-post.js
pages/index.tsx
const Home = () => {
  return <div>Home</div>
}
export default Home

pages/posts/first-post.tsx
const FirstPost = () => {
  return <div>FirstPost</div>
}
export default FirstPost

Next.js では、すでに用意されている Link コンポーネントを使用すると、アプリのページ間をリンクすることができ、クライアントサイドナビゲーションを可能にする

import Link from 'next/link';

先ほど作成したコードでページ遷移を実行してみる

pages/index.tsx
import Link from 'next/link'

const Home = () => {
  return (
    <>
      <div>Home</div>
      <Link href="/posts/first-post">Go to FirstPost</Link>
    </>
  )
}
pages/posts/first-post.tsx
import Link from 'next/link'

const FirstPost = () => {
  return (
    <>
      <div>FirstPost</div>
      <Link href="/">Back to Home</Link>
    </>
  )
}

コード分割と prefetching

コード分割

Next.js はコードを自動的に分割するため、各ページはそのページに必要なものだけが読み込まれる。
それにより、以下の2つの恩恵を得ることができる。

  • たくさんのページがあっても高速に読み込むことが可能
  • あるページがエラーを起こしても残りは正常に動作する

prefetching

Next.js の本番ビルドでは、Link コンポーネントが存在する場合、自動的に prefetch するようになっている。すなわち、Link コンポーネントでリンクされたコードを事前に取得している。
それにより、高速にページ遷移が可能となる。

脚注
  1. JavaScript を使用したページ遷移で、URL を切り替えてもページ再読み込みが不要だったり、ブラウザのページ遷移よりも高速になる ↩︎

セレナーデセレナーデ

アセットやメタデータ、CSS

アセット

Next.js では public ディレクトリ配下に画像などの静的アセットを配置できる。

<img src="./images/vercel.svg alt="vercel" />

また、Next.js では、HTML の <img> 要素を拡張した next/image を用意している。
これにより、画像サイズの最適化や Lazy Load [1] を自動で行うことができる。

import Image from 'next/image';

const YourComponent = () => (
  <Image
    src="/images/vercel.svg"
    height={144}
    width={144}
    alt="vercel"
  />
);

メタデータ

React では index.html に設定した <head> タグしか編集できないが、Next.js ではページごとに <head> タグを編集できる。
これにより、以下の恩恵を受けることができる。

  • 検索エンジンにページの内容を正しく伝えることができ、SEO 的に強くなる
  • <title> タグの内容がページのタイトルに反映される
pages/posts/first-post.tsx
import Head from 'next/head'
import Link from 'next/link'

const FirstPost = () => {
  return (
    <>
      <Head>
        <title>First Post</title>
      </Head>
      <div>First Post</div>
      <Link href="/">← Back to home</Link>
    </>
  )
}

Third-Party製 JavaScript

  • TBW

CSS Styling

  • TBW
脚注
  1. 画像の読み込みに時間差を設けて表示させ、画面表示の高速化を図る仕組み ↩︎

セレナーデセレナーデ

Pre-rendering と Data Fetching

概要

  • 2つの Pre-rendering 手法

Pre-rendering

  • Pre-rendering:事前に HTML を生成すること(Next.js ではデフォルトで全ページを Pre-render している)
    • 通常の React ではブラウザ側で HTML を組み立てる(クライアントサイドレンダリング)
    • ブラウザではない場所で、事前に生成することでブラウザの負荷を減らし、表示を高速化できる
    • レンダリングしたページをクローラーに見せられるため、SEO 的に強くなる
Pre-rendering No Pre-rendering

2つの Pre-rendering

Next.js は Pre-rendering に2つの形式を用意している。

  • Static Generation(SG):ビルド時に HTML を生成する(表示が一番早い)
  • Server-Side Rendering(SSR):ユーザーのリクエストごとに HTML を生成する
Static Site Genereation No Pre-rendering

※開発環境では、開発を容易にするために Static Generation でもすべてのリクエストで Pre-rendering される。本番環境では、Static Generation はビルド時に一度だけ行われ、すべてのリクエストで行われるわけではない。

Pre-rendering の使い分け

Next.js ではページごとに SG と SSR の使い分けをすることができる(基本的には SSG を推奨)。

以下のような使い分けが求められる。

  • Static Site Generation:更新頻度が低いページ
    • ブログ
    • ECサイト
    • Landing Page
    • 問い合わせ
  • Server-Side Rendering:更新頻度が高いページ
    • SNS
    • チャット

Static Generation のデータの有無による違い

  • 外部データなし
    • ビルド時に HTML をレンダリング
  • 外部データあり
    • ビルド時に DB や外部 API からデータを取得
    • 取得したデータを使用して HTML をレンダリング

上記のように外部データを取得したい場合は getStaticProps という非同期関数を使用する。

export default const Home = (props) => { ... }

export const getStaticProps = async() => {
  // Get external data from the file system, API, DB, etc.
  const data = ...

  // The value of the `props` key will be
  //  passed to the `Home` component
  return {
    props: ...
  }
}

※開発環境では、各リクエストごとに実行される。

getStaticProps の例として、ファイルシステムからデータを読み込んでシンプルなブログを作成する。

lib/posts.ts
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'

const postsDirectory = path.join(process.cwd(), 'posts')
export type PostData = {
  id: string
  date: string
  title: string
}
export const getSortedPostsData = (): PostData[] => {
  // posts 以下のファイルを取得
  const fileNames = fs.readdirSync(postsDirectory)
  const allPostsData = fileNames.map((fileName) => {
    const id = fileName.replace(/\.md$/, '')
    
    // マークダウンファイルを文字列として読み込む
    const fullPath = path.join(postsDirectory, fileName)
    const fileContents = fs.readFileSync(fullPath, 'utf8')
    
    // メタデータ部分の解析
    const matterResult = matter(fileContents)

    return {
      id,
      ...(matterResult.data as { date: string; title: string }),
    }
  })
  
  // postを日付順に並べ替える
  return allPostsData.sort((a, b) => {
    if (a.date < b.date) {
      return 1
    } else {
      return -1
    }
  })
}

ここで、getStaticProps を使用して、getSortedPostsData からデータを fetch する。

pages/index.tsx
import { getSortedPostsData, PostData } from '../lib/posts'

type Props = {
  allPostsData: PostData[]
}
export const getStaticProps = async () => {
  const allPostsData = getSortedPostsData()
  return {
    props: {
      allPostsData,
    },
  }
}

const Home = ({ allPostsData }: Props) => {
  return (
    <section>
      <h2>Blog</h2>
      <ul>
        {allPostsData.map(({ id, date, title }) => (
          <li key={id}>
            {title}
            <br />
            {id}
            <br />
            {date}
          </li>
        ))}
      </ul>
    </section>
  )
}

export default Home

getStaticProps の詳細

前節では、ファイルシステムからデータを取得したが、外部 API からデータを取得しても問題なく動作する。

export const getSortedPostsData = async () => {
  const res = await fetch('..');
  return res.json();
}

また、getStaticProps はページからしか書き出せない。
それは、ビルド時に実行することを前提としており、レンダリングされる前に必要なデータをすべて持っている必要があるため。

なので、頻繁に更新されたり、ユーザーのリクエストごとに変更されるデータは適していない。
その場合は Server-Side Rendering を使用する必要がある。

リクエスト時のデータ取得

Server-Side Rendering を使用するためには、getStaticProps の代わりに getServerSideProps を export する必要がある。

export const getServerSideProps = async (context) => {
  return {
    props: {
      // props for your component
    },
  };
}
セレナーデセレナーデ

Dynamic Routes

外部データに依存するページの path

Next.js では、外部データに依存する path を持つページを静的に生成することができる。

実現したいこととしては前章で用いた各ファイルを表示するページ URL を以下のように動的に変化させたい。

  • /posts/file1
  • /posts/file2

Next.js では [] で括られたページが動的ルートとなり、そのファイルは pages/posts/[id].tsx のように命名する。

getStaticPaths の実装

ファイルの ID を取得する関数を追加する。
ただし、返されるリストは(今回は id キーを持つ)オブジェクトの配列である必要がある。

lib/posts.ts
export const getAllPostIds = () => {
  const fileNames = fs.readdirSync(postsDirectory)

  return fileNames.map((fileName) => {
    return {
      params: {
        id: fileName.replace(/\.md$/, ''),
      },
    }
  })
}

そして、getAllPostIds のメソッドを import して getStaticPaths の中で使用する。

pages/posts/[id].tsx
import { getAllPostIds } from '../../lib/posts';

const Post = () => {
  return <div>...</div>
}

export const getStaticPaths = async () => {
  const paths = getAllPostIds();
  return {
    paths,
    fallback: false,
  };
}

export default Post

getStaticPropsの実装

指定された id のファイルをレンダリングするために必要なデータを fetch する。

lib/posts.ts
export const getPostData = (id: string) => {
  const fullPath = path.join(postsDirectory, `${id}.md`)
  const fileContents = fs.readFileSync(fullPath, 'utf8')

  // メタデータ部分を解析
  const matterResult = matter(fileContents)

  // データをidで結合
  return {
    id,
    ...matterResult.data,
  }
}

そして、getPostData のメソッドを import して getStaticProps の中で使用する。

pages/posts/[id].tsx
import type { GetStaticProps } from 'next'

export const getStaticProps: GetStaticProps = async ({ params }) => {
  const postData = getPostData(params?.id as string)
  return {
    props: {
      postData,
    },
  }
}