古生代紀のブログ

主にアプリ開発/その他ナレッジ

Next.js基本編学習メモ

はじめに

このブログはこの方のnext.jsの動画講義のメモです
GitHub - GomaGoma676/nextjs-lesson-hp: Nextjs + Tailwind CSS + Django REST Framework で学ぶモダンReact開発 / Project 1 🚀


Next.jsが使用されているアプリケーションの代表


App構成


認証基盤

  • JWT

Q. SSG = Static Site Generationってなんぞ?
A. HTMLを事前に生成するものらしい。高速表示できるメリット

学習準備・環境構築

プロジェクト生成, Tailwind CSSのインストール

npx create-next-app . --use-npm
npm install tailwindcss

使用できるコマンド

npm run dev
Starts the development server. サーバーの起動. 基本これを使用
npm run build
Builds the app for production.
npm start

この手順通りにやる
Install Tailwind CSS with Next.js - Tailwind CSS

ブログ機能の実装から基本を学ぶ

next.jsにおけるpagesとcomponentsの位置関係

pageは一つ一つのページに対応し、componentはヘッダー・フッターなどのコンポーネントに対応する


チートシートを参照しながらCSSは実装していく
Tailwind CSS Cheat Sheet


Home.module.cssの内容をTailwindに書き換えていく

書き換え後はCSSを別ファイルに記述することなくスタイルを適用できていることがわかる

<div className="min-h-screen py-0 px-2 flex flex-col justify-center items-center">

Tailwindを使用する場合はHome.module.cssは削除できる

Layout.jsは各画面で共有できる画面コンポーネントを記述する

icon などの画像ファイルはpublic直下に配置する

Tips!

remはfontに対するサイズ
fontのデフォルトは16pxなので、0.5remは8px

getStaticProps()について

ビルド時に実行されるメソッド
ビルド時にサーバーにアクセスしてHTMLを事前生成することに寄与する

以下の特徴がある

  • 必ずsever sideで実行
  • pages内でのみ使用可能
  • npm run dev リクエスト毎に実行
  • npm start ビルド時に実行される

基本形
ビルド時にgetStaticProps()のpropsが実行・取得されてファンクションコンポーネントの方のpropsに渡る

const Blog = ({datas}) => {
    return (
        <Layout title="Blog">
            Blog Page
        </Layout>
    );
};

export async function getStaticProps() {
    const datas = await サーバーからデータを取得するメソッド(); // libに書く
    return {
        props: {datas}
    };
};

サーバーサイドで実行されるgetAllPostsData();の処理はlibフォルダを作成してそのフォルダ直下に記載する
posts.jsを作成する
サーバーサイドのFetchと取得したいエンドポイントとバックエンドからデータを取得するメソッドの定義をする
fetchしてjsonに変換して返す

import fetch from "node-fetch";
const apiUrl = "https://jsonplaceholder.typicode.com/posts";

export async function getAllPostsData() {
    const res = await fetch(new URL(apiUrl))
    const posts = await res.json();
    return posts;
}


blogのpagesの方に実装

import { getAllPostsData } from "../lib/posts";

const Blog = ({ posts }) => {
    return (
        <Layout title="Blog">
            <ul className="m-10">
                {posts && posts.map((post) => <Post key={post.id} post={post} />)}
            </ul>
        </Layout>
    );
};

export async function getStaticProps() {
    const posts = await getAllPostsData();
    return {
        props: { posts },
    };
};


componentにPost.jsのコンポーネント作成

const Post = ({ post }) => {
    return (
        <div>
            <span>{post.id}</span>
            {" : "}
            <span className="cursor-pointer text-blue-500 border-b border-blue-500 hover:bg-gray-200">
                {post.title}
            </span>
        </div>
    )
}

export default Post
動作確認

npm run buildをするとビルド段階で.next>server>pages>blog-page.htmlにバックエンドから取得されるデータが事前にHTMLに埋め込まれていることがわかる
次にnpm startで起動しておく

Dynamic route()について

next.jsはpages以下のプロジェクト構成とルーティング名が一致している
postsの詳細ページを[posts_id].jsにする場合、pages/posts/[id].jsのような構成をとることができる
例えば100個記事があった場合は、100個の記事を(ビルド時に)事前にHTMLとして作成しておくことが可能

詳細ページのpre-renderingの流れ
1. getStaticPaths() idの一覧取得
2. getStaticProps() 各idを使用して個別のデータを取得
3. 取得したデータをpropsでRectComponentに渡してpre-rendering

基本形はこんな感じ
0 pathsメソッドを使用するまえにposts.jsでバックエンドからidsに関わるデータを取得しておく

export async function getAllPostIds() {
    const res = await fetch(new URL(apiUrl));
    const posts = await res.json();

    return posts.map((post) => {
        return {
            params: {
                id: String(post.id),
            },
        };
    });
}

0-2. postsフォルダをpages直下に作成し、その下に[id].js (このままの名前)を作成する
必要なものをインポート

import Link from "next/link";
import Layout from "../../components/Layout";


1 基本形
[id].jsにpathsを記述する

import { getAllPostIds } from "../../lib/posts";

export async function getStaticPaths() {
    const paths = await getAllPostIds();

    return {
        paths,
        fallback: false, // 例えば100個しかない記事データにユーザーが101番目などのデータ範囲外にアクセスした場合の挙動を決める falseの場合は404 not found
    };
}

fallback: falseについては、例えば100個しかない記事データにユーザーが101番目などのデータ範囲外にアクセスした場合の挙動を決める
falseの場合は404 not found
trueの場合は動的にデータが増えるニュースサイトの場合のケースなどに対応できる

2 基本形
getStaticProps() 各idを使用して個別のデータを取得

export async function getStaticProps({ params }) {
    const { post: post } = await getPostData(params.id);
    return {
        props: {
            post,
        },
    };
}

3 基本形
個別にデータを作成する処理を書く

export default function Post({ post }) {
    if (!post) {
        return <div>Loading...</div>
    }
    return (
        <>
           中身!!!!!!!!
        </>
    );
}

