מדריך: איחסון קבצים גדולים בגיט עם git lfs
כולם אומרים שגיט לא טוב באיחסון קבצים בינאריים. בפוסט זה אני רוצה להסביר קצת מה הבעיה של גיט עם קבצים בינאריים גדולים, ולהראות את התוסף git lfs שפותר חלק גדול מאותה בעיה.
טיפים קצרים וחדשות למתכנתים
כולם אומרים שגיט לא טוב באיחסון קבצים בינאריים. בפוסט זה אני רוצה להסביר קצת מה הבעיה של גיט עם קבצים בינאריים גדולים, ולהראות את התוסף git lfs שפותר חלק גדול מאותה בעיה.
למתכנתים צעירים רבים אין בעיה לבוא בהצעות מרחיקות לכת, להכניס כלים חדשים ופריימוורקים חדשים למערכת ואפילו ליזום ולהעביר הרצאות על נושאים שהם כמעט לא מכירים. אותם מתכנתים מבינים את הפוטנציאל במוניטין שיוכלו לצבור, ויודעים שממילא אין להם הרבה מוניטין לאבד.
ככל שאנחנו גדלים המוכנות "לקחת סיכונים" נחלשת. הרווח שיצא לי מלהכניס כלי חדש למערכת הוא כנראה לא גבוה, והסיכון משמעותי. מתכנתת וותיקה עשויה להרגיש שמצפים ממנה להכיר לעומק כלי מסוים לפני שהיא ממליצה עליו לצוות; היא יודעת בדיוק מה זה אומר "להכיר לעומק" כי היא מכירה לעומק את הכלים שעכשיו עובדים איתם, ויודעת שכרגע (או בעתיד הנראה לעין) אין לה זמן ללמוד לעומק את אותו כלי חדש.
הפחד מהפאדיחה ליפול היה מה שעצר אותי כשכולם עברו מ perl ל python: ידעתי שכשאני מגיע ללקוח לעזור בבעיית פרל לא משנה מה יהיה אני אוכל למצוא לו פיתרון טוב, כי כבר ראיתי אינסוף בעיות פרל ופיתרונות שלהן באותה תקופה. ב python הסיפור היה שונה לגמרי - ההיכרות שלי עם פייתון היתה מוגבלת, ולפני שרואים פרויקטים ופותרים בעיות אי אפשר באמת לקבל ביטחון. פרילאנסרים צעירים ממני נכנסו בלי להתבלבל לפרויקטים בפייתון, ואני הסתכלתי מהצד וחיכיתי, וככל שחיכיתי הקפיצה רק נראתה יותר קשה.
הבעיה עם הפחד מהפאדיחה היא שבכל מקרה ספציפי ההחלטה לקחת סיכון היא באמת החלטה יותר קשה. מה טוב כבר יצא לי מלקחת פרויקט ייעוץ בפייתון כשאני לא יודע מספיק טוב פייתון? אני רק אשב המון שעות על כל בעיה קטנה, ובסוף אתן ללקוח קוד בינוני. אבל, כשמסתכלים על התמונה הגדולה, ברור שאחרי 3-4 פרויקטים אני כבר אכיר את השפה ממש טוב ואוכל להיות מועיל לעצמי ולסביבה.
וכשחוזרים לעבודה היום יומית כמתכנתת בצוות - להמליץ על כלי בודד שאולי לא יעבוד טוב זה מסוכן, אבל להיות זאת שממליצה על טכנולוגיות חדשות זה כבר משהו ששווה לשאוף אליו. העניין שהדרך תמיד עוברת דרך הפאדיחה, דרך הפעם הראשונה, דרך הנפילה.
במקום לפחד מהפאדיחה ליפול - כדאי להתרגל פשוט ליפול. להפוך את הנפילה לחלק מהחיים ולהבין שבטווח הרחוק, כשאנחנו בונים סגנון חיים שבו הנפילה היא לא פאדיחה אלא חלק מהיום יום שלנו, אנחנו רק מרוויחים.
לפני כמה שנים לקחתי פרילאנסר לעזור בבניית HTML ו CSS לאתר שהייתי צריך ללקוח. הפרילאנסר עבד מעולה ויצר קוד שהתאים במדויק לעיצוב שהיה לנו, ואני הייתי בטוח שמצאתי את הדרך אל האושר.
עד שהיינו צריכים לשנות משהו.
כי הבעיה עם CSS היא שהכל תלוי שם בהכל: מאפיין של גודל גופן על אלמנט מסוים משפיע על הגופן אצל הילדים שלו, מאפיין position באלמנט מסוים משפיע על ההתנהגות של מאפייני top ו left אצל הילדים שלו. בקיצור CSS מרגיש כמו מגדל קלפים - זה עובד, אבל שינוי הכי קטן ודברים יוצאים מהמקום.
נשווה את זה ל Tailwind CSS, שהוא, לטובת מי מהקוראים שלא מכירים, פריימוורק שמעביר את ה CSS לתוך ה HTML, או אולי יותר נכון לנסח את זה בתור פריימוורק שיוצר דינמית הגדרות CSS שמתאימות לקלאסים עם שמות מאוד מסוימים ב HTML.
אז ב Tailwind CSS אני יכול לכתוב שורת HTML כזאת:
<img
src="http://placekitten.com/100/100"
alt="cat"
class="py-4 pl-4 aspect-square"
/>
ואני אקבל תמונה של חתלתול עם ריפוד עליון וריפוד משמאל של 16 פיקסלים כל אחד ומאפיין aspect-ratio עם ערך של 1/1
.
ומה כל כך מלהיב בלהעביר את כל כללי העיצוב ל HTML? אם נחזור לסיפור של הפרילאנסר, לתחזק קוד Tailwind CSS של מישהו אחר (בהנחה שאנחנו מכירים טיילווינד) זה הרבה יותר קל מלתחזק קוד CSS של מישהו אחר. אני לא צריך להיכנס ולהבין איזה קלאסים הוא בחר ומה המבנה ב HTML שמתאים לקוד ה CSS הקיים; הכל פשוט מופיע מול העיניים.
רוצים לדעת עוד על טיילווינד והקלאסים שלו? הפוסט הזה הוא מקום טוב להתחיל בו: https://codingthesmartway.com/tailwind-css-for-absolute-beginners/
וכמובן התיעוד המעולה שלהם בקישור: https://tailwindcss.com/docs/installation
דוד בוב אוהב לדבר על קוד נקי - קוד שאין בו חזרות, שמסודר נכון, שקל להרחיב אותו, שקל לקרוא אותו, שעושה בדיוק את מה שצריך ולא עושה דברים שלא צריך. כל אחד היה רוצה לכתוב רק קוד נקי כל הזמן.
אבל אם ניקח את המטאפורה של קוד נקי ברצינות, נשים לב ששום דבר לא יוצא נקי מהמפעל, ושעם הזמן גם דברים נקיים מתלכלכים.
ככה גם קוד: הניסיון הראשון בפיתרון בעיה לא ייצא נקי. אחרי הקידוד צריך לנקות, כלומר לעשות Refactoring, להוסיף תיעוד ולכתוב בדיקות. גם אחרי שעשינו את הכל כמו בספר, תוך כמה שבועות או חודשים הקוד יתלכלך שוב, בדיוק כמו כל דבר אחר בעולם.
קוד נקי הוא לא תוצאה של מתכנתים גאונים שיודעים איך לכתוב קוד, והוא לא תוצאה של שינון הכללים וניסיון ללכת לפי הספר. קוד נקי הוא תוצאה של גישה ושיטת עבודה. שיטה שמשאירה זמן ונותנת עדיפות גם לניקיון ולא רק לפיתוח הראשוני. קוד נקי הוא לא מצב קיים אלא תהליך, תהליך שמתחיל בקוד מלוכלך.
לפני שנתיים כתבתי כאן משחק זיכרון עם ריאקט בעזרת הפונקציה useReducer. היום אני רוצה לנסות משחק דומה עם Redux כדי לראות מה התחדש ברידאקס בשנים האחרונות ובמיוחד כדי לשחק עם Redux Toolkit, ספריה שהופכת את הכתיבה ברידאקס למשחק הרבה יותר מהנה.
לא מזמן דיברתי עם מתכנת ריאקט מאוד מוכשר, ששיכנע אותי בנאומים חוצבי להבות שהדרך הנכונה לכתוב קוד ריאקט היא להשתמש ב useEffect וב Custom Hooks כדי להעביר כמה שיותר לוגיקה, כולל לוגיקה של תקשורת, לתוך הקומפוננטות. הרבה יותר קל, הוא טען, לנהל את הלוגיקה של השליפות כשהשליפה קרובה לקוד שמציג את התוצאה שלה. בנוסף, כשאתה מוחק את הקומפוננטה אוטומטית אתה מוחק את קוד השליפה שקשור אליה, ולא נשאר עם מנגנוני שליפה שאף אחד לא משתמש בהם.
ואז מצאתי את המשפט הבא בתיעוד של Redux Toolkit:
With RTK Query, you usually define your entire API definition in one place. This is most likely different from what you see with other libraries such as swr or react-query, and there are several reasons for that. Our perspective is that it's much easier to keep track of how requests, cache invalidation, and general app configuration behave when they're all in one central location in comparison to having X number of custom hooks in different files throughout your application.
בתרגום וקיצור הם אומרים שהם בנו את RTK Query כי הרבה יותר קל לנהל תקשורת כשכל הקוד שמתעסק בה נמצא במקום אחד.
"מי צודק" זאת בכלל לא השאלה כאן. הרבה אתגרים בתכנות בנויים ככה שאפשר לפתור אותם בכל מיני דרכים. כל גישה מגיעה עם הכלים שלה ובכל גישה אפשר לבנות פיתרונות טובים (או פחות טובים).
הדרך להיות מתכנתים טובים יותר היא לא למצוא את "הגישה האחת" שתמיד נכונה, אלא להיפתח ליותר גישות, ולהבין בתוך גישה מסוימת מה הסטנדרטים שמקובלים בה, מה זה אומר לכתוב קוד טוב (או גרוע) כשהלוגיקה בתוך הקומפוננטות, ומה זה אומר לכתוב קוד טוב (או גרוע) כשהלוגיקה נמצאת במקום אחר, ואיזה מלכודות או סכנות צפויות לנו בכל אחת מהגישות.
ריאן קרניאטו, היוצר של סוליד, העיד על עצמו שלקח לו הרבה זמן להבין מה אנשים מוצאים בריאקט ולהתאהב בפריימוורק בעצמו. עבורו הרגע המכריע היה החשיפה של React Hooks, שנתנו עדיפות לקומפוננטות פונקציונאליות ואיפשרו לארגן קוד לשימוש חוזר במקום אחד - בתוך Custom Hook - ואז להשתמש בו שוב ושוב במספר גדול של קומפוננטות.
אבל בגלל איך שריאקט בנוי ל Hooks יש בעיה: כמעט כל Hook בריאקט חייב לקבל רשימת תלויות, בגלל שריאקט לא יודע "לראות" לבד את הקשר בין קוד לבין הדברים שהוא תלוי בהם. אותו חיבור שהוא הבסיס של מה שנקרא ריאקטיביות.
במילים אחרות, כשקוד ריאקט רוצה ליצור אפקט על משתנה סטייט מסוים, למשל שכל פעם שמשתנה סטייט מסוים מתעדכן אז לעדכן אוטומטית את ה document.title, הוא יצטרך להיראות כך:
function useDocumentTitle(title) {
useEffect(() => {
document.title = title;
}, [title]);
}
אבל הדיון על "רשימת התלויות" הולך הרבה יותר רחוק מאיך לבנות Custom Hooks בריאקט. אם נחשוב על זה, כל המהות של קומפוננטה זה להיות משהו שמתעדכן כש State או Props שלו מתעדכנים - ושוב יש לנו את משחק התלויות. מידע, ששמור בתור סטייט או פרופ, משפיע על הדבר שאנחנו רואים על המסך.
הדרך שבה ריאקט מזהה ומעדכן את "התוצאות" כשהתלויות משתנות היא Virtual DOM. ריאקט שומר מבנה נתונים גדול של מה צריך להיות ומה קיים כרגע, ויודע לזהות הבדלים בין שני המצבים. כל פעם שתלות מסוימת משתנה ריאקט יחשב מחדש את ההבדלים ויכתוב את התוצאה המעודכנת למסך.
סוליד, הפריימוורק שיצר ריאן קרניאטו, לוקח טייק אחר לגמרי.
במקום לעקוב אחר תלויות במבני נתונים שנקראים Virtual DOM, הוא שומר על קשר ישיר בין משתנה לבין הדברים שתלויים בו. הריאקטיביות של סוליד היא ריאקטיביות ברזולוציה גבוהה - אף פעם לא מעדכנים מחדש קומפוננטה מלאה רק בגלל שמשהו בסטייט השתנה, אלא כל דבר שמשתנה גורם לעדכון מדויק של הדברים שמושפעים ממנו.
התוצאה היא פריימוורק שנראה דומה לריאקט אבל עובד אחרת לגמרי:
הקומפוננטות הן פונקציות וקוד לשימוש חוזר הוא Custom Hook, אבל התלויות מזוהות בצורה אוטומטית ואין שום אילוץ להפעיל Hooks רק מתוך קוד של קומפוננטה.
פונקציית הקומפוננטה עצמה מורצת רק פעם אחת כדי "לחבר" בין משתני הסטייט לבין הדברים שרואים על המסך. משם, כל פעם שמשתנה סטייט מתעדכן הוא מייצר עדכון אוטומטי של ה DOM.
אין Virtual DOM ולכן יש הרבה פחות חשיבות (מבחינת ביצועים) לחלוקה נכונה לקומפוננטות.
בהרבה מאוד מצבים נקבל ביצועים טובים יותר עבור קוד כמעט זהה.
ביום חמישי הקרוב אעביר בזום וובינר של שעה על סוליד בו אראה לכם איך לעבוד עם הפריימוורק וגם נראה את ההבדל מבחינת ביצועים בינה לבין ריאקט. בסיום הוובינר, בנוסף להיכרות עם פריימוורק די מדליק, גם תבינו מהי ריאקטיביות ואיך פריימוורק ריאקטיבי צריך להיראות. גם אם בסוף לא תעבדו עם סוליד, היכרות איתו ועם שיטת המחשבה שלו תעזור לכם להבין את ריאקט טוב יותר.
אפשר להצטרף לוובינר בחינם בלחיצת כפתור בדף האירוע כאן: https://www.tocode.co.il/workshops/116
נתראה ינון
ריאקט 18 יצא לפני כמה ימים והפיצ'ר המרכזי שלו נקרא Concurrent Mode. פיצ'ר זה מאפשר לריאקט להתחיל לרנדר משהו, להבין שזה לוקח יותר מדי זמן או שיש משהו יותר חשוב לעשות ואז לעצור, לרנדר את הדבר היותר חשוב ולחזור לעבודה הארוכה יותר.
אחד השימושים של הפיצ'ר הזה הוא הפונקציה useDeferredValue שמגדירה לריאקט שחישוב מסוים הוא פחות חשוב ואפשר לחכות קצת עם ה render-ים שתלויים בו כדי לא להאט את כל העמוד.
הפונקציה יעילה במיוחד עבור רשימה עם תיבת סינון: כשמשתמש מקליד טקסט בתיבה אנחנו רוצים לראות תגובה מיידית, אבל סינון הפריטים עצמו יכול לקחת קצת יותר זמן ולעבוד ברקע.
הקוד הבא ממחיש את ההתנהגות הרצויה תוך שימוש ב useDeferredValue:
export default function App() {
const [search, setSearch] = useState("");
const dsearch = useDeferredValue(search, { timeoutMs: 10000 });
return (
<div className="App">
<input
type="search"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<FilteredList search={dsearch} isPending={dsearch !== search} />
</div>
);
}
ואפשר לראות אותו בפעולה בקודסנדבוקס הבא:
קצת הסברים על הקוד:
הסטייט search מייצג את הטקסט בתיבה; המשתנה dsearch הוא עותק של search שהאלמנטים שתלויים בו פחות חשובים.
בפרק הזמן בין עדכון search לעדכון dsearch ריאקט מנסה ברקע לרנדר את הקומפוננטה FilteredList שמושפעת מ dsearch
. בהתחלה הערכים של dsearch ו search יהיו שונים ואחרי שהרינדור האיטי יסתיים הערכים יהיו שווים. אני בודק את ההבדל בין שני המשתנים כדי לדעת אם ריאקט עכשיו עובד ברקע, ומשקף את זה למשתמשים באמצעות המשתנה isPending
.
אם תכנסו לקודסנדבוקס ותשחקו שם עם המספרים תגלו שאומנם עבור 10,000 שורות הקוד עובד ממש בסדר, ככל שמעלים את מספר השורות אנחנו מצליחים לשבור את useDeferredValue
ולמשל ב 90,000 שורות האיטיות כבר מורגשת בעדכון תיבת הטקסט. אולי זה ייפתר בגירסאות עתידיות של ריאקט ובכל מקרה זו תזכורת טובה לכך שתיקון גנרי בפריימוורק בדרך כלל לא יכול לפתור לכולם את הבעיות, וגם בריאקט 18 נצטרך לשים לב למה אנחנו מרנדרים כדי לשמור על ביצועים טובים.
המונח "חוב טכני" (Technical Debt) מתאר מצב שבו אנחנו עושים פשרה במערכת כדי להתקדם מהר יותר. הבחירה במונח "חוב" באה להזכיר לנו שלפשרה הזאת יש מחיר, ושעוד מעט נצטרך "להחזיר" את החוב כלומר לארגן מחדש את הקוד בצורה נכונה. למי שצובר הרבה חובות טכניים יהיה קשה להתקדם ובסופו של דבר יצטרך להכריז על "פשיטת רגל", כלומר לזרוק את כל הקוד ולהתחיל מחדש.
הסיבות הקלאסיות לחוב טכני כוללות-
בחירת פריימוורק שייתן פיתרון מהיר למרות שאנחנו יודעים שהוא לא מספיק גמיש או כולל בעיות ביצועים (היוש פונגאפ)
כתיבת קוד ללא בדיקות או ללא תיעוד
כתיבת קוד שלא מטפל במקרי שגיאה נפוצים
אי שידרוג תלויות כשגירסאות חדשות יוצאות
בכל המקרים האלה אנחנו בוחרים "לוותר" על איכות של קוד כדי להגיע מהר יותר ללקוחות עם מוצר שעובד (גם אם לא עובד מושלם), כדי שנוכל לקבל פידבק על המוצר ואחרי זה להמשיך לסבב תיקונים.
חוץ מהסיבות הקלאסיות לחוב טכני יש עוד שני מקרים שמצדיקים חזרה אחורה ותיקון, למרות שהם הרבה פחות מכוונים:
אחד הפיצ'רים שאני הכי אוהב ב vim הוא רשימת הקפיצות. אם אתם לא מכירים (ואולי גם אם כן) הנה הסבר קצר מה זה ולמה זה טוב.