Hello, apa khabar? Ini ialah Vítor, kembali dengan projek baharu untuk membantu anda meningkatkan kemahiran pengaturcaraan anda. Sudah lama sejak kali terakhir saya menerbitkan tutorial. Sejak beberapa bulan lalu, saya mengambil sedikit masa untuk berehat dan fokus kepada aktiviti lain. Dalam tempoh ini, saya membangunkan projek web kecil: blog, yang menjadi tumpuan tutorial ini.
Dalam panduan ini, kami akan mencipta bahagian hadapan halaman blog yang mampu memaparkan Markdown. Aplikasi ini akan merangkumi laluan awam dan peribadi, pengesahan pengguna dan keupayaan untuk menulis teks Markdown, menambah foto, memaparkan artikel dan banyak lagi.
Jangan teragak-agak untuk menyesuaikan aplikasi anda mengikut kehendak anda—saya juga menggalakkannya.
Anda boleh mengakses repositori untuk aplikasi ini di sini:
npm i npm run start
você pode encontrar or servidor dessa applicação em server
Tutorial ini juga termasuk penulisan pelayan Node.js yang akan digunakan dalam panduan ini:
Saya harap anda menikmatinya.
Selamat pengekodan!
Berikut ialah ringkasan perpustakaan yang digunakan dalam projek ini:
Kami akan menggunakan versi terkini rangka kerja Next.js, yang, pada masa menulis tutorial ini, ialah versi 13.4.
Jalankan arahan berikut untuk mencipta projek:
npm i npm run start
Semasa pemasangan, pilih tetapan templat. Dalam tutorial ini, saya akan menggunakan TypeScript sebagai bahasa pengaturcaraan dan rangka kerja Tailwind CSS untuk menggayakan aplikasi kami.
Sekarang mari pasang semua perpustakaan yang akan kami gunakan.
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
Kemudian bersihkan struktur awal pemasangan anda dengan mengalih keluar semua yang tidak akan kami gunakan.
Ini adalah struktur akhir aplikasi kami.
npm i react-icons @types/react-icons
Dalam akar projek, dalam fail next.config.js, mari kita konfigurasikan alamat domain dari mana kita akan mengakses imej untuk artikel kita. Untuk tutorial ini, atau jika anda menggunakan pelayan tempatan, kami akan menggunakan localhost.
Pastikan anda memasukkan konfigurasi ini untuk memastikan pemuatan imej yang betul dalam aplikasi anda.
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
Dalam folder akar aplikasi src/, cipta middleware.ts untuk mengesahkan akses kepada laluan peribadi.
const nextConfig = { images: { domains: ["localhost"], }, };
Untuk mengetahui lebih lanjut tentang perisian tengah dan semua yang anda boleh lakukan dengannya, semak dokumentasi.
Di dalam folder /app, cipta fail bernama route.ts dalam api/auth/[...nextauth]. Ia akan mengandungi konfigurasi untuk laluan kami, menyambung ke API pengesahan kami menggunakan CredentialsProvider.
Penyedia Kredensial membolehkan anda mengendalikan log masuk dengan bukti kelayakan sewenang-wenangnya, seperti nama pengguna dan kata laluan, domain, pengesahan dua faktor, peranti perkakasan, dll.
Pertama, dalam akar projek anda, buat fail .env.local dan tambahkan token yang akan digunakan sebagai rahsia kami.
npm i npm run start
Seterusnya, mari tulis sistem pengesahan kami, di mana NEXTAUTH_SECRET ini akan ditambahkan pada rahsia kami dalam fail src/app/auth/[...nextauth]/routes.ts.
npx create-next-app myblog
Mari kita cipta penyedia pengesahan, konteks, yang akan berkongsi data pengguna kami merentasi halaman laluan peribadi kami. Kami akan menggunakannya kemudian untuk membungkus salah satu fail susun atur.tsx kami.
Buat fail dalam src/context/auth-provider.tsx dengan kandungan berikut:
npm i markdown-it @types/markdown-it markdown-it-style github-markdown-css react-markdown
Secara keseluruhan, dalam aplikasi kami, kami akan menggunakan CSS Tailwind untuk mencipta gaya kami. Walau bagaimanapun, di sesetengah tempat, kami akan berkongsi kelas CSS tersuai antara halaman dan komponen.
remark remark-gfm remark-react
Sekarang mari kita tulis reka letak, baik peribadi mahupun awam.
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
Aplikasi kami akan membuat beberapa panggilan ke API kami, dan anda boleh menyesuaikan aplikasi ini untuk menggunakan mana-mana API luaran. Dalam contoh kami, kami menggunakan aplikasi tempatan kami. Jika anda belum melihat tutorial bahagian belakang dan penciptaan pelayan, semaknya.
Dalam src/services/, mari tulis fungsi berikut:
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> ) };
Seterusnya, mari tulis setiap komponen yang digunakan sepanjang aplikasi.
Komponen ringkas dengan dua pautan navigasi.
/*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; }
Komponen pemuatan mudah, digunakan sementara menunggu panggilan API selesai.
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> ); }
Komponen penomboran yang digunakan pada halaman kami memaparkan semua artikel kami, dalam laluan peribadi kami. Anda boleh mendapatkan artikel yang lebih terperinci tentang cara menulis komponen ini di sini
npm i npm run start
Komponen kad untuk memaparkan artikel bertulis.
Komponen ini juga mengandungi pautan yang akan membawa kepada kedua-dua halaman paparan artikel dan halaman untuk mengedit artikel yang ditulis sebelum ini.
npx create-next-app myblog
Komponen yang bertanggungjawab untuk membuat panggilan API dan memaparkan respons.
Di sini, kami akan menggunakan dua panggilan API melalui fungsi yang kami tulis:
Kami akan menggunakan komponen Pagination.tsx, yang ditulis sebelum ini, untuk membahagikan bilangan artikel merentas halaman.
npm i markdown-it @types/markdown-it markdown-it-style github-markdown-css react-markdown
Seterusnya, kami akan melalui setiap halaman kami, dibahagikan dengan laluan masing-masing.
Ini adalah halaman utama aplikasi kami. Ia adalah halaman yang mudah, dan anda boleh mengubah suainya mengikut kesesuaian anda. Pada halaman ini, kami akan menggunakan fungsi log masuk yang disediakan oleh pustaka navigasi pengesahan seterusnya.
Dalam fail src/app/pages/public/login/page.tsx.
remark remark-gfm remark-react
Untuk mencipta halaman bacaan artikel, kami akan membangunkan halaman dinamik.
Setiap platform blog yang anda lawati berkemungkinan mempunyai halaman khusus untuk membaca artikel, boleh diakses melalui URL. Sebabnya ialah laluan halaman dinamik. Nasib baik, Next.js memudahkan perkara ini dengan kaedah AppRouter baharunya, menjadikan kehidupan kita lebih mudah.
Pertama: kita perlu mencipta laluan dalam struktur kita dengan menambahkan folder [id]. Ini akan menghasilkan struktur berikut: 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
Kedua: gunakan pustaka MarkdownIt untuk membolehkan halaman memaparkan teks dalam format Markdown.
npm i react-icons @types/react-icons
Dan akhirnya,
setelah halaman itu sedia, dengan mengakses, sebagai contoh, localhost:3000/articles/1 dalam penyemak imbas, anda akan dapat melihat artikel dengan ID yang disediakan.
Dalam kes kami, ID akan dihantar melalui navigasi apabila mengklik pada salah satu komponen ArticleCards.tsx, yang akan dipaparkan pada halaman utama laluan peribadi kami.
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
Berikut ialah halaman peribadi kami, yang hanya boleh diakses setelah pengguna disahkan dalam aplikasi kami.
Di dalam folder apl/halaman/ kami, apabila fail diisytiharkan di dalam (), ini bermakna laluan itu sepadan dengan /.
Dalam kes kami, folder (Laman Utama) merujuk kepada halaman utama laluan peribadi kami. Ia adalah halaman pertama yang pengguna lihat apabila mengesahkan ke dalam sistem. Halaman ini akan memaparkan senarai artikel daripada pangkalan data kami.
Data akan diproses oleh komponen ArticlesList.tsx kami. Jika anda belum menulis kod ini lagi, rujuk semula bahagian komponen.
Dalam apl/(halaman)/(peribadi)/(rumah)/halaman.tsx.
npm i npm run start
Ini adalah salah satu halaman paling penting dalam aplikasi kami, kerana ia membolehkan kami mendaftarkan artikel kami.
Halaman ini akan membolehkan pengguna untuk:
Halaman menggunakan beberapa cangkuk:
Untuk ini, kami akan menggunakan dua komponen:
Semasa membina halaman ini, kami akan menggunakan API kami:
Kami juga akan menggunakan cangkuk useSession, yang disediakan oleh perpustakaan pengesahan seterusnya, untuk mendapatkan token pengesahan pengguna, yang akan digunakan untuk mendaftarkan artikel pada pelayan.
Ini akan melibatkan tiga panggilan API yang berbeza.
Dalam app/pages/(private)/newArticle/page.tsx.
"gunakan klien"; import React, { ChangeEvent, useCallback, useState } daripada "react"; import { useSession } daripada "next-auth/react"; import { ubah hala } daripada "next/navigation"; import postArtical daripada "@/services/postArticle"; import { AiOutlineFolderOpen } daripada "react-icons/ai"; import { RiImageEditLine } daripada "react-icons/ri"; import Imej daripada "seterusnya/imej"; import TextEditor daripada "@/components/textEditor"; import Pratonton daripada "@/komponen/PreviewText"; import { AiOutlineSend } daripada "react-icons/ai"; import { BsBodyText } daripada "react-icons/bs"; eksport fungsi lalai NewArticle(params:any) { const { data: session }: any = useSession({ dikehendaki: benar, onUnauthenticated() { redirect("/log masuk"); }, }); const [imageUrl, setImageUrl] = useState<objek>({}); const [previewImage, setPreviewImage] = useState<rentetan>(""); const [previewText, setPreviewText] = useState<boolean>(false); const [title, setTitle] = useState<rentetan>(""); const [doc, setDoc] = useState<string>("# Escreva o seu texto... n"); const handleDocChange = useCallback((newDoc: any) => { setDoc(newDoc); }, []); jika (!session?.user) mengembalikan null; const handleArticleSubmit = tak segerak (e:mana-mana) => { e.preventDefault(); token const: rentetan = session.user.token; cuba { const res = menunggu postArtical({ id: session.user.userId.toString(), token: token, imageUrl: imageUrl, tajuk: "tajuk," doc: doc, }); console.log('re--->', res); redirect('/success'); } tangkap (ralat) { console.error('Ralat menghantar artikel:', ralat); // Kendalikan ralat jika perlu ralat lontaran; } }; const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => { jika (e.target.files && e.target.files.length > 0) { fail const = e.target.files[0]; const url = URL.createObjectURL(file); setPreviewImage(url); setImageUrl(fail); } }; const handleTextPreview = (e: mana-mana) => { e.preventDefault(); setPreviewText(!previewText); }; kembali ( <section className="w-full h-penuh min-h-skrin relatif py-8"> {previewTeks && ( <div className="kanan mutlak-16 atas-5 p-5 sempadan-2 sempadan-slate-500 bg-slate-100 rounded-xl w-full max-w-[33em] z-30"> <Pratonton doc={doc} tajuk={tajuk} previewImage={previewImage} onPreview={() => setPreviewText(!previewText)} /> </div> )} <form className="relative mx-auto max-w-[700px] h-penuh min-h-[90%] w-penuh p-2 sempadan-2 sempadan-slate-200 bulat-md bg-slate-50 drop-shadow-xl flex flex-col gap-2 "> {" "} <div className="flex justify-antara item-center"> <butang className="border-b-2 rounded-md border-slate-500 p-2 flex items-center gap-2 hover:border-slate-400 hover:text-slate-800" onClick={handleTextPreview} > <BsBodyText /> Pratonton </button>{" "} <butang className="group border border-b-2 border-slate-500 rounded-md p-2 flex items-center gap-2 hover:border-slate-400 hover:text-slate-800 " onClick={handleArticleSubmit} > Enviar Texto <AiOutlineSend className="w-5 h-5 group-hover:text-red-500" /> </butang> </div> <div className="header-wrapper flex flex-col gap-2 "> <div className="image-box"> {previewImage.length === 0 && ( <div className="select-image"> <label htmlUntuk="imej" className="p-4 sempadan putus-putus sempadan-4 sempadan-slate-400 kursor-penunjuk flex flex-col item-pusat justify-center" > <AiOutlineFolderOpen className="w-7 h-7" /> drang dan lepaskan imej </label> <masukan > <h4> Edit Artikel </h4> <p>Halaman yang serupa dengan <em>Artikel Baharu</em> (Artikel baharu), dengan beberapa perbezaan.</p> <p>Pertama, kami mentakrifkan laluan dinamik di mana kami menerima id sebagai parameter navigasi. Ini hampir sama dengan apa yang dilakukan pada halaman membaca artikel. <br> app/(pages)/(private)/editArticle/[id]/page.tsx<br> </p><pre class="brush:php;toolbar:false">"gunakan klien"; import React, { useState, useEffect, useCallback, useRef, ChangeEvent } daripada "react"; import { useSession } daripada "next-auth/react"; import { ubah hala } daripada "next/navigation"; import Imej daripada 'next/image'; import { IArticle } daripada "@/interfaces/article.interface"; import { AiOutlineEdit } daripada "react-icons/ai"; import { BsBodyText } daripada "react-icons/bs"; import { AiOutlineFolderOpen } daripada "react-icons/ai"; import { RiImageEditLine } daripada "react-icons/ri"; import Pratonton daripada "@/komponen/PreviewText"; import TextEditor daripada "@/components/textEditor"; import Pemuatan daripada '@/komponen/Pemuatan'; import editArtical daripada "@/services/editArticle"; eksport fungsi lalai EditArticle({ params }: { params: any }) { const { data: session }: any = useSession({ dikehendaki: benar, onUnauthenticated() { redirect("/log masuk"); }, }); const id: nombor = params.id; const [artikel, setArticle] = useState<IArticle | null>(null); const [imageUrl, setImageUrl] = useState<objek>({}); const [previewImage, setPreviewImage] = useState<rentetan>(""); const [previewText, setPreviewText] = useState<boolean>(false) const [title, setTitle] = useState<rentetan>(""); const [doc, setDoc] = useState<rentetan>(''); const handleDocChange = useCallback((newDoc: any) => { setDoc(newDoc); }, []); const inputRef= useRef<HTMLInputElement>(null); const fetchArticle = tak segerak (id: nombor) => { cuba { respons const = tunggu ambil( `http://localhost:8080/articles/getById/${id}`, ); const jsonData = menunggu respons.json(); setArticle(jsonData); } tangkap (err) { console.log("sesuatu telah berlaku:", err); } }; useEffect(() => { jika (artikel !== null || artikel !== tidak ditentukan) { fetchArticle(id); } }, [id]); useEffect(()=>{ if(artikel != null && article.content){ setDoc(article.content) } if(artikel !=null && article.image){ setPreviewImage(`http://localhost:8080/` article.image) } },[artikel]) const handleArticleSubmit = tak segerak (e:mana-mana) => { e.preventDefault(); token const: rentetan = session.user.token; cuba{ const res = menunggu editArtical({ id: id, token: token, imageUrl:imageUrl, tajuk: tajuk, doc: doc, }); console.log('re--->',res) kembalikan semula; } tangkap (ralat){ console.log("Ralat:", ralat) } }; const handleImageClick = ()=>{ console.log('hiii') if(inputRef.current){ inputRef.current.click(); } }const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => { jika (e.target.files && e.target.files.length > 0) { fail const = e.target.files[0]; const url = URL.createObjectURL(file); setPreviewImage(url); setImageUrl(fail); } }; const handleTextPreview = (e: mana-mana) => { e.preventDefault(); setPreviewText(!previewText); console.log('hello dari pratonton!') }; if(!article) return <Memuatkan/> jika(artikel?.kandungan) kembali ( <section className='w-penuh h-penuh min-h-skrin relatif py-8'> {previewTeks && ( <div className="kanan mutlak-16 atas-5 p-5 sempadan-2 sempadan-slate-500 bg-slate-100 rounded-xl w-full max-w-[33em] z-30"> <Pratonton doc={doc} tajuk={tajuk} previewImage={previewImage} onPreview={() => setPreviewText(!previewText)} /> </div> )} <div className='relative mx-auto max-w-[700px] h-min-h-penuh-[90%] w-penuh p-2 sempadan-2 sempadan-slate-200 bulat-md bg-titik putih- shadow-md flex flex-col gap-2'> <form className='relative mx-auto max-w-[700px] h-penuh min-h-[90%] w-penuh p-2 sempadan-2 sempadan-slate-200 bulat-md bg-slate-50 drop-shadow-md flex flex-col gap-2 '> {" "} <div className='flex justify-antara item-center'> <butang className='border-b-2 rounded-md border-slate-500 p-2 flex items-center gap-2 hover:border-slate-400 hover:text-slate-800' onClick={handleTextPreview} > <BsBodyText /> Pratonton </button>{" "} <butang className='group border border-b-2 border-slate-500 rounded-md p-2 flex items-center gap-2 hover:border-slate-400 hover:text-slate-800 ' onClick={handleArticleSubmit} > Edit artigo <AiOutlineEdit className='w-5 h-5 group-hover:text-red-500' /> </butang> </div> <div className='header-wrapper flex flex-col gap-2 '> <div className='image-box'> {previewImage.length === 0 && ( <div className='select-image'> <label htmlFor='image' className='p-4 sempadan putus-putus sempadan-4 sempadan-slate-400 kursor-penunjuk flex flex-col item-pusat justify-center' > <AiOutlineFolderOpen className='w-7 h-7' /> drang dan lepaskan imej </label> <masukan > <h2> Kesimpulan </h2> <p>Pertama, saya ingin mengucapkan terima kasih kerana meluangkan masa untuk membaca tutorial ini, dan saya juga ingin mengucapkan tahniah kepada anda kerana telah menyelesaikannya. Saya harap ia berfungsi dengan baik dan arahan langkah demi langkah mudah diikuti.</p> <p>Kedua, saya ingin menyerlahkan beberapa perkara tentang perkara yang baru kami bina. Ini adalah asas sistem blog, dan masih banyak yang perlu ditambah, seperti halaman awam yang memaparkan semua artikel, halaman pendaftaran pengguna, atau halaman ralat 404 tersuai. Jika, semasa tutorial, anda tertanya-tanya tentang halaman ini dan terlepasnya, ketahui bahawa ini adalah disengajakan. Tutorial ini memberikan anda pengalaman yang mencukupi untuk membuat halaman baharu ini sendiri, menambah banyak lagi dan melaksanakan ciri baharu.</p> <p>Terima kasih banyak-banyak, dan sehingga kali seterusnya. o/</p>
Atas ialah kandungan terperinci Membina Papan Pemuka Blog Dinamik dengan Next.js. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!