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

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

היום למדתי: למה לשים מרכאות מסביב לפורטים ב docker-compose

07/05/2024

בואו נשחק עם רובי, כי היא קלה ויש תמיכה מובנית ב YAML. זאת התוכנית:

require 'yaml'
require 'pp'

data = YAML.safe_load(<<-END
  apache:
  image: httpd:latest
  container_name: my-apache-app
  ports:
    - 80:80
    - 24:42
  volumes:
    - ./website:/usr/local/apache2/htdocs
END
)

pp data

אפשר לראות אותו לייב ב URL הזה: https://tinyurl.com/mwk97ybd

עכשיו התוכנית בסך הכל לוקחת YAML, קוראת אותו ומדפיסה אותו כמילון. יכולים לנחש מה יהיה במפתח ports של המילון? רמז: לא מה שאתם חושבים. זה נראה ככה:

"ports"=>["80:80", 88920],

אני מבין מאיפה הגיע ה 80:80, אבל מה הסיפור עם ה 88920? מה קרה ל 24:42? נו התשובה בכותרת הפוסט. כשקוראים YAML שיש בו שני מספרים נקודותיים ואז עוד שני מספרים המפענח חושב שזה תיאור זמן (שעות ודקות). אז הוא מכפיל את 24 ב 60 ומוסיף 42 וכך מקבל 1482, ואז הופך את זה לשניות ומקבל 88920 שזה המספר שאנחנו רואים כאן.

מה עושים? נזכרים ש YAML הוא שדה מוקשים אחד גדול ותמיד כותבים מחרוזות בתוך מרכאות.

הערך הייחודי

06/05/2024

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

מה הערך שאת מביאה לפרויקט?

מה הופך אותך לבן אדם שהכי מתאים להביא את הערך הזה?

מה האג'נדה שלך?

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

מה צריך לעשות בשביל להוסיף קומפוננטה לעמוד?

05/05/2024

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

ועכשיו נשאל - מה צריך לעשות בשביל להוסיף קומפוננטת קרוסלה כזאת לעמוד?

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

export function TopProducts() {
    const {data} = useSWR('/top-products');
    // render the list
    return <>...</>
}

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

export function TopProducts() {
    const topProducts = useSelector(state => state.topProducts);
    // render the list
    return <>...</>;
}

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

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

וזה הטרייד אוף שבריאקט לא מצליחים לצאת ממנו, המעגל שכולם מנסים לרבע. אז לרמיקס יש מנגנון שאוטומטית אוסף את כל משיכות המידע (ה loaders) מכל הילדים כדי לנסות למקבל אותם, ו React Server Components ו next מנסים לבטל לגמרי את קריאות ה API בעלייה כי הכל יתרנדר על השרת, אבל זה בסך הכל להזיז את הבעיה מצד לצד. בסוף יש לנו בעיה מהותית במודל ו Trade Off אמיתי שצריך להחליט לגביו: חווית פיתוח או ביצועים.

ומה אם שתי קומפוננטות מקשיבות לאותו SSE ?

04/05/2024

כשהייתי צריך לכתוב קומפוננטת ריאקט שמקשיבה ל Server Sent Events ומעדכנת את התצוגה לפי האירוע שמגיע האינטואיציה הראשונה שלי היתה להוסיף אפקט:

useEffect(() => {
  const evtSource = new EventSource("/notifications");
  const handler = (e: MessageEvent) => {
    setPosts(p => [...p, JSON.parse(e.data)]);
  };
  evtSource.addEventListener('message', handler);

  return () => {
    evtSource.close();
  }
}, []);

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

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

דרך אחת לבנות State גלובאלי לאפליקציה היא לשלב פריימוורק כמו רידאקס (או Jotai או preact-signals או zustand או אפילו מובאקס). פריימוורק לניהול סטייט מכריח אותי לכתוב את הקוד שמתחבר ל SSE Endpoint ולהחליט איפה יישמר המידע ומתי מפסיקים להקשיב לאירועים.

