הבלוג של ינון פרק

טיפים קצרים וחדשות למתכנתים

למה בנית את זה ככה?

03/06/2024

נתונה קומפוננטת ריאקט:

function ItemData() {
    const { id } = useParams();
    const { data, error, isLoading } = useSWR('/api/user', fetcher);

    if (isLoading) return <p>Loading...</p>;
    if (error) return <p>Error</p>;

    return <ItemView item={data} />
}

הקומפוננטה לוקחת את הפרמטר id מהגדרות הנתיב, פונה לשרת כדי למשוך את המידע על הפריט ואז מציגה את המידע ב ItemView. התצוגה בקומפוננטה נפרדת כדי שאפשר יהיה לבדוק אותה בלי קשר למשיכת המידע וגם זה יותר נוח לתחזק ככה את הקוד, אבל למה לפנות לשרת רק אחרי שהקומפוננטה מתרנדרת כדי למשוך את המידע?

המשך קריאה

שלושה יתרונות של ניהול הרשאות מבוסס Policy

02/06/2024

ניהול הרשאות במערכת זה המנגנון שמאפשר לנו מרגע שמשתמש התחבר להבין מה מותר לאותו משתמש לעשות. באנגלית זה נקרא Authorization ובדרך כלל אנחנו מפרידים את מנגנון ניהול ההרשאות ממנגנון ההזדהות (Authentication). ההזדהות מאפשרת לי להגיד שמי שנכנס הרגע הוא משתמש X וניהול ההרשאות זה מה שמאפשר לי להגיד שלמשתמש X מותר להיכנס לדף מסוים.

במנגנון ניהול ההרשאות אנחנו מוצאים שתי גישות מרכזיות (עם ספריות קוד תואמות). הגישה הראשונה היא ניהול הרשאות מבוסס משתמש. פה יש לנו ב Rails את cancancan וב JavaScript/Typescript את casl. בגישה זאת אנחנו מתחילים את התוכנית בלקחת אוביקט משתמש ו"להדביק" לו הרשאות:

import { defineAbility } from '@casl/ability';

export default (user) => defineAbility((can) => {
  can('read', 'Article');

  if (user.isLoggedIn) {
    can('update', 'Article', { authorId: user.id });
    can('create', 'Comment');
    can('update', 'Comment', { authorId: user.id });
  }
});

הרבה פעמים אני מחלק את קטע הקוד הזה לפונקציות ותהיה לי פונקציה אחת שמטפלת במשתמש מסוג "גולש רגיל" פונקציה אחרת ל"גולש מחובר" פונקציה שלישית ל"מנוי" ורביעית ל"מנהל האתר". בכל פונקציה אני מדביק למשתמש את ההרשאות המתאימות.

פה באתר למשל אני משתמש בספריה כזאת בקוד ריילס ויש לי הרשאות כמו:

def guest_user(user)
  can :read, BlogPost do |post|
    post.published_at <= Time.now
  end

  can :read, Lesson, free: true
end

גישה כזאת עובדת מאוד טוב בתחילת הפרויקט או בפרויקטים קטנים, כל עוד כל ההרשאות נכנסות בקובץ אחד.

גישה שנייה לניהול הרשאות היא ניהול מבוסס מדיניות. בגישה זו אני מגדיר Policy לגישה ל"דבר" מסוים ובתוך המדיניות אני מגדיר מי רשאי לעשות מה עם אותו דבר. זאת הגישה שאנחנו פוגשים בספריות ריילס כמו pundit ו action_policy, פאנדיט גם זמינה ב JavaScript. הנה קטע מתוך התיעוד של action_policy כדי שנראה את ההבדל:

class PostPolicy < ApplicationPolicy
  # everyone can see any post
  def show?
    true
  end

  def update?
    # `user` is a performing subject,
    # `record` is a target object (post we want to update)
    user.admin? || (user.id == record.user_id)
  end
end

או ב JavaScript מתוך התיעוד של pundit:

import { Policy } from 'pundit'

export default class PostPolicy extends Policy {
  constructor(user, record) {
    super(user, record)
    this.setup.apply(this)
  }

  edit() {
    return this.user.id === this.record.userId
  }

  destroy() {
    return this.user.isAdmin
  }
}

שלושה יתרונות מהירים של מדיניות הרשאות מבוססת מודלים הם:

  1. כל ההרשאות של מודל מסוים מרוכזות במקום אחד.

  2. אפשר לשתף Policy בין כמה מודלים.

  3. ניהול שינויים - אם אני רוצה לקבוע ממחר שתוכן מסוג מסוים כבר לא חופשי בגישה הראשונה אני צריך לשנות בשני מקומות (גם בפונקציה שמגדירה הרשאות לאורחים וגם בפונקציה שמגדירה הרשאות למשתמשים רשומים). בגישה השנייה מספיק לעדכן את פונקציית הצפייה ב Policy שמתאים לדבר אותו אני מזיז.

