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

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

דינו מאפשר הרצת חבילות גם מ npm

21/11/2022

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

לא מזמן יצאה גירסה 1.28 של דינו שהוסיפה תמיכה בחבילות מ npm. בכך נשבר חסם משמעותי לאימוץ דינו, למרות שהוא עדיין כנראה פחות יציב מ node. בכל מקרה למשחקים זה עובד מצוין והתמיכה ב TypeScript מהקופסה היא כמו הדובדבן על הקצפת. הנה דוגמה שלי המבוססת על הדוגמה באתר שלהם לעבודה עם אקספרס:

import express from "npm:express@4.18.2";

function twice(x: number) {
  return x * 2;
}

const app = express();

app.get("/", (req, res) => {
  res.send("Welcome to the Dinosaur API!");
});

app.get("/twice", (req, res) => {
  const n = Number(req.query.n || 0);
  res.send(`${n} * 2 = ${twice(n)}`);
});

app.listen(8000);

ובשביל להריץ, אם יש לכם כבר דינו 1.28 ומעלה מותקן מפעילים:

$ deno run --allow-net --allow-read --allow-env main.ts

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

ביצועים ושפות מודרניות

20/11/2022

אלגוריתם Fisher-Yates לערבוב מערך מציע לבצע את הצעדים הבאים:

  1. בחרו אינדקס אקראי במערך.
  2. מחקו את האיבר באינדקס שבחרתם וכתבו אותו בסוף הרשימה.
  3. המשיכו עד שמחקתם את כל האיברים.

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

function shuffle(arr) {
  let end = arr.length;
  while (end >= 0) {
    const nextIndex = Math.floor(Math.random() * end);
    arr.push(arr[nextIndex]);
    arr.splice(nextIndex, 1);
    end -= 1;
  }
  return arr;
}

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

function shuffle(arr) {
  for (let i=arr.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * i);
    const item = arr[i];
    arr[i] = arr[j];
    arr[j] = item;
  }

  return arr;
}

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

הודעות שגיאה מטעות

19/11/2022

שגיאה בתוכנה הדפיסה לי את ההודעה הבאה ללוג:

Permissions should be u=rwx (0700).

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

$ ls -l b.txt
-rw-rw-r-- 1 ubuntu ubuntu 0 Nov 16 12:39 b.txt

ומריץ את הפקודה:

$ chmod u=rwx b.txt

ההרשאות עדיין יהיו לא מתאימות:

$ ls -l b.txt
-rwxrw-r-- 1 ubuntu ubuntu 0 Nov 16 12:39 b.txt

למעשה אפשר להסיק מהודעת השגיאה שתי פקודות שונות להריץ - האחת, chmod u=rwx, תשנה רק את ההרשאות לבעלים של הקובץ; השניה, chmod 0700, באמת תוריד גם את ההרשאות מכל האחרים.

הודעת שגיאה טובה יותר היתה מציגה שתי אפשרויות שקולות, למשל היה עדיף לכתוב:

Permissions should be a=,u=rwx (0700).

או אפילו:

Permissions should be u=rwx,g=,o= (0700).

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

למה את קוראת "טעות"?

18/11/2022

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

  1. האם קוד גרוע הוא קוד שלא עובד? או שגם קוד עובד יכול להיות גרוע?

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

  3. האם קוד גרוע קשור למה שמסביבו? האם קוד יכול להיות טוב אם אין לו בדיקות? אם אין לו תיעוד?

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

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

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

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

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

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

התיקון הלא נכון (או: למה הוובינר הופיע בשעה הלא נכונה)

16/11/2022

רבים מכם שמו לב שהוובינר באתר הופיע בשני מקומות שונים בשתי שעות שונות: בדף רשימת הוובינרים הוא הופיע בשעה הנכונה (עשר בבוקר), אבל בדף האירוע עצמו הוא הופיע בשעה שמונה בבוקר.

בואו נראה למה זה קרה ומה אפשר ללמוד מזה.