コンポーネントの中身の部分
ボタンのStyleはclassnがclassNameにするのとは踏んでつながっているところをlowerCamelCaseにすること(ビルドでエラーが出る)

        <Layout title={post.title}>
            <p className="m-4">
                {"ID : "}
                {post.id}
            </p>
            <p className="m-8 text-xl font-bold">{post.title}</p>
            <p className="px-10">{post.body}</p>
            <Link href="/blog-page">
                <div className="flex cursor-pointer mt-12">
                    <svg 
                        xmlns="http://www.w3.org/2000/svg" 
                        className="h-6 w-6 mr-3" 
                        fill="none" 
                        viewBox="0 0 24 24" 
                        stroke="currentColor"
                    >
                        <path 
                            strokeLinecap="round" 
                            strokeLinejoin="round" 
                            strokeWidth="2" 
                            d="M11 19l-7-7 7-7m8 14l-7-7 7-7" />
                    </svg>
                    <span>Back to blog-page</span>
                </div>
            </Link>
        </Layout>

出てくるアイコン元
https://heroicons.com/


一覧から詳細へ飛ぶためのリンクを設定する
components/Post.js

<Link href={`/posts/${post.id}`} ></Link>

なお、講義のspanタグ後ろの{" "}は抜くこと
あるとエラーが発生する

詳細画面が完成したら動作確認npm run devをして動作がもっさりすることを確認したら、次にプロセスを切って、

npm run buil 

をすると.nextにpost/[id].jsが1.jsなど個別に事前生成されるのがわかる
これで速くなるらしい(自分の環境ではまだもっさりした)

デプロイ

1.
最初にGithubにプッシュするので、まずはリポジトリを作成する

2.
作成したリポジトリの4行目あたりのコードを引っ張ってくる

git remote add origin https://github.com/[ユーザー名]/[リポジトリ名]

VSCodeでコミットメッセージを書いてコミット後、上記のコマンドを実行する
そしてプッシュする

git push -u origin main

3.
プッシュ出来たらVercelの公式ドキュメントの方を参照する
Next.js by Vercel - The React Framework
Deploy to Vercel - Deploying Your Next.js App | Learn Next.js

Import your [作ったリポジトリ名] repository
のところのリンクを押したら、リポジトリの選択画面が出てくるので、今作ったリポジトリのimportボタンを押す
デフォルトの状態でデプロイを実行
ビルドとかあるのでしばらくすると成功する
Visitをクリックすると自分の作ったサイトが動作する
f:id:shinseidaiki:20211109213343p:plain

めっちゃサクサク動く

バーセルにアカウント連携してのダッシュボードにアクセスするとサイトの管理ができる
Dashboard – Vercel

修正

アロー関数表記になっているファンクションコンポーネントをファンクションの形に直す(blog-page.js etc)

export default function Blog({ posts }) {

修正したものを再度コミットプッシュすると自動的にデプロイが走るようになる
コミットすると次はプッシュするだけでいい

git push -u origin main

上記コードには冗長なコードになっている実装があり修正できる
1

  // return {          <- ---変更箇所
  //   post,
  // };
  return post;        <- +++変更箇所

2

//const { post: post } = await getPostData(params.id);      <- ---変更箇所
const post = await getPostData(params.id);           <- +++変更箇所

プロジェクト2を作る(Django

ソースコード
GitHub - GomaGoma676/nextjs_restapi: Nextjs + Tailwind CSS + Django REST Framework で学ぶモダンReact開発 / Project 2 (REST API) 🚀

DjangoはわからなくなったらこっちのDjangoのこと書いてる箇所を見るように↓
フルスタック開発 Django x React プロジェクト作成手順 - 古生代紀のブログ


pycharmだと現在フォルダにプロジェクトを作成する方法が自分の環境では不具合が出るため以下のコードで実施

django-admin startproject backend


初めて見るパッケージ

pip install python-decouple
pip install dj-database-url
pip install dj-static
pip install PyJWT==2.0.0


Setting.pyに記載されたシークレットキーとデバッグモードの変数を環境変数化する
プロジェクトbackendフォルダ直下に .envファイルを作成する
ファイル形式を聞かれるのでtextを選択

.envの中身にSECRET_KEYを記載する
スペースとシングルクオテーションは取り除く
setting.pyに余計なインポートがあるとエラーが発生するため不要なインポートはしないこと

SECRET_KEY=h........................
DEBUG=False

setting.pyに記載されている当該部分を以下のように書き換える

SECRET_KEY = config('SECRET_KEY')
DEBUG = config('DEBUG')

CORS_ORIGIN_WHITELISTには最終的にフロントエンドのデプロイ先のURLを記載する


Databaseの内容を書きかえる
デフォルト(ローカル)ではsqliteを使用して、本番環境ではDATABASE_URLを使用する構成にする
from dj_database_url import parse as dburlを使用している

default_dburl = 'sqlite:///' + str(BASE_DIR / 'db.sqlite3')

DATABASES = {
    'default': config('DATABASE_URL', default=default_dburl, cast=dburl),
}

タイムゾーンの変更

TIME_ZONE = 'Asia/Tokyo'

StaticRootの設定
Herokuにデプロイする際に、ばらばらに配置されている静的ファイルを一つのフォルダにまとめてくれる設定

STATIC_ROOT = str(BASE_DIR / 'staticfiles')


djangoのユーザー作成の際には単純なcreate()で生成してはいけない
※パスワードが平文で保存されてしまうため
create_user()を使用すること
※ハッシュ化を行う
ex.

    def create(self, validated_data):
        user = User.objects.create_user(**validated_data)
        return user

DateTmeFieldはフォーマットを書き換えることができる

DateTimeField(format="%Y-%m-%d %H:%M:%S")


views,pyでユーザー作成ログインにかかるviewは誰でもアクセスするようにする

permission_classes = (AllowAny,)

※setting.pyの以下の記述が認証ユーザー以外のアクセスをデフォルトで禁じるため

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ],
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ],
}

特定idに基づいた特定の情報を取得するViewは○○RetrievwViewと書くのが一般的な模様

