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

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

קומפוננטות ווב Web Components עובדים ממש טוב

09/05/2024

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

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

בכל מקרה כתבתי דוגמה קטנה של מונה לחיצות ב Web Component בשביל לשחק עם ה API. ה HTML הוא:

<my-counter></my-counter>
<my-counter></my-counter>
<my-counter></my-counter>

<template id="counter">
  <p>I'm a counter. value = <span class="value">0</span>
    <button class="inc">+1</button>
    <button class="dec">-1</button>
  </p>
</template>

והוא כולל את הטמפלייט ויצירה של הקומפוננטה 3 פעמים, וה JavaScript שגורם לכל העסק לעבוד הוא:

customElements.define(
  "my-counter",
  class extends HTMLElement {
    connectedCallback() {
      let template = document.getElementById("counter");
      let templateContent = template.content;
      const shadowRoot = this.attachShadow({ mode: "open" });

      shadowRoot.appendChild(templateContent.cloneNode(true));

      let value = 0;
      const valueSpan = shadowRoot.querySelector('.value')
      shadowRoot.querySelector('.inc').addEventListener('click', () => {
        value += 1;
        valueSpan.textContent = value;
      })

      shadowRoot.querySelector('.dec').addEventListener('click', () => {
        value -= 1;
        valueSpan.textContent = value;
      })
    }
  }
);

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

גירסה 19 של ריאקט שתבוא עלינו לטובה באזור יוני או יולי כוללת לפי הסיפורים תמיכה מלאה ב Web Components כך שנוכל לשלב אותם ביישום ריאקט בלי בעיה. יהיה מעניין.

נ.ב. זה הלינק לקודפן עם הקומפוננטת ווב, תרגישו חופשי לשחק ולשבור: https://codepen.io/ynonp/pen/qBGWQEw

אני כזה מקסים פשוט תנו לי את העבודה

08/05/2024

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

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

דרן קופ כותב אחרי שנכשל בראיון:

I understand the problems associated with hiring the wrong people as well, which may well be the actual reason we are rightfully stuck with such fearful/timid hiring practices.

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

I’ve also spent hours fixing code that had issues directly stemming from not using the correct data structure/algorithm that I may not have had to if we had been better at screening the employees during the interview process.

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

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

  1. גיוס אנשים שפשוט מדביקים קוד מ Stack Overflow או מנועי AI בלי להבין מה הם כותבים וכתוצאה מכך מייצרים ערימות בוץ שאי אפשר לנקות.

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

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

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

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

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

היום למדתי: למה לשים מרכאות מסביב לפורטים ב 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