ToCode
Открыть в Telegram
1 419
Подписчики
-124 часа
-17 дней
-130 день
Архив постов
1 419
<span className="ml-2 w-2 h-2 bg-blue-500 rounded-full inline-block"></span>
}
</div>
))}
</nav>
)
}
עכשיו קומפוננטת צד הלקוח עושה שני דברים: היא גם מקבלת בצורה דינמית את רשימת הדפים מהשרת, וגם משתמשת ב usePathname כדי להציג את העיגול ליד הפריט הפעיל בתפריט. ובאופן כללי נרצה לבנות יישומי Next.js במבנה של שיתוף פעולה בין קומפוננטות צד שרת לקומפוננטות צד לקוח. קומפוננטת צד שרת תמשוך את המידע, למשל דרך פנייה לבסיס נתונים, ל API מרוחק או למערכת קבצים, תעביר את המידע לקומפוננטת צד לקוח דרך props וקומפוננטת צד לקוח תוכל להציג את המידע בצורה אינטרקטיבית תוך שימוש בסטייט. כשאותה קומפוננטת צד לקוח תצטרך לעדכן חזרה את השרת נשתמש במנגנון שראינו בפרק הקודם של המדריך וכך העדכון יגרום לריענון של קומפוננטות צד השרת שבעמוד ובמידה והמידע ישתנה גם קומפוננטות צד הלקוח יקבלו פרופס חדשים ויציגו את התוכן המעודכן על המסך.1 419
מדריך Next.JS חלק 6 - שילוב קומפוננטות צד-לקוח וצד-שרת
בואו נחזור לתפריט העליון שכתבנו באפליקציה. אם עקבתם אחרי המדריך והתרגילים צריכות להיות לכם שתי גירסאות לקומפוננטה זו - גירסת קומפוננטת צד-שרת של התפריט העליון, שמחפשת בצורה דינמית את כל התיקיות שמייצגות "דפים" ומציגה תפריט שמיוצר אוטומטית מהדפים הקיימים, וגירסת קומפוננטת צד-לקוח שמשתמשת ברשימה קבועה של דפים אבל כוללת עיגול קטן ליד הדף הפעיל.
בחלק זה נראה איך לשלב את שתי הקומפוננטות כדי ליהנות מהטוב משני העולמות - גם לקבל תפריט שנוצר דינמית מתוך התיקיות בשרת, וגם לקבל סימון לנתיב הפעיל בעזרת JavaScript בצד לקוח.
מה בעצם הבעיה
בואו ניזכר בשתי קומפוננטות התפריט ובבעיות של כל אחת מהן. התפריט בקומפוננטת צד לקוח נראה כך:
"use client";
import Link from 'next/link';
import { usePathname } from 'next/navigation'
export default () => {
const pathname = usePathname()
return (
<nav className='flex my-4 border-4 border-indigo-200 border-l-indigo-500'>
<div className='flex-1 px-2'>
<Link href="/" >Home</Link>
{pathname == "/" &&
<span className="ml-2 w-2 h-2 bg-blue-500 rounded-full inline-block"></span>
}
</div>
<div className='flex-1 px-2'>
<Link href="/about" >About</Link>
{pathname == "/about" &&
<span className="ml-2 w-2 h-2 bg-blue-500 rounded-full inline-block"></span>
}
</div>
</nav>
)
}
בגלל שזו קומפוננטת צד לקוח אני לא יכול להשתמש בפונקציות של מערכת הקבצים של השרת כמו readdir כדי לקבל את רשימת כל התיקיות על השרת, ולכן אני משתמש ברשימה קבועה.
בצד השרת שלחתי אתכם באחד התרגילים לכתוב קומפוננטת צד-שרת שמוצאת את כל הדפים שצורה דינמית ויוצרת מהם תפריט, ואולי כתבתם קומפוננטה שנראית כך:
import { readdir } from 'node:fs/promises';
import Link from 'next/link';
import { existsSync } from 'node:fs';
export default async () => {
const pages = [
{href: '/', text: 'home'},
...(await readdir('./src/app', { withFileTypes: true }))
.filter(f => f.isDirectory)
.filter(f => existsSync(\${f.path}/${f.name}/page.tsx\))
.map(f => (
{href: '/' + f.name, text: f.name}
))
]
return (
<nav className='flex my-4 border-4 border-indigo-200 border-l-indigo-500'>
{pages.map(page => (
<div className='flex-1 px-2'>
<Link href={page.href} >{page.text}</Link>
</div>))}
</nav>
)
}
בקומפוננטה זו אני לא יכול להשתמש ב usePathname() מאחר והגישה ל pathName מוגבלת לקומפוננטות צד-לקוח בלבד.
הפיתרון - שילוב כוחות
המבנה של קומפוננטת צד שרת שיודעת לגשת למידע וקומפוננטת צד לקוח שיודעת להציג את המידע בצורה אינטרקטיבית הוא בעצם תבנית די נפוצה באפליקציות ווב ובאפליקציות next.js במיוחד. הפיתרון לאתגר הזה הוא פשוט לחלק את העבודה לשתי קומפוננטות, כך שקומפוננטת צד שרת מרנדרת קומפוננטת צד לקוח ומעבירה לה את המידע שהיא צריכה בתור props.
בקובץ אחד בשם menu.tsx אני כותב את התוכן הבא לקומפוננטת צד השרת:
import Menu from './components/menu';
import { readdir } from 'node:fs/promises';
import { existsSync } from 'node:fs';
export default async () => {
const pages = (await readdir('./src/app', { withFileTypes: true }))
.filter(f => f.isDirectory)
.filter(f => existsSync(\${f.path}/${f.name}/page.tsx\))
.map(f => (
{href: '/' + f.name, text: f.name}
))
return <Menu items={[
{href: "/", text: "home"},
...pages
]} />
}
ובקובץ אחר בשם components/menu.tsx אני כותב את התוכן הבא:
"use client";
import Link from 'next/link';
import { usePathname } from 'next/navigation'
export default ({ items }: {
items: Array<{href: String, text: String}>
}) => {
const pathname = usePathname()
return (
<nav className='flex my-4 border-4 border-indigo-200 border-l-indigo-500'>
{items.map(item => (
<div className='flex-1 px-2'>
<Link href={item.href} >{item.text}</Link>
{pathname == item.href &&1 419
משחק טוב עם הקוד בנקודה הזאת יהיה להוסיף עוד פעולות על הפוסטים - נסו להוסיף אפשרויות לעריכה ומחיקה של פוסטים. אחרי שזה עובד הוסיפו קומפוננטה חדשה שמציגה את מספר הפוסטים בעמוד ושימו לב שהמידע שבה מתרענן כל פעם שאתם מוסיפים או מוחקים פוסט.
1 419
מדריך Next.JS חלק 5 - עדכון בסיס הנתונים מטופס בדפדפן
בחלק הקודם של המדריך בנינו רשימת פוסטים שנוצרה בצורה סטטית על השרת בעזרת סקריפט seed.ts. הכח האמיתי של next.js הוא האינטגרציה בין קוד צד שרת לקוד צד לקוח, שעובדת כמעט בלי שנשים לב שיש פה רכיבי תוכנה שרצים על מכונות שונות. בואו נראה איך זה עובד דרך הוספת טופס ליישום שיוסיף פוסט חדש לעמוד הפוסטים.
פונקציה ליצירת פוסט חדש
ניצור תיקייה בשם db ובתוכה קובץ בשם posts.ts עם התוכן הבא:
"use server"
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
export async function createPost(author: string, text: string) {
const res = await prisma.post.create({
data: {
author,
text,
}
})
return res;
}
הקוד דומה מאוד למה שכתבנו בקובץ ה seed.ts, רק שהפעם נמצא בתוך פונקציה שאפשר להשתמש בה גם מקבצים אחרים. הפתיח use server לקובץ גורם ל next.js לא להכניס את הקוד הזה ל JavaScript שירוץ בצד הלקוח.
קומפוננטת צד לקוח עבור הטופס
בפריימוורקים "רגילים" לכתיבת קוד צד שרת היינו צריכים עכשיו לכתוב נתיב ב API שיקרא לפונקציה, ואז לכתוב קוד בצד של ריאקט שיפנה לנתיב הזה כשלוחצים על איזשהו כפתור באפליקציה. ב next.js חסכו לנו את כל העבודה ואנחנו יכולים פשוט "לקרוא" לפונקציה createPost ישירות מקומפוננטת ריאקט. אני יוצר קובץ חדש בתיקיית posts בשם newpost.tsx והקוד שלו נראה ככה:
"use client";
import { useRef } from 'react';
import { useRouter } from 'next/navigation';
import { createPost
} from "../db/posts"
export default () => {
const router = useRouter();
const textFieldRef = useRef<HTMLInputElement>(null);
async function handleCreate(formData: FormData) {
const author = formData.get('author') as string;
const text = formData.get('text') as string;
const newPost = await createPost(author, text);
console.log(newPost);
if (textFieldRef.current) {
textFieldRef.current.value = '';
}
router.refresh();
}
return (
<form action={handleCreate}>
<label>
Author:
<input type="text" name="author" className="text-black"/>
</label>
<label>
Text:
<input type="text" name="text" className="text-black" ref={textFieldRef} />
</label>
<input type="submit" value="Create" />
</form>
)
}
הבלוק הראשון לקרוא כאן הוא הגדרת הפונקציה handleCreate. בואו נסתכל עליה:
async function handleCreate(formData: FormData) {
const author = formData.get('author') as string;
const text = formData.get('text') as string;
const newPost = await createPost(author, text);
console.log(newPost);
if (textFieldRef.current) {
textFieldRef.current.value = '';
}
router.refresh();
}
הפונקציה מקבלת אוביקט מסוג FormData, מושכת ממנו שני שדות עבור כותב הפוסט והטקסט של הפוסט ואז מגיעה לשורת המחץ שזו הקריאה ל createPost. זו בעצם הקריאה דרך API לפונקציה שנמצאת בצד השרת.
החלק השני של הפונקציה, זה שמופיע אחרי ה await ל createPost ייקרא אחרי שהשרת יסיים ליצור את הפוסט החדש. בשלב הזה יש לנו שתי משימות: אנחנו צריכים לנקות חלק מהשדות בטופס (אני רציתי לנקות את שדה ה text כדי שיהיה קל להמשיך לכתוב פוסטים), ולרענן את רשימת הפוסטים כדי לראות את הפוסט החדש. הפונקציה router.refresh שמגיעה מתוך ה router של next היא שאחראית לקסם השני. הפונקציה מרעננת את קומפונטטות צד השרת שנמצאות על הדף הנוכחי, בלי למחוק את הסטייט של קומפוננטות צד הלקוח. כך רשימת הפוסטים מתעדכנת אבל שאר העמוד לא מושפע.
כל מה שצריך בשביל לסיים את הדוגמה הוא להוסיף לקובץ posts/page.tsx את קומפוננטת הטופס החדש שבנינו ונוכל להיכנס לעמוד ולהוסיף פוסטים:
import { PrismaClient } from '@prisma/client'
import NewPost from './newpost';
const prisma = new PrismaClient()
export default async () => {
const posts = await prisma.post.findMany();
return (
<main className='p-2'>
<NewPost />
<ul>
{posts.map(post => (
<li><b>{post.author}</b> {post.text}</li>
))}
</ul>
</main>
)
}
עכשיו אתם1 419
2. בואו נשחק עם מנגנון ה Cache של נקסט - הוסיפו עמוד נוסף שמציג את התאריך ושעה הנוכחיים עם new Date. נסו לנווט אליו דרך התפריט ושימו לב מתי הערך ישתנה. נסו גם לרענן את העמוד עם F5 ושימו לב לשינויים.
3. הוציאו את ה Link יחד עם העיגול שלו לקומפוננטה נפרדת כדי שתוכלו להפוך את קומפוננטת התפריט חזרה לקומפוננטת צד-שרת.
1 419
מדריך Next.JS חלק 3 - ניווט בין דפים
אחד האתגרים המשמעותיים בפיתוח Single Page Application ושימוש ב JavaScript Frameworks היה תמיד הניווט בין דפים. בפעולה הרגילה שלו בדפדפן כל פעם שעוברים לדף חדש באתר הדפדפן טוען קובץ HTML חדש ואת כל הנכסים שלו ומציג את התוכן החדש, וכמובן מנקה את כל הזיכרון של ה JavaScript. הבעיה ביישומי Single Page Applications, או אולי יותר נכון להגיד היתרון ביישומים כאלה, הוא שעכשיו אין צורך לטעון את כל הדף מחדש במעבר בין דפים, ושבעזרת JavaScript אפשר לתת למשתמש הרגשה של מעבר בין דפים בלי לעבור דרך מנגנון זה בדפדפן, וכך לקבל ביצועים הרבה יותר טובים.
איך זה עובד
ב Next.JS מנגנון המעבר בין דפים מובנה בתוך הפריימוורק. ל next יש קומפוננטה מיוחדת בשם Link שלחיצה עליה גורמת ל JavaScript לטעון את הקוד של ה"דף" הבא ולהציג אותו. כמובן שהם מטפלים בשבילנו בלחיצות על כפתור אחורה בדפדפן ובשינוי כתובת העמוד (בשביל סימניות ושיתוף). בנוסף Next ישמור בזיכרון מטמון בדפדפן את התוצאות שקיבל מהשרת כך שניווט לדפים אחורה יהיה מהיר יותר.
את הדפים עצמם אנחנו מגדירים בתור תיקיות חדשות בתיקיית הפרויקט כאשר בכל תיקיה שמייצגת דף צריך להיות קובץ בשם page.tsx שאחראי על התוכן של הדף.
בואו ננסה את זה עם האפליקציה שהתחלנו לכתוב בחלקים הקודמים של המדריך. ניצור תיקיה חדשה בשם about ובתוכה קובץ בשם page.tsx עם התוכן הבא:
export default () => {
return (
<div>
<h1>About Us</h1>
<p>This is an example lesson for using next.js router</p>
</div>
)
}
רק עד כאן זה מספיק כדי לראות את שני הדפים שלנו. נווטו לשתי הכתובות:
http://localhost:3000
http://localhost:3000/about
כדי לראות את שני הדפים שיצרנו.
מעבר בין דפים
נוסיף שורת תפריט עליון כדי לעבור בין הדפים. צרו קובץ חדש בתיקיה הראשית בשם menu.tsx עם התוכן הבא:
import Link from 'next/link';
export default () => {
return (
<nav className='flex my-4 border-4 border-indigo-200 border-l-indigo-500'>
<Link href="/" className='flex-1 px-2'>Home</Link>
<Link href="/about" className='flex-1 px-2'>About</Link>
</nav>
)
}
נוסיף את התפריט העליון לקובץ layout.tsx באמצעות השינוי הבא:
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import TopMenu from './menu';
import './globals.css'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>
<TopMenu />
{children}
</body>
</html>
)
}
עכשיו כשנכנס מחדש לדף אנחנו רואים תפריט עליון עם שני הלינקים, ובנוסף לחיצה על כל אחד מהלינקים תגרום לשינוי הדף הפעיל ולהצגת הדף עליו לחצנו.
הצגת הדף הנוכחי
בשביל להבין מי הדף הנוכחי ולשנות את העיצוב שלו בתפריט אנחנו יכולים להשתמש ב Hooks בשם usePathname. העדכון הבא לקומפוננטת התפריט יגרום להצגת עיגול קטן ליד הדף הנוכחי כדי שנדע איפה אנחנו:
"use client";
import Link from 'next/link';
import { usePathname } from 'next/navigation'
export default () => {
const pathname = usePathname()
return (
<nav className='flex my-4 border-4 border-indigo-200 border-l-indigo-500'>
<div className='flex-1 px-2'>
<Link href="/" >Home</Link>
{pathname == "/" &&
<span className="ml-2 w-2 h-2 bg-blue-500 rounded-full inline-block"></span>
}
</div>
<div className='flex-1 px-2'>
<Link href="/about" >About</Link>
{pathname == "/about" &&
<span className="ml-2 w-2 h-2 bg-blue-500 rounded-full inline-block"></span>
}
</div>
</nav>
)
}
עכשיו אתם
1. הוסיפו דף נוסף לאפליקציה בשם Contact והציגו בו טופס "צור קשר". שימו לב לעדכן את התפריט כדי להציג את שלושת הדפים.1 419
הנה עוד כמה משחקים שכדאי לנסות עם קומפוננטות צד-שרת וקומפוננטות צד-לקוח כדי להבין טוב יותר את ההבדלים ביניהן:
1. הוסיפו את הודעת ה console.log לכל אחת משלושת הקומפוננטות ושימו לב איפה הודעת ההדפסה מופיעה בכל קומפוננטה.
2. חשבו: למה הודעות ההדפסה שכתבנו בקומפוננטות צד-לקוח מופיעות גם בקונסול של הדפדפן וגם בחלון שורת הפקודה?
1 419
קומפוננטות צד-שרת, קומפוננטות צד-לקוח ו State
זוכרים את useState שלא עבד לנו בפרק הקודם? בפרק זה נבין למה ונלמד עוד טיפ על הארכיטקטורה של next.js.
קומפוננטת צד-שרת
בשביל לשפר ביצועים הארכיטקטורה הבסיסית של next.js מחלקת את קומפוננטות ריאקט שאנחנו כותבים לקומפוננטות צד-שרת וקומפוננטות צד-לקוח. קומפוננטות צד-לקוח זה מה שאנחנו מכירים מריאקט "הרגיל" עם תוספת של SSR, כלומר השרת לוקח את הקומפוננטה, הופך אותה ל HTML, שולח את זה לדפדפן יחד עם ה JavaScript של הקומפוננטה שם היא עוברת רינדור נוסף וקוד הטיפול באירועים "מתלבש" על ה HTML שנשלח מהשרת.
קומפוננטת צד שרת היא בסך הכל רעיון דומה חוץ מזה שמוותרים על החלק של לשלוח את ה JavaScript לדפדפן ולרנדר את הקומפוננטה שוב בצד הלקוח, כלומר נשארים רק עם HTML שנשלח מהשרת ללקוח.
מתוך הארכיטקטורה השונה נגזרות גם היכולות השונות:
1. קומפוננטות צד-לקוח יכולות להשתמש ב State, כיוון שדברים יכולים לקרות ולשנות את מה שמוצג למשתמש.
2. קומפוננטות צד-שרת לא יכולות להשתמש ב State, כי אין להן דרך להוסיף קוד טיפול באירועים. מצד שני קומפוננטות צד-שרת יכולות לגשת בצורה ישירה לבסיס הנתונים או למערכת ההפעלה של השרת, כי הקוד רץ על השרת בלבד.
הקומפוננטה page.tsx שכתבנו בשיעור הפתיחה היתה קומפוננטת צד שרת, כי זו ברירת המחדל של קומפוננטות ב next.js. בשביל להוסיף לה State אני צריך להפוך אותה לקומפוננטה צד-לקוח בעזרת הוספת השורה:
"use client";
בראש הקובץ.
לדוגמה אכתוב את התוכן הבא בקובץ page.tsx כדי להציג קומפוננטה של מונה לחיצות:
"use client";
import Image from 'next/image'
import { useState } from 'react';
export default function Home() {
const [count, setCount] = useState(0);
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<button onClick={() => setCount(c => c + 1)}>{count}</button>
</main>
)
}
אפשר גם לפצל את הקומפוננטה לכמה קומפוננטות כדי לשים מספר מונים על המסך:
"use client";
import { useState } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}
export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<Counter />
<Counter />
<Counter />
</main>
)
}
שילוב קומפוננטות צד-שרת עם קומפוננטות צד-לקוח
בואו נעשה עוד ניסוי, נוציא את Counter לקובץ אחר בשם counter.tsx ואז בקובץ page.tsx נשאיר רק את הקוד הבא:
import Counter from './counter'
export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<Counter />
<Counter />
<Counter />
</main>
)
}
זה אגב יהיה הקוד ב counter.tsx:
"use client";
import { useState } from 'react';
export default () => {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}
כיף לשים לב שאין ל next שום בעיה כשאני משלב קומפוננטות צד-לקוח בתוך קומפוננטות צד-שרת. קומפוננטות צד הלקוח יישלחו לדפדפן וירונדרו גם שם, בעוד שהחלקים שהם קומפוננטות צד שרת ירונדרו רק בצד השרת.
מצב הפוך הוא קצת יותר מסובך. ניצור קובץ בשם header.tsx עם התוכן הבא:
export default () => (<h1>Counter Header</h1>);
ונעדכן את counter.tsx כך שיכיל את הקוד:
"use client";
import { useState } from 'react';
import Header from './header';
export default () => {
const [count, setCount] = useState(0);
return (
<div>
<Header />
<button onClick={() => setCount(c => c + 1)}>{count}</button>
</div>
)
}
למרות ש header.tsx לא כולל את הכותרת use client הוא עדיין קומפוננטת צד-לקוח, בגלל שעשיתי לו import מתוך קומפוננטת צד-לקוח. דרך קלה להיווכח בזה היא להוסיף console.log בתוך הקומפוננטה ולראות אם ההדפסה מופיעה גם בקונסול של הדפדפן וגם בחלון שורת הפקודה, או רק בחלון שורת הפקודה.
עכשיו אתם1 419
3. הקוד ב page.tsx יכול להשתמש ב import כדי לטעון קומפוננטות מקבצים אחרים. צרו בקובץ אחר קומפוננטה לתפריט עליון והוסיפו אותה לעמוד עם import.
4. נסו להוסיף לאחת הקומפוננטות קריאה ל useState ושימו לב לשגיאה שמוצגת על העמוד. שחקו עם זה כדי להבין איזה hooks עובדים ואיזה לא.
1 419
src="/next.svg"
alt="Next.js Logo"
width={180}
height={37}
priority
/>
</div>
<div className="mb-32 grid text-center lg:max-w-5xl lg:w-full lg:mb-0 lg:grid-cols-4 lg:text-left">
<a
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={\mb-3 text-2xl font-semibold\}>
Docs{' '}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
->
</span>
</h2>
<p className={\m-0 max-w-[30ch] text-sm opacity-50\}>
Find in-depth information about Next.js features and API.
</p>
</a>
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={\mb-3 text-2xl font-semibold\}>
Learn{' '}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
->
</span>
</h2>
<p className={\m-0 max-w-[30ch] text-sm opacity-50\}>
Learn about Next.js in an interactive course with quizzes!
</p>
</a>
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={\mb-3 text-2xl font-semibold\}>
Templates{' '}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
->
</span>
</h2>
<p className={\m-0 max-w-[30ch] text-sm opacity-50\}>
Explore starter templates for Next.js.
</p>
</a>
<a
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={\mb-3 text-2xl font-semibold\}>
Deploy{' '}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
->
</span>
</h2>
<p className={\m-0 max-w-[30ch] text-sm opacity-50\}>
Instantly deploy your Next.js site to a shareable URL with Vercel.
</p>
</a>
</div>
</main>
)
}
הצעות לתרגול
הקובץ layout.tsx כולל הגדרות כלליות לעמוד והקובץ page.tsx כולל את קוד קומפוננטת ריאקט של העמוד.
1. צרו פרויקט next.js חדש והפעילו את שרת הפיתוח.
2. עדכנו את הקוד ב page.tsx כדי שיציג הודעת פתיחה שלכם, ושימו לב איך הקוד מתעדכן בזמן אמת על המסך.1 419
מדריך Next.JS - פרק 1 התקנה ופרויקט ראשון
השבוע הבלוג יצא לפגרה אבל כדי לא להשאיר את תיבת המייל שלכם ריקה מדי אשלח במהלך התקופה מדריך next.js שכתבתי מראש. מקווה שתמצאו אותו מועיל ונחזור אחרי חנוכה בכוחות מחודשים.
מהו Next.JS
נקסט ג'יי אס הוא סביבה לפיתוח יישומי Full Stack מבוססת ריאקט, המאפשרת לנו בפרויקט אחד לכתוב גם את קוד צד השרת וגם את קוד צד הלקוח. הדגש של הפריימוורק הוא על קלות בניית פרויקט ואינטגרציה חזקה בין קוד צד שרת לקוד צד לקוח, מה שהופך אותו לבחירה מאוד קלה לפרויקטי ריאקט חדשים. יותר מזה, next.js נוצר על ידי חברת vercel ויש לו כלי אוטומטי ל deployment על הענן שלהם, כך שאפשר להביא את הפרויקט ממכונת הפיתוח לאינטרנט בכמה דקות.
פרויקט ראשון
בשביל ליצור פרויקט next.js ראשון אני צריך רק להתקין את node.js ואז להפעיל את הפקודה:
npx create-next-app@latest 01-intro
ולענות על מספר שאלות (אני בחרתי בכולן את ברירות המחדל). זה משמעותית יותר מהיר וקל מלהקים קונפיגורציית וובפאק בעצמנו וזה אפילו יותר קל מיצירת פרויקט עם vite. אחרי יצירת הפרויקט אפשר לפתוח שרת לפיתוח מקומי על ידי:
cd 01-intro
npm run dev
פתיחת הפרויקט שנוצר בסביבת פיתוח תראה לנו שיש בסך הכל שני קבצים מעניינים בינתיים בתיקיית app, הראשון נקרא layout.tsx והשני נקרא page.tsx. ב layout אני מוצא את הקוד הבא:
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
)
}
וב page.tsx את הקוד הבא:
import Image from 'next/image'
export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex">
<p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
Get started by editing
<code className="font-mono font-bold">src/app/page.tsx</code>
</p>
<div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:h-auto lg:w-auto lg:bg-none">
<a
className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0"
href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
By{' '}
<Image
src="/vercel.svg"
alt="Vercel Logo"
className="dark:invert"
width={100}
height={24}
priority
/>
</a>
</div>
</div>
<div className="relative flex place-items-center before:absolute before:h-[300px] before:w-[480px] before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-[240px] after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 before:lg:h-[360px] z-[-1]">
<Image
className="relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert"1 419
חידת גרמלין - Coalesce
הקוד הבא היה אמור לבדוק אם קיים ורקטקס עם הערך bob במאפיין name, ואם לא קיים ליצור חדש, אבל הוא לא עושה בדיוק את מה שרצינו. מה עושה הקוד, מה הבעיה ואיך נתקן אותו?
g
.V()
.coalesce(
__.has('person', 'name', 'bob'),
__.addV('person').property('name', 'bob')
)
.iterate()
הבעיה בקוד
הפקודה coalesce של גרמלין מקבלת מספר אפשרויות להתקדמות בגרף ומבצעת אותן לפי הסדר עד שמגיעה לאפשרות שמחזירה צומת תקין. לכן לכאורה הקוד היה אמור להתחיל מכל הצמתים בגרף, למצוא צומת שיש לו את המאפיין name עם הערך bob, אם קיים להחזיר אותו ואם לא קיים להגיע ל addV. אבל לא ככה זה עובד.
הבעיה בקוד היא ש coalesce מופיע אחרי V() ולכן הוא יופעל על כל הצמתים בגרף, כלומר לכל צומת נבדוק בנפרד אם הוא מתאים ל has ואם לא ננסה להוסיף צומת כזה עם addV, מה שיגרום להוספה של המון צמתים. דוגמת הרצה בסקאלה עם הקוד הזה:
@main def main() =
val graph = TinkerGraph.open
val g = traversal.withEmbedded(graph)
println(g.addV("person").property("name", "a").next())
println(g.addV("person").property("name", "b").next())
println("--- 1")
println(g.V().elementMap().toList)
g.V()
.coalesce(
has("person", "name", "bob"),
addV("person").property("name", "bob"))
.iterate()
println("--- 2")
println(g.V().elementMap().toList)
מדפיסה את הפלט:
--- 1
[{id=0, label=person, name=a}, {id=2, label=person, name=b}]
--- 2
[{id=0, label=person, name=a}, {id=2, label=person, name=b}, {id=4, label=person, name=bob}, {id=6, label=person, name=bob}]
כלומר נוצרו שני צמתים חדשים עם השם bob.
פיתרון בעזרת fold
איך מתקנים? הכי פשוט זה קודם לחפש את הצומת ואז עליו להפעיל את coalesce. הבעיה היא שניסיון כזה עלול להיכשל אם הצומת לא קיים ולעצור את כל השאילתה. לכן יש להוסיף צעד fold שאוסף את כל הצמתים המתאימים לרשימה ולפתוח את אותה רשימה בתוך ה coalesce. או בקיצור בקוד:
@main def main() =
val graph = TinkerGraph.open
val g = traversal.withEmbedded(graph)
println(g.addV("person").property("name", "a").next())
println(g.addV("person").property("name", "b").next())
println("--- 1")
println(g.V().elementMap().toList)
g.V()
.has("person", "name", "bob")
.fold()
.coalesce(
unfold(),
addV("person").property("name", "bob"))
.iterate()
println("--- 2")
println(g.V().elementMap().toList)
מה שיוצר רק צומת אחד עם השם bob.
פיתרון בעזרת merge
בגירסאות מספיק חדשות של טינקרפופ אפשר להשתמש ב merge כדי לחסוך את כל הבלאגן הזה כשאנחנו מנסים למזג צומת לפי מאפיין כמו name. הקוד נראה ככה:
g.mergeV(Map[Object, Object](
T.label -> "person",
"name" -> "bob"
).asJava)
.iterate()
כל עוד המיזוג הוא לפי מאפיינים שאנחנו מכירים זה יהיה הפיתרון הכי טוב. כשצריך למזג לפי יחסים לצמתים אחרים (לדוגמה אם קיים צומת person שהוא friend של bob) אז נצטרך לחזור לטריק של ה coalesce וה fold.1 419
תיקונים קלים, תיקונים קשים ובדיקות
בפרויקט Node.JS שעבדתי עליו מצאתי את השורה הזאת:
const [host, port] = address.split(':');
והמשימה היתה להוסיף פורט ברירת מחדל, כך שאם הכתובת לא מסתיימת בנקודותיים ומספר נשתמש בברירת המחדל 8080.
התיקון הקל הוא בסך הכל:
const [host, port = '8080'] = address.split(':');
התיקון הקשה היה להתמודד עם כל שאר הדברים שמשתמשים יכלו להכניס בתוך address, למשל להבין שמשתמשים יכולים לכתוב http בהתחלה, להוסיף נתיב, לטעות בשם של ה host, לכתוב כתובת IP לא תקינה או מיליון טעויות אחרות בסגנון. קוד טוב צריך להפריד בין המנגנון שמפענח את הקלט למנגנון שעושה משהו עם הקלט המפוענח, ולאפשר לכל אחד משני המנגנונים לדווח שגיאות בצורה שתתאים הכי טוב למשתמש. שינוי כזה הוא קשה כי השורה היתה קבורה בפונקציה של עשרות שורות והמנגנונים היו מעורבבים יחד.
כשיש סט בדיקות טוב אנחנו מרגישים בנוח לבצע גם תיקונים קשים, אפילו כאלה שדורשים ריפקטורינג משמעותי.
לצערנו ברוב המערכות כשהמנגנונים השונים בקוד מעורבבים יחד, זה בא יחד עם זה שלמערכת אין בדיקות ויש המון מקרי קצה לא מתועדים, כך שהתיקון הקשה נראה כמעט בלתי אפשרי.
כן שווה לזכור שהתיקון הקשה לא יהיה התיקון הקשה האחרון בקוד. אם בכל זאת אנחנו מקבלים הזדמנות לבצע אותו, כדאי לנצל אותה כדי לכתוב סט בדיקות עבור התיקון הקשה הבא.