class PostRetrieveView(generics.RetrieveAPIView):


ModelViewSetを継承したviewはCRUDをデフォルトですべて利用できる

class TaskViewSet(viewsets.ModelViewSet):
    queryset = Task.objects.all()
    serializer_class = TaskSerializer

permission_classes を記載しなければJWTがデフォルトで要求される



url.pyのルーティング方法

① viewでModelViewSetを継承しているView -> routersで登録
② viewでgenericsを継承しているView -> urlpatternsで登録


JWT取得のエンドポイント
'djoser'を使用するとauth/jwt/createというエンドポイントを自動的に作成してくれる
存在するusernameとpassswordを渡すことでjwtをレスポンスするapiエンドポイント

path('auth/', include('djoser.urls.jwt')),


URLルートパスはrouterに設定

path('', include(router.urls)),

api endpointの確認

1 ユーザー登録
/api/register/

2 JWT取得
authen/jwt/create

POSTMANを利用する
カスタムユーザーを使っていない場合はkeyはusernameとpasswordとなる
HeadersのkeyにAutherizationを指定しておくことを忘れずに


3 api/tasks
tasksエンドポイントに関してはJWTトークンが必要と言われる
"detail": "Authentication credentials were not provided."
ModHeaderを利用してトークンをつけてAPIエンドポイントを再確認

他割愛

Herokuへのアップロード

GitHubにアップロード

まずはGitHubにPush
設定ボタンのVCSのenable version ...の設定画面を開いてGitになっていることを確認してOKを押す
.envをGitにアップロードしないために 右クリック>Git>add to .gitignore を選択する
右上のGit: のところにある緑のチェックマークのコミットボタンでコミットする
Unversioned Filesでチェックのついていないものをすべてチェックしてコミットする(.envがないことも確認する)

VCS>Importinto versiojn...>Share Project on GitHubを選択
VCSがGitになっている場合は Git>GitHub>Share Project on GitHubを選択

表示されるダイアログに必要な情報を入力してOKを押す(基本はPrivateにチェックを入れる)

最後に push をする(pushアイコンを選択すれば大丈夫)

GitHubにアクセスして反映されていれば大丈夫

Herokuにデプロイするための設定(wsgi.py)

wsgi.pyにClingをインポートしてapplicationの関数をラップする(変更箇所のみ)

from dj_static import Cling

application = Cling(get_wsgi_application())


次にターミナルで以下のコマンドを実施して、プロジェクトにインポートされているモジュールの一覧を出力する

pip freeze > requirements-dev.txt

requirements-dev.txt

asgiref==3.2.10
astroid==2.4.2
autopep8==1.5.4
certifi==2021.10.8
........

次にrequirements.txtをプロジェクト直下に作成して、以下の3行を追加する

-r requirements-dev.txt
gunicorn
psycopg2
  • r requirements-dev.txtは開発時に使用していたモジュールの一覧を読み込んでいる

psycopg2は本番環境で利用するpostgreSQL用のモジュール

警告文でpackage requirements '', '' are note satisfiedが表示されている場合も無視して大丈夫

Procfileの作成

続いてプロジェクト直下に Procfile という名前(拡張子なし)のファイルを作成する
pycharmにどの種類のファイル化聞かれた場合はtextにしておく
以下を追記

web: gunicorn backend.wsgi --log-file -

backend.wsgiのbackendはアプリケーションフォルダ名に対応しているため名前が違う場合は変える必要がある

Runtimeの作成

runtime.txtをプロジェクト直下に作成する
以下を追記

python-3.8.7
Herokuサイトにアクセス

1.
無料プランに登録(アカウント登録)

2.
登録完了後にアクセスできる自身のダッシュボードにアクセス

3.
create new appで新たなアプリを作成する

4.
app name はユニーク名
resionはUSとEuropeのみのためU.S.を選択

5.
新規作成するとHerokuがダッシュボードに手順を記載しているためその手順に従ってコマンドを実行していく

pycharmのターミナルにて以下のコマンドを実行

heroku plugins:install heroku-config

なお、Heroku CLIのインストールができていない場合は以下のコマンドを先に実行する※Macの場合

brew tap heroku/brew && brew install heroku
heroku login

Windowsの場合
公式サイトからインストーラーをダウンロードして実行する
その後 heroku ligin コマンドを実行する
Heroku CLI | Heroku Dev Center

heroku login

ブラウザが立ち上がって自動でログインしてくれる(認証情報をブラウザが保持している場合)


6
gitのリポジトリをHerokuと結びつける
git の設定は済んでいるのでgit initは省略できる
以下のコマンドを実行

heroku git:remote -a next-lesson-hp-backend

7.
GitHubリポジトリからHerokuにDjango側で環境変数に指定していたsetting.pyの値(DEBUGとSECRET_KEY)を反映させる

heroku config:push

8.
Heroku プロジェクトダッシュボードに戻ってSettingsの設定画面を開く
Reveal Config Varsのボタンをクリックする
DEBUGとSECRET_KEYがHerokuのConfigrationとして認識されていることを確認できれば大丈夫

9.
gitからherokuへのデプロイ
手順書のコマンドを丸コピーでよい
最後のpushで自動的にデプロイが走る

git add .
git commit -m "initial commit to heroku"
git push heroku master

デプロイ終了時にへロクのアプリURLが自動で生成される
https://[app-name].herokuapp.com/ deployed to Heroku


10. ホストのアクセス許可設定

アプリのURLにアクセスするとDisallowedHostエラーが発生してアクセス拒否されるため、ホストのアクセスを許可する設定をsetting.pyに記載する
setting.pyのALLOWED_HOSTSに先ほど生成されたURLを追記する

ALLOWED_HOSTS = [
    '[app-name].herokuapp.com'
]

シングルクオテーションで囲み、最後のスラッシュと先頭の https:// を削除して追記する

11.
9と同じ手順を実行

git add .
git commit -m "added host url"
git push heroku master

12.
本番環境ではpostgreSQLを使用しているため、再度マイグレーションと管理者ユーザーを作成する必要がある
heroku runを行うことでheroku側のCLIを実行することが可能となる