דרך שנייה לבנות State גלובאלי היא לשים את המידע ב Context ולמשוך אותו דרך Hook. הרבה פעמים נראה את הפיתרון הזה ב Hooks מוכנים, למשל use-event-source. אני מדביק כאן את הקוד שלו כי זה מעניין:

import { createContext, useContext, useEffect, useState } from "react";

export interface EventSourceOptions {
    init?: EventSourceInit;
    event?: string;
}

export type EventSourceMap = Map<
    string,
    { count: number; source: EventSource }
>;

const context = createContext<EventSourceMap>(
    new Map<string, { count: number; source: EventSource }>(),
);

export const EventSourceProvider = context.Provider;

/**
 * Subscribe to an event source and return the latest event.
 * @param url The URL of the event source to connect to
 * @param options The options to pass to the EventSource constructor
 * @returns The last event received from the server
 */
export function useEventSource(
    url: string | URL,
    { event = "message", init }: EventSourceOptions = {},
) {
    let map = useContext(context);
    let [data, setData] = useState<string | null>(null);

    useEffect(() => {
        let key = [url.toString(), init?.withCredentials].join("::");

        let value = map.get(key) ?? {
            count: 0,
            source: new EventSource(url, init),
        };

        ++value.count;

        map.set(key, value);

        value.source.addEventListener(event, handler);

        // rest data if dependencies change
        setData(null);

        function handler(event: MessageEvent) {
            setData(event.data || "UNKNOWN_EVENT_DATA");
        }

        return () => {
            value.source.removeEventListener(event, handler);
            --value.count;
            if (value.count <= 0) {
                value.source.close();
                map.delete(key);
            }
        };
    }, [url, event, init, map]);

    return data;
}

ה Hook יוצר מפה גלובאלית של Event Listeners שמקשיבים לאירועים לפי ה URL. כל פעם שקומפוננטה צריכה להתחבר ל SSE Endpoint הנתיב מצטרף למפה ואם הוא כבר קיים שם אז ערך ה count עולה ב-1. כשאף קומפוננטה לא מקשיבה לאירועים ה SSE נסגר ונמחק מהמפה.

השימוש ב Hook נראה כך (מתוך הדוגמה שלהם ב Readme):

// app/components/counter.ts
import { useEventSource } from "remix-utils/sse/react";

function Counter() {
    // Here `/sse/time` is the resource route returning an eventStream response
    let time = useEventSource("/sse/time", { event: "time" });

    if (!time) return null;

    return (
        <time dateTime={time}>
            {new Date(time).toLocaleTimeString("en", {
                minute: "2-digit",
                second: "2-digit",
                hour: "2-digit",
            })}
        </time>
    );
}

עכשיו בחזרה להתלבטות שלנו - האם לנהל את המידע שנשמר בצד לקוח בצורה מובלעת בתוך ה Hooks או בפריימוורק לניהול סטייט צד לקוח מסודר? כמו תמיד בשאלות האלה אין תשובה חד משמעית. הנטייה שלי היא לבחור בפריימוורק צד לקוח כדי לקבל שליטה טובה יותר בתזמונים. בדוגמה של אירועי Server Sent Events קל לדמיין שאני ארצה להמשיך להקשיב לאירועים גם בלי קומפוננטה על המסך. למשל אם קומפוננטה מציגה רשימת עדכונים ומשתמש עובר למסך אחר, אז כשאני חוזר למסך רשימת העדכונים אני אראה שם את העדכונים שנשלחו גם בזמן שהייתי במסך האחר. יחד עם זאת אין ספק שבמקרים פשוטים ה Hook במיוחד כזה שפשוט מדביקים מהאינטרנט חוסך כתיבת קוד ויכול לקצר זמני פיתוח.

מה דעתכם? איזה גישה עבדה לכם טוב יותר?

איך בוחרים בין הגיוני למועיל?

