Next.js を使用した動的なブログ ダッシュボードの構築
導入
こんにちは、お元気ですか?こちらは Vítor です。プログラミング スキルの向上に役立つ新しいプロジェクトを携えて戻ってきました。最後にチュートリアルを公開してからしばらく時間が経ちました。過去数か月間、私は休息を取り、他の活動に集中する時間を取りました。この期間中に、私は小さな Web プロジェクト、つまりこのチュートリアルの焦点となるブログを開発しました。
このガイドでは、Markdown をレンダリングできるブログ ページのフロントエンドを作成します。このアプリケーションには、パブリックおよびプライベート ルート、ユーザー認証、Markdown テキストの作成、写真の追加、記事の表示などの機能が含まれます。
お好みに合わせてアプリケーションを自由にカスタマイズしてください。私はそれをお勧めします。
このアプリケーションのリポジトリにはここからアクセスできます:
ゴンドラク08
/
ブログプラットフォーム
Next.js/typescript で作られたブログ プラットフォーム。
Platforma パラブログ
- テキストによるチュートリアル
材料
- next-auth - Next.js の認証文書
- github.com/markdown-it/markdown-it - マークダウン図書館。
- github.com/sindresorhus/github-markdown-css- 最も重要なエディターのマークダウン。
- github.com/remarkjs/react-markdown - レンダリング マークダウンとそのコンポーネントの反応に関する参考文献。
- github.com/remarkjs/remark-react/tree/4722bdf - React の Markdown を変換するプラグイン
- codemirror.net - ウェブ用エディターコンポーネント
- 反応アイコン - 反応するアイコンのライブラリ
コモ・ユーザー
npm i npm run start
サーバー
サーバーアプリケーションのサーバーを起動します
このチュートリアルには、このガイドで使用される Node.js サーバーの作成も含まれています。
楽しんでいただければ幸いです。
コーディングを楽しんでください!
図書館
このプロジェクトで使用されるライブラリの概要は次のとおりです:
- next-auth - Next.js の認証ライブラリ
- github.com/markdown-it/markdown-it - マークダウン ライブラリ。
- github.com/sindresorhus/github-markdown-css - Markdown エディターのスタイル設定用。
- github.com/remarkjs/react-markdown - React コンポーネントで Markdown をレンダリングするためのライブラリ。
- github.com/remarkjs/remark-react/tree/4722bdf - Markdown を React に変換するプラグイン
- codemirror.net - Web コンポーネント エディター。
- act-icons - React 用のアイコン ライブラリ
React プロジェクトの作成
Next.js フレームワークの最新バージョンを使用します。このチュートリアルの作成時点ではバージョン 13.4 です。
次のコマンドを実行してプロジェクトを作成します:
npm i npm run start
インストール中に、テンプレート設定を選択します。このチュートリアルでは、プログラミング言語として TypeScript を使用し、アプリケーションのスタイル設定に Tailwind CSS フレームワークを使用します。
構成
次に、使用するすべてのライブラリをインストールしましょう。
マークダウン
npx create-next-app myblog
リアクションコメント
npm i markdown-it @types/markdown-it markdown-it-style github-markdown-css react-markdown
コードミラー
remark remark-gfm remark-react
アイコン
npm @codemirror/commands @codemirror/highlight @codemirror/lang-javascript @codemirror/lang-markdown @codemirror/language @codemirror/language-data @codemirror/state @codemirror/theme-one-dark @codemirror/view
次に、使用しないものをすべて削除して、インストールの初期構造をクリーンアップします。
建築
これがアプリケーションの最終的な構造です。
npm i react-icons @types/react-icons
最初のステップ
next.config の構成
プロジェクトのルートのファイル next.config.js で、記事の画像にアクセスするドメイン アドレスを設定しましょう。このチュートリアルでは、またはローカル サーバーを使用している場合は、localhost を使用します。
アプリケーションに画像が正しく読み込まれるように、必ずこの構成を含めてください。
src- |- app/ | |-(pages)/ | | |- (private)/ | | | |- (home) | | | |- editArticle/[id] | | | | | | | |- newArticle | | | - (public)/ | | | - article/[id] | | | - login | | | api/ | |- auth/[...nextAuth]/route.ts | |- global.css | |- layout.tsx | | - components/ | - context/ | - interfaces/ | - lib/ | - services/ middleware.ts
ミドルウェアの構成
アプリケーション src/ のルート フォルダーに、プライベート ルートへのアクセスを確認するための middleware.ts を作成します。
const nextConfig = { images: { domains: ["localhost"], }, };
ミドルウェアとそれを使用して実行できるすべてのことについて詳しくは、ドキュメントを確認してください。
認証ルートの構成
/app フォルダー内の api/auth/[...nextauth] に、route.ts という名前のファイルを作成します。これには、CredentialsProvider を使用して認証 API に接続するルートの構成が含まれます。
CredentialsProvider を使用すると、ユーザー名とパスワード、ドメイン、2 要素認証、ハードウェア デバイスなどの任意の資格情報を使用してログインを処理できます。
まず、プロジェクトのルートに .env.local ファイルを作成し、シークレットとして使用されるトークンを追加します。
npm i npm run start
次に、認証システムを作成しましょう。この NEXTAUTH_SECRET が src/app/auth/[...nextauth]/routes.ts ファイル内のシークレットに追加されます。
npx create-next-app myblog
認証プロバイダー
プライベート ルートのページ間でユーザーのデータを共有する認証プロバイダー、つまりコンテキストを作成しましょう。後でこれを使用して、layout.tsx ファイルの 1 つをラップします。
次の内容を含むファイルを src/context/auth-provider.tsx に作成します。
npm i markdown-it @types/markdown-it markdown-it-style github-markdown-css react-markdown
グローバルスタイル
全体として、私たちのアプリケーションでは、Tailwind CSS を使用してスタイルを作成します。ただし、一部の場所では、ページとコンポーネント間でカスタム CSS クラスを共有します。
remark remark-gfm remark-react
レイアウト
次に、プライベートとパブリックの両方のレイアウトを書きましょう。
app/layout.tsx
npm @codemirror/commands @codemirror/highlight @codemirror/lang-javascript @codemirror/lang-markdown @codemirror/language @codemirror/language-data @codemirror/state @codemirror/theme-one-dark @codemirror/view
ページ/レイアウト.tsx
npm i react-icons @types/react-icons
API呼び出し
私たちのアプリケーションは API を複数回呼び出します。このアプリケーションを任意の外部 API を使用するように適応させることができます。この例では、ローカル アプリケーションを使用しています。バックエンドのチュートリアルとサーバーの作成をまだ見ていない場合は、チェックしてください。
src/services/ に次の関数を書きましょう:
- authService.ts: サーバー上でユーザーを認証する機能。
src- |- app/ | |-(pages)/ | | |- (private)/ | | | |- (home) | | | |- editArticle/[id] | | | | | | | |- newArticle | | | - (public)/ | | | - article/[id] | | | - login | | | api/ | |- auth/[...nextAuth]/route.ts | |- global.css | |- layout.tsx | | - components/ | - context/ | - interfaces/ | - lib/ | - services/ middleware.ts
2.refreshAccessToken.tsx:
const nextConfig = { images: { domains: ["localhost"], }, };
- getArticles.tsx: データベースに保存されているすべての記事を取得する関数:
export { default } from "next-auth/middleware"; export const config = { matcher: ["/", "/newArticle/", "/article/", "/article/:path*"], };
- postArticle.tsx: 記事データをサーバーに送信する機能。
.env.local NEXTAUTH_SECRET = SubsTituaPorToken
- editArticle.tsx: データベース内の特定の記事を変更する関数。
import NextAuth from "next-auth/next"; import type { AuthOptions } from "next-auth"; import CredentialsProvider from "next-auth/providers/credentials"; import { authenticate } from "@/services/authService"; import refreshAccessToken from "@/services/refreshAccessToken"; export const authOptions: AuthOptions = { providers: [ CredentialsProvider({ name: "credentials", credentials: { email: { name: "email", label: "email", type: "email", placeholder: "Email", }, password: { name: "password", label: "password", type: "password", placeholder: "Password", }, }, async authorize(credentials, req) { if (typeof credentials !== "undefined") { const res = await authenticate({ email: credentials.email, password: credentials.password, }); if (typeof res !== "undefined") { return { ...res }; } else { return null; } } else { return null; } }, }), ], session: { strategy: "jwt" }, secret: process.env.NEXTAUTH_SECRET, callbacks: { async jwt({ token, user, account }: any) { if (user && account) { return { token: user?.token, accessTokenExpires: Date.now() + parseInt(user?.expiresIn, 10), refreshToken: user?.tokenRefresh, }; } if (Date.now() < token.accessTokenExpires) { return token; } else { const refreshedToken = await refreshAccessToken(token.refreshToken); return { ...token, token: refreshedToken.token, refreshToken: refreshedToken.tokenRefresh, accessTokenExpires: Date.now() + parseInt(refreshedToken.expiresIn, 10), }; } }, async session({ session, token }) { session.user = token; return session; }, }, pages: { signIn: "/login", signOut: "/login", }, }; const handler = NextAuth(authOptions); export { handler as GET, handler as POST };
- deleteArticle.tsx: データベースから特定の記事を削除する関数。
'use client'; import React from 'react'; import { SessionProvider } from "next-auth/react"; export default function Provider({ children, session }: { children: React.ReactNode, session: any }): React.ReactNode { return ( <SessionProvider session={session} > {children} </SessionProvider> ) };
コンポーネント
次に、アプリケーション全体で使用される各コンポーネントを記述しましょう。
コンポーネント/Navbar.tsx
2 つのナビゲーション リンクを持つ単純なコンポーネント。
/*global.css*/ .container { max-width: 1100px; width: 100%; margin: 0px auto; } .image-container { position: relative; width: 100%; height: 5em; padding-top: 56.25%; /* Aspect ratio 16:9 (dividindo a altura pela largura) */ } .image-container img { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; } @keyframes spinner { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .loading-spinner { width: 50px; height: 50px; border: 10px solid #f3f3f3; border-top: 10px solid #293d71; border-radius: 50%; animation: spinner 1.5s linear infinite; }
コンポーネント/Loading.tsx
API 呼び出しが完了するのを待機している間に使用される、単純な読み込みコンポーネント。
import type { Metadata } from "next"; import { Inter } from "next/font/google"; import "./globals.css"; import Provider from "@/context/auth-provider"; import { getServerSession } from "next-auth"; import { authOptions } from "./api/auth/[...nextauth]/route"; const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { title: "Markdown Text Editor", description: "Created by <@vitorAlecrim>", }; export default async function RootLayout({ children, }: { children: React.ReactNode; }) { const session = await getServerSession(authOptions); return ( <Provider session={session}> <html lang="en"> <body className={inter.className}>{children}</body> </html> </Provider> ); }
コンポーネント/ページネーション.tsx
プライベート ルートで、すべての記事を表示するページで使用されるページネーション コンポーネント。このコンポーネントの作成方法に関する詳細な記事は、こちらでご覧いただけます
npm i npm run start
コンポーネント/ArticleCard.tsx
書かれた記事を表示するためのカードコンポーネント。
このコンポーネントには、記事表示ページと以前に書いた記事を編集するページの両方にアクセスするリンクも含まれています。
npx create-next-app myblog
コンポーネント/ArticleList.tsx
API 呼び出しを実行し、応答を表示するコンポーネント。
ここでは、作成した関数を通じて 2 つの API 呼び出しを使用します。
- getArticles.ts - コンポーネントに表示されるすべての記事を返します。
- RemoveArticle - リストとサーバーから特定の記事を削除します。
以前に作成した Pagination.tsx コンポーネントを使用して、記事の数をページ間で分割します。
npm i markdown-it @types/markdown-it markdown-it-style github-markdown-css react-markdown
ページ
次に、それぞれのルートごとに分けて、各ページを見ていきます。
公開ページ
ログイン
これは私たちのアプリケーションのホームページです。シンプルなページなので、必要に応じて変更してください。このページでは、next-auth ナビゲーション ライブラリが提供するサインイン機能を使用します。
ファイル src/app/pages/public/login/page.tsx.
remark remark-gfm remark-react
記事ページ
記事閲覧ページを作成するために、動的ページを開発します。
あなたがアクセスしたことのあるすべてのブログ プラットフォームには、記事を読むための専用ページがあり、URL からアクセスできます。この理由は、動的なページ ルートです。幸いなことに、Next.js の新しい AppRouter メソッドでこれが簡単になり、私たちの生活がはるかにシンプルになりました。
最初に: [id] フォルダーを追加して、構造内にルートを作成する必要があります。これにより、pages/(public)/articles/[id]/pages.tsx.
という構造になります。- ID はナビゲーション ルートのスラッグに対応します。
- params は、ナビゲーション スラッグを含むアプリケーションのツリーを介して渡されるプロパティです。
npm @codemirror/commands @codemirror/highlight @codemirror/lang-javascript @codemirror/lang-markdown @codemirror/language @codemirror/language-data @codemirror/state @codemirror/theme-one-dark @codemirror/view
2 番目: MarkdownIt ライブラリを使用して、ページに Markdown 形式でテキストを表示できるようにします。
npm i react-icons @types/react-icons
そして最後に、
ページの準備ができたら、たとえばブラウザで localhost:3000/articles/1 にアクセスすると、指定された ID で記事を表示できるようになります。
この場合、ArticleCards.tsx コンポーネントのいずれかをクリックすると、ID がナビゲーションを介して渡され、プライベート ルートのメイン ページにレンダリングされます。
src- |- app/ | |-(pages)/ | | |- (private)/ | | | |- (home) | | | |- editArticle/[id] | | | | | | | |- newArticle | | | - (public)/ | | | - article/[id] | | | - login | | | api/ | |- auth/[...nextAuth]/route.ts | |- global.css | |- layout.tsx | | - components/ | - context/ | - interfaces/ | - lib/ | - services/ middleware.ts
プライベートページ
ここにプライベート ページがあります。このページには、ユーザーがアプリケーションで認証された場合にのみアクセスできます。
家
app/pages/ フォルダー内で、ファイルが () 内で宣言されている場合、ルートが /.
に対応することを意味します。この場合、(ホーム) フォルダーはプライベート ルートのホームページを指します。これは、ユーザーがシステムに認証されたときに最初に表示されるページです。このページには、データベースの記事のリストが表示されます。
データは ArticlesList.tsx コンポーネントによって処理されます。このコードをまだ書いていない場合は、コンポーネントのセクションに戻って参照してください。
app/(pages)/(private)/(home)/page.tsx 内
npm i npm run start
新しい記事
これは、記事を登録できるため、アプリケーションの最も重要なページの 1 つです。
このページにより、ユーザーは次のことが可能になります:
- Markdown 形式で記事を書きます。
- 記事に画像を割り当てます。
- サーバーに送信する前に、Markdown テキストをプレビューします。
このページでは複数の フック が使用されています:
- useCallback - 関数をメモ化するために使用されます。
- useState - コンポーネントに状態変数を追加できます。
- useSession - ユーザーが認証されているかどうかを確認し、認証トークンを取得します。
このために、2 つのコンポーネントを使用します:
- TextEditor.tsx: 以前に作成したテキスト エディター。
- Preview.tsx: Markdown 形式でファイルを表示するためのコンポーネント。
このページを構築する際、次の API を使用します:
- POST: 関数 postArticle を使用して、記事をサーバーに送信します。
また、next-auth ライブラリによって提供される useSession フックを使用して、サーバーに記事を登録するために使用されるユーザーの認証トークンを取得します。
これには 3 つの異なる API 呼び出しが含まれます。
app/pages/(private)/newArticle/page.tsx.
内
「クライアントを使用する」; import React, { ChangeEvent, useCallback, useState } from "react"; import { useSession } から "next-auth/react"; import { リダイレクト } から "next/navigation"; "@/services/postArticle" から postArtical をインポートします。 import { AiOutlineFolderOpen } from "react-icons/ai"; import { RiImageEditLine } から "react-icons/ri"; 「next/image」から画像をインポートします。 "@/components/textEditor" から TextEditor をインポートします。 "@/components/PreviewText" からプレビューをインポートします。 import { AiOutlineSend } から "react-icons/ai"; import { BsBodyText } から "react-icons/bs"; デフォルト関数をエクスポート NewArticle(params:any) { const { データ: セッション }: any = useSession({ 必須: true、 onUnauthenticated() { リダイレクト("/ログイン"); }、 }); const [imageUrl, setImageUrl] = useState<object>({}); const [previewImage, setPreviewImage] = useState<string>(""); const [previewText, setPreviewText] = useState<boolean>(false); const [title, setTitle] = useState<string>(""); const [doc, setDoc] = useState<string>("# Escreva o seu texto... n"); const handleDocChange = useCallback((newDoc: any) => { setDoc(newDoc); }、[]); (!session?.user) が null を返す場合。 const handleArticleSubmit = async (e:any) => { e.preventDefault(); const トークン: 文字列 = session.user.token; 試す { const res = await postArtical({ id: session.user.userId.toString()、 トークン: トークン、 画像 URL: 画像 URL、 タイトル: "タイトル"、 ドキュメント: ドキュメント、 }); console.log('re--->', res); リダイレクト('/成功'); } キャッチ (エラー) { console.error('記事の送信エラー:', error); // 必要に応じてエラーを処理します エラーをスローします。 } }; const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => { if (e.target.files && e.target.files.length > 0) { const ファイル = e.target.files[0]; const url = URL.createObjectURL(ファイル); setPreviewImage(url); setImageUrl(ファイル); } }; const handleTextPreview = (e: any) => { e.preventDefault(); setPreviewText(!previewText); }; 戻る ( <section className="w-full h-full min-h-screen相対 py-8"> {プレビューテキスト && ( <div className="absolute right-16 top-5 p-5 border-2 border-slate-500 bg-slate-100rounded-xl w-full max-w-[33em] z-30"> <プレビュー ドキュメント={ドキュメント} タイトル={タイトル} プレビュー画像={プレビュー画像} onPreview={() => setPreviewText(!previewText)} /> </div> )} <form className="relative mx-auto max-w-[700px] h-full min-h-[90%] w-full p-2 border-2 border-slate-200rounded-md bg-slate-50ドロップシャドウ-xl フレックス フレックスコル ギャップ-2 "> {" "} <div className="flex justify-between items-center"> <ボタン className="border-b-2rounded-md border-slate-500 p-2 flex items-center gap-2 hover:border-slate-400 hover:text-slate-800" onClick={handleTextPreview} > <BsBodyText /> プレビュー </ボタン>{" "} <ボタン className="グループ ボーダー ボーダー-b-2 ボーダー-スレート-500 ラウンド-MD p-2 フレックス アイテム-センター ギャップ-2 hover:border-slate-400 hover:text-slate-800 " onClick={handleArticleSubmit} > エンビア テキスト <AiOutlineSend className="w-5 h-5 group-hover:text-red-500" /> </ボタン> </div> <div className="header-wrapper flex flex-col gap-2 "> <div className="画像ボックス"> {previewImage.length === 0 && ( <div className="選択画像"> <AiOutlineFolderOpen className="w-7 h-7" /> ドラッグ アンド ドロップ画像 </ラベル> <h4> 記事を編集する </h4> <p><em>新しい記事</em> (newArticle) に似たページですが、いくつかの違いがあります。</p> <p>まず、ナビゲーション パラメーターとして ID を受け取る動的ルートを定義します。これは、記事閲覧ページで行われたことと非常によく似ています。 <br> app/(pages)/(private)/editArticle/[id]/page.tsx<br> </p><pre class="brush:php;toolbar:false">「クライアントを使用する」; import React, { useState, useEffect, useCallback, useRef, ChangeEvent } from "react"; import { useSession } から "next-auth/react"; import { リダイレクト } から "next/navigation"; 'next/image' から画像をインポートします。 import { IArticle } から "@/interfaces/article.interface"; import { AiOutlineEdit } から "react-icons/ai"; import { BsBodyText } から "react-icons/bs"; import { AiOutlineFolderOpen } from "react-icons/ai"; import { RiImageEditLine } から "react-icons/ri"; "@/components/PreviewText" からプレビューをインポートします。 "@/components/textEditor" から TextEditor をインポートします。 import Loading from '@/components/Loading'; editArtical を "@/services/editArticle" からインポートします。 デフォルト関数をエクスポート EditArticle({ params }: { params: any }) { const { データ: セッション }: any = useSession({ 必須: true、 onUnauthenticated() { リダイレクト("/ログイン"); }、 }); 定数 ID: 数値 = params.id; const [article, setArticle] = useState<iarticle null>(null); const [imageUrl, setImageUrl] = useState<object>({}); const [previewImage, setPreviewImage] = useState<string>(""); const [previewText, setPreviewText] = useState<boolean>(false) const [title, setTitle] = useState<string>(""); const [doc, setDoc] = useState<string>(''); const handleDocChange = useCallback((newDoc: any) => { setDoc(newDoc); }、[]); const inputRef= useRef<htmlinputelement>(null); const fetchArticle = async (id:number) => { 試す { const 応答 = fetch を待ちます( `http://localhost:8080/articles/getById/${id}`、 ); const jsonData = 応答を待ちます.json(); setArticle(jsonData); } キャッチ (エラー) { console.log("何か問題が発生しました:", err); } }; useEffect(() => { if (記事 !== null || 記事 !== 未定義) { fetchArticle(id); } }, [id]); useEffect(()=>{ if(記事 != null && 記事.コンテンツ){ setDoc(article.content) } if(記事 !=null && 記事.画像){ setPreviewImage(`http://localhost:8080/`article.image) } }、[記事]) const handleArticleSubmit = async (e:any) => { e.preventDefault(); const トークン: 文字列 = session.user.token; 試す{ const res = await editArtical({ ID: ID、 トークン: トークン、 画像URL:画像URL、 タイトル: タイトル、 ドキュメント: ドキュメント、 }); console.log('re--->',res) 応答を返します。 キャッチ(エラー){ console.log("エラー:", エラー) } }; const handleImageClick = ()=>{ console.log('hiii') if(inputRef.current){ inputRef.current.click(); } }const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => { if (e.target.files && e.target.files.length > 0) { const ファイル = e.target.files[0]; const url = URL.createObjectURL(ファイル); setPreviewImage(url); setImageUrl(ファイル); } }; const handleTextPreview = (e: any) => { e.preventDefault(); setPreviewText(!previewText); console.log('プレビューからこんにちは!') }; if(!article) return <Loading/> if(記事?.コンテンツ) 戻る ( <section className='w-full h-full min-h-screen相対 py-8'> {プレビューテキスト && ( <div className="absolute right-16 top-5 p-5 border-2 border-slate-500 bg-slate-100rounded-xl w-full max-w-[33em] z-30"> <プレビュー ドキュメント={ドキュメント} タイトル={タイトル} プレビュー画像={プレビュー画像} onPreview={() => setPreviewText(!previewText)} /> </div> )} <div className='relative mx-auto max-w-[700px] h-full min-h-[90%] w-full p-2 border-2 border-slate-200rounded-md bg-whitedrop-シャドウ-MD フレックス フレックス-コル ギャップ-2'> <form className='relative mx-auto max-w-[700px] h-full min-h-[90%] w-full p-2 border-2 border-slate-200rounded-md bg-slate-50ドロップシャドウ-MD フレックス フレックスコル ギャップ-2 '> {" "} <div className='flex justify-between items-center'> <ボタン className='border-b-2rounded-md border-slate-500 p-2 flex items-center gap-2 hover:border-slate-400 hover:text-slate-800' onClick={handleTextPreview} > <BsBodyText /> プレビュー </ボタン>{" "} <ボタン className='グループ ボーダー ボーダー-b-2 ボーダー-スレート-500 ラウンド-MD p-2 フレックス アイテム-センター ギャップ-2 hover:border-slate-400 hover:text-slate-800 ' onClick={handleArticleSubmit} > アーティゴを編集する <AiOutlineEdit className='w-5 h-5 group-hover:text-red-500' /> </ボタン> </div> <div className='header-wrapper flex flex-col gap-2 '> <div className='画像ボックス'> {previewImage.length === 0 && ( <div className='select-image'> <AiOutlineFolderOpen className='w-7 h-7' />; ドラッグ アンド ドロップ画像 </ラベル> <h2> 結論 </h2> <p>まず、このチュートリアルをお読みいただくために時間を割いていただきありがとうございます。また、このチュートリアルが完了したことをお祝いしたいと思います。ご参考になり、ステップごとの手順が分かりやすかったら幸いです。</p> <p>次に、構築したものについていくつかの点を強調したいと思います。これはブログ システムの基礎であり、すべての記事を表示する公開ページ、ユーザー登録ページ、さらにはカスタム 404 エラー ページなど、追加すべき点はまだたくさんあります。チュートリアル中にこれらのページについて疑問に思って見逃した場合は、これが意図的なものであることを知ってください。このチュートリアルでは、これらの新しいページを自分で作成し、他のページを追加し、新機能を実装するための十分な経験を提供しました。</p> <p>次回まで、本当にありがとうございました。 o/</p> </htmlinputelement></iarticle>
以上がNext.js を使用した動的なブログ ダッシュボードの構築の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

ホットAIツール

Undresser.AI Undress
リアルなヌード写真を作成する AI 搭載アプリ

AI Clothes Remover
写真から衣服を削除するオンライン AI ツール。

Undress AI Tool
脱衣画像を無料で

Clothoff.io
AI衣類リムーバー

Video Face Swap
完全無料の AI 顔交換ツールを使用して、あらゆるビデオの顔を簡単に交換できます。

人気の記事

ホットツール

メモ帳++7.3.1
使いやすく無料のコードエディター

SublimeText3 中国語版
中国語版、とても使いやすい

ゼンドスタジオ 13.0.1
強力な PHP 統合開発環境

ドリームウィーバー CS6
ビジュアル Web 開発ツール

SublimeText3 Mac版
神レベルのコード編集ソフト(SublimeText3)

ホットトピック











フロントエンドのサーマルペーパーチケット印刷のためのよくある質問とソリューションフロントエンド開発におけるチケット印刷は、一般的な要件です。しかし、多くの開発者が実装しています...

JavaScriptは現代のWeb開発の基礎であり、その主な機能には、イベント駆動型のプログラミング、動的コンテンツ生成、非同期プログラミングが含まれます。 1)イベント駆動型プログラミングにより、Webページはユーザー操作に応じて動的に変更できます。 2)動的コンテンツ生成により、条件に応じてページコンテンツを調整できます。 3)非同期プログラミングにより、ユーザーインターフェイスがブロックされないようにします。 JavaScriptは、Webインタラクション、シングルページアプリケーション、サーバー側の開発で広く使用されており、ユーザーエクスペリエンスとクロスプラットフォーム開発の柔軟性を大幅に改善しています。

スキルや業界のニーズに応じて、PythonおよびJavaScript開発者には絶対的な給与はありません。 1. Pythonは、データサイエンスと機械学習でさらに支払われる場合があります。 2。JavaScriptは、フロントエンドとフルスタックの開発に大きな需要があり、その給与もかなりです。 3。影響要因には、経験、地理的位置、会社の規模、特定のスキルが含まれます。

同じIDを持つ配列要素をJavaScriptの1つのオブジェクトにマージする方法は?データを処理するとき、私たちはしばしば同じIDを持つ必要性に遭遇します...

JavaScriptを学ぶことは難しくありませんが、挑戦的です。 1)変数、データ型、関数などの基本概念を理解します。2)非同期プログラミングをマスターし、イベントループを通じて実装します。 3)DOM操作を使用し、非同期リクエストを処理することを約束します。 4)一般的な間違いを避け、デバッグテクニックを使用します。 5)パフォーマンスを最適化し、ベストプラクティスに従ってください。

この記事の視差スクロールと要素のアニメーション効果の実現に関する議論では、Shiseidoの公式ウェブサイト(https://www.shisido.co.co.jp/sb/wonderland/)と同様の達成方法について説明します。

JavaScriptの最新トレンドには、TypeScriptの台頭、最新のフレームワークとライブラリの人気、WebAssemblyの適用が含まれます。将来の見通しは、より強力なタイプシステム、サーバー側のJavaScriptの開発、人工知能と機械学習の拡大、およびIoTおよびEDGEコンピューティングの可能性をカバーしています。

Console.log出力の違いの根本原因に関する詳細な議論。この記事では、Console.log関数の出力結果の違いをコードの一部で分析し、その背後にある理由を説明します。 �...