כמעט פרידה מ nodemon

01/06/2024

נודמון היה חבר נאמן במשך הרבה שנים. מספיק קשה לזכור לשמור את הקובץ כל פעם אחרי שינויים, בכתיבת אפליקציית ווב צד שרת באקספרס בלעדיו היה צריך גם לסגור ולפתוח מחדש ידנית את השרת. נודמון נתן פיתרון כל כך טוב שכבר שכחנו שפיצ'ר בסיסי כמו לטעון מחדש את המערכת אחרי שמשנים קבצים זה משהו שצריך להיות מובנה בכלי העבודה.

והנה הגיע נוד 20 והפך את כל העסק להיסטוריה.

במקום להתקין ולהפעיל nodemon אנחנו יכולים כבר לכתוב:

node --watch ./bin/www

וכשיהיה שינוי אוטומטית נוד יטען מחדש את האפליקציה.

אז למה רק כמעט? לצערי מצב watch של node עדיין לא יודע לעבוד עם קבצי TypeScript ו ts-node לא מכיר עדיין את המתג הזה, לכן אם האפליקציה היא בטייפסקריפט אנחנו עדיין נצטרך את nodemon, שמקמפל אוטומטית את הטייפסקריפט עם כל שינוי.

חווית המתכנת

31/05/2024

כן יש מושג כזה ואפילו עם ראשי תיבות - DX, Developer Experience.

קו המחשבה כאן הוא שאם אנחנו כותבים כלים שלמתכנתים ולמתכנתות יהיה קל להשתמש בהם אז התוכנות שאותם אנשים יכתבו יהיו טובות יותר, מהירות יותר ובטוחות יותר. מצד שני כשאנשים צריכים לעבוד קשה כדי ללמוד איך להשתמש "נכון" בכלי שלך הם יעשו יותר טעויות והתוצאה (האפליקציה שהם כותבים) תהיה פחות טובה.

דוגמה טובה ל DX היא כשאנחנו בונים API. אם קל להשתמש ב API הזה וקל למשוך ממנו רק את המידע שהמתכנתים צריכים אז האפליקציות שייכתבו עם אותו API יצאו טובות יותר. אם ה API מכריח את המשתמשים למשוך הרבה יותר מידע ממה שהם צריכים אז למפתחים שישתמשו באותו API לא תהיה ברירה והם יאלצו לכתוב מערכות פחות טובות.

או אם אני כותב פריימוורק עם תיעוד טוב אז מתכנתים ישתמשו בפריימוורק כמו שתכננתי והמערכת שהם כותבים תהיה מהירה יותר. אם אני לא כותב תיעוד הם ישתמשו בה בצורה שלא חשבתי עליה והתוצאה תהיה באגים מוזרים ומערכת איטית ולא מאובטחת.

הבעיה ב DX מתחילה כש DX מתחיל לבוא על חשבון UX, וכשאנחנו מבלבלים בין כלים לכתיבת מערכות טובות ל"כלים שכיף לנו להשתמש בהם". פריזמה מפורסמת בתור ה ORM עם ה DX הטוב ביותר בעולם, אבל בלי אפשרות לעשות JOIN בין טבלאות כל יישום שישתמש בפריזמה יצטרך לעשות הרבה יותר עבודה (נכון אוטומטית בלי לערב את המתכנתים, אבל עדיין) ולכן יישומים רבים יהיו איטיים בהרבה בהשוואה לכאלה שייכתבו עם דריזל.

עוד דוגמה מפורסמת היתה פונגאפ (ואחריה קורדובה) שנתנה למפתחים אפשרות לכתוב אפליקציות מובייל בכלים של פיתוח ווב שהם כבר מכירים, אבל ייצרה אפליקציות שעבדו לאט והיו מוגבלות מבחינת היכולות והאינטגרציה עם הטלפון.

כשעובדים בצוות האתגר הוא כפול: אותם כלים שמספקים DX מהאגדות הרבה פעמים עברו מספיק ליטוש כדי שלא נראה את הבעיות באפליקציות POC קטנות. לפעמים יש תחושה שאותם כלים נכתבו ספציפית עבור אותן אפליקציות קטנות. הרבה פעמים החברים בצוות לא להוטים לוותר על חווית המפתח המושקעת מתוך הרגשה שבעיות הביצועים הם דאגה של העתיד או שאולי בכלל לא יגיעו אלינו.

