Next.js おぼえがき
sudo n
が機能しない問題
手始めに Node.js のアップデートをしておこうと思ったら、sudo n
が機能してなかった。
$ n latest
installing : node-v21.6.0
mkdir : /usr/local/n/versions/node/21.6.0
mkdir: cannot create directory ‘/usr/local/n’: Permission denied
Error: sudo required (or change ownership, or define N_PREFIX)
$ sudo n latest
sudo: n: command not found
調べると以下のような記事がヒット。
どうやらn
をインストールした際に現在使用しているユーザーのフォルダに対してインストールしていたのが問題で、sudo
は別のフォルダを参照しているため、うまく機能していなかったっぽい。
これを解決するため、sudo
の設定をちょっといじる。
sudo visudo
そしたら、以下の行をコメントアウト。
Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin"
かわりに以下の一行を追加。
Defaults env_keep +="PATH"
そしたら保存。
最後にもう一度 sudo n latest
してみると...
$ sudo n latest
installing : node-v21.6.0
mkdir : /usr/local/n/versions/node/21.6.0
fetch : https://nodejs.org/dist/v21.6.0/node-v21.6.0-linux-x64.tar.xz
copying : node/21.6.0
installed : v21.6.0 to /usr/local/bin/node
active : v18.12.0 at /home/yotu/.nodenv/versions/18.12.0/bin/node
うむ。問題なく機能した。
プロジェクト作成~サーバーを立てる
適当な場所にプロジェクトフォルダを作る。名前は何でもOK。
mkdir nextjs
cd nextjs
必要なライブラリのインストールをしておく。
npm install next@latest react@latest react-dom@latest typescript@latest @types/react @types/node
そしたら生成されたpackage.json
にコマンドを追記。
{
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "lint": "next lint"
+ },
"dependencies": {
...
}
}
そしたら表示するページを作っていく。
mkdir app
cd app
touch layout.tsx
touch page.tsx
layout.tsx
は以下の通り。
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="ja">
<body>{children}</body>
</html>
)
}
page.tsx
は以下の通り。
export default function Page() {
return <h1>Top Page</h1>;
}
ここまでできたら、ファイル構造は以下の通り。
├── app
│ ├── layout.tsx
│ └── page.tsx
├── next-env.d.ts
├── node_modules
├── package-lock.json
├── package.json
└── tsconfig.json
そしたら起動する。
npm run dev
ブラウザを開いて、localhost:3000
にアクセスする。
このような画面が表示されればOK。
page.tsx
最初に作成したpage.tsx
だが、これは「/
に対するページ」として自動的にルーティングされる。
これは Next.js の機能の一つで、たとえば/hoge
にページを作りたかったら/app/hoge/page.tsx
を作成してあげるとよい。
layout.tsx
すべてのページに適用されるレイアウトを記述する。
たとえば「/app/hoge
以下のすべてのページ」など適用範囲を絞りたい場合は、app/hoge
に別途layout.tsx
を作ってあげるとよい。
template.tsx
layout.tsx
とほぼ同等の機能だが、こちらはページを遷移するたびにDOMを再生成するという特徴を持つ。
ファイル名
指定のフォーマットでファイル名を付けることにより、ルーティングを調整したりできる。
_xxx
)
プライベートフォルダ(たとえば_hoge/page.tsx
を作成したとして、/hoge
もしくは_hoge
にアクセスしたとき、ほかのファイルとは違いアクセスできない。これは_hoge
がプライベートフォルダだからであり、たとえroute.ts
などを配置したとしてもアクセスはできなくなる。
(xxx)
)
ルートグループ(通常、app
内に作成されたフォルダはすべてルートとして認識される。
しかし、「ルートには含めたくないけど、いくつかのファイルをグループとしてフォルダにまとめたい」ということがある。そういう時はフォルダ名を丸括弧()
で囲むことで実現できる。
たとえばhoge/(huga)/page.tsx
にアクセスするとき、URLはlocalhost:3000/hoge
になる。
ちなみに、この機能を使ってルートが衝突した場合、より階層が浅いpage.tsx
ファイルが参照されてるみたい(要検証)。
[xxx]
)
ダイナミックルーティング(urlにパラメータを含めたい場合は、ダイナミックルーティングを使うことができる。
たとえば/user/{id}
のようなurlにアクセスして、ユーザーの詳細ページへ遷移したいとする。こういった場合はapp/user/[id]/page.tsx
のように鍵括弧をつけたフォルダを作成することで表現できる。
ちなみに、コード内では以下のように引数としてパラメータを受け取ることで値を使用できる。
export default function Page({ params }: { params: { id: number } }) {
return <h1>{ params.id }</h1>;
}
認証機能を作る
よく躓くので作ってみる。
Basic認証は今後のサービスで使いたくないので、今回は適当にGoogleアカウント認証として作ってみる。
Next.jsの認証機能はnext-auth
を使って作成していく。
GCP上にプロジェクトを作成する
まずは下準備として、Google Cloud Platformにプロジェクトを作成する。
そしたら「OAuth 同意画面」に移動して、「User Type」に「外部」を選択して「作成」を押す。
フォームが表示されるので、必要な情報を埋めていく。
入力を終えたら「保存して次へ」を押す。
そしたらスコープの設定画面に飛ぶ。ここではメールアドレスを認証に使うので「Googleアカウントのメインのメールアドレスの参照」にチェックを入れ、下部「更新」を押す。
スコープ一覧に問題なく追加されていれば「保存して次へ」。
次に、テストユーザーを設定する。
デフォルトでは誰もアクセスできないようになっているので、テストユーザーとして自分を追加しておく。
ここまでできたら「保存して次へ」を押した後、「認証情報」をクリック。
Next.jsからGoogleの認証にアクセスするには、トークンが必要となる。
上部「認証情報を作成」から「OAuth クライアント ID」をクリック。
そしたら必要事項を入力していく。
種類は「ウェブアプリケーション」、リンクは以下の通りにする。
- 承認済みのJavaScript 生成元:
http://localhost:3000
- 承認済みのリダイレクトURI:
http://localhost:3000/api/auth/callback/google
書き終えたら「作成」をクリックする。
そしたら以下のような画面に遷移するので、「クライアントID」「クライアントシークレット」をそれぞれメモする。
最後に「ライブラリ」をクリックし、「Google+ API」を検索して「有効にする」をクリック。
環境変数の設定
そしたらNext.jsのプロジェクトフォルダへ戻る。
環境変数を扱うのでdotenv
をインストールしておく。
npm i dotenv
そしたらルートフォルダに.env
ファイルを作成し、中に環境変数を書いておく。
NEXTAUTH_SECRET
はopenssl rand -base64 32
で生成したものを書いておく。
GOOGLE_CLIENT_ID="xxx"
GOOGLE_CLIENT_SECRET="xxx"
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="xxx"
ルーティングの設定
まずはnext-auth
をインストールする。
npm i next-auth
そしたらsrc/app/api/auth/[...nextauth]/route.ts
を作成し、以下のように記述する。
import NextAuth from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';
const handler = NextAuth({
secret: process.env.NEXTAUTH_SECRET,
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID ?? '',
clientSecret: process.env.GOOGLE_CLIENT_SECRET ?? '',
}),
]
});
export { handler as GET, handler as POST };
次にsrc/providers/NextAuth.tsx
を作成し、以下のように記述する。
'use client'; // SessionProviderがuseContextを使用するので必要
import { SessionProvider } from 'next-auth/react';
import { ReactNode } from 'react';
const NextAuthProvider = ({ children }: { children: ReactNode }) => {
return <SessionProvider>{children}</SessionProvider>;
};
export default NextAuthProvider;
これでセッション情報をほかの画面で使用することができる。
そしたらこれをlayout.tsx
に組み込んでおく。
import NextAuthProvider from "../src/providers/NextAuth";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ja">
<body>
<NextAuthProvider>{children}</NextAuthProvider>
</body>
</html>
);
}
ここまでで認証を組み込む下準備は完了。
コンポーネントを作成する
app/components
にLogin.tsx
とLogout.tsx
を作成する。
Login.tsx
import React from "react";
import { useSession, signIn } from "next-auth/react";
export default function Login() {
const { data: session, status } = useSession();
if (status === "loading") {
return <div>Loading...</div>;
}
if (status !== "authenticated") {
return (
<div>
<p>あなたはログインしていません</p>
<button onClick={() => signIn("google", {}, { prompt: "login" })}>
Googleでログイン
</button>
</div>
);
}
return null;
}
Logout.tsx
import React from "react";
import { useSession, signOut } from "next-auth/react";
export default function Logout() {
const { data: session, status } = useSession();
if (status === "authenticated") {
return (
<div>
<button onClick={() => signOut()}>ログアウト</button>
</div>
);
}
return null;
}
そしたらこれをルートページに組み込む。
"use client";
import { useSession } from "next-auth/react";
import Login from "./components/Login";
import Logout from "./components/Logout";
export default function Page() {
const { data: session, status } = useSession();
return (
<div>
{status === "authenticated" ? (
<div>
<p>セッションの期限:{session.expires}</p>
<p>ようこそ、{session.user?.name}さん</p>
<img
src={session.user?.image ?? ``}
alt=""
style={{ borderRadius: "50px" }}
/>
<div>
<Logout />
</div>
</div>
) : (
<Login />
)}
</div>
);
}
そしたらlocalhost:3000
にアクセスする。
「Googleでログイン」をクリック。
アカウント一覧が表示されるので、ログインしたいアカウントでログインする。
以下のようにログインが通ったらOK。