heroku run python3 manage.py migrate
heroku run python3 manage.py createsuperuser

管理者ユーザーは厳重に管理する必要がある
emailは設定しなくてもよい


13. Admin画面にアクセスできることを確認する
https://[app-name].herokuapp.com/admin
で先ほど作成したユーザーでログインできればよい



トラブルシュート

1. pythonのruntime
python-3.8.7ではなくpython-3.8.12 を指定すると解決するかもしれない
https://devcenter.heroku.com/articles/python-support#supported-runtimes

2. equirements.txtファイル
psycopg2モジュールのversionを2.8.6にすると解決するかもしれない

  • r requirements-dev.txt

gunicorn
psycopg2==2.8.6

プロジェクト2を作る(Next.js)

参考
GitHub - GomaGoma676/nextjs-blog-todos: Nextjs + Tailwind CSS + Django REST Framework で学ぶモダンReact開発 / Project 2 Blog + Todos 🚀

学習準備・環境構築

上記で記載した手順にならって実行
https://tailwindcss.com/docs/guides/nextjs


プロジェクト1の時と同じようにcomponents/Layout.jsを作成する

またプロジェクト2ではログイン画面としてcomponents下にAuth.jsも作成する

基本形 ガワのみ(Auth.js)

export default function Auth() {
    return <></>
}
index.jsを編集

デフォルトだとテンプレートが表示されるので、自作のLayout.jsとAuth.jsを読み込んでカスタマイズしていく
アプリのホーム画面はログイン画面とする実装に盛大に書き換える

import Auth from "../components/Auth";
import Layout from "../components/Layout";

export default function Home() {
  return (
    <Layout title="Login">
      <Auth />
    </Layout>
  )
}
ログイン画面の実装(Auth.js)

Tailwindのテンプレを利用する
https://tailwindui.com/components/application-ui/forms/sign-in-forms

サンプルにはrequire('@tailwindcss/forms'),を必要としているとあるので、npm istallする
npmのサイトに行って調べる

今回は
tailwind.config.jsのpluginに追記も必要な模様

plugins: [require("@tailwindcss/forms")],

テンプレをコピペして少しカスタマイズする
中央寄せなどの処理はLayout.jsで定義済みのため内側をコピーして貼り付けるだけ

return (
        <div className="max-w-md w-full space-y-8">
            <div>
                {/* eslint-disable-next-line @next/next/no-img-element */}  
                <img
                    className="mx-auto h-12 w-auto"
                    src="https://tailwindui.com/img/logos/workflow-mark-indigo-600.svg"
                    alt="Workflow"
                />
                <h2 className="mt-6 text-center text-3xl font-extrabold text-white">
                    Sign in to your account
                </h2>
            </div>
            <form className="mt-8 space-y-6" action="#" method="POST">
            <input type="hidden" name="remember" defaultValue="true" />
            <div className="rounded-md shadow-sm -space-y-px">
                <div>
                <input
                    id="email-address"
                    name="email"
                    type="email"
                    autoComplete="email"
                    required
                    className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
                    placeholder="Email address"
                />
                </div>
                <div>
                <input
                    id="password"
                    name="password"
                    type="password"
                    autoComplete="current-password"
                    required
                    className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
                    placeholder="Password"
                />
                </div>
            </div>

            <div className="flex items-center justify-center">
                <div className="text-sm">
                <span className="cursor-pointer font-medium text-white hover:text-indigo-500">
                    Change Mode?
                </span>
                </div>
            </div>

            <div>
                <button
                type="submit"
                className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
                >
                <span className="absolute left-0 inset-y-0 flex items-center pl-3">
                    <LockClosedIcon className="h-5 w-5 text-indigo-500 group-hover:text-indigo-400" aria-hidden="true" />
                </span>
                Sign in
                </button>
            </div>
            </form>
        </div>
    );
Tips

"Sign-in and Registration"テンプレートのreact版を使用するとclass ->className変更などの手作業の手間が減る
f:id:shinseidaiki:20211113152611p:plain
https://tailwindui.com/components/application-ui/forms/sign-in-forms


追加対応としてはこちらが必要
1. @heroicons/reactのnpm install

npm install @heroicons/react

2. Auth.jsに下記 import文の追加

import { LockClosedIcon } from '@heroicons/react/solid'

3.
-> タグに対するES Lint拡張機能の無効化
Nextjs ver11.0以降

{/* eslint-disable-next-line @next/next/no-img-element */}  
<img ............
Authコンポーネントの機能作成
環境変数の作成

rest apiにアクセスするための環境変数を作成する
プロジェクト直下に.env.localを作成
.env.localにHerokuにデプロイしたAppのURLを記載する
Herokuのダッシュボードからopen appで開いた時のURLを記載すればよい

NEXT_PUBLIC_RESTAPI_URL=https://[YOUR_APP_NAME].herokuapp.com/

なおnext.jsにおいてNEXT_PUBLIC_で始まる環境変数を定義した場合、アプリケーションの中でprocess.envという名前で呼び出すことが可能となる

Cookieの利用準備

CookieとFetchの際に使用するVercel社から推奨されているuse SWRをインストール

npm install universal-cookie
npm install swr


Auth.jsに必要なモジュールをインポート
準備

import { useState } from 'react';
import { useRouter } from 'next/router';
import Cookie  from 'universal-cookie';

const cookie = new Cookie()


functionコンポーネント で使用する変数の定義