אין לי פיתרון קל למצבים האלה. ככלל ככל שאנחנו מבינים יותר טוב את הכלים ואת המגבלות המובנות שלהם קל לנו יותר לבחור את אותם כלים שמשלבים חווית מפתחים טובה עם מוצר טוב ללקוח.

פיתרון Advent Of Code יום 18 בסקאלה

30/05/2024

מזמן לא כתבנו Advent Of Code וזו הזדמנות מצוינת להיזכר ולהתקדם. יש לנו 25 חידות סך הכל אז אנחנו ממש מתקרבים לסיום. בואו נראה את יום 18, את הניסיון הראשון הארוך והלא מוצלח שלי ואז את הפיתרון האמיתי.

המשך קריאה

הבלבול בין "האם" ל"מתי"

29/05/2024

שלוש שאלות לפני שמתקדמים - האם צריך? האם צריך עכשיו? מתי כדאי?

האם צריך לכתוב בדיקות? וודאי האם צריך עכשיו לכתוב בדיקות? לא בטוח מתי הזמן הכי טוב לכתוב בדיקות?

האם צריך תואר? וודאי האם צריך תואר עכשיו? לא חושב מתי הזמן הכי טוב ללמוד תואר באוניברסיטה?

האם צריך לשדרג את הפוסטגרס? ברור האם צריך עכשיו לשדרג את הפוסטגרס? לא חייב מתי הזמן הכי טוב לשדרג תלויות בפרויקט?

שימו לב איך השאלה היחידה שפותחת מקום לדיון היא "מתי", ולכן זאת השאלה היחידה מבין השלוש ששווה לדבר עליה. כשיש לנו את ה"מתי" במקום אפשר להתקדם בביטחה בלי לדאוג מתי ה"האם" יהפוך ל"האם עכשיו".

המחיר של האבסטרקציה הלא נכונה

28/05/2024

כשבוחרים אבסטרקציה לא נכונה יש רק שתי אפשרויות:

  1. אפשר לעבוד מסביב לבעיה ולבנות המון מעקפים וטיפול במקרי הקצה הלא הגיוניים.

  2. אפשר לקחת צעד אחורה ולשנות אבסטרקציה.

השיטה השנייה רק מרגישה יותר יקרה.

הקסם של Suspense בריאקט

27/05/2024

הרבה זמן היה קשה לראות את זה, אבל עכשיו עם Server Components הקומפוננטה Suspense מקבלת כח חדש והופכת להכרחית בהרבה אפליקציות next.js. הסיפור הולך כך:

  1. יש לנו עמוד עם חלקים שונים שזמן הטעינה שלהם שונה - לדוגמה אזור תוכן מרכזי שמגיע מהר מהזיכרון או קבצים, כתבות ארוכות יותר שמגיעות מקריאה לבסיס נתונים ואזור צד שמגיע מקריאה ל API צד שלישי (שרת לשרת).

  2. אנחנו רוצים לשלוח ללקוח את העמוד כמה שיותר מהר, וכשיגיעו הנתונים מבסיס הנתונים או מה API "להשלים" אותם בעמוד.

בעבר הדרך היחידה לעשות את זה היתה מתוך קוד צד לקוח - אנחנו מקבלים את העמוד ואז JavaScript בצד לקוח פונה לשרת ומתחיל שתי בקשות רשת חדשות עבור המידע מבסיס הנתונים והמידע מה API צד-שלישי. כל בקשה תחזור בזמן שלה והתוכן יוצג כשהתוצאות יגיעו. החיסרון במנגנון הזה הוא שאנחנו צריכים לחכות שהדפדפן יטען את אפליקציית הריאקט, ישלח הודעות חדשות לשרת ורק אז השרת פונה למשוך את המידע הנוסף שצריך. בנוסף (וזו סתם תכונה מעצבנת של ריאקט), אנחנו צריכים להתמודד בצד לקוח עם מצב של קומפוננטה שנמצאת במצב טעינה ולהציג תוכן חלופי בקוד הקומפוננטה.

הפונקציה use שתיכנס בריאקט 19 תאפשר לשפר קצת את המצב מבחינת אפליקציית צד הלקוח בכך שנוכל לעצור את הרינדור באמצע ולחכות ל Promise (קצת כמו await באמצע קוד קומפוננטה), ואז לא נצטרך לטפל במצב שהקומפוננטה "טוענת". מנגנון Server Components וה Component Streaming מאפשר ללכת עוד צעד קדימה ולטפל בכל הסיפור הזה בצד השרת.

שימו לב לדוגמה הבאה:

import { Suspense } from 'react'; 

/* We've moved data fetching into the changlog compoment */
async function ChangelogWithDataFetching() {
  const changelogData = await getChangelogData()
  return <Changelog data={changelogData} />
}

