Memo

メモ > 技術 > フレームワーク: Next.js

概要
Next.js は、React を使ってWebページを作成できるフレームワーク 「ネクスト・ジェイエス」と読む ただし今新規に作成するなら、Next.js ではなく Nuxt.js でいいのかもしれない Next.js by Vercel - The React Framework https://nextjs.org/ なぜNext.jsを採用するのか? - mottox2 blog https://mottox2.com/posts/429 Next.js - Qiita https://qiita.com/nkzawa/items/1e0e93efd13fb982c8c0 TypeScript と Redux に入門しながら Next.js でサンプルアプリケーションを作ってみた - Qiita https://qiita.com/monzou/items/8ff07258a42042075d7b Next.js 4年目の知見:SSRはもう古い、VercelにAPIサーバを置くな - Qiita https://qiita.com/jagaapple/items/faf125e28f8c2860269c 無償版PaaSだけでGithubでログインするNext.jsアプリを作る - Qiita https://qiita.com/greenteabiscuit/items/b32a5715d48cbc794a70 Next.js は本当にSEOに強いのか調べてみた https://zenn.dev/fukurose/articles/e15df7129cc421 結局ReactとNext.jsのどちらで開発を進めればいいの? - Qiita https://qiita.com/hiroki-yama-1118/items/b3388c5dcb155e2e367d ■App Router Next.jsでは「Page Router」と呼ばれる手法が使われていたが、後から「App Router」と呼ばれる手法が導入された 新しく作るアプリケーションでは「App Router」を使うことが推奨されるようだが、これにより考えることが多くなった Next.js って App Router が出てきて平和じゃなくなったよね https://zenn.dev/noko_noko/articles/3ccc64c389259c App Router時代のZero-Runtimeの理解を深める https://zenn.dev/blueish/articles/e8bc1a5caf139f App Router で1年間開発した知見と後悔 https://zenn.dev/ficilcom/articles/091abe948f44fb Next.jsの考え方 https://zenn.dev/akfm/books/nextjs-basic-principle それなら、シンプルな構成のRemixに移行しよう…という動きもあるらしい Remixについては「Remix.txt」を参照
起動
>cd C:\path\to\next_project >npx create-next-app next_app Need to install the following packages: create-next-app@14.2.3 Ok to proceed? (y) √ Would you like to use TypeScript? ... No / Yes √ Would you like to use ESLint? ... No / Yes √ Would you like to use Tailwind CSS? ... No / Yes √ Would you like to use `src/` directory? ... No / Yes √ Would you like to use App Router? (recommended) ... No / Yes √ Would you like to customize the default import alias (@/*)? ... No / Yes Creating a new Next.js app in C:\localhost\home\test\public_html\react\next_app.
next_app フォルダが作成され、その中にファイルが作成される プロジェクトの操作はこのフォルダ内で行うため、フォルダ内に移動しておく
>cd next_app
以下でプロジェクトを実行する
>npm run dev > next_app@0.1.0 dev > next dev ▲ Next.js 14.2.3 - Local: http://localhost:3000 Starting... Attention: Next.js now collects completely anonymous telemetry regarding usage. This information is used to shape Next.js' roadmap and prioritize features. You can learn more, including how to opt-out if you'd not like to participate in this anonymous program, by visiting the following URL: https://nextjs.org/telemetry Ready in 6.6s
上記のように表示され、表示されている http://localhost:3000 にアクセスするとページが表示される
基本の構成
src/app 内のファイルを以下のように編集し、テキストが表示されるだけの状態にする これをもとに、ファイルの編集を試していく src/app/page.tsx
export default function Home() { return ( <main> <h1 className="text-2xl font-bold m-5">Next.js</h1> <p className="m-5">これはサンプルアプリケーションです。</p> </main> ) }
src/app/layout.tsx
import type { Metadata } from 'next' import { Inter } from 'next/font/google' import './globals.css' const inter = Inter({ subsets: ['latin'] }) export const metadata: Metadata = { title: 'Next.js', description: 'Generated by create next app', } export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode }>) { return ( <html lang="ja"> <body className={inter.className}>{children}</body> </html> ) }
src/app/globals.css
@tailwind base; @tailwind components; @tailwind utilities; :root { --foreground-rgb: 0, 0, 0; --background-start-rgb: 255, 255, 255; --background-end-rgb: 255, 255, 255; } @media (prefers-color-scheme: dark) { :root { --foreground-rgb: 255, 255, 255; --background-start-rgb: 0, 0, 0; --background-end-rgb: 0, 0, 0; } } body { color: rgb(var(--foreground-rgb)); background: linear-gradient( to bottom, transparent, rgb(var(--background-end-rgb)) ) rgb(var(--background-start-rgb)); } @layer utilities { .text-balance { text-wrap: balance; } }
基本の機能
■Reactによる機能実装とTailwind CSSによる装飾 src/app/page.tsx
'use client' import { useState } from 'react' export default function Home() { const [name, setName] = useState('') const [message, setMessage] = useState('名前を入力してください。') const doInput = (e: any)=> { setName(e.target.value) } const doClick = (e: any)=> { e.preventDefault() setMessage('こんにちは、' + name + 'さん。') } return ( <main> <h1 className="title">Next.js</h1> <p className="message">{message}</p> <form className="form"> <input type="text" className="input" onChange={doInput} /> <button className="button" onClick={doClick}>クリック</button> </form> </main> ) }
src/app/globals.css (追加分)
.title { @apply text-2xl font-bold m-5; } .message { @apply m-5; } .form { @apply m-5; } .input { @apply p-1 border-solid border-2 border-gray-400; } .button { @apply ms-1 px-4 py-2 bg-gray-800 text-white text-sm rounded-lg; }
■ルーティングとページ遷移 src/app/page.tsx
'use client' import { useState } from 'react' import Link from 'next/Link' export default function Home() { const [name, setName] = useState('') const [message, setMessage] = useState('名前を入力してください。') const doInput = (e: any)=> { setName(e.target.value) } const doClick = (e: any)=> { e.preventDefault() setMessage('こんにちは、' + name + 'さん。') } return ( <main> <h1 className="title">Next.js</h1> <p className="message">{message}</p> <form className="form"> <input type="text" className="input" onChange={doInput} /> <button className="button" onClick={doClick}>クリック</button> </form> <div className="m-5"> <Link href="/other" className="text-blue-600 underline">Other pageへ</Link> </div> </main> ) }
src/app/other/page.tsx
'use client' import Link from 'next/Link' export default function Other() { return ( <main> <h1 className="title">Other page</h1> <p className="message">これは別のページです。</p> <div className="m-5"> <Link href="/" className="text-blue-600 underline">戻る</Link> </div> </main> ) }
■ローカルCSS ローカルCSSを作成し、「import './style.css'」で読み込むことができる これで src/app/globals.css の内容が上書きされる src/app/other/style.css
.title { @apply text-4xl text-red-600; }
src/app/other/page.tsx
'use client' import Link from 'next/Link' import './style.css' export default function Other() { return ( <main> <h1 className="title">Other page</h1> <p className="message">これは別のページです。</p> <div className="m-5"> <Link href="/" className="text-blue-600 underline">戻る</Link> </div> </main> ) }
■Styled JSX src/app/other/page.tsx
'use client' import Link from 'next/Link' import './style.css' import JSXStyle from 'styled-jsx/style' export default function Other() { return ( <main> <JSXStyle> {`p.message { text-align: center; font-weight: bold; }`} </JSXStyle> <h1 className="title">Other page</h1> <p className="message">これは別のページです。</p> <div className="m-5"> <Link href="/" className="text-blue-600 underline">戻る</Link> </div> </main> ) }
■ダイナミックルーティング src/app/list/[id]/page.tsx
'use client' import Link from 'next/Link' export default function List({params}:{params:{id: number}}) { return ( <main> <h1 className="title">Lis page</h1> <p className="message">指定されたIDは「{params.id}」です。</p> <div className="m-5"> <Link href="/" className="text-blue-600 underline">戻る</Link> </div> </main> ) }
http://localhost:3000/list/1 にアクセスすると「指定されたIDは「1」です。」と表示され、 http://localhost:3000/list/2 にアクセスすると「指定されたIDは「2」です。」と表示される src/app/list/page.tsx
'use client' import Link from 'next/Link' export default function List() { return ( <main> <h1 className="title">Lis page</h1> <p className="message">IDを指定してアクセスしてください。</p> <div className="m-5"> <Link href="/" className="text-blue-600 underline">戻る</Link> </div> </main> ) }
http://localhost:3000/list/ にアクセスすると「IDを指定してアクセスしてください。」と表示される ■クエリパラメータ src/app/query/page.tsx
'use client' import { useSearchParams } from 'next/navigation' export default function Query() { const params = useSearchParams() return ( <main> <h1 className="title">Query sample</h1> <p className="message">パラメータの取得。</p> <ul className="m-5"> <li>id: {params.get('id')}</li> <li>pass: {params.get('pass')}</li> </ul> </main> ) }
http://localhost:3000/query?id=test&pass=abcd1234 のようにアクセスすると、指定してidとpassの値が表示される
データアクセス
■サーバコンポーネントからデータアクセス ページにアクセスすると、JSONデータを取得して画面に表示する src/app/public/sample.json
{ "message":"これはデータのサンプルです。", "data":[ {"name":"太郎","mail":"taro@example.com","age":"39"}, {"name":"花子","mail":"hanako@example.com","age":"28"}, {"name":"幸子","mail":"sachico@example.com","age":"17"} ] }
src/app/fetch/page.tsx
'use server' async function getSampleData() { const resp = await fetch( 'http://localhost:3000/sample.json', { cache: 'no-store' } ) const result = await resp.json() return result } export default async function Fetch() { const data = await getSampleData() return ( <main> <h1 className="title">Fetch sample</h1> <p className="message">{data.message}</p> </main> ) }
■クライアントコンポーネントからデータアクセス ボタンをクリックすると、JSONデータを取得して画面に表示する src/app/public/sample.json
{ "message":"これはデータのサンプルです。", "data":[ {"name":"太郎","mail":"taro@example.com","age":"39"}, {"name":"花子","mail":"hanako@example.com","age":"28"}, {"name":"幸子","mail":"sachico@example.com","age":"17"} ] }
src/app/fetch/page.tsx
'use client' import { useState } from 'react' async function getSampleData() { const resp = await fetch( 'http://localhost:3000/sample.json', { cache: 'no-store' } ) const result = await resp.json() return result } export default function Fetch() { const [message, setMessage] = useState('No data.') const doInput = (e: any) => { setMessage(e.target.value) } const doClick = (e: any)=> { e.preventDefault() getSampleData().then(resp => { setMessage(resp.message) }) } return ( <main> <h1 className="title">Fetch sample</h1> <p className="message">{message}</p> <form className="form"> <button className="button" onClick={doClick}>クリック</button> </form> </main> ) }
サーバアクション
src/app/public/sample.json
{ "message":"これはデータのサンプルです。", "data":[ {"name":"太郎","mail":"taro@example.com","age":"39"}, {"name":"花子","mail":"hanako@example.com","age":"28"}, {"name":"幸子","mail":"sachico@example.com","age":"17"} ] }
src/app/action/page.tsx
'use client' import { getSampleData } from './get-sample-data' export default async function Action() { return ( <main> <h1 className="title">Action sample</h1> <p className="message">ボタンをクリックしてください。</p> <form className="m-5" action={getSampleData}> <input type="number" className="input" name="number" /> <button className="button">クリック</button> </form> </main> ) }
src/app/action/get-sample-data.tsx
'use server' import { redirect } from 'next/navigation' export async function getSampleData(form) { const number = form.get('number') const resp = await fetch( 'http://localhost:3000/sample.json', { cache: 'no-store' } ) const result = await resp.json() console.log(number) let data = result.data[number] if (data == null) { data = {name:'-',mail:'-',age:0} } const query = 'name=' + data.name + '&mail=' + data.mail + '&age=' + data.age const searchParams = new URLSearchParams(query) redirect('/action-result?' + searchParams.toString()) }
src/app/action-result/page.tsx
'use client' import Link from 'next/Link' import { useSearchParams } from 'next/navigation' export default async function ActionResult() { const params = useSearchParams() return ( <main> <h1 className="title">Action sample</h1> <p className="message">以下のデータを取得しました。</p> <ul className="m-5"> <li>name: {params.get('name')}</li> <li>mail: {params.get('mail')}</li> <li>age: {params.get('age')}</li> </ul> <div className="m-5"> <Link href="/action" className="text-blue-600 underline">戻る</Link> </div> </main> ) }
ファイルアクセス
■サーバコンポーネントからファイルアクセス src/app/memo/page.tsx
'use server' import { saveMemo } from './memo-action' export default async function Memo() { return ( <main> <h1 className="title">Memo</h1> <p className="message">メモを入力してください。</p> <form className="m-5" action={saveMemo}> <div><textarea className="input" name="memo" cols="30" rows="3"></textarea></div> <button className="button">保存</button> </form> </main> ) }
src/app/memo/memo-action.tsx
'use server' import { redirect } from 'next/navigation' import fs from 'fs' const filename = './src/app/memo/data.txt' export async function saveMemo(form) { const memo = form.get('memo') fs.writeFileSync(filename, memo) redirect('/memo-complete') } export async function readMemo() { return fs.readFileSync(filename, 'utf8') }
src/app/memo-complete/page.tsx
'use server' import Link from 'next/Link' import { readMemo } from '../memo/memo-action' export default async function MemoComplete() { const memo = await readMemo() return ( <main> <h1 className="title">Memo</h1> <p className="message">メッセージを保存しました。</p> <pre className="message">{memo}</pre> <div className="m-5"> <Link href="/memo" className="text-blue-600 underline">戻る</Link> </div> </main> ) }
src/app/memo/data.txt
(カラのファイル)
■クライアントコンポーネントからファイルアクセス src/app/memo-show/page.tsx
'use client' import { readMemo } from '../memo/memo-action' import { useState, useEffect } from 'react' export default function MemoShow() { const [memo, setMemo] = useState('No data.') useEffect(() => { readMemo().then(res => { setMemo(res) }) }, []) return ( <main> <h1 className="title">Memo</h1> <p className="message">メッセージは以下のとおりです。</p> <pre className="message">{memo}</pre> </main> ) }
APIアクセス
src/app/request-api/route.ts
'use server'; export async function GET(request: Request) { const res = { content: 'Hello, World!' }; return new Response(JSON.stringify(res), { status: 200, headers: { 'Content-Type': 'application/json' }, }); } export async function POST(request: Request) { const data = await request.json(); return new Response(JSON.stringify(data), { status: 200, headers: { 'Content-Type': 'application/json' }, }); }
src/app/request/page.tsx
'use client' import { useState, useEffect } from 'react' export default function Request() { const [data, setData] = useState('') const [input, setInput] = useState('') useEffect(() => { fetchData() }, []) const fetchData = async () => { try { const response = await fetch('http://localhost:3000/request-api', { method: 'GET' }) if (!response.ok) { throw new Error('Network response was not ok.') } const json = await response.json() setData(json) } catch (error) { console.error('Failed to fetch data: ', error) } } const doSubmit = async (event) => { event.preventDefault() try { const response = await fetch('http://localhost:3000/request-api', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ content: input }), }) if (!response.ok) { throw new Error('Network response was not ok.') } const json = await response.json() setData(json) } catch (error) { console.error('Failed to post data: ', error) } } return ( <main> <h1 className="title">Request</h1> <form className="m-5" onSubmit={doSubmit}> <input type="text" value={input} className="input" onChange={(e) => setInput(e.target.value)} /> <button className="button" type="submit">送信</button> </form> <div className="message"> {data ? ( <pre>{JSON.stringify(data, null, 2)}</pre> ) : ( <p>Now Loading...</p> )} </div> </main> ) }
ページ遷移アニメーション
JavaScriptでURLを書き換えてページ遷移したように見せることができる 恐らく、内部的には同じ方法が使われているのだと思われる 「a」ではなく「Link」でページ遷移することで、実際のページ遷移が発生しないようにしている 検証ページ https://www.refirio.org/memos/javascript/state/ JavaScriptによるページ遷移なので、ページ遷移時のアニメーションを凝ったものにするなどができそう Next.js + Framer Motion でページ遷移アニメーションを実装する #TypeScript - Qiita https://qiita.com/arrow2nd/items/b16385cf22c567fbbf33 Next.js x Framer Motion : ページ遷移時にふわっと切り替わるアニメーション https://zenn.dev/takna/articles/next-framer-motion-page-fade-in-animation Next.js13.4 + Framer Motion でページ遷移時の ふわっ とした動きを実装してみた。 https://zenn.dev/bloomer/articles/3a814d9f054198 以下は遷移前と遷移後のページを同時に存在させて、トランジションを設定する解説みたい また確認したい Next.jsでPage Transitionを組む方法|東京のWEB制作会社・ホームページ制作会社|株式会社GIG https://giginc.co.jp/blog/giglab/Nextjs-page-transition ■作業前 src/app/motion/page.tsx
'use client' import Link from 'next/Link' export default function Motion() { return ( <main> <h1 className="title">Motion</h1> <p className="message">ページ遷移します。</p> <div className="m-5"> <Link href="/motion-after" className="text-blue-600 underline">進む</Link> </div> </main> ) }
src/app/motion-after/page.tsx
'use client' import Link from 'next/Link' export default function Motion() { return ( <main> <h1 className="title">Motion</h1> <p className="message">ページ遷移しました。</p> <div className="m-5"> <Link href="/motion" className="text-blue-600 underline">戻る</Link> </div> </main> ) }
■アニメーション設定 まずは framer-motion をインストールする
>npm install framer-motion
アニメーション用のコンポーネントを作成する
'use client' import { usePathname } from 'next/navigation' import { AnimatePresence } from 'framer-motion' import { motion } from 'framer-motion' export default function MotionWrapper({ children, }: { children: React.ReactNode }) { // 一意のキーを設定するためにラップした画面のパスを取得 const pathName = usePathname() return ( // アンマウント時の動きをつけるために必要な記述 <AnimatePresence mode="wait"> // 動きをつけるために必要な記述 // 具体的な動きを記述 // 今回はopacityを使用して「ふわっ」を実現 <motion.div key={pathName} initial={{ opacity: 0 }} // 初期状態 animate={{ opacity: 1 }} // マウント時 exit={{ opacity: 0 }} // アンマウント時 transition={{ type: 'linear', duration: 2, }} > {children} </motion.div> </AnimatePresence> ) }
以下のようにMotionWrapperを指定すると、ページを表示させた際にアニメーションが表示される
'use client' import Link from 'next/Link' import MotionWrapper from '@/components/motionWrapper/motionWrapper' export default function Motion() { return ( <MotionWrapper> <main> <h1 className="title">Motion</h1> <p className="message">ページ遷移しました。</p> <div className="m-5"> <Link href="/motion" className="text-blue-600 underline">戻る</Link> </div> </main> </MotionWrapper> ) }
EC2での稼働
■参考になりそうな記事 AWS EC2でNext.jsの環境構築する際の備忘録 #AWS - Qiita https://qiita.com/naniwadari/items/d74fc4a649e69477fa6f Next.js(&TS)アプリをEC2にデプロイする https://zenn.dev/eng_o109/articles/41611f14917ba2 React/Next.jsアプリケーションを作成し、AWS EC2を使って本番環境にデプロイするまで #JavaScript - Qiita https://qiita.com/longtime1116/items/18553e43bfb44cbc9d81 【2020年】AWS EC2でNext.jsの環境構築 - VIVITABLOG https://blog.vivita.io/entry/2020/08/17/112932 初心者がNext.jsをECSで動かすまで #Next.js - Qiita https://qiita.com/mkt1234/items/52735503624d12243cf9 ■想定の手順 ※ChatGPTに確認した概要。正しい内容かどうかは未検証 ※Next.jsとLaravelを同一EC2で稼働させる場合の例 ※これで問題ないなら、Nginxはローカル開発用のDockerにも含めておくと良さそう Next.jsを3000番ポートで稼働させる トップページにリクエストされた場合などは、このポートで処理を行う想定とする Laravelを8000番ポートで稼働させる /api 配下にリクエストされた場合に限り、このポートで処理を行う想定とする Nginxを80番ポートで稼働させ、以下のように設定する
server { listen 80 default_server; server_name _; # 静的ファイルへのリクエストはNext.jsが処理 location / { proxy_pass http://localhost:3000; # Next.jsのポート proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; } # APIリクエストをLaravelに転送 location /api { proxy_pass http://localhost:8000; # Laravelのポート proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }
ロードバランサーでは80番ポートと443番ポートのみを開放する 80番ポートにアクセスがあれば、443番ポートに転送する 443番ポートにアクセスがあれば、Nginxの80番ポートに転送する これでNext.jsアプリケーションが稼働するはず
メモ
■入門 とほほのNext.js入門 - とほほのWWW入門 https://www.tohoho-web.com/ex/nextjs.html ■CRUD Next.js + LaravelでCRUD機能を作成する #Laravel - Qiita https://qiita.com/masakiwakabayashi/items/5286e61f5cb664e1dab9 ■サーバサイドAPIの作成 Next.jsのAPI Routesを利用して、データベースアクセスなども含めて対応することはできる ただしNext.jsはフロントエンドに特化したフレームワークなので、バックエンドに関する機能は力不足(記述が煩雑)の場合がある その場合、バックエンド部分にLaravelを使うという選択肢がある …らしい Next.js入門: データベースとの連携を学びながらToDoリストを作ろう https://zenn.dev/mostlove/articles/c9e6f1aa45ea09 Next.jsでDBにデータを登録する(MySQL) https://zenn.dev/kaikusakari/articles/f2cc55db8c90c4 Next.js + LaravelでCRUD機能を作成する #Laravel - Qiita https://qiita.com/masakiwakabayashi/items/5286e61f5cb664e1dab9 ■その他 Next.jsでDataLoaderを使ってコンポーネントの責務を明確にする https://zenn.dev/praha/articles/716bc6b0f16cff

Advertisement