export default function Auth() {

    const router = useRouter();
    const [username, setUsername] = useState("")
    const [password, setPassword] = useState("")
    const [isLogin, setIsLogin] = useState(true);


functionコンポーネント で使用するメソッドの定義
loginメソッド

const login = async () => {
        try {
            await fetch(
                `${process.env.NEXT_PUBLIC_RESTAPI_URL}api/auth/jwt/create/`,
                {
                    method: "POST",
                    body: JSON.stringify({ username:username, password:password }),
                    headers: {
                        "Content-Type": "application/json",
                    },
                }
            )
            .then((res) => {
                if (res.status === 400) {
                    throw "authentication failed";
                } else if (res.ok) {
                    return res.json();
                }
            })
            .then((data) => {
                const options = { path: "/"};
                cookie.set("access_token", data.access, options);
            })
            router.push("/main-page");    
        } catch (err) {
            alert(err);
        }
    };

then((data) => のoptionsのところは "/ "すなわちルートのパス以下でクッキーが有効であるという実装となる
"access_token"は自由な名前を付けられる



formでサブミットボタンが押された際に呼び出されるauthUserメソッドの実装
デフォルトでは送信ボタン押下時に画面リロードが走るため、画面再読み込みをしないような記述を以下にとる

    const authUser = async (e) => {
        e.preventDefault();
    };

ログインの場合はログインメソッドを呼び出し、そうでない場合はアカウント作成の処理を行う

const authUser = async (e) => {
        e.preventDefault();
        if (isLogin) {
            login();
        } else {
            try {
                await fetch(`${process.env.NEXT_PUBLIC_RESTAPI_URL}api/register/`, {
                    method: "POST",
                    body: JSON.stringify({ username:username, password:password }),
                    headers: {
                        "Content-Type": "application/json",
                    }
                })
                .then((res) => {
                    if (res.status === 400) {
                        throw "authentication failed";
                    } 
                });
                login();    
            } catch (err) {
                alert(err);
            }
        }
    };


コンポーネントレイアウト(return)の中身の変更箇所

表記

<h2 className="mt-6 text-center text-3xl font-extrabold text-white">
                    { isLogin ? "Login" : "Sign Up"}

フォームにauthUserを紐づける

<form className="mt-8 space-y-6" action="#" method="POST"> 
------>
<form className="mt-8 space-y-6" onSubmit={authUser}>

フォームにおけるidは不要 email->username

<input
                    name="username"
                    type="text"
                    autoComplete="username"
                    required
                    className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
                    placeholder="Username"
                    value={username}
                    onChange={e => setUsername(e.target.value)}
                />

passwordフォーム

<input
                    name="password"
                    type="password"
                    autoComplete="current-password"
                    required
                    className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
                    placeholder="Password"
                    value={password}
                    onChange={e => setPassword(e.target.value)}
                />

ログイン作成画面の状態切り替え

<span onClick={() => setIsLogin(!isLogin)} className="cursor-pointer font-medium text-white hover:text-indigo-500">
      Change Mode?

フォーム送信ボタンの表記

                {isLogin ? "Login with JWT" : "Create new user"}
ログイン成功後に遷移するmainページの作成

pagesの下にmain-page.jsを作成
ログアウト機能を持たせてまずは作成

import { useRouter } from "next/router";
import Cookie from "universal-cookie";

const cookie = new Cookie();

export default function MainPage() {
    const router = useRouter();
    const logout = () => {
        cookie.remove("access_token");
        router.push("/");
    };

    return (<></>)
}

ログアウトアイコンはhero iconsから拝借する
mt-10 cursor-pointerだけクラスネームに追加
onClick={logout}を追加

return (
        <Layout title="Main page">
            <svg 
                onClick={logout}
                xmlns="http://www.w3.org/2000/svg" 
                className="mt-10 cursor-pointer h-6 w-6" 
                fill="none" 
                viewBox="0 0 24 24" 
                stroke="currentColor"
            >
                <path 
                    strokeLinecap="round" 
                    strokeLinejoin="round" 
                    strokeWidth={2} 
                    d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" 
                />
            </svg>
        </Layout>
    );


ユーザー作成・ログイン・ログアウトの動作確認を試してトークンがF12のアプリケーションtokenで取得削除できていることを確認できていればよい


Main-page実装

blog-page.jsとtask-page.jsをpages直下に作成

blog-page.jsの戻るボタン実装
https://heroicons.com/の戻るボタンを使用

import Layout from "../components/Layout";
import Link from "next/link";

export default function BlogPage() {
    <Layout title="Blog Page">
        <Link href="/main-page">
            <div className="flex cursor-pointer mt-12">
                <svg 
                    xmlns="http://www.w3.org/2000/svg" 
                    className="h-6 w-6 mr-3" 
                    fill="none" 
                    viewBox="0 0 24 24" 
                    stroke="currentColor"
                >
                    <path 
                        strokeLinecap="round" 
                        strokeLinejoin="round" 
                        strokeWidth={2} 
                        d="M11 19l-7-7 7-7m8 14l-7-7 7-7" 
                    />
                </svg>
                <span>Back to main page</span>
            </div>
        </Link>
    </Layout>
}

task-page.jsもblog-pageと同様に作成



main-page.jsからブログとタスクへリンク作成

import Link from "next/link";

..........
           <div className="mb-10">
                <Link href="/blog-page">
                    <a className="bg-indigo-500 mr-8 hover:bg-indigo-600 text-white px-4 py-12 rounded" >
                        Visit Blog by SSG + ISR 
                    </a>
                </Link>
                <Link href="/task-page">
                    <a className="bg-indigo-500 mr-8 hover:bg-indigo-600 text-white px-4 py-12 rounded" >
                        Visit Task by SSG + ISR 
                    </a>
                </Link>
            </div>
blog-pageの詳細作成

プロジェクト1で使用したjson-placeholderのAPIと同じようにDjangoで作成したAPIエンドポイントからデータを取得してくる機能を実装していく

libフォルダを作成
posts.jsファイルをその下に作成

APIアクセスファイルの基本形

import fetch from "node-fetch";

export async function getAllPostsData() {
    
}

posts.js詳細

import fetch from "node-fetch";

export async function getAllPostsData() {
    const res = await fetch(
        new URL(`${process.env.NEXT_PUBLIC_RESTAPI_URL}api/list-post/`)
    );
    const posts = await res.json();
    const filteredPosts = posts.sort(
        (a,b) => new Date(b.created_at) - new Date(a.created_at) 
    );
    return filteredPosts;
}

sort((a,b) => new Data(b.created_at) - new Data(a.created_at) )は作成日の大きい順にソートするjavascriptの標準関数

この関数はビルド時にサーバーサイドで実行される
blog-page.jsに事前レンダリングできるような実装を施す

blog-page.js (step 1 getStaticProps()の実装)

import { getAllPostsData } from "../lib/posts";


......


export async function getStaticProps() {
    const filteredPosts = await getAllPostsData();
    return {
        props: { filteredPosts },
    };
}


blog-page.js (step 2 レンダリングの実装)

export default function BlogPage({ filteredPosts }) {
    return (
        <Layout title="Blog Page">
            <ul>
                { filteredPosts &&
                    filteredPosts.map((post) => <Post key={post.id} post={post} />)}
            </ul>

Postコンポーネントを作成する

ComponentsにPost.jsを作成
Post.js

export default function Post({ post }) {
    return (
        <div>
            <span>{post.id}</span>
            {" : "}
            <span className="cursor-pointer text-white border-b border-gray-500 hover:bg-gray-600 " >
                {post.title}
            </span>
        </div>
    );
}


npm run devで動作確認
npm run buildでビルド後、_next>server>pages>blog-page.htmlを開いて、Shit + Alt + Fでフォーマットを整えて、事前レンダリングがなされているかを確認する

npm run dev
npm run build

事前レンダリングされている


    2 : title2
    1 : Two Forms of Pre-rendering


Dynamic Router (Step3)

post.js

export async function getAllPostIds() {
    const res = await fetch(
        new URL(`${process.env.NEXT_PUBLIC_RESTAPI_URL}api/list-post/`)
    );
    const posts = await res.json();
    return posts.map((post) => {
        return {
            params: {
                id: String(post.id)
            },
        };
    });
}


export async function getPostData(id) {
    const res = await fetch(
        new URL(`${process.env.NEXT_PUBLIC_RESTAPI_URL}api/detail-post/${id}/`)
    );
    const post = await res.json();
    return {post,};
}


Post.jsに個別idへのリンクを記載する

            <Link href={`/posts/${post.id}`}>
                <span className="cursor-pointer text-white border-b border-gray-500 hover:bg-gray-600 " >
                    {post.title}
                </span>
            </Link>

pagesにpostsフォルダ作成
その下に[id].jsファイル作成

[id].js

import Link from "next/link";
import { useRouter } from "next/router";
import Layout from "../components/Layout";
import { getAllPostIds, getPostData } from "../../lib/posts";

export default function Post({ post }) {
    const router = useRouter();
    if (!post) {
        return <div>Loading...</div>;
    }
    return (
        <Layout title={post.title}>
            <p className="m-4">
                {"ID : "}
                {post.id}
            </p>
            <p className="mb-4 text-xl font-bold">{post.title}</p>
            <p className="mb-12">{post.created_at}</p>
            <p className="px-10">{post.content}</p>
            <Link href="/blog-page">
                <div className="flex cursor-pointer mt-12">
                    <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 mr-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                        <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 19l-7-7 7-7m8 14l-7-7 7-7" />
                    </svg>
                    <span>Back to blog-page</span>
                </div>
            </Link>
        </Layout>  
    );
}

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


export async function getStaticProps({ params }) {
    const { post: post } = await getPostData(params.id);
    return {
        props: {
            post,
        }
    };
}

アプリの動作確認をしてブログを確認する
確認できたら次にnpm run buildを実行する
_next>server>pages>postsの中に1.jsなどのファイルが事前レンダリングされていることを確認する

1.js



ID :

1

Two Forms of Pre-rendering

2021-11-13 14:47:28


Next.js has two forms of pre-rendering: **Static Generation** and
**Server-side Rendering**. The difference is in **when** it
generates the HTML for a page. - **Static Generation** is the
pre-rendering method that generates the HTML at **build time**. The
pre-rendered HTML is then _reused_ on each request. - **Server-side
Rendering** is the pre-rendering method that generates the HTML on
**each request**. Importantly, Next.js lets

確認出来たらnpm startを実行

fallback: false,になっているときは3.jsなどの存在しないものにアクセスしようとしたらNot Foundとなる
ただし、動的にデータが増えて3.jsができた場合もnot foundとなってしまうので、trueにする
その場合はif文の条件式もfallbackの効果を考慮する
[id].js

...
   if (router.isFallback || !post) {
        return <div>Loading...</div>;
    }
....

export async function getStaticPaths() {
    const paths = await getAllPostIds();
    return {
        paths,
        fallback: true,
    };
}

こうすると、新たなデータを追加してもNot Foundがでないようになる
そして
npm run buid
npm start
を再度実行

データを追加して、初めて新しいファイルにアクセスすると、フォールバックが働き事前レンダリングが行われたファイルが作成される
次回以降はこちらのファイルが参照されて高速アクセスが可能となる
ただし、あたらに作ったファイルを変更した場合は変更分は反映されない

変更も反映させたい場合はincrementalStaticGenerationを利用する


変更を反映させる(incrementalStaticGeneration)
incrementalStaticGenerationを有効化するのは
revalidateを一行追加することで実現できる

[id].jsにrevalidateを秒単位で指定して追加
例の場合は3秒となる

export async function getStaticProps({ params }) {
    const { post: post } = await getPostData(params.id);
    return {
        props: {
            post,
        }, 
        revalidate: 3
    };
}

そして
npm run buid
npm start
を再度実行

Tips!

incrementalStaticGenerationが気になる場合は頑張ってこれをよむしかない
https://vercel.com/docs/concepts/next.js/incremental-static-regeneration

また上記の実装では一覧画面においてのincrementalStaticGenerationは適用されていないので、適用させる場合はblog-page.jsにもrevalidateを同様に追記する

export async function getStaticProps() {
    const filteredPosts = await getAllPostsData();
    return {
        props: { filteredPosts },
        revalidate: 3,
    };
}

そして
npm run buid
npm start
を再度実行


task-page.jsの実装

基本はPostと同じ実装

useSWRの実装

SSG + PreFetch + Client Side Fetching
の組み合わせによるビルドスタイル

ニュースサイトなどのSEO対策が必要でかつリアルタイムのデータの反映が必要な場合に用いられる
task-page.jsにuseSWRを使用する
urlを受け取ってjsonに返すfetcherを定義しておく

import useSWR from "swr";

const fetcher = (url) => fetch(url).then((res) => res.json());
const apiUrl = `${process.env.NEXT_PUBLIC_RESTAPI_URL}api/list-task/`;

functionalコンポーネント内の実装

useSWRを使用する
build時に取得したデータを初期値として渡す
基本形

const { data:tasks, mutate } = useSWR(apiUrl, fetcher, {
        initialData: staticFilteredTasks,
    });

返り値がデータとしてかえって来るのでtasksという名前で格納している
mutateという関数もかえって来る
mutateはデータのキャッシュを最新に更新する機能が備わっている

トラブルシュート

task.-pagejsでuseSWRのinitialDataがfallbackDataに名前が変更になっている
さすがに名前から連想できない名前にされてて詰まった勘弁してほしい
https://swr.vercel.app/docs/options



ソートを行い、表示はstaticFilteredTasksと置換する

const filteredTasks = tasks?.sort(
        (a, b) => new Date(b.created_at) - new Date(a.created_at) 
    );


useSWR(client side fetch)を使用することによってbuild時だけでなく、useSWRを呼んでいる箇所で毎回最新情報をfetchしてくる
useSWRは非同期処理で取得でき次第data: tasksにデータを渡す
そしてfilteredTasksは上書きされる

task-page.jsが最新情報を取得してきた際に確実に画面に値をマウント反映させるためにuseEffectを利用する
TaskPageマウント時に一度だけ呼ばれるようにする

useEffect(()=>{
        mutate();
    }, [])

mutateを使用することでdata: tasksにキャッシュの情報を最新に反映させる

動作確認

npm run build 
npm start

SSG + PreFetch + Client Side Fetching の実装で作っているので、サーバー側でデータを変更したときにClient Side Fetching(useSWR)の効果ですぐにそのデータが反映されているのがわかる

ただし、この状態のClient Side Fetching(useSWR) + ISRの実装ではまだ、pre-renderingはまだ行われず、新規データが増えていたりデータが更新されている場合、リロードをすると古いデータが表示された後に最新データが表示されるという挙動が行われる
この状態ではまだ、JavaScriptを無効化(F12のsetting>Preference>Debuger>Disable JavaScript)すると最新情報を取得できなくなる

SWRの以下の説明ではSEO対策をしつつ、クライアントフェッチによってダイナミックにデータを取得できると記載されているが、ただしドキュメントには古いコンテンツと新しいコンテンツの乖離が著しくなってきた場合の対策方法についての記述がないが、Incremental Regeneration (revalidate) を利用すると最新情報にユーザーがアクセスしたタイミングで最新情報をHTMLにレンダリングして静的コンテンツを作成するようになり、現状起こっている不具合を解消することができる
Usage with Next.js – SWR

export async function getStaticProps() {
    const staticFilteredTasks = await getAllTasksData();
    return {
        props: { staticFilteredTasks },
        revalidate: 3,
    };
}

再度実行

npm run build 
npm start

この状態になるとビルド時のプリレンダリングだけでなく最新情報も一度ユーザーが最新情報にアクセスした時点でHTMLに事前レンダリングして静的コンテンツを作成しておくようになり、古い情報と新しい情報との乖離が限りなく少なくなる
ここまでくると、SSG + PreFetch + Client Side Fetching + ISR の最も実用的な実装となる

タスクの詳細画面の実装

tasksの[id].jsを作成する
基本的にはpostsと同じなので、割愛

useSWRを使用する部分が変わってくる
postと比べて差異のある部分を記述
tasks/[id].js

import { useEffect } from "react";
import useSWR from "swr";

const fetcher = (url) => fetch(url).then((res) => res.json());


// .............


export async function getStaticProps({ params }) {
    const { task: staticTask } = await getTaskData(params.id);
    return {
        props: {
            id: staticTask.id,
            staticTask,
        }, 
        revalidate: 3,
    };
}


ラブルシュート
useSWRのinitialDataがfallbackDataに名前が変更になっている


全体
tasks/[id].js

import { useEffect } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import Layout from "../../components/Layout";
import useSWR from "swr";
import { getAllTaskIds, getTaskData } from "../../lib/tasks";

const fetcher = (url) => fetch(url).then((res) => res.json());

export default function Post({ staticTask, id }) {
    const router = useRouter();
    const { data: task, mutate } = useSWR(
        `${process.env.NEXT_PUBLIC_RESTAPI_URL}api/detail-task/${id}`,
        fetcher,
        {
            fallbackData: staticTask,
        }
    );
    useEffect(() => {
        mutate();
    }, []);

    if (router.isFallback || !task) {
        return <div>Loading...</div>
    }
    return (
        <Layout title={task.title}>
            <span className="mb-4">
                {"ID : "}
                {task.id}
            </span>
            <p className="mb-4 text-xl font-bold">{task.title}</p>
            <p className="mb-12">{task.created_at}</p>
        
            <Link href="/task-page">
                <div className="flex cursor-pointer mt-12">
                    <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 mr-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                        <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 19l-7-7 7-7m8 14l-7-7 7-7" />
                    </svg>
                    <span>Back to task-page</span>
                </div>
            </Link>
        </Layout>
    );

}


export async function getStaticPaths() {
    const paths = await getAllTaskIds();
    return {
        paths,
        fallback: true,
    };
}


export async function getStaticProps({ params }) {
    const { task: staticTask } = await getTaskData(params.id);
    return {
        props: {
            id: staticTask.id,
            staticTask,
        }, 
        revalidate: 3,
    };
}

npm run devの時とbuildを一度した後ではビルド後は静的コンテンツを配信しているのでかなり高速にレスポンスが返ってくる
JavaScriptを無効化してもプリレンダリングされているので、表示されることがわかる

コンテンツを追加しても表示が追加される
コンテンツの変更においても同じ


TaskのCRUD作成

Task.jsを編集する

Cookieを利用する

import Cookie from "universal-cookie";

const cookie = new Cookie();
削除機能メソッドの追加
..............function Task({ task, taskDeleted }) {

const deleteTask = async () => {
        await fetch(`${process.env.NEXT_PUBLIC_RESTAPI_URL}api/tasks/${task.id}`, {
            method: "DELETE",
            headers: {
                "Content-Type": "application/json",
                Authorization: `JWT ${cookie.get("access_token")}`,
            },
        }).then((res) => {
            if (res.status === 401) {
                alert("JWT Token not valid");
            }
        });
        taskDeleted();
    }

削除後の一覧画面の更新を行う場合は一覧画面のmutateを受け取ってdeleteの処理で呼び出す必要があるためtask-page.jsのmutateを渡す

{ filteredTasks &&
                    filteredTasks.map((task) => <Task key={task.id} task={task} taskDeleted={mutate} />)}


ゴミ箱アイコンの表示はheroiconsから拝借
Heroicons


削除の動作確認

npm run build
npm start

タスクを削除したときにすぐに消えればOK

編集機能の実装

Contextフォルダを作成してStateContext.jsを作成

StateContext.js基本形

import { createContext, useState } from "react";

export const StateContext = createContext();

export default function StateContextProvider(props) {
    const [selectedTask, setSelectedTask] = useState({ id: 0, title: ""});
    
    return (
        <StateContext.Provider
            value={{
                selectedTask,
                setSelectedTask,
            }}
        >
            {props.children}
        </StateContext.Provider>
    )
}


task-pageにproviderを利用する
task-page.jsのファンクションコンポーネントで囲む

import StateContextProvider from "../context/StateContext";


........  function 

......   return (
         <StateContextProvider>
                   .............................
          </StateContext>
         )
編集機能 編集ボタン・メソッド実装

Task.js

import { useContext } from "react";
import { StateContext } from "../context/StateContext";

....アイコン実装

編集アイコンはeditで検索してpencilを使用する
編集アイコンのオンクリックは setSelectedTask(task)を呼ぶようにすることで選択されたタスクがフォームにセットされる
ファンクションコンポーネントのreturn以外でのProviderはStateContextを使用する
編集アイコンはfloat-leftのCSSを利かせないと上下に並ぶ異常なレイアウトが構築されてしまう

Formの作成

components下にTaskForm.jsを作成
Create, Update機能の実装をする

import { useContext } from "react";
import { StateContext } from "../context/StateContext";
import Cookie from "universal-cookie";

const cookie = new Cookie();

export default function TaskForm({ taskCreated }) {
    const { selectedTask, setSelectedTask } = useContext(StateContext);

    const create = async (e) => {
        e.preventDefault();
        await fetch(`${process.env.NEXT_PUBLIC_RESTAPI_URL}api/tasks`, {
            method: "POST",
            body: JSON.stringify({ title: selectedTask.title }),
            headers: {
                "Content-Type": "application/json",
                Authorization: `JWT ${cookie.get("access_token")}`,
            },
        }).then((res) => {
            if (res.status === 401) {
                alert("JWT Token not valid");
            }
        });
        setSelectedTask({ id: 0, title:"" });
        taskCreated();
    };
    
    const update = async (e) => {
        e.preventDefault();
        await fetch(`${process.env.NEXT_PUBLIC_RESTAPI_URL}api/tasks/${selectedTask.ids}`, {
            method: "PUT",
            body: JSON.stringify({ title: selectedTask.title }),
            headers: {
                "Content-Type": "application/json",
                Authorization: `JWT ${cookie.get("access_token")}`,
            },
        }).then((res) => {
            if (res.status === 401) {
                alert("JWT Token not valid");
            }
        });
        setSelectedTask({ id: 0, title:"" });
        taskCreated();
    };

    return (
        <div>
            <form onSubmit={selectedTask.id !== 0 ? update : create}>
                <input 
                    className="text-black mb-8 px-2 py-1"
                    type="text"
                    value={selectedTask.title}
                    onChange={ (e) => setSelectedTask({ ...selectedTask, title: e.target.value}) }
                />
                <button 
                    type="submit"
                    className="bg-gray-500 ml-2 hover:bg-gray-600 text-sm px-2 py-1 rounded uppercase"
                >
                    {selectedTask.id !== 0 ? "Update": "Create"}
                </button>
            </form>
        </div>
    );
};

taskCreated()はmutate()でありtask-pageからmutateを受け取る必要がある(updateの方もcreateのmutateを利用する)



task-pageに実装
task-page.js

import TaskForm from "../components/TaskForm";

.....
   <Layout.......>
         <TaskForm taskCreated={mutate} />

mutateを渡すことを忘れないこと

動作確認

CRUDの動作が確認できていればよい


Vercelにデプロイ

GitHubにpushして
Deploy to Vercel - Deploying Your Next.js App | Learn Next.js
mport your nextjs-blog repositoryのリンクをクリック
https://vercel.com/import/git.


** 環境変数のデプロイ

環境変数をローカルで利用している場合は(例: .env.local)Environment Variablesに設定する必要がある
f:id:shinseidaiki:20211117215942p:plain
例:ホスト名を環境変数
Name NEXT_PUBLIC_RESTAPI_URL
Value https://[appname].herokuapp.com/
入力が終わればAddする
デプロイが終わった後はDjangoのCORSのWHITELISTの設定を修正する

バックエンド(Django)の接続許可(CORS)の設定

CORS_ORIGIN_WHITELIST = [
"http://localhost:3000",
]

にVercelのデプロイ先のホストを追記する
末尾のスラッシュは削除

CORS_ORIGIN_WHITELIST = [
    "http://localhost:3000",
    "https://[APPNAME].vercel.app"
]

その後、Herokuにデプロイする

git add .
git commit -m "added cors white list"
git push heroku master


デプロイ後、フロントから機能が使えているか動作確認をして使えれば大丈夫


End.

修正

無駄なオブジェクト形式の修正
return {task,}みたいになっているところはreturn tasksでよい
const { task: staticTask }みたいになってるところはconst staticTaskでいい