02/05/2024

הטיפוס object ב TypeScript נכנס לשפה בגירסה 2.2 של טייפסקריפט כדי לפתור בעיה. באותו זמן דפדפנים כללו את הפונקציה Object.create שיוצרת אובייקט חדש שהפרוטוטייפ שלו הוא הפרמטר שקיבלה, אבל הפרמטר ש create יכלה לקבל היה חייב להיות משהו שאינו "פרימיטיב" כלומר משהו שאפשר להוסיף לו מפתחות, לדוגמה מערך, פונקציה או אוביקט.

אז עכשיו אנחנו יכולים להגדיר את הטיפוס:

create(o: object | null): any;

ורגע אם כבר הגדרנו את create אפשר להמשיך לרוץ עם אותו רעיון ולהגדיר גם את:

setPrototypeOf(o: any, proto: object | null): any;

ויש עוד כמה הגדרות שאפשר למצוא ב es5.d.ts שהמשותף לכולן הוא ה object | null. בעצם יש רק מקום אחד ש object מופיע בלי החיבור ל null וזה הטיפוס של הפונקציה Object.keys:

keys(o: object): string[];

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

בחזרה לאתגר שלנו - למה null הוא לא חלק מ object? למה להגדיר טיפוס חדש כשכל פעם שמשתמשים בו זה כחלק מהחיבור object | null?

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

typeof null === 'object'

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

היום למדתי: nth-child וקלאס ב CSS

01/05/2024

נניח שיצרתם טבלה עם CSS Grid ועכשיו אתם רוצים לצבוע רק שורה מסוימת - או יותר טוב, להדגיש את הגבול מסביב לשורה מסוימת. בעולם הישן של table היה מספיק למצוא את ה tr שמתאים לשורה ולהגדיר לו גבול, אבל בגריד הטבלה לא כוללת אלמנט tr. כל ה HTML שלה הוא בסך הכל:

<div class='container'>
  <div>0</div>
  <div>1</div>
  <div>2</div>
  <div>3</div>
  <div>4</div>
  <div>5</div>
  <div>6</div>
  <div>7</div>
  <div>8</div>
  <div>6</div>
  <div>7</div>
  <div>8</div>
</div>

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

<div class='container'>
  <div>0</div>
  <div>1</div>
  <div>2</div>
  <div class='selected-row'>3</div>
  <div class='selected-row'>4</div>
  <div class='selected-row'>5</div>
  <div>6</div>
  <div>7</div>
  <div>8</div>
  <div>6</div>
  <div>7</div>
  <div>8</div>
</div>

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

.container .selected-row{
  border-top-color: red;
  border-bottom-color: red;
}

ושני התאים בקצה? אפשר היה לדמיין להשתמש פה ב nth-child(1):

.container .selected-row:nth-child(1) {
  border-left-color: red;
}

אבל זה לא עובד. ה div הראשון עם הקלאס selected-row הוא לא הילד הראשון ולכן כלום לא נצבע. מה עושים? מסתבר שיש ל nth-child טריק כדי לתפוס את הילד ה n-י שמתאים לקלאס מסוים וזה נראה ככה:

.container :nth-child(1 of .selected-row) {
  border-left-color: red;
}

.container :nth-child(3 of .selected-row) {
  border-right-color: red;
}

הדוגמה כאן למי שרוצה לשחק עם זה: https://codepen.io/ynonp/pen/zYXQqmO

מה בעצם אנחנו פוחדים לאבד?

29/04/2024

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

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

דרך אחרת להסתכל על הבעיה היא לשנות את השאלה - במקום לשאול איך אני רוצה לשפר את המצב אני אשאל "מה בעצם אני פוחד לאבד פה?", מה ה Best Case שיקרה אם אשאר?

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

מטלות והזדמנויות

28/04/2024

הנה רשימת הדברים שאתה צריך לעשות בשביל להצליח.

מול-

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

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

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