/*
  ...and wrapped that component in Suspense.
  The user can see the page immediately, while the changelog component loads
*/
export default function Page() {
  return (
    <Layout>
      <Sidebar>
        {/* other sidebar stuff */}
        <Suspense fallback={<ChangelogPlaceholder />}>
          <ChangelogWithDataFetching />
        </Suspense>
      </Sidebar>
      {/* other page stuff */}
    </Layout>
  )
}

קוד הגיע מכאן.

יש להם את הקומפוננטה ChangelogWithDataFetching שבסך הכל מוסיפה קריאה לפונקציה אסינכרונית כדי למשוך מידע ואז מרנדרת את הקומפוננטה Changelog. המקום היחיד שיצטרך לדאוג לאיך הקומפוננטה נראית במצב טעינה זה הקומפוננטה ChangelogPlaceholder שמופיעה ב Suspense. הצעד הזה קדימה - גם בקומפוננטות צד שרת אבל גם תכף בקומפוננטות צד לקוח עם use - נותן ל Suspense כח על חדש ואני חושב שיגרום לנו להשתמש הרבה יותר בקומפוננטה זו.

תרגיל בריפקטורינג טייפסקריפט

26/05/2024

בואו נתחיל עם ממשק טייפסקריפט הבא של פריט ופונקציה לעדכון הפריט:

interface Item {
    id: number;
    text: string;
    likes: number;
    price: number;
}

function updateItem(item: Pick<Item, 'id'> & Partial<Item>) {
    return fetch(`/api/update/${item.id}`, {
        method: 'POST',
        headers: { contentType: 'application/json' },
        body: JSON.stringify(item)
    });
}

הפונקציה updateItem מקבלת אחד או יותר מהשדות של Item אבל חייבת לקבל את השדה id ושולחת את המידע לשרת לצורך עדכון.

המשך קריאה

ריאקט, אפקט וספריות משיכת מידע

25/05/2024

בחור פירסם ברדיט (אני חושב שזה בחור כי הכינוי שלו מסתיים ב boyy) שהוא ניסה להתקבל לעבודה, הגיש מבחן בית בריאקט והפיתרון שלו לא היה מספיק טוב ולכן לא התקבל. מה שמיוחד בפוסט הזה זה שהחברה העבירה לו פידבק על הדברים בגללם הוא נכשל ואחד הסעיפים בפידבק היה:

Data is requested twice in development mode, this is due to using a useEffects in Strict Mode for querying - We’d recommend using a library to help with fetching and checking out this: https://react.dev/learn/you-might-not-need-an-effect

המאמר בקישור ארוך ומדבר על המון מקרים בהם אנשים משתמשים באפקטים בצורה לא נכונה. החלק שמדבר על אפקטים ומשיכת מידע משרת מתחיל בכותרת Fetching data. אבל בחלק זה הם דווקא מסבירים שמשיכת מידע משרת היא אכן אפקט ויש להשתמש ב useEffect בשבילה. ככה גם אני מלמד בקורסים פה וככה נכון לחשוב על הסיפור. הבעיה כאן היא לא שימוש במנגנון לא נכון (אפקט), אלא שימוש לא נכון במנגנון. האפקט לא צריך למשוך תמיד את המידע אלא לסנכרן בין המידע שעל השרת למידע ששמור בצד לקוח באיזשהו סטייט גלובאלי של האפליקציה.

השימוש בספרייה לשליפת מידע כמו react-query או swr או RTK Query לא מחליף את האפקט. לספריה אין מנגנון קסם לשליפת מידע מלבד useEffect, אלא שהאפקט שממומש בקוד הספריה עושה יותר עבודה מהמימוש הנאיבי שאותו משתמש הגיש.

(והסקרנים שרוצים לגלות מה בדיוק ספריית fetch עושה מוזמנים להציץ במימוש של swr. שימו לב שם ש useIsomorphicLayoutEffect קורא ל useEffect או ל useLayoutEffect).

נ.ב. בעיה שניה שהיתה לי עם הפידבק הזה היא הפוקוס על הבעיה במצב פיתוח. נכון מימוש נאיבי של אפקט גורם למשיכת מידע מהשרת פעמיים במצב פיתוח, אבל זה לא חשוב בשום צורה וזאת לא הבעיה עם מימוש זה. רוב הזמן האפליקציה ממילא לא תרוץ במצב פיתוח. הבעיה במימוש היא שסטייט של קומפוננטה הוא לא המקום הנכון לשמור Cache של מידע שמגיע מהשרת כי הרבה פעמים אנחנו צריכים את אותו מידע במופעים שונים של אותה קומפוננטה או בקומפוננטות שונות על העמוד.