המשך קריאה

הזמנה לוובינר: טעויות נפוצות עם ריאקט

15/11/2022

ריאקט היא רק ספריית תצוגה.

לריאקט יש ביצועים מצוינים.

ריאקט הרבה פחות מסובכת מ X/Y/Z ולכן קוד ריאקט תמיד יוצא יותר יעיל.

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

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

  1. טעויות נפוצות ב Data Flow בין קומפוננטות.

  2. טעויות נפוצות בנושא Immutable Data.

  3. טעויות נפוצות בשימוש ב Hooks, במיוחד במערך התלויות ו useEffect.

  4. איך לבחור קומפוננטות בצורה יותר יעילה.

  5. זמן לשאלות שלכם.

הכנתי הרבה דוגמאות אז תבואו ערניים ונתראה בחמישי, אה כמעט שכחתי - קישור להרשמה: https://www.tocode.co.il/workshops/121.

קוד טוב, קוד עובד

14/11/2022

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

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

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

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

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

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

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

  3. שימוש ב Git Hooks כדי לוודא הודעות קומיט מפורטות בגיט.

  4. הכנסת שידרוגי תלויות ותשתית בתור משימה חוזרת ב Jira, פעם ב X ספרינטים.

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

עדכונים אופטימיים ב Redux Toolkit Query

13/11/2022

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

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

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

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

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

ככה זה נראה בקוד:

    createNote: builder.mutation({
      query: (noteText) => ({
        url: `/notes`,
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ text: noteText }),
      }),      
      async onQueryStarted(noteText, { dispatch, queryFulfilled }) {
        let newNoteId = nanoid();

        const patchResult = dispatch(
          notesApi.util.updateQueryData('getNotes', undefined, (draft) => {
            return [...draft, { text: noteText, id: newNoteId }];
          }));
        try {
          const data = await queryFulfilled
          dispatch(
            notesApi.util.updateQueryData('getNotes', undefined, (draft) => {        
              return draft.map(d => d.id === newNoteId ? data.data : d )
            }));
       } catch (err) {
        console.log(`error`);
        patchResult.undo();
       }
      }
    })

הקוד מגדיר פעולה חדשה בשם createNote, עבור אפליקציה ששומרת רשימה של פתקים. בשביל ההגדרה אני מפעיל את builder.mutation ומעביר לו אוביקט עם שני מפתחות:

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

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

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

const patchResult = dispatch(
  notesApi.util.updateQueryData('getNotes', undefined, (draft) => {
    return [...draft, { text: noteText, id: newNoteId }];
}));

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

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

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

const data = await queryFulfilled

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

dispatch(
  notesApi.util.updateQueryData('getNotes', undefined, (draft) => {
    return draft.map(d => d.id === newNoteId ? data.data : d )
  }));

ואם היתה שגיאה והשרת לא הצליח לשמור את הפתק? בשביל זה יש לי בלוק catch:

} catch (err) {
 patchResult.undo();
}

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

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

} catch (err) {
 dispatch(
   notesApi.util.updateQueryData('getNotes', undefined, (draft) => {
     return draft.filter(note => note.id !== newNoteId);
   }));
  }

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

טיפ רידאקס - השתמשו ב preloadedState כדי לבדוק יותר בקלות

12/11/2022

בדוגמת ההתחלה מהירה של Redux Tolkit הם יוצרים וימייצאים את מחסן המידע באותה שורה באופן הבא:

import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '../features/counter/counterSlice'

export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
})

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

export function createStore(initialState) {
  return configureStore(Object.assign({}, {
    reducer: {
      counter: counterReducer,
    },
  },
  initialState ? { preloadedState: initialState } : {}));
}

וככה אני יכול להשתמש בפונקציה כדי לבדוק את ה store בכל state ראשוני שארצה:

test('inc from 10 to 11', () => {
  const store = createStore({ counter: { value: 10 }});
  store.dispatch(increment());
  expect(store.getState().counter).toEqual({ value: 11 });
});

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