Closed6

【Next.js】チュートリアルの実施+周辺知識のキャッチアップ

Yuki YukiYuki Yuki

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"でアクセス可能

通常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.jsabout.jsを<Link>指定している場合は、ローディングなしに画面遷移するために、リンク先のページもバックグラウンドで自動的に事前取得する。

--pages 
  -- index.js 
  -- about.js <- indexで<Link>コンポーネントのリンク先に指定されている
  -- plan.js
Yuki YukiYuki Yuki

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スクロールされた時に初めて画像が読み込まれる。

下記のようにあらかじめwidthheight を指定しておくと、そのサイズまであらかじめ画像を圧縮する。
また、レスポンス表示で幅が小さくなった場合も自動でそのサイズにトリミングした画像を生成する。

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-componentsemotionの使用も可能。

また、素のCSSファイルを書くことも可能。

素のCSSの書き方

ここではCSSをコンポーネントレベル(各ページごと)で制御して使用するためにCSS Modulesを使用する。
※CSS Modulesを使用する際はファイル名をxxx.module.cssにする必要がある

CSSファイルを定義

layout.module.css
.container {
  max-width: 36rem;
  padding: 0 1rem;
  margin: 3rem auto 6rem;
}

定義したCSSファイルを使用

layout.js
// ① 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という名前で作成。

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ファイルを使用

_app.js
// 下記のように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ファイルを作成する
alert.module.css
.success {
  color: green;
}
.error {
  color: red;
}
  • UIコンポーネントで使用する
alert.js
// 定義した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 YukiYuki Yuki

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つある

  1. Static Generation(SSG): Build時にHMTLファイルを生成する
  2. 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から値を取得するなど)がある場合とない場合が想定される。

外部データの取得が必要ない場合

  1. Build時に静的なHTMLファイルが生成される

公式サイト https://nextjs.org/learn/basics/data-fetching/with-data

外部データの取得が必要な場合

  1. Build時にNext.jsがDBにアクセスし必要な外部データを取得する
  2. その取得データを元にHTMLファイルを生成する

公式サイト https://nextjs.org/learn/basics/data-fetching/with-data

Static Generation with Data using getStaticProps

外部データの取得が必要な場合のデータ取得には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 YukiYuki Yuki

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にpathsfallbackを必ず返さなければならない。

  • paths
    Build時に定義したルーティング対象パスの一覧。
    ※上記でいうssg-ssrpre-rendering
  • fallback
    Build時に定義したパス以外にアクセスしたときの動作を決めるもの。

fallback: falseの場合

getStaticPathsで定義されていないパスにアクセスすると、404ページに遷移される。

【使い所】
動的に変更されるパスの数が小さなアプリケーション。
予め定義したルーティング対象パス以外のアクセスは全て404ページに遷移したい時に使用。

fallback:trueの場合

getStaticPathsで定義されていないパスにアクセスしても404ページにならない。

【使い所】
動的に変更されるパスの数が多い場合。
この場合、全ての大量のPathをBuild時に定義するとBuild時間がかかってしまうので、Build時に全てのルーティング対象パスを予め定義したくない。
しかし、予め定義したもの以外のパスにアクセスがあったとしても404ページを返却したくない(=予め定義したもの以外のパスを許容したい)。

上記のような場合に、fallback:trueが使える。

fallback:trueの流れ】
例えば、予めgetStaticPathspathsにid=1,2が登録されている場合に、id=3のRequestが来たとする。

  1. /posts/3のパスにRequestが来る。
    pathsに定義されていないidであるが、fallback:trueなので、404ページを返却しない
  2. 裏側でサーバー側がgetStaticPropsを実行し、id=3に紐づく静的なファイルを生成する
  3. 生成が完了したら作成した静的ファイルを返す
  4. これ以降の/posts/3は2で作成した静的ファイルを返す
    ※2で生成された静的ファイルは、内容の更新があったとしても静的ファイル自体の更新はされない。
     つまり、2で一度静的ファイルが作成されたら4以降ではその静的ファイルをずっと参照する。
     更新した静的ファイルを参照するためにはIncremental Static Regeneration(ISR)(後述)を使用する。

以下、サンプルコードにて説明

pages/posts/[id].js
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が使えるようになる。

blog.js
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時に静的ページとして作成される。

pages/404.js
export default function Custom404() {
  return <h1>404 - Page Not Found</h1>
}
Yuki YukiYuki Yuki

API Routes

Creating API Routes

API Routesを使えば、Next.js内でNode.jsのWeb APIサーバを簡単に作成できる。
この機能により、WebフロントからサーバまですべてNext.js内で完結させることができるようになる。
作成する際はpages/api配下にJavaScriptファイルを作成する。

pages/api/example.js
// { 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 YukiYuki Yuki

Deply周りと、多少飛ばした部分はあるが、大まかな流れがわかったので、クローズ

このスクラップは2021/02/23にクローズされました