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

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

ניהול פוקוס ב Shadow DOM

13/06/2024

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

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

אפשר למצוא את הקוד והתוצאה בסטאקבליץ כאן: https://stackblitz.com/edit/vitejs-vite-r29prn?file=src%2Fmy-element.ts

וזה הקוד בהדבקה:

import { LitElement, css, html, unsafeCSS } from 'lit'
import { customElement, property } from 'lit/decorators.js'
import litLogo from './assets/lit.svg'
import viteLogo from '/vite.svg'
import _ from 'lodash';
import style from "./index.css?inline";

@customElement('my-cell')
export class MyCell extends LitElement {
  static styles = unsafeCSS(style);
  static shadowRootOptions = {...LitElement.shadowRootOptions, delegatesFocus: true};


  handleKeypress(ev: KeyboardEvent) {
    if (ev.key === "Enter") {
      if (this.shadowRoot?.activeElement === this.shadowRoot?.querySelector('td')) {
        // td is in focus
        this.dispatchEvent(new Event('focusinside', {composed: true, bubbles: true}))
      } else {
        this.shadowRoot?.querySelector('td').focus();
      };
    } else if (ev.key === "ArrowRight") {
      this.dispatchEvent(new Event('focusright', {composed: true, bubbles: true}))
    } else if (ev.key === "ArrowLeft") {
      this.dispatchEvent(new Event('focusleft', {composed: true, bubbles: true}))
    } else if (ev.key === "ArrowUp") {
      this.dispatchEvent(new Event('focusup', {composed: true, bubbles: true}))
    } else if (ev.key === "ArrowDown") {
      this.dispatchEvent(new Event('focusdown', {composed: true, bubbles: true}))
    }
  }

  takeFocus(ev: KeyboardEvent) {
    if (ev.key === "Enter") {
      this.shadowRoot?.querySelector('td')?.focus();
    }
  }

  render() {
    return html`<td
      tabindex="1"
      @keyup=${this.handleKeypress}
      class="border-dashed border border-1 border-gray-500 focus:border-solid focus:border-green-200 focus:border-2"
    >
      <slot></slot>
    </td>
    `
  }
}

@customElement('my-table')
export class MyTable extends LitElement {
  static styles = unsafeCSS(style);

  @property({ type: Number })
  rows = 4

  @property({ type: Number })
  columns = 2

  focusUp(e: any) {
    const me = e.target;
    const tr = me.parentNode;
    const index = Array.prototype.indexOf.call(tr.children, me);
    tr.previousElementSibling.children[index].focus();
  }

  focusDown(e: any) {
    const me = e.target;
    const tr = me.parentNode;
    const index = Array.prototype.indexOf.call(tr.children, me);
    tr.nextElementSibling.children[index].focus();
  }

  render() {
    return html`
      <table class="bg-orange border-collapse"
        @focusinside=${(e: any) => e.target.firstElementChild.focus()}
        @focusright=${(e: any) => e.target.nextElementSibling.focus()}
        @focusleft=${(e: any) => e.target.previousElementSibling.focus()}
        @focusup=${this.focusUp}
        @focusdown=${this.focusDown}
      >
        <tbody>
          ${_.range(this.rows).map((i) => (
            html`
            <tr>
              ${_.range(this.columns).map((j) => (
              html`<my-cell>
                <input
                type="text"
                value="${`${i}, ${j}`}"
                class="outline-none bg-transparent border-none border-2"
              /></my-cell>`
              ))}
            </tr>
          `
          ))}
        </tbody>
      </table>
    `
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'my-table': MyTable
    'my-cell': MyCell
  }
}

נעבור בצורה זריזה על הרעיונות החשובים שלמדתי מכתיבת הדוגמה:

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

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

  3. אבל כשאני רוצה להעביר את הפוקוס מה input ל td שמכיל אותו - פה צריך כבר לקרוא ל focus מקומפוננטת ה"תא בטבלה" כי היא זאת שיצרה את ה td. בקיצור כל קומפוננטה ב Shadow DOM אחראית על האלמנטים שהיא יוצרת.

עוד שתי נקודות שלמדתי מהדוגמה הזאת על ליט:

  1. הגדרת קובץ CSS לכל העמוד לא משפיעה על הקומפוננטות של lit. כל קומפוננטה חייבת להגדיר את קובץ ה CSS בעצמה. מאוד מעייף כשרוצים להגדיר עיצוב גלובאלי באפליקציית lit.

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

שלוש דוגמאות lit כדי לראות איך זה עובד

12/06/2024

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

המשך קריאה

הפונקציות toReversed ו toSorted ב JavaScript

11/06/2024

אני תמיד אוהב כששפת תכנות לוקחת עוד צעד בכיוון הפונקציונאלי, וזה יהיה הסיפור היום עם שתי פונקציות חדשות ב JavaScript - הפונקציות toReversed ו toSorted, הגירסאות הפונקציונאליות של reverse ו sort. בעוד שהפונקציות המקוריות שינו את מבנה הנתונים, הגירסאות החדשות שלהן מחזירות לנו עותק של מבנה הנתונים ממוין או הפוך.

