ToCode
Відкрити в Telegram
1 419
Підписники
Немає даних24 години
Немає даних7 днів
Немає даних30 день
Архів дописів
1 419
עשר דקות עם Tanstack Router
עכשיו ש React Router עדכנו שוב את הגירסה, הפעם כדי להפוך לפריימוורק במיזוג עם Remix, אני חושב שהגיע הזמן לבחון אפשרויות אחרות, והאפשרות הפופולרית ביותר היום מלבד React Router היא Tanstack Router. בואו נראה קצת קוד לדוגמה כדי להבין איך זה עובד. קוד הדוגמה של הפוסט זמין בגיטהאב בקישור:
https://github.com/ynonp/tanstack-router-simple-demo
קובץ ה Layout
טנסטאק ראוטר, בדומה ל next.js, עובד לפי שמות קבצים. הוא מתחבר ל vite וכל פעם שאנחנו רצים במצב פיתוח או בנייה הוא מחפש את הקבצים שנראים כמו נתיבים בתיקייה ויוצר מכולם יחד קובץ מידע מרוכז על נתיבים, בו הוא ישתמש בקוד כדי לספק Type Safety, כלומר אלמנט Link יוכל לדעת אם הוא מוביל למקום שבאמת מתאים לנתיב מתוך ה Router ואם היתה שגיאת כתיב אז נוכל לראות את זה כבר בזמן כתיבת הקוד.
בשביל להתקין את Tanstack Router יש תהליך די מסורבל שמפורט בדף ההתקנה שלהם:
https://tanstack.com/router/latest/docs/framework/react/installation
בגדול מוסיפים אותו ל vite, וטוענים אותו מהקובץ הראשי ביישום.
אחרי ההתקנה אפשר ליצור את קבצי הנתיבים וקובץ הנתיב הראשי בדוגמה שלהם נקרא
__root.tsx. זה הקוד שהם כתבו עם קצת שינויים שלי:
import { createRootRoute, Link, Outlet } from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/router-devtools'
export const Route = createRootRoute({
component: () => (
<>
<div className="p-2 flex gap-2">
<Link to="/" className="[&.active]:font-bold">
Home
</Link>{' '}
<Link to="/about" className="[&.active]:font-bold ">
About
</Link>
<Link
to="/posts/$postId"
params={{postId: "7"}}
className="[&.active]:font-bold ">
Read Post
</Link>
</div>
<hr />
<Outlet />
<TanStackRouterDevtools />
</>
),
})
הפקודה createRootRoute אומרת שמדובר בנתיב ראשי כלומר קובץ Layout. אוביקט ה component שלה הוא הקומפוננטה הראשית של העמוד ובתוכו קומפוננטת Outlet תציג את התוכן האמיתי כלומר את הקומפוננטה שמתאימה לנתיב הנוכחי. מעל ה Outlet יש לנו תפריט ניווט ומתחת יש לנו קומפוננטה של כלי פיתוח. כלי הפיתוח זה בעצם חלון צף קטן שנותן המון מידע על ה Router והנתיבים השונים בו.
קבצי הנתיבים
שני הקבצים האחרים בתוכנית הדוגמה שהוצעה בתיעוד הם about.lazy.tsx ו index.lazy.tsx. השם שלהם כבר מסמן לנו שמדובר בנתיבים עצלים, כלומר שהקוד שלהם ייטען רק כשמשתמש ייכנס אליהם בפעם הראשונה. כך נראה תוכן הקובץ about.tsx:
import { createLazyFileRoute, Link } from '@tanstack/react-router'
export const Route = createLazyFileRoute('/about')({
component: About,
})
function About() {
return <div className="p-2">
Hello from About!
<Link to='/'>Type Safe?</Link>
</div>
}
לא מסובך: הפקודה createLazyFileRoute מקבלת את הנתיב וקומפוננטה ומוסיפה אותו לרשימת הנתיבים (זאת שנוצרת בצורה אוטומטית בקובץ routeTree.gen.ts). ה Link שמופיע שם הוא Type Safe, כלומר אם אני מנסה לכתוב משהו שלא מתאים לשום נתיב מופיע שם קו אדום בגלל הטייפסקריפט.
נתיב דינמי
בשביל המשחק יצרתי קובץ נוסף בשם posts/$postId.lazy.tsx עם התוכן הבא:
import { createLazyFileRoute } from '@tanstack/react-router'
export const Route = createLazyFileRoute('/posts/$postId')({
component: Post,
})
function Post() {
const { postId } = Route.useParams()
return <div className="p-2">This is post number: {postId}</div>
}
שם הקובץ כבר מרמז לנו שיש פה משהו מיוחד. הדולר בתחילת שם הקובץ אומר ש postId זה שם של פרמטר ובאמת בתוך גוף הקומפוננטה אני יכול להשתמש ב useParams כדי לגשת לערך של אותו פרמטר. פה אפשר להוציא בקשת רשת כדי למשוך מידע לגבי אותו פוסט לפי המזהה שלו, או להיעזר במנגנון ה Data Fetching המובנה של Tanstack Router (עליו אולי אכתוב בפוסט אחר) כדי לשלב את הראוטר עם מנגנון שליפת המידע.
על מה אפשר להתלונן?1 419
מה נשאר מחוץ לקורס
האתגר הכי גדול בבניית קורס הוא להחליט מה יהיה בקורס. באמת. אחרי שיש לכם רשימת נושאים טובה כל השאר זה טכני, אפילו Chat GPT יכול לכתוב את המצגת.
ההחלטה מה יהיה בקורס ועל מה לא מדברים היא קודם כל ההחלטה הכי חשובה שיש בבניית קורס. למדו דברים בסיסיים מדי ואנשים ישתעממו, למדו דברים מסובכים מדי ואנשים ילכו לאיבוד. ההחלטה על הנושאים היא חשובה כי היא בעצם המהות של מה אנחנו מלמדים. אבל מה שמאתגר זה שההחלטה הזאת ממשיכה להיות קשה ברקורסיה גם לתוך הנושאים עצמם, כי בתוך כל נושא צריך לבחור על איזה תתי נושאים מדברים וגם שם אפשר לצלול עמוק מדי ולהישאר לבד, או לדבר שטחי מדי ושוב להישאר לבד.
יש כל מיני רעיונות שלא עובדים, לדוגמה:
1. תגיד כל מה שאתה יודע על הנושא כדי לא להשמיט כלום (לא עובד. אם אתה צריך ללמד משהו אתה כנראה יודע עליו יותר ממה שהתלמידים שלך צריכים לדעת).
2. תגיד את מה שיגרום לכולם להרגיש שהם מבינים (עובד זמנית. הם אולי יתנו לך את הציון הכי גבוה במשוב בסוף הקורס, אבל מהר מאוד הם יבינו שלא הסברת את הדברים הקשים והם בעצם לא מוכנים לעולם האמיתי).
3. קח ממישהו מצגת ותגיד מה שכתוב בה (רוב המצגות שלקחתי מאחרים לא היו מספיק טובות).
מה שכן עובד הוא דווקא הכיוון הכי פשוט: להסתכל על האנשים שמגיעים לקורס. לשאול מה הם יודעים לעשות היום ומה הם צריכים לדעת לעשות בעתיד, ועכשיו לבנות את המסלול המהיר ביותר שיביא אותם לשם. אני אוהב להתחיל מהתרגולים ולבנות רשימה של משימות בדרגת קושי עולה, כאשר המשימות האחרונות הן הדברים שאנשים באמת צריכים לעשות בסוף הקורס. אחרי שיש לי את הרשימה אני מתחיל לעבות אותה בהסברים של "לפני" ו"אחרי" התרגיל. כשנגמרו ההסברים אני אוסיף עוד דוגמאות כדי לחזק את העקרונות שנלמדו בהסבר. בבנייה כזאת השאלה היא לא "מה לא ללמד" אלא מה כן - איך מגיעים מנקודה א ל-ב.
1 419
שליטה על Input ב React לעומת Vue
הקוד הבא ב Vue יכול להטעות:
<script setup lang="ts">
import {ref} from 'vue';
const data = ref('a');
function handleInput(e: any) {
if (Math.random() * 10 < 2) {
data.value = e.target.value;
}
}
</script>
<template>
<input type="text" @input="handleInput" :value="data" />
</template>
במיוחד אם משווים אותו לקוד מקביל בריאקט:
function App() {
const [data, setData] = useState('a');
function handleInput(e) {
if (Math.random() * 10 < 2) {
setData(e.target.value);
}
}
return <input type="text" value={data} onChange={handleInput} />
}
גירסת הריאקט תעדכן את הטקסט בתיבה רק ב 20% מהמקרים. בשאר המקרים גם המשתנה data וגם תיבת הטקסט שמופיעה על המסך יישארו מסונכרנים ויציגו את הטקסט הישן. ריאקט יתעלם מהלחיצות.
בגירסת ה vue ההתנהגות שונה: אנחנו עדיין נכנסים לפונקציה ועדיין רק ב 20% מהמקרים נכנסים לבלוק לביצוע של התנאי, אבל הטקסט בתיבה תמיד ישתנה - בלי קשר למצב של המשתנה data. נשים לב ששינוי של data ממקום אחר עדיין יגרום לעדכון של תיבת הטקסט, כלומר בגירסת ה vue אני יכול להוסיף כפתור:
<input type="text" @input="handleInput" :value="data" />
<button @click="data = ''">Reset</button>
ואז לחיצות על הכפתור יאפסו את המשתנה וגם את הטקסט בתיבה. המנגנון הריאקטיבי של Vue אומר שכשיש שינוי במשתנה גם תיבת הטקסט על המסך תעודכן, אבל כשאין שינוי במשתנה לתיבה לא יקרה כלום.
גם הגדרת v-model עם אפשרות לכתיבה תישאר עם אותה התנהגות מבלבלת:
<script setup lang="ts">
import {computed, ref} from 'vue';
const data = ref('a');
const dataModel = computed({
get() {
return data.value;
},
set(newValue) {
if (Math.random() * 10 < 2) {
data.value = newValue;
}
}
})
</script>
<template>
<Todos />
<input type="text" v-model="dataModel" />
<button @click="data = ''"></button>
</template>
גם במצב זה ב 20% מהמקרים תהיה כתיבה למשתנה אבל בשאר 80% לא תהיה כתיבה למשתנה והטקסט בתיבת הטקסט יצא מסינכרון, התיבה תציג את הטקסט שנכתב אבל המשתנה לא יכיל אותו.
מה בכל זאת אפשר לעשות? אם אתם רוצים את ההתנהגות של ריאקט תצטרכו לכתוב אותה בעצמכם:
function handleInput(e) {
if (Math.random() * 10 < 2) {
data.value = e.target.value;
} else {
e.target.value = data.value;
}
}1 419
להחזיר או לזרוק
שפות רבות מאפשרות לנו לבחור אם להחזיר ערך שמסמן שגיאה או לזרוק את השגיאה כשדברים לא קורים כמו שתכננו. כך בפייתון אני יכול להוציא ערך ממפה עם הפונקציה
get, ואז אם המפתח לא קיים אני מקבל None:
>>> d = {"a": 10, "b": 20}
>>> d.get("a")
10
>>> d.get("d")
או עם אופרטור סוגריים מרובעים שזורק שגיאה אם הערך לא קיים:
>>> d['a']
10
>>> d['d']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'd'
בסקאלה הפונקציה get מחזירה ערך ממפה ואם הוא לא קיים מחזירה None:
scala> Map("a" -> 10, "b" -> 20).get("a")
val res1: Option[Int] = Some(10)
scala> Map("a" -> 10, "b" -> 20).get("d")
val res3: Option[Int] = None
ואפשר גם להפעיל את המפה כמו פונקציה על המפתח בשביל לשלוף ערך או לזרוק שגיאה אם המפתח לא קיים:
scala> Map("a" -> 10, "b" -> 20)("a")
val res2: Int = 10
scala> Map("a" -> 10, "b" -> 20)("d")
java.util.NoSuchElementException: key not found: d
at scala.collection.immutable.Map$Map2.apply(Map.scala:316)
... 30 elided
הרבה זמן חשבתי שכדאי להשתמש בפונקציות אלה לפי ההקשר. כשצריך לזרוק שגיאה אני אבחר את הסוג שזורק שגיאות, וכשצריך להחזיר ערך שגיאה אני אשתמש בפונקציות מהסוג שמחזיר ערכים.
שמתי לב שלאורך זמן אני מתחיל לבנות מנגנונים סביב שיטת העבודה שבחרתי, פונקציות שקוראות לפונקציות והרגלים איך לדבג כל בעיה, וככל שהמערכת גדלה ערבוב שני הסוגים באותו פרויקט גורר כתיבה של קוד כפול מסביב. לאחרונה אני מנסה להתרגל להשתמש תמיד באותו סט של פונקציות וחייב להודות שזה בינתיים מוכיח את עצמו.
ומה אתכם? איזה משתי השיטות אתם מעדיפים? ומה לגבי ערבוב השיטות באותו פרויקט?1 419
על הערך של "להיות בעשייה"
שלושה פחדים שמשותפים להרבה אנשים שלומדים נושא חדש:
1. "לעולם לא אהיה מספיק טוב בזה. אני רק מבזבז את הזמן עכשיו".
2. "עד שאהיה מספיק טוב כבר יהיה מאוחר מדי (הנושא לא יהיה רלוונטי, אני כבר לא אוהב אותו)".
3. "אני לא בכיוון הנכון. בדרך הזאת לעולם לא אצליח ללמוד את הנושא".
הפחדים האלה עלולים להוריד אותנו מהמסלול, דווקא מתוך מקום טוב של רצון להצלחה. זה המצב כשמרוב שאתה רוצה להצליח ללמוד ריאקט אתה רק עובר בין קורסים לחפש את הטוב ביותר, כל פעם מתחיל, מרגיש שזה לא מספיק טוב ואז מחפש משהו יותר טוב. כל מעבר רק מגדיל את הלחץ כי הזמן קצר וצריך דחוף למצוא עבודה חדשה. החיפוש בא על חשבון העשייה ולא מאפשר להתקדם.
נשווה את זה למישהי שמחליטה ללמוד ריאקט דרך עשייה. היא תתחיל עם הקורס הראשון שהיא מוצאת או אפילו המדריך החינמי הראשון ביוטיוב, תבנה אפליקציה ראשונה ואז תמשיך לנסות לבנות עוד מערכות ומערכות מתוחכמות יותר. כן היא הולכת להיתקע, ולעשות טעויות ולבנות דברים לא נכון אבל היא ממשיכה לעשות את זה כל יום חצי שעה או שעה ביום. וזה הבסיס. לאט לאט אותה תלמידה תחפש ותמצא טיפים ברשת לעזור בדברים עליהם היא נתקעת וגם עוד רעיונות לתרגילים ופרויקטים שהיא יכולה לבנות. אחרי מספיק זמן היא תוכל גם לקחת קורס ויהיה לה הרבה יותר קל לבחור ולהבין.
העשייה היא שלכם. היא קורית בקצב שלכם, סביב הדברים שמעניינים אתכם ומהווה את הבסיס לתהליך הלימודי. כשאנחנו בונים את כל שאר התהליך סביבה אנחנו מבטיחים לעצמנו הנאה וגם מגדילים מאוד את סיכויי ההצלחה.
1 419
אבל ביקשתי לשים את זה באמצע
נתבונן על הגדרת ה CSS הבאה:
body, html {
height: 100vh;
padding: 0;
margin: 0;
}
main {
display: flex;
place-items: center;
background: #a0a0a0;
}
וה HTML שבתוך ה body כולל בסך הכל:
<main>hello world</main>
קצת מפתיע לגלות שהטקסט לא מופיע באמצע המסך, לא אופקית ולא אנכית. מה קורה כאן?
1. אלמנט ה body אכן מוגדר לתפוס את כל הגובה של המסך, אבל אין הגדרת גובה על ה main ולכן גובהו ייקבע לפי התוכן. מאחר והתוכן הוא שורה בודדת זה יהיה גובה האלמנט.
2. הפקודה place-items: center באמת קובעת ל main למקם את הילדים שלו באמצע שלו. אבל שימו לב שכל גובהו הוא שורה אחת ולכן השורה הבודדת תופסת את כל הגובה.
3. ומה לגבי הרוחב? אלמנט main תופס בברירת מחדל את כל רוחב העמוד, ולכן כשמנסים לשים אותו "באמצע" הוא עדיין תופס את כל רוחב העמוד.
איך בכל זאת נשים את הטקסט באמצע המסך, גם אופקית וגם אנכית? ברגע שמבינים את הבעיה התיקונים הם פשוטים:
1. נקבע את הגובה של main למספר קבוע או לגובה של המיכל שלו, במקום לגובה של התוכן שלו, על ידי הגדרת height: 100%.
2. אם אנחנו רוצים שהטקסט יהיה באמצע ה main נוכל להגדיר מאפיין text-align על ה main. הגדרת text align קובעת את יישור הטקסט בתוך בלוק בתצוגה.
3. אם אנחנו רוצים להגדיר מאפייני בלוק נוספים לטקסט נוכל להגדיר סביבו p ולקבוע את רוחב הפיסקה להיות בדיוק רוחב התוכן. כך אפשר יהיה להוסיף עוד פיסקאות ולקבל מספר אלמנטים באמצע.
סך הכל הדוגמה עם היישור בקודפן:
<iframe height="300" style="width: 100%;" scrolling="no" title="Untitled" src="https://codepen.io/ynonp/embed/WbeeQqm?default-tab=html%2Cresult" frameborder="no" loading="lazy" allowtransparency="true" allowfullscreen="true">
See the Pen <a href="https://codepen.io/ynonp/pen/WbeeQqm">
Untitled</a> by Ynon Perek (<a href="https://codepen.io/ynonp">@ynonp</a>)
on <a href="https://codepen.io">CodePen</a>.
</iframe>
ובקישור:
https://codepen.io/ynonp/pen/WbeeQqm1 419
מורים בעידן ה Chat GPT
מבחינת תפקיד המורה Chat GPT רק חיזק תהליכים שכבר היו לפניו: כל הידע ממילא קיים ברשת, לכל שאלה יש כבר תשובות מצוינות והסברים מדויקים, ועכשיו אפשר אפילו לשאול כשלא מבינים ולקבל הסבר מותאם אישית.
אז מה נשאר למורים לעשות? הנה שלוש משימות חשובות:
1. לקבוע את התפריט - מסתבר שאין דרך אחת ללמד React (או כל נושא אחר). מורים שונים יוכלו לבנות תוכניות לימודים שונות, כלומר יבחרו את הנושאים וסדר הנושאים שעלינו ללמוד. כן יש הכל באינטרנט, אבל הלימוד הוא הרבה יותר יעיל כשמישהו עוזר לי לבחור מה הדבר הבא שצריך לדעת.
2. לזהות טעויות - אני יודע לשאול את Chat GPT מה אפשר לשפר בקוד שכתבתי, ולפעמים יהיו לו עצות טובות אבל הוא רחוק שנות אור מלהבין את הצרכים האמיתיים שלי ולזהות פערי הבנה. אני צריך מורה שיעזור לי לשים לב כשאני עושה דברים לא בסדר, מתוך הבנה של עולם התוכן שלי והאתגרים הספציפיים בתוכו.
3. לתת מילה טובה - אחרי ההתלהבות הראשונית לימוד דורש התמדה לאורך זמן. הרבה יותר קל להתמיד כשאנחנו לא לבד, כשיש מישהו שמחכה לראות כמה התקדמתי השבוע וישמח איתי כשדברים מצליחים (או יעודד אותי כשאני רוצה לוותר).
ככל שנקדים להבין שאין לנו בעלות על הידע נוכל להתפנות לאתגרים האמיתיים של מורים במאה ה-21. כי אם יש משהו שלמדנו מהשנים האחרונות הוא שיותר ידע זה לא מספיק בשביל לעשות אנשים חכמים יותר.
1 419
מורים בעידן ה Chat GPT
מבחינת תפקיד המורה Chat GPT רק חיזק תהליכים שכבר היו לפניו: כל הידע ממילא קיים ברשת, לכל שאלה יש כבר תשובות מצוינות והסברים מדויקים, ועכשיו אפשר אפילו לשאול כשלא מבינים ולקבל הסבר מותאם אישית.
אז מה נשאר למורים לעשות? הנה שלוש משימות חשובות:
1. לקבוע את התפריט - מסתבר שאין דרך אחת ללמד React (או כל נושא אחר). מורים שונים יוכלו לבנות תוכניות לימודים שונות, כלומר יבחרו את הנושאים וסדר הנושאים שעלינו ללמוד. כן יש הכל באינטרנט, אבל הלימוד הוא הרבה יותר יעיל כשמישהו עוזר לי לבחור מה הדבר הבא שצריך לדעת.
2. לזהות טעויות - אני יודע לשאול את Chat GPT מה אפשר לשפר בקוד שכתבתי, ולפעמים יהיו לו עצות טובות אבל הוא רחוק שנות אור מלהבין את הצרכים האמיתיים שלי ולזהות פערי הבנה. אני צריך מורה שיעזור לי לשים לב כשאני עושה דברים לא בסדר, מתוך הבנה של עולם התוכן שלי והאתגרים הספציפיים בתוכו.
3. לתת מילה טובה - אחרי ההתלהבות הראשונית לימוד דורש התמדה לאורך זמן. הרבה יותר קל להתמיד כשאנחנו לא לבד, כשיש מישהו שמחכה לראות כמה התקדמתי השבוע וישמח איתי כשדברים מצליחים (או יעודד אותי כשאני רוצה לוותר).
ככל שנקדים להבין שאין לנו בעלות על הידע נוכל להתפנות לאתגרים האמיתיים של מורים במאה ה-21. כי אם יש משהו שלמדנו מהשנים האחרונות הוא שיותר ידע זה לא מספיק בשביל לעשות אנשים חכמים יותר.
1 419
1. יצירת קבצי לוגיקה לגישה לבסיס הנתונים, בדרך כלל ליד הגדרת הסכימה. אני אוהב את דריזל ואת kysely, עבדתי עם Sequelize ב JavaScript (לפני טייפסקריפט) וגם הייתי מרוצה. את TypeORM לא אהבתי. אבל בסוף זה רק עניין של טעם כולם טובים.
2. יצירת קומפוננטות צד שרת שמושכות את המידע והופכות אותו ל JSX.
3. יצירת קומפוננטות צד לקוח שיפעילו עם Server Actions את קבצי הלוגיקה.
האתגר המרכזי שעדיין נשאר באינטגרציה הוא לקחת את התוצאה של ה Server Action ולעדכן את המידע שעל המסך. בדוגמה שלי הייתי צריך להגדיר את likesCount בתור סטייט בשביל שאוכל לעדכן אותו. גישה אחרת שראיתי זה להפעיל refresh ולתת לשרת לרנדר מחדש את ה Server Components עם המידע המעודכן.
1 419
2. בעת לחיצה על הכפתור הפונקציה פונה לשרת ומפעילה את הפונקציה
likeLink שמוגדרת שם. זה נראה כמו הפעלה רגילה של פונקציה אבל בעצם מדובר על Server Action - כלומר קריאת Ajax שגורמת להפעלת פונקציה בצד שרת והחזרת התוצאה. הפונקציה ממשיכה לבדיקת התוצאה ולפי זה מחליטה אם העדכון הצליח ואם כן היא מעלה את מספר הלייקים של הלינק. באפליקציה גדולה יותר היינו יכולים לשלב רידאקס ואז היינו יכולים בצד שרת ליצור את אוביקט ה Action המתאים והפונקציה בצד הדפדפן היתה עושה לו dispatch.
3. אתם שמים לב שהעברתי את ה user בתור prop. לצערי אין ל RSC תמיכה ב Context. יש כמה טריקים שאפשר לעשות כדי לעקוף את זה ולהגדיר Context בצד לקוח אבל ארחיב על זה בפוסט אחר. זה לא היה מספיק חשוב לכאן. ממילא במערכת אמיתית תשתמשו במנגנון ניהול משתמשים חכם יותר.
סקריפטים לניהול
בעבודה עם דריזל קיט הפקודה:
$ npx drizzle-kit generate
מסתכלת על הסכימה ועל בסיס הנתונים ומייצרת קובץ SQL שמעדכן את בסיס הנתונים כדי להתאים לסכימה. הקובץ נשמר בתיקיית drizzle ואפשר לראות שבפרויקט הדוגמה יש לי קובץ אחד כזה עם התוכן הבא:
CREATE TABLE \likes\ (
\id\ integer PRIMARY KEY AUTOINCREMENT NOT NULL,
\user_id\ integer NOT NULL,
\link_id\ integer NOT NULL,
FOREIGN KEY (\user_id\) REFERENCES \users\(\id\) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (\link_id\) REFERENCES \links\(\id\) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE \links\ (
\id\ integer PRIMARY KEY AUTOINCREMENT NOT NULL,
\user_id\ integer NOT NULL,
\href\ text NOT NULL,
FOREIGN KEY (\user_id\) REFERENCES \users\(\id\) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE \users\ (
\id\ integer PRIMARY KEY AUTOINCREMENT NOT NULL,
\name\ text NOT NULL
);
שימו לב להערה --> statement-breakpoint - היא חייבת להופיע בין פקודות שונות בקובץ כדי שירוצו כל הפקודות. היא נוצרת אוטומטית מפקודת generate אבל אם אתם עושים עדכון ידני אל תשכחו לכתוב אותה.
בנוסף ליצירת הסכימה אני אוהב להחזיק סקריפטים להכנסת מידע לבסיס הנתונים או מחיקתו. בפרויקט הדוגמה יצרתי סקריפט אחד ששומר מידע לפיתוח:
import { db } from './db';
import { usersTable, linksTable, likesTable } from './db/schema';
async function seedDatabase() {
// Insert users
const userIds = await db.insert(usersTable).values([
{ name: 'Alice' },
{ name: 'Bob' },
{ name: 'Charlie' },
{ name: 'Dave'},
]).returning({ id: usersTable.id });
// Insert links
const linkIds = await db.insert(linksTable).values([
{ user_id: userIds[0].id, href: 'https://www.duckduckgo.com' },
{ user_id: userIds[1].id, href: 'https://www.tocode.co.il' },
{ user_id: userIds[2].id, href: 'https://nextjs.org/blog/next-15' },
]).returning({ id: linksTable.id });
// Insert likes
await db.insert(likesTable).values([
{ user_id: userIds[0].id, link_id: linkIds[1].id },
{ user_id: userIds[1].id, link_id: linkIds[2].id },
{ user_id: userIds[2].id, link_id: linkIds[1].id },
{ user_id: userIds[1].id, link_id: linkIds[0].id },
{ user_id: userIds[0].id, link_id: linkIds[1].id },
]);
console.log('Database seeded successfully!');
}
seedDatabase().catch(console.error);
וסקריפט אחר שמוחק את המידע:
import { db } from './db';
import { usersTable, linksTable, likesTable } from './db/schema';
async function truncateDatabase() {
await db.delete(usersTable)
await db.delete(linksTable);
await db.delete(likesTable);
}
truncateDatabase().catch(console.error);
לדריזל אין עדיין דרך מובנית להריץ סקריפטים כאלה אז הוספתי את הפקודות ל package.json:
"scripts": {
"dev": "next dev",
"seed-dev": "tsx src/seed-dev.ts",
"truncate-dev": "tsx src/truncate-dev-db.ts",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
סיכום
החלקים המרכזיים בפרויקט next עם בסיס נתונים הם בסך הכל:1 419
.leftJoin(users, eq(links.user_id, users.id))
.leftJoin(likes, eq(links.id, likes.link_id))
.groupBy(links.id, users.name);
}
export async function likeLink(user_id: number, link_id: number) {
console.log(\user id = ${user_id}, link_id = ${link_id}\);
// Check if the like already exists
const existingLike = await db
.select()
.from(likes)
.where(and(eq(likes.user_id, user_id), eq(likes.link_id, link_id)))
.limit(1);
if (existingLike.length > 0) {
console.log('User has already liked this link.');
return { success: false, message: 'You have already liked this link.' };
}
// Add the like
const newLike = await db
.insert(likes)
.values({ user_id, link_id })
.returning();
console.log('Like added successfully:', newLike);
return { success: true, message: 'Like added successfully!', like: newLike };
}
עכשיו דריזל יכול להיות מעייף ועמוס, אבל לקירבה ל SQL יש גם יתרונות. קודם כל ברור מאוד מה קורה בכל פונקציה, ומאוד קל ל AI לקבל סכימה ולכתוב בשבילכם שאילתות דריזל. בניגוד לכלי ORM, פה אם יש טעות בשאילתה או בעיית ביצועים רואים אותה מיד.
העברת המידע לדפדפן
החלק השני בתוכנית הוא העברת המידע לדפדפן, ופה next.js עושה את הקסם שלו. הקובץ page.tsx הוא Server Component ולכן הוא יכול להפעיל כל פונקציה בצד שרת, כולל פונקציות גישה לבסיס הנתונים. זה הקוד:
import { queryHomepageLinks } from "@/db/links";
import { login } from "@/db/users";
import Link from './client/Link';
export default async function Home() {
const homepageLinks = await queryHomepageLinks();
const currentUser = (await login('Dave')).user!;
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<ul className="space-y-4 text-left text-gray-500 dark:text-gray-400">
{homepageLinks.map(link => (
<Link user={currentUser} key={link.linkId} link={link} />
))}
</ul>
</main>
</div>
);
}
בשביל הפשטות כל מערכת ההתחברות פה מאוד מנוונת. כל מי שנכנס למערכת מזדהה בתור "Dave" ובלי סיסמה. יותר מעניין זה המערך homepageLinks, שמגיע מהפונקציה queryHomepageLinks שהוגדרה בקובץ הגישה לבסיס הנתונים. במערך הזה אנחנו כבר משתמשים בכתיבת הקומפוננטה.
עכשיו פה המקום להזכיר שעד לפני כמה שנים (ועדיין במערכות רבות שאני מכיר) שיטת העבודה עם מידע שמגיע מבסיס הנתונים היתה כתיבת שאילתה כפולה - קודם לוקחים את ה HTML, CSS, JS מהשרת, ואז יש קריאת Ajax שמושכת את המידע הראשוני (מערך הלינקים בדוגמה שלנו). העבודה עם RSC חוסכת את הכפילות ויותר מזה, מאחר שהקומפוננטה מתרנדרת רק בצד השרת והקוד שלה אפילו לא נשלח לדפדפן.
מה שכן נשלח לדפדפן זה הקוד של קומפוננטת Link שמוגדרת בקובץ client/link.tsx:
'use client'
import { likeLink } from "@/db/links";
import { useState } from "react";
export default function Link({link, user}: {
link: {
linkId: number,
href: string,
likesCount: number,
},
user: { id: number, name: string },
}) {
const [likesCount, setLikesCount] = useState(link.likesCount);
async function handleClick() {
const result = await likeLink(user.id, link.linkId);
if (result.success) {
setLikesCount(c => c + 1);
} else {
alert(result.message);
}
}
return <li className="flex items-center space-x-3 rtl:space-x-reverse">
<span className="mx-2 inline-block w-4">{likesCount}</span>
<button onClick={handleClick} className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-1 px-2 rounded">+1</button>
<span className="font-bold">{link.href}</span>
</li>
};
הקומפוננטה מקבלת את הלינק מקומפוננטת צד השרת ומציגה אותו, אבל יש לה עוד שני טריקים:
1. משתמש יכול ללחוץ על פלוס כדי לעשות "לייק" ללינק. בשביל זה הקומפוננטה מגדירה משתנה state שמתחיל עם הערך שהגיע מהשרת אבל יוכל להשתנות בעקבות לחיצה על כפתורים.1 419
פרויקט דוגמה: next עם בסיס נתונים drizzle
הרבה זמן אני רוצה להקליט דוגמת וידאו שמסבירה איך לבנות פרויקט ב next.js בשילוב עם בסיס נתונים. מאחר ואני לא בטוח מתי אגיע לזה חשבתי להתחיל עם מדריך טקסט שיראה את כל החלקים החשובים. יום אחד אולי גם יהיה וידאו ואז אוסיף לינק לפוסט הזה.
מבנה המערכת
האמת ש Server Actions ו React Server Components אחראיים לרוב מה שקורה בפרויקט הזה, ובסיס הנתונים הוא רק החלק הקטן.
את הקוד המלא אתם יכולים למצוא בקישור:
https://github.com/ynonp/demo-next-sql-drizzle
אלה החלקים המרכזיים בקוד:
1. ספריית src/db מחזיקה את כל הקוד שקשור לעבודה עם בסיס הנתונים.
2. בזכות השימוש ב Server Actions ו RSC אין צורך להגדיר API. הגישה לשרת היא בסך הכל הפעלת פונקציה. בפרויקט הדוגמה יש גישה אחת מתוך קוד צד שרת בקובץ
page.tsx וגישה אחת מתוך קוד צד לקוח בקובץ client/link.tsx.
3. בתיקייה הראשית של הפרויקט הגדרתי שני סקריפטים להכנסת מידע לדוגמה לבסיס הנתונים ומחיקתו. בנוסף בתיקיית הפרויקט הראשית הקובץ drizzle.config.ts אחראי על הגדרת החיבור לבסיס הנתונים.
עכשיו בואו נעבור על שלושת החלקים.
בסיס הנתונים
בסיס הנתונים נשמר בדוגמה בתור קובץ SQLite ואנחנו רואים את פרטי ההתחברות בקובץ הקונפיגורציה של דריזל:
import 'dotenv/config';
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
out: './drizzle',
schema: './src/db/schema.ts',
dialect: 'sqlite',
dbCredentials: {
url: process.env.DB_FILE_NAME!,
},
});
הפרויקט משתמש ב drizzle-kit כך שהגדרות בסיס הנתונים מגיעות מקובץ הסכימה. בפרויקט הדוגמה שכתבתי יש משתמשים, המשתמשים מפרסמים לינקים ויכולים לעשות "לייק" ללינקים שהם או אחרים פרסמו. זאת הסכימה:
import { int, sqliteTable, text } from "drizzle-orm/sqlite-core";
export const usersTable = sqliteTable("users", {
id: int().primaryKey({ autoIncrement: true }),
name: text().notNull(),
});
export const linksTable = sqliteTable("links", {
id: int().primaryKey({ autoIncrement: true }),
user_id: int().references(() => usersTable.id, {onDelete: 'cascade'}).notNull(),
href: text().notNull(),
});
export const likesTable = sqliteTable('likes', {
id: int().primaryKey({autoIncrement: true}),
user_id: int().references(() => usersTable.id, {onDelete: 'cascade'}).notNull(),
link_id: int().references(() => linksTable.id, {onDelete: 'cascade'}).notNull(),
});
בנוסף אני מאוד אוהב להגדיר פונקציות לעבודה עם בסיס הנתונים בקבצים קטנים בתיקיית db. אני מסדר את הפונקציות לפי נושאים ולכל נושא יש קובץ, לדוגמה הקובץ users.ts אחראי על פונקציות שקשורות למשתמשים. זה תוכנו:
import {linksTable, usersTable, likesTable as likes} from './schema';
import {db} from './index';
import { eq } from 'drizzle-orm';
export async function login(name: string) {
// Check if the user already exists
const existingUser = await db
.select()
.from(usersTable)
.where(eq(usersTable.name, name))
.limit(1);
if (existingUser.length > 0) {
console.log('User logged in:', existingUser[0]);
return { success: true, user: existingUser[0] };
} else {
return { success: false, user: null };
}
}
בקבצים האלה תישמר כל הלוגיקה של המערכת. בגלל ש Drizzle לא קשיח כמו TypeORM או Sequelize, אפשר לארגן את הקבצים בכל צורה שמתאימה לכם. אני אוהב להתחיל פרויקט עם קובץ לוגיקה אחד ולאט לאט לפצל אותו לקבצים יותר קטנים כשהוא מתחיל להרגיש עמוס.
אגב קובץ הלוגיקה השני מהתיקייה נקרא links ושם כתבתי את השאילתות למשיכת פרטי הלינקים:
'use server';
import {
linksTable as links,
usersTable as users,
likesTable as likes} from './schema';
import {db} from './index';
import { eq, sql, and } from 'drizzle-orm';
/**
* Returns a list of all links joined with their authors
* and number of likes
*/
export async function queryHomepageLinks() {
return db.select({
linkId: links.id,
href: links.href,
authorName: users.name,
likesCount: sql<number>\COUNT(${likes.id})\, // Aggregates likes
})
.from(links)