שתי דרכים טובות ואחת לא ממש כדי להדפיס אורך שורה ביוניקס
יש לכם תוכנית שמייצרת פלט ואתם צריכים לגלות מה האורך של כל שורה בפלט ולהדפיס את האורך לפני השורה עצמה. איך עושים את זה? הנה שלושה רעיונות:
טיפים קצרים וחדשות למתכנתים
יש לכם תוכנית שמייצרת פלט ואתם צריכים לגלות מה האורך של כל שורה בפלט ולהדפיס את האורך לפני השורה עצמה. איך עושים את זה? הנה שלושה רעיונות:
רבים מכם שמו לב שהוובינר באתר הופיע בשני מקומות שונים בשתי שעות שונות: בדף רשימת הוובינרים הוא הופיע בשעה הנכונה (עשר בבוקר), אבל בדף האירוע עצמו הוא הופיע בשעה שמונה בבוקר.
בואו נראה למה זה קרה ומה אפשר ללמוד מזה.
ריאקט היא רק ספריית תצוגה.
לריאקט יש ביצועים מצוינים.
ריאקט הרבה פחות מסובכת מ X/Y/Z ולכן קוד ריאקט תמיד יוצא יותר יעיל.
האמת היא שלא משנה מהי ספריית הפיתוח שתבחרו לפרויקט שלכם, אתם צריכים להכיר אותה ואת השטויות שלה כדי לא ליפול בבורות כשעוברים מ Tutorials ודוגמאות של קורסים לקוד אמיתי. ובריאקט יש לא מעט בורות כאלה.
ביום חמישי הקרוב בעשר בבוקר אני אדגים מספר בורות בעבודה עם ריאקט דרך תוכניות ריאקט שמכילות באגים מעניינים, ולכל תוכנית נראה מה מקור הבעיה ומה אפשר ללמוד על ריאקט מתוך הבנה של הבעיה. הוובינר מיועד לאנשים שמכירים וכתבו ריאקט ורוצים להכיר את הבורות שלו כדי לא ליפול בהם בקוד אמיתי. בין הנושאים שאדבר עליהם יהיו:
טעויות נפוצות ב Data Flow בין קומפוננטות.
טעויות נפוצות בנושא Immutable Data.
טעויות נפוצות בשימוש ב Hooks, במיוחד במערך התלויות ו useEffect.
איך לבחור קומפוננטות בצורה יותר יעילה.
זמן לשאלות שלכם.
הכנתי הרבה דוגמאות אז תבואו ערניים ונתראה בחמישי, אה כמעט שכחתי - קישור להרשמה: https://www.tocode.co.il/workshops/121.
אלה שני צירים שונים. בציור אפשר לדמיין מערכת צירים, ציר ה x מתאים לקוד עובד או לא עובד (וכן זו סקאלה, כי תמיד יש באגים או פשרות בביצועים או מה לא), וציר y מתאים לקוד טוב או לא טוב.
בכתיבת קוד חדש כולם חולמים על הרבע הימני-עליון של המפה, חיובי בציר ה"עובד" וחיובי בציר ה"טוב", אבל ברוב המקרים אנחנו לא מבינים את הדרישות מספיק טוב ונוחתים איפשהו ליד האפס.
החלק היותר מעניין בעבודה הוא עדכון קוד קיים. פה ברירת המחדל של רוב המתכנתים היא להתמקד ב"עובד", ולנסות לשנות כמה שפחות כדי לתקן את הבאג או לממש את הפיצ'ר. ריפקטור שיהפוך את הקוד ליותר טוב בדרך כלל לא בא בחשבון כי הוא מחוץ לסקופ של אותו פיצ'ר או באג. אבל אם נחזור למערכת הצירים שלנו, תהליך כזה מבטיח שלאורך זמן יהיה לנו קוד רע. אנחנו מתקדמים בציר אחד ונותנים למזל לעשות את שלו בציר השני.
רוב הקוד הגרוע שאנחנו כותבים לא נכתב בשבוע הראשון של הפרויקט, אלא הוא נוצר באבולוציה כשאנחנו לוקחים אבסטרקציה לא נכונה וממשיכים לבנות עליה עוד ועוד מנגנונים, במקום לתקן את היסודות. כל בניה כזאת מורידה אותנו עוד כמה נקודות בציר ה"טוב".
הפיתרון היחיד לדעתי הוא לייצר נהלי עבודה שמחייבים אותנו לשפר כל קוד שאנחנו נוגעים בו תוך כדי תנועה. דוגמאות לכאלה נהלים יהיו-
חובה לייצר בדיקות אוטומטיות לכל באג שמתקנים.
להשאיר מקום למתכנתים להוסיף משימות ריפקטורינג ל Jira כבר בספרינט הבא, ולעודד מתכנתים לייצר לעצמם כאלה משימות.
שימוש ב Git Hooks כדי לוודא הודעות קומיט מפורטות בגיט.
הכנסת שידרוגי תלויות ותשתית בתור משימה חוזרת ב Jira, פעם ב X ספרינטים.
יש לכם רעיונות נוספים לנהלי עבודה שמשפרים את איכות הקוד לאורך זמן? עכשיו החזרתי את התגובות אז אפשר לשתף כאן או בטלגרם.
המושג "עדכון אופטימי" מתאר עדכון שבו אנחנו בטוחים שהכל יעבוד, ולכן אנחנו מעדכנים את ממשק המשתמש במידע החדש עוד לפני שקיבלנו אישור מהשרת שאכן המידע תקין, ורק אם השרת ידווח על שגיאה אז נבטל את העדכון שעשינו. עדכון אופטימי מציע חווית משתמש יותר טובה כי משתמשים לא צריכים לחכות ששרת יאשר משהו שאנחנו יודעים שב 90% מהמקרים מצליח.
ל RTK Query יש דרך מובנית ומאוד נוחה לממש עדכונים אופטימיים בזכות שילוב של מספר מנגנונים של הספריה:
ספריית RTK Query מחזיקה את כל התוצאות של כל השאילתות בזיכרון, ומאפשרת לנו לעדכן את השאילתות השמורות.
הספריה משולבת עם Immer ומאפשרת לנו "לזכור" את המצב לפני העדכון ולבטל אותו בפקודה אחת מובנית.
באופן אוטומטי כל קוד ה 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
ומעביר לו אוביקט עם שני מפתחות:
המפתח query
יוצר את קוד בקשת ה POST שאני שולח לשרת כדי ליצור פתק חדש. לפתק חדש יש בסך הכל את הטקסט בפתק.
המפתח 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.
בדוגמת ההתחלה מהירה של 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 שוב עם הסטייט שיצרתם.
ספריית react-testing-library תומכת בצורה מלאה בקומפוננטות שעושות דברים ברקע בצורה אסינכרונית ובבדיקה שלהן. בפוסט זה נדבר על שעונים ונראה איך לבדוק קומפוננטות שמושפעות מזמן.
תוכנות כמו virtualbox או vmware הופכות את ההתקנה של מכונת לינוקס וירטואלית למאוד פשוטה, ובזכותן אפשר להתנסות עם הפצות חדשות ולהינות מהסביבה הגרפית השונה ומהאווירה השונה של כל הפצה. אבל לפעמים אנחנו רק רוצים להריץ משהו במסוף או לבדוק משהו יותר קטן, או לחלופין להריץ מספר מכונות בלי GUI שיתקשרו ביניהן ברשת. למצבים האלה תוכנות הוירטואליזציה עשויות להיות "יותר מדי". תוכנת Multipass מציעה פיתרון הרבה יותר מדויק ואלגנטי למי שרוצה להרים מכונות אובונטו וירטואליות, נטולות ממשק גרפי, על המכונה המקומית.
זה אולי לא קורה לעתים קרובות, אבל לפעמים יש לנו פקודה או כמה פקודות מההיסטוריה שאנחנו רוצים להריץ שוב ושוב, ובזה בדיוק fc יכולה לעזור.
בשימוש הפשוט שלה הפקודה יודעת למצוא פקודות ישנות שהרצתם. לכן אם אני מפעיל:
mkdir foo
touch foo/bar
cp /etc/passwd foo
אז אני יכול אחרי זה לכתוב:
fc -l mkdir cp
ולקבל את רשימת כל הפקודות מ mkdir עד cp כולל:
544 mkdir foo
545 touch foo/bar
546 cp /etc/passwd foo
מה שיותר מדליק קורה אם אני מוותר על ה -l
, ואז fc פותח את הרשימה בתוך עורך טקסט, מאפשר לי לשנות את הפקודות ובסוף מריץ את כולן. בדוגמה שלנו אני מוותר על ה -l
ומקבל את כל הרשימה בעורך טקסט, משנה כל מופע של foo ל bar ומקבל את הקובץ:
mkdir bar
touch bar/bar
cp /etc/passwd bar
שומר ויוצא וכך יצרתי את תיקיית bar בדיוק כמו שיצרתי קודם את תיקיית foo.
שימו לב רק שאתם בודקים טוב טוב מה אתם מריצים כשאתם בתוך העורך, כי מרגע ש fc יצא לדרך אי אפשר לעצור אותו וזה שפקודה מסוימת הופיעה בהיסטוריה לא בהכרח אומר שאתם רוצים להריץ אותה שוב.
לא תמיד אנחנו מסכימים עם השגעונות של ג'אווהסקריפט אבל הרבה פעמים כן יש איזה היגיון פנימי שיכול לעזור להסביר את הטירוף. ועם ההקדמה הזאת בואו נדבר על מערכים.