你好,你好嗎?我是 Vítor,帶著一個新專案回來了,可以幫助您提高程式設計技能。自從我上次發布教程以來已經有一段時間了。在過去的幾個月裡,我花了一些時間休息並專注於其他活動。在此期間,我開發了一個小型網路專案:博客,它成為本教程的重點。
在本指南中,我們將建立能夠渲染 Markdown 的部落格頁面的前端。該應用程式將包括公共和私人路由、用戶身份驗證以及編寫 Markdown 文字、新增照片、顯示文章等功能。
隨意自訂您的應用程序,無論您喜歡什麼——我甚至鼓勵這樣做。
您可以在此處存取此應用程式的儲存庫:
npm i npm run start
você pode encontrar o server dessa aplicação em server
本教學還包括本指南中將使用的 Node.js 伺服器的編寫:
希望您喜歡。
編碼愉快!
以下是此項目中使用的庫的摘要:
我們將使用最新版本的 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.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 可讓您處理使用任意憑證的登錄,例如使用者名稱和密碼、網域、雙重認證、硬體設備等。
首先,在專案的根目錄中,建立一個 .env.local 檔案並新增一個令牌,該令牌將用作我們的秘密。
npm i npm run start
接下來,讓我們來寫我們的驗證系統,這個 NEXTAUTH_SECRET 將會被加入到 src/app/auth/[...nextauth]/routes.ts 檔案中的秘密中。
npx create-next-app myblog
讓我們建立一個身份驗證提供程序,一個上下文,它將在我們的私有路由的頁面上共享使用者的資料。稍後我們將使用它來包裝我們的layout.tsx 檔案之一。
在 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
現在讓我們來寫私有和公有的佈局。
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
我們的應用程式將多次呼叫我們的 API,您可以調整此應用程式以使用任何外部 API。在我們的範例中,我們使用本機應用程式。如果你還沒有看過後端教學和伺服器創建,請查看。
在 src/services/ 中,我們寫以下函數:
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"], }, };
export { default } from "next-auth/middleware"; export const config = { matcher: ["/", "/newArticle/", "/article/", "/article/:path*"], };
.env.local NEXTAUTH_SECRET = SubsTituaPorToken
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 };
'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> ) };
接下來,讓我們編寫整個應用程式中使用的每個元件。
一個有兩個導航連結的簡單元件。
/*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; }
一個簡單的載入元件,在等待 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> ); }
我們頁面上使用的分頁元件,在我們的私有路徑中顯示我們的所有文章。您可以在這裡找到有關如何編寫此組件的更詳細的文章
npm i npm run start
用來顯示書面文章的卡片組件。
該組件還包含一個鏈接,該鏈接將指向文章顯示頁面和編輯先前撰寫的文章的頁面。
npx create-next-app myblog
負責進行 API 呼叫並顯示回應的元件。
在這裡,我們將透過我們編寫的函數使用兩個 API 呼叫:
我們將使用先前編寫的 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.
npm @codemirror/commands @codemirror/highlight @codemirror/lang-javascript @codemirror/lang-markdown @codemirror/language @codemirror/language-data @codemirror/state @codemirror/theme-one-dark @codemirror/view
第二:使用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/資料夾中,當在()內宣告一個檔案時,就表示該路由對應於/。
在我們的例子中,(Home) 資料夾指的是我們私人路線的主頁。這是使用者在系統中進行身份驗證後看到的第一個頁面。此頁面將顯示我們資料庫中的文章清單。
資料將由我們的 ArticlesList.tsx 元件處理。如果您還沒有編寫此程式碼,請參閱元件部分。
在應用程式/(頁面)/(私人)/(主頁)/page.tsx。
npm i npm run start
這是我們應用程式中最重要的頁面之一,因為它允許我們註冊我們的文章。
此頁面將使用戶能夠:
頁面使用了多個鉤子:
為此,我們將使用兩個組件:
在建立此頁面時,我們將使用我們的 API:
我們也將使用 next-auth 函式庫提供的 useSession 鉤子來取得使用者的驗證令牌,該令牌將用於在伺服器上註冊文章。
這將涉及三個不同的 API 呼叫。
在 app/pages/(private)/newArticle/page.tsx.
「使用客戶端」; 從「react」匯入 React, { ChangeEvent, useCallback, useState }; 從“next-auth/react”導入{useSession}; 從“下一步/導航”導入{重定向}; 從“@/services/postArticle”匯入 postArtical; 從“react-icons/ai”導入{AiOutlineFolderOpen}; 從“react-icons/ri”導入 { RiImageEditLine }; 從“下一個/圖像”導入圖像; 從“@/components/textEditor”導入文字編輯器; 從“@/components/PreviewText”導入預覽; 從“react-icons/ai”導入{AiOutlineSend}; 從“react-icons/bs”導入{BsBodyText}; 匯出預設函數 NewArticle(params:any) { const { 資料:會話 }:任何 = useSession({ 要求:真實, onUnauthenticated(){ 重定向(“/登入”); }, }); const [imageUrl, setImageUrl] = useState<object>({}); const [previewImage, setPreviewImage] = useState<string>(""); const [previewText, setPreviewText] = useState<boolean>(false); const [標題,setTitle] = useState<string>(""); const [doc, setDoc] = useState<string>("# Escreva o seu texto... n"); const handleDocChange = useCallback((newDoc: any) => { setDoc(newDoc); }, []); if (!session?.user) 回傳 null; const handleArticleSubmit = async (e:any) =>; { e.preventDefault(); const token: string = session.user.token; 嘗試 { const res = 等待 postArtical({ id: session.user.userId.toString(), 令牌:令牌, 圖片網址: 圖片網址, 標題:“標題” 文檔: 文檔, }); 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(文件); 設定預覽影像(網址); setImageUrl(文件); } }; const handleTextPreview = (e: 任意) => { e.preventDefault(); setPreviewText(!previewText); }; 返回 ( <section classname="w-full h-full min-h-screenrelative py-8"> {預覽文字&&( <div classname="absolute right-16 top-5 p-5 border-2 border-slate-500 bg-slate-100 rounded-xl w-full max-w-[33em] z-30">; ; setPreviewText(!previewText)} >> </div>; )} <form classname="relative mx-auto max-w-[700px] h-full min-h-[90%] w-full p-2 border-2 border-slate-200 rounded-md bg-slate-50 drop -shadow-xl flex flex-col 間隙-2“> {“”} <div className=" flex justify- between items-center> <bsbodytext></bsbodytext>> 預覽 按鈕>{" "} 恩維亞爾·特克斯托 <aioutlinesend classname="w-5 h-5 group-hover:text-red-500"></aioutlinesend> 按鈕> ; <div classname="header-wrapper flex flex-col gap-2"> <div classname="image-box"> {previewImage.length === 0 && ( <div classname="select-image"> <aioutlinefolderopen classname="w-7 h-7"></aioutlinefolderopen> 拖放影像 標籤> <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">「使用客戶端」; 從「react」匯入 React, { useState, useEffect, useCallback, useRef, ChangeEvent }; 從“next-auth/react”導入{useSession}; 從“下一步/導航”導入{重定向}; 從“下一個/圖像”導入圖像; 從“@/interfaces/article.interface”導入{IArticle}; 從“react-icons/ai”導入{AiOutlineEdit}; 從“react-icons/bs”導入{BsBodyText}; 從“react-icons/ai”導入{AiOutlineFolderOpen}; 從“react-icons/ri”導入 { RiImageEditLine }; 從“@/components/PreviewText”導入預覽; 從“@/components/textEditor”導入文字編輯器; 從'@/components/Loading'導入載入; 從“@/services/editArticle”導入 editArtical; 匯出預設函數 EditArticle({ params }: { params: any }) { const { 資料:會話 }:任何 = useSession({ 要求:真實, onUnauthenticated(){ 重定向(“/登入”); }, }); const id: 數字 = params.id; const [文章,setArticle] = useState<iarticle>(空); const [imageUrl, setImageUrl] = useState<object>({}); const [previewImage, setPreviewImage] = useState<string>(""); const [previewText, setPreviewText] = useState<boolean>(false) const [標題,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) =>; { 嘗試 { 常量響應 = 等待獲取( `http://localhost:8080/articles/getById/${id}`, ); const jsonData = 等待回應.json(); setArticle(jsonData); } 捕獲(錯誤){ console.log("出了點問題:", err); } }; useEffect(() => { if (文章 !== null || 文章 !== 未定義) { 取得文章(id); } }, [ID]); useEffect(()=>{ if(文章!= null && 文章.內容){ setDoc(文章.內容) } if(文章!=null && 文章.image){ setPreviewImage(`http://localhost:8080/` 文章.image) } },[文章]) const handleArticleSubmit = async (e:any) =>; { e.preventDefault(); const token: string = session.user.token; 嘗試{ const res = 等待 editArtical({ 身分證字號: 身分證號, 令牌:令牌, 圖片網址:圖片網址, 標題: 標題, 文檔: 文檔, }); 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(文件); 設定預覽影像(網址); setImageUrl(文件); } }; const handleTextPreview = (e: 任意) => { e.preventDefault(); setPreviewText(!previewText); console.log('預覽版你好!') }; if(!article) return > if(文章?.內容) 返回 ( <section classname="w-full h-full min-h-screenrelative py-8"> {預覽文字&&( <div classname="absolute right-16 top-5 p-5 border-2 border-slate-500 bg-slate-100 rounded-xl w-full max-w-[33em] z-30">; ; setPreviewText(!previewText)} >> </div> )} <div classname="relative mx-auto max-w-[700px] h-full min-h-[90%] w-full p-2 border-2 border-slate-200 rounded-md bg-white drop- Shadow -md flex flex-col 間隙-2"> <form classname="relative mx-auto max-w-[700px] h-full min-h-[90%] w-full p-2 border-2 border-slate-200 rounded-md bg-slate-50 drop -shadow-md flex flex-col 間隙-2 "> {“”} <div classname="flex justify- Between items-center">; <bsbodytext></bsbodytext>> 預覽 按鈕>{" "} 編輯阿蒂戈 <aioutlineedit classname="w-5 h-5 group-hover:text-red-500"></aioutlineedit>> 按鈕> </div>; <div classname="header-wrapper flex flex-col gap-2">; <div classname="image-box">; {previewImage.length === 0 && ( <div classname="select-image">; <aioutlinefolderopen classname="w-7 h-7"></aioutlinefolderopen>> 拖放影像 標籤> <h2> 結論 </h2> <p>首先,我要感謝您花時間閱讀本教程,並且我還要祝賀您完成它。我希望它對您有幫助,並且逐步說明很容易遵循。 </p> <p>其次,我想強調一下關於我們剛剛建立的內容的幾點。這是部落格系統的基礎,還有很多東西需要添加,例如顯示所有文章的公共頁面、用戶註冊頁面,甚至是自訂的 404 錯誤頁面。如果在教程期間您對這些頁面感到好奇並錯過了它們,請知道這是故意的。本教學為您提供了足夠的經驗來自行創建這些新頁面、添加許多其他頁面以及實現新功能。 </p> <p>非常感謝,下次再見。哦/</p> </div> </div> </div> </form> </div></section></htmlinputelement></htmlinputelement></string></string></boolean></string></object></iarticle>
以上是使用 Next.js 建立動態部落格儀表板的詳細內容。更多資訊請關注PHP中文網其他相關文章!