הפונקציה reverse של JavaScript מופעלת על מערך והופכת את האיברים בו:

> const x = [1, 2, 3, 4];
undefined
> x.reverse()
[ 4, 3, 2, 1 ]

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

> const x = [1, 2, 3, 4]
undefined
> x.toReversed()
[ 4, 3, 2, 1 ]
> x
[ 1, 2, 3, 4 ]

נשים לב שכשהמערך מקונן האלמנטים הפנימיים לא מועתקים, לכן:

> const x = [{a: 10}, {a: 10}, {a: 20}, {a: 30}]
undefined
> x.toReversed()
[ { a: 30 }, { a: 20 }, { a: 10 }, { a: 10 } ]
> x.toReversed()[0].a = 40
40
> x
[ { a: 10 }, { a: 10 }, { a: 20 }, { a: 40 } ]

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

> const values = [1, 10, 21, 2];
undefined

> values.toSorted()
[ 1, 10, 2, 21 ]

> values.toSorted((a, b) => a - b)
[ 1, 2, 10, 21 ]

אפשר לחשוב על שתיהן בתור גירסה מהירה וברורה יותר של array.slice().reverse() ו array.slice().sort:

> x.slice().reverse()
[ 9, 1, 2, 10 ]
> x
[ 10, 2, 1, 9 ]

> x.slice().sort()
[ 1, 10, 2, 9 ]
> x
[ 10, 2, 1, 9 ]

חמישה טיפים ל Pair Programmning יעיל יותר

10/06/2024

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

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

  2. בחרו טכנולוגיות אותן לפחות אחד הצדדים מבין או מבינה היטב. המטרה של Pair Programming היא הדיון, אז אנחנו רוצים לצמצם זמני Debug וזמני חיפוש באינטרנט.

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

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

  5. גיט סטאש הוא חבר - חלק מהמשחק של לעבוד ביחד זה להיחשף לרעיונות חדשים שלא הייתם מנסים אם הייתם לבד. חלק מהרעיונות האלה טובים, חלק גרועים וחלק לא מספיק מלוטשים. אני משתדל לקבור מהר עם git restore את הרעיונות הגרועים, אבל את הלא מלוטשים להשאיר בצד עם git stash. יום אחד אולי יהיה זמן לחקור אותם שוב. או שלא. וזה בסדר.

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

עדכון אימג'ים אוטומטי עם Watchtower

09/06/2024

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

ואם אנחנו כבר מריצים מערכת עם Docker Compose, מדי פעם אנחנו צריכים גם להעלות גירסה חדשה של אחד האימג'ים. בדיוק בשביל זה נוצר Watchtower.

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

איך זה עובד? יחסית פשוט. Watchtower הוא סרביס שאנחנו מוסיפים ל docker-compose.yml, אפשר בנוסף לסרביס שלנו או בקומפוז אחר לגמרי:

version: "3"
services:
  cavo:
    image: ynonp/myapp:latest
    ports:
      - "443:3443"
      - "80:3080"
  watchtower:
    image: containrrr/watchtower
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    command: --interval 30

כן צריך לשים לב ל-3 נקודות שאפשר ליפול בהן:

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

  2. האינטרוול קובע כל כמה זמן צריך לבדוק אם קיימת גירסה חדשה של האימג'. ברירת המחדל היא פעם ביום אז בשביל מנגנון של דיפלוימנט ב CI/CD כדאי להוריד את זה. המספר 30 בדוגמה מציין 30 שניות.

  3. אם לא כתבתם אחרת ה watchtower יעדכן את כל האימג'ים בכל הקונטיינרים שרצים על המכונה. אפשר לסנן קונטיינרים אם נעביר אחרי האינטרוול את שם הקונטיינר שאנחנו רוצים לעדכן (שימו לב שזה שם הקונטיינר לא שם הסרביס בקומפוז), או שיטה יותר פשוטה לדעתי היא להגדיר label על כל קונטיינר שצריך עדכון. נשים לב שאם יש לנו מספר פרויקטים שונים עם קבצי docker-compose שונים אז עדיין watchtower של אחד עשוי לשדרג קונטיינרים של הפרויקט השני.

סך הכל שילוב שלושת הסעיפים מביא אותנו ל docker-compose.yml שנראה בערך כך:

services:
  db:
    image: postgres:16.3
    volumes:
      - ./tmp/db:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: ${DBPASSWORD}

  watchtower:
    image: containrrr/watchtower
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /home/ynon/.docker/config.json:/config.json
    command: --interval 30 --label-enable

  web:
    image: my.private.registry.com/webapp:latest
    labels:
      com.centurylinklabs.watchtower.enable: true
    environment:
      DATABASE_PASSWORD: ${DBPASSWORD}
      DATABASE_HOST: db
    ports:
      - "3000:3000"
    depends_on:
      - db

מבנה פרויקט Rails, React ו TypeScript

08/06/2024

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

המשך קריאה

זמן לימוד ארוך

07/06/2024

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

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

הכל בסדר טייפסקריפט?

06/06/2024

הקוד הבא בטייפסקריפט הצליח לבלבל אותי לרגע-

type MyItem = {type: 'a', url: string, id: number}

function go(item: MyItem) {}

const f = {type: 'a', url: 'url', id: 0}
go(f)

הוא לא מתקמפל.

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

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

const f: {
    type: string;
    url: string;
    id: number;
}

כלומר ה type הוא מסוג string ולא מהסוג הספציפי 'a'. מבחינת טייפסקריפט מותר לשנות את ה type לערך אחר, כלומר הקוד הבא כן מתקמפל:

type MyItem = {type: 'a', url: string, id: number}

function go(item: MyItem) {}

const f = {type: 'a', url: 'url', id: 0}
f.type = 'b';

כשטייפסקריפט רואה את הקריאה:

go(f)

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

איך פותרים את זה? אז מסתבר שיש כמה דרכים - קודם כל אפשר להגיד לטייפסקריפט בצורה מפורשת מה הטיפוס של f:

type MyItem = {type: 'a', url: string, id: number}

function go(item: MyItem) {}

const f: MyItem = {type: 'a', url: 'url', id: 0}

עכשיו אפשר יהיה להעביר אותו לפונקציה go ואי אפשר יהיה לשנות את הערך של f.type.

עוד אופציה היא להגדיר ש type הוא קבוע בתוך הגדרת האוביקט:

const f = {type: 'a' as const, url: 'url', id: 0}

ודרך שלישית לקבל את אותו טיפוס היא להגדיר את type ממש להיות a:

const f = {type: 'a' as 'a', url: 'url', id: 0}

מכירים רעיונות נוספים? שתפו בתגובות אשמח לשמוע.

מה map מחזירה

05/06/2024

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

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

scala> Set(1, 2, 3).map(_ * 2)
val res1: Set[Int] = Set(2, 4, 6)

scala> List(1, 2, 3).map(_ * 2)
val res4: List[Int] = List(2, 4, 6)

קלוז'ר לעומתה שמה את הדגש על הפונקציה ולכן הפעלה של map על כל דבר מחזירה תמיד LazySeq:

user=> (type (map (partial * 2) #{1 2 3}))
clojure.lang.LazySeq
user=> (type (map (partial * 2) [1 2 3]))
clojure.lang.LazySeq

ו JavaScript מסכימה להגדיר map רק על רשימה ולכן:

new Set([1, 2, 3]).map(i => i * 2)
TypeError: new Set([1, 2, 3]).map is not a function. (In 'new Set([1, 2, 3]).map(i => i * 2)', 'new Set([1, 2, 3]).map' is undefined)

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

נ.ב. ומה מחזיר הקוד הבא ברובי? נסו לבדוק ותראו אם הבנתם למה:

3.1.1 :002 > [1, 2, 3].map { _ * 2 }

אופטימיזציית זנב הרקורסיה ב JavaScript ב 2024

04/06/2024

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

https://www.tocode.co.il/blog/2018-09-javascript-tco

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

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

הנה התוכנית:

function factors_of(number, i=2) {
    if (number < i) {
          return [];
        }

    if (number % i === 0) {
          return [i, ...factors_of(number / i, i)];
        }

    return factors_of(number, i+1);
}

console.log(factors_of(909090909090909090909090));

והרצה בצד שרת ב node:

node a.js
/Users/ynonp/tmp/a.js:1
function factors_of(number, i=2) {
                   ^

RangeError: Maximum call stack size exceeded
    at factors_of (/Users/ynonp/tmp/a.js:1:20)
    at factors_of (/Users/ynonp/tmp/a.js:10:12)
    at factors_of (/Users/ynonp/tmp/a.js:10:12)
    at factors_of (/Users/ynonp/tmp/a.js:10:12)
    at factors_of (/Users/ynonp/tmp/a.js:10:12)
    at factors_of (/Users/ynonp/tmp/a.js:10:12)
    at factors_of (/Users/ynonp/tmp/a.js:10:12)
    at factors_of (/Users/ynonp/tmp/a.js:10:12)
    at factors_of (/Users/ynonp/tmp/a.js:10:12)
    at factors_of (/Users/ynonp/tmp/a.js:10:12)

Node.js v21.7.1

ב Deno:

deno run a.js
error: Uncaught (in promise) RangeError: Maximum call stack size exceeded
function factors_of(number, i=2) {
                   ^
    at factors_of (file:///Users/ynonp/tmp/a.js:1:20)
    at factors_of (file:///Users/ynonp/tmp/a.js:10:12)
    at factors_of (file:///Users/ynonp/tmp/a.js:10:12)
    at factors_of (file:///Users/ynonp/tmp/a.js:10:12)
    at factors_of (file:///Users/ynonp/tmp/a.js:10:12)
    at factors_of (file:///Users/ynonp/tmp/a.js:10:12)
    at factors_of (file:///Users/ynonp/tmp/a.js:10:12)
    at factors_of (file:///Users/ynonp/tmp/a.js:10:12)
    at factors_of (file:///Users/ynonp/tmp/a.js:10:12)
    at factors_of (file:///Users/ynonp/tmp/a.js:10:12)

ורק bun מפתיע לטובה:

$ bun a.js
[
  2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
  71, 101, 251, 401, 9384251
]