איך להתמודד עם FormData מתוך קוד Express
אקספרס היא עדיין פריימוורק מאוד פופולרית לבניית שרתים קטנים ב Node.JS ולכן קצת הפתיע אותי לגלות שאין לה תמיכה מהקופסה במידע שנשלח מטפסים בצורה אסינכרונית. בואו נראה את הבעיה בקצרה ופיתרון לא מסובך.
טיפים קצרים וחדשות למתכנתים
אקספרס היא עדיין פריימוורק מאוד פופולרית לבניית שרתים קטנים ב Node.JS ולכן קצת הפתיע אותי לגלות שאין לה תמיכה מהקופסה במידע שנשלח מטפסים בצורה אסינכרונית. בואו נראה את הבעיה בקצרה ופיתרון לא מסובך.
בעבודה עם ריאקט אנחנו אוהבים להגיד שסטייט שייך לקומפוננטה, במובן זה שכשסטייט משתנה ריאקט צריך לחשב מחדש איך נראית הקומפוננטה. בגירסת הקלוז'ר של ריאקט שנקראת Reagent, הכניסו מנגנון נוסף וריאקטיבי לניהול סטייט - לא רק הסטייט הפנימי המשויך לקומפוננטה של useState
, אלא יש להם גם סטייט מיוחד שנקרא r/atom
שיכול להיות מוגדר בכל מקום בתוכנית, ושינוי שלו גם יגרום לחישוב מחדש של הקומפוננטה.
עכשיו הבעיה שלהם הפכה להיות איך לזהות ש r/atom
כזה השתנה, והתשובה היא ריקטיביות. בדומה ל Vue ועוד יותר ממנו ל Solid, גם ריאייג'נט מסתכל בזמן חישוב הקומפוננטה לאיזה אטומים ניגשו, ומחבר אותם לקומפוננטה כך ששינוי בערך האטום יגרום לחישוב מחדש של הקומפוננטה.
לכן הקוד הבא ל Counter עובד:
(defn counter []
;; run on init, skip on re-calculations
(r/with-let [value (r/atom 0)]
[:div
{:style {:margin "10px"}}
[:p (str "Value " @value)]
[:button {:on-click #(swap! value inc)} "+"]
[:button {:on-click #(swap! value dec)} "-"]]))
ומציע מנגנון קלוז'רי הדומה מספיק לריאקט בשביל לא להפחיד.
אבל קלוז'ר היא לא JavaScript ומבנים מסוימים, גם אם יש להם שמות דומים, עשויים להתנהג אחרת. הקוד הבא למשל כבר לא מתנהג כמו שהיינו עשויים לצפות:
(defn textboxes []
(r/with-let [value (r/atom "hello")]
[:div
(for [i (range 5)]
[:input {:value @value
:on-change (fn [ev] (reset! value (.. ev -target -value)))}])]))
הקוד אמור להציג 5 תיבות טקסט כך ששינוי טקסט באחת התיבות יועתק לכל האחרות. זה לא קורה.
הבעיה עם הקוד היא ש for
מייצרת לולאה עצלה. רק בכתיבה ל DOM הלולאה תיפתח ותשוערך. מאחר והגישה היחידה למשתנה האטומי היא מתוך ה for, הפעלה פשוטה של הפונקציה לא תיגש לערך של האטום ולכן כשריאייג'נט יקשיב איזה אטומים מושפעים מהקומפוננטה הוא יפספס את החיבור.
התיקון הוא פשוט אחרי שמבינים את הבעיה - רק צריך להפוך את ה for מלולאה עצלה ללולאה מלאה עם doall והכל מסתדר:
(defn textboxes []
(r/with-let [value (r/atom "hello")]
[:div
(doall
(for [i (range 5)]
[:input {:value @value
:on-change (fn [ev] (reset! value (.. ev -target -value)))}]))]))
מימוש פיתרון לבעיה בתוכנה לוקח זמן, גם במקרה הטוב, ורוב הזמן אנחנו לא במקרה הטוב. מתחילים לכתוב קוד, דברים לא עובדים כמו בספר, מנסים דרך אחרת, קצת שואלים את Chat GPT ומנסים לא להישאב להזיות שלו. בקיצור זה לא נדיר לעבוד שבוע-שבועיים על פיצ'ר ויש פיצ'רים שגם לוקחים יותר.
ברור לנו שבתוך הזמן הזה שעובדים על פיצ'ר יהיו רגעי משבר, אבל אם אין איזה חריגה קיצונית מהלו"ז אנחנו בכל זאת מעדיפים להמשיך קדימה ולהגיע לקוד שעובד. זה השלב שאני קורא לו "נעולים על פיתרון".
והכרחי להינעל על פיתרון כדי לבנות אותו. אם לא נהיה מוכנים לשבור כמה קירות כדי לגרום לפיתרון שלנו לעבוד אז יהיה לנו קשה מאוד לכתוב קוד מעניין. אבל הינעלות על פיתרון בשלב מוקדם מדי בתהליך המחקר עלולה להוביל למלחמה בטחנות רוח - ניסיונות אינסופיים לעקוף בעיות רק בשביל להיתקל בבעיות חדשות.
מתי אם כן כדאי להפסיק להסתכל הצידה ו"להינעל" על פיתרון? מה צריך לוודא לפני שהולכים All In בכתיבת פיצ'ר בדרך מסוימת? זאת הרשימה שלי, מוזמנים להוסיף את שלכם בתגובות-
צריך להיות בטוחים שאפשר לפתור את הבעיה בדרך שאתם רוצים ובצורה יציבה. הכי טוב זה אם אפשר להצביע על מתחרה שעושה דבר דומה או פיתרון קוד פתוח שעובד.
צריך להכיר את המגבלות של הפיתרון. איזה דברים יכולים להשתנות בעולם שבעקבותיהם הוא יפסיק לעבוד. באיזה מצבים הוא ייתן תוצאה לא טובה.
צריך להבין ולסגור את הממשקים של הפיתרון. אם זה פיצ'ר שמשפיע על ממשק משתמש צריך שיהיה לנו את העיצוב והממשק ביד. אם זה פיצ'ר צד-שרת צריך להבין מה יהיו הפרמטרים שיקבל ומה פורמט המידע שיוחזר.
צריך להבין את הבעיה ולהשתכנע שהפיתרון המוצע באמת פותר אותה.
וכן אני יודע שכמה שבועות של תכנות יכולים לחסוך כמה שעות של תכנון, אבל לא בטוח שזו דרך הפעולה המשתלמת ביותר לפרויקט שלכם.
זה מעולה. שווה גם לנסות למצוא אלגוריתם יותר יעיל.
זה מעולה. שווה גם לטפל במצב שנגמר לנו באמצע המקום על הדיסק.
זה מעולה. שווה גם לבדוק איך הפיתרון מתמודד עם עומסים.
זה מעולה. שווה גם לנסות לכתוב בדיקות אוטומטיות.
זה מעולה. שווה גם לחשוב על הבן אדם הבא שיבוא לקרוא את הקוד. תיעוד יותר טוב יכול לעזור.
האתגר הוא לא למצוא רעיונות לשיפור לפעם הבאה, אלא למצוא את המוטיבציה לחזור לקוד שכבר כתבת כדי לשפר אותו. הרבה פעמים בגלל שאנחנו כל כך לא מאמינים שתהיה פעם הבאה אנחנו מנסים לתקן את הכל עכשיו, ונשארים עם Code Reviews ארוכים רק בשביל לא להכניס קוד בינוני למערכת. התוצאה היא השקעה מאסיבית של זמן במהלך כתיבת הקוד בשיפור שלו, לפעמים על חשבון קטעי קוד גרועים יותר במקומות אחרים במערכת או פיצ'רים שהלקוח צריך יותר בדחיפות.
אם אתם תקועים בלופ כזה של פרפקציוניזם, אפשר לנסות לארגן אחרת את המשימות - בכל הכנסה של קוד נוסיף לג'ירה משימות תיקון ספציפיות שאנחנו יודעים שנצטרך, ובכל גירסה נקפיד לסגור 2-3 משימות שידרוג קוד שיצרנו בעבר. כך נקבל יותר שליטה על קצב העבודה ונוכל להשקיע את הזמן בקטעי הקוד הגרועים ביותר במערכת, במקום לשפר דברים שהם מספיק טובים.
ניהול סטייט בריאקט, כלומר חלוקה של היישום לקומפוננטות וההחלטה איזה קומפוננטה שומרת איזה משתני סטייט (ואיזה משתני סטייט מחוברים לסינגלטונים שנשמרים בספריית ניהול סטייט חיצונית), זו המשימה הכי מורכבת וחשובה בבניית ארכיטקטורה ליישום ריאקט. בנייה נכונה כאן וכל השאר מסתדר מעצמו. טעויות עלולות להביא אתכם לאינסוף מעקפים ויקשו על כל צעד בפיתוח.
בתוך זה בואו נראה דוגמה קטנה כדי להמחיש כמה מטעה יכולה להיות האינטואיציה הראשונית ואיך לפעמים צריך לעבוד קשה בשביל לבנות את הפיתרון ההגיוני.
אם הייתם רואים בן אדם רוכב על אופניים פעם ראשונה הייתם יכולים לדמיין שכל מה שיש שם זה לסובב את הפדאלים. כמובן שמספיק לעלות פעם אחת על אופניים בשביל להבין שהפדאלים הם רק חלק קטן מהסיפור - שיווי המשקל, אינסוף תנועות קטנות ולא מורגשות של הגוף שקורות כל הזמן, הן שדואגות שלא ניפול הצידה.
אבל רק בגלל שמבחוץ קשה לראות את העבודה האמיתית לא אומר שהיא לא שם.
כשאנחנו מנסים להשתמש בספריה מסוימת בדיוק כמו שראינו במדריך, ודברים לא עובדים, קל להאשים את הספריה, את המדריך או את העולם. אבל זה קצת כמו להאשים את הרוכב שראינו מהצד.
כשיש באגים שאנחנו לא מבינים בקוד קל להתעצבן ולקפוץ למסקנות לא נכונות, רק בגלל שאנחנו "צריכים" תשובה ועכשיו.
יותר מועיל להסכים להשהות את השיפוט; להקשיב, לחפש רמזים לדברים שאנחנו עושים לא נכון או לא בדיוק לפי ההצעה, להבין שהבן אדם שכתב את הטוטוריאל כנראה לא טיפש ואולי ראה דברים אחרים ממה שאנחנו רואים עכשיו. לפעמים יעזור לכתוב בעצמכם Tutorial חדש רק בשביל להתעמק עוד קצת בדברים שנראים ברורים.
המטרה היא לא למצוא את התשובה הכי מהר, אלא למצוא את התשובה הנכונה.
כשאנחנו מתחילים ללמוד משהו חדש ולפני שהדבר החדש הזה מתחיל לתת תוצאות (כלומר תקופת זמן די ארוכה, כי לימוד של דברים מעניינים לוקח מלא זמן), יש תקופת זמן רגישה בה אנחנו צריכים להשקיע יותר זמן בלימוד, גם על חשבון דברים אחרים שהיינו רגילים לעשות, ובאותו זמן לא רואים עדיין את הערך של אותו הדבר שאנחנו לומדים.
במצבים כאלה אנחנו יכולים להיות קשים עם עצמנו ולראות במבט של החברים, החברים לעבודה או אפילו המשפחה סוג של ביקורת.
״מה את כל היום במחשב? אולי עדיף למצוא עבודה במשהו אחר״
״מה קורה עם הגירסה? שוב נצטרך לדחות את הדדליין?״
״התגלה עוד באג בפרודקשן! עכשיו זה לא זמן ללמוד״
וזה מוזר כי מצד אחד אנחנו יודעים שהאנשים האלה בצד שלנו. הם עודדו אותנו ללמוד את הדבר החדש ועזרו לנו להתחיל. אז מה קורה פה? למה הם לא מפרגנים יותר? למה לא נותנים לי זמן ללמוד את הדבר שאני צריך כדי להתקדם?
כמה מחשבות שאולי יעזרו לעשות סדר-
לפעמים הביקורת שאנחנו שומעים היא רק בראש שלנו. המנהלת ששואלת על הגירסה מבינה שאולי יהיה צריך לדחות את הדדליין, אבל זה המוח שלנו שהופך את ההבנה הזאת לביקורת.
כמו שלנו קשה לעשות את השינוי כך הוא קשה גם לאנשים שסביבנו. בראש הם יודעים שצריך לתת לכם עוד זמן לימוד, אבל ההרגל לשאול על הגירסה או על הבאג לא נעלם בקלות. אי אפשר לצפות מכל הסביבה שלכם להשתנות רק בגלל שאתם החלטתם להשקיע זמן בלימוד משהו חדש.
לפעמים אנחנו שנמצאים בתוך תהליך הלימוד מבינים טוב יותר מהאנשים שמסביב מה הוא דורש. ברור שראש הצוות שלך לא רואה למה את צריכה להשקיע שעתיים-שלוש ביום בשביל ללמוד ריאקט, אבל זה לא אומר שאפשר ללמוד ריאקט בפחות.
האחריות שלנו אם כן כלפי עצמנו היא לבחור את סדרי העדיפויות שמתאימים לנו, ולעזור לסביבה להבין שאלה הדברים שאנחנו צריכים לעשות כדי להתקדם. וכן לשים בצד ביקורת מרומזת או מפורשת. המטרה של כולם הוא שתמצאו את זמן הלימוד שאתם צריכים, אבל אתם היחידים שיכולים לגרום לזה לקרות.
בעבודה עם Azure Devops API מתוך קוד Back End, ה API של Azure Devops צריך לדעת שהיישום שלי מורשה לקבל מידע ועבור איזה משתמש הוא מורשה לקבל את המידע. פיתרון קלאסי לבעיה כזאת הוא OAuth, וזה באמת עובד כשאני כותב אפליקציית ווב, אבל בכתיבת אוטומציה שלא קשורה לממשק גרפי מסוים לא תמיד זה נוח. במקום זה הייתי רוצה לקבל טוקן לצד שרת ותמיד לעבוד איתו.
ב Azure Devops API טוקן כזה כבר לא מונפק ב OAuth אלא בשיטת אימות ישנה יותר שנקראת Basic Auth. ב Basic Auth אנחנו צריכים להעביר שם משתמש וסיסמה בתוך ה Authorization Header של הבקשה, והטוקן משמש בתור הסיסמה. את שם המשתמש משאירים ריק.
כל הסיפור הזה הוא הקדמה לתשובה ב Stack Overflow שמסבירה איך לדבר עם ה API הזה מפייתון:
import requests
import base64
pat = 'tcd******************************tnq'
authorization = str(base64.b64encode(bytes(':'+pat, 'ascii')), 'ascii')
headers = {
'Accept': 'application/json',
'Authorization': 'Basic '+authorization
}
response = requests.get(
url="https://dev.azure.com/jack0503/_apis/projects?api-version=5.1", headers=headers)
print(response.text)
הקוד לא ארוך אבל משאיר שאלה גדולה פתוחה - למה הם הוסיפו נקודותיים לפני הטוקן?
אתם, שקראתם את ההקדמה כבר יודעים. הנקודותיים שם בשביל להפריד בין שם המשתמש הריק לסיסמה (שהיא הטוקן), אבל אני די בטוח שמי שיקרא קוד כזה אחריכם לא ממש יבין על מה מדובר. יכול להיות שזה יוביל לבאגים נוספים, יכול להיות שישכחו לשים את הנקודותיים במקומות אחרים או יכתבו אותם גם איפה שלא צריך. בכל מקרה טוב לא יצא מזה.
בכתיבת קוד אנחנו רוצים לנסות לספר את סיפור הרקע בתוך הקוד במידת האפשר, ואם אי אפשר לצרף הערה. זאת הגירסה שלי לאותו קטע שכבר כן מספרת חלק מסיפור הרקע:
import requests
import base64
def basic_authorization_header(username: str, password: str):
return 'Basic '+str(base64.b64encode(bytes(username + ':'+pat, 'ascii')), 'ascii')
# Personal Access Token received from the web UI
pat = 'tcd******************************tnq'
headers = {
'Accept': 'application/json',
'Authorization': basic_authorization_header(username='', password=pat)
}
response = requests.get(
url="https://dev.azure.com/jack0503/_apis/projects?api-version=5.1", headers=headers)
print(response.text)
נ.ב. כמובן שלא כדאי לשמור את הטוקן בתוך הקוד ויש לקרוא אותו ממשתנה סביבה, אבל זה לא חשוב לסיפור שלנו אז השארתי כמו שמצאתי אותו. נ.ב.ב. כמובן של requests יש תמיכה מובנית ב Basic Auth ולפעמים שווה להשתמש בה. היתרון של הכתיבה המפורשת של ה Headers הוא שכך יותר קל לתרגם את הקוד גם לשפות אחרות אם יהיה צורך.
למה אתה מסתבך עם Deployment דרך קוברנטיס? אצלנו פשוט עושים rsync לשרת והכל עובד.
למה את צריכה לכתוב סקריפט שיבצע את השינוי המבני ב DB בצורה מתועדת? אצלנו אני מתחבר ל DB ומריץ את הפקודות מתוך ה phpmyadmin.
למה אתה מסתבך עם Ansible? בעשרים דקות אני מרים לך שרת לינוקס עם כל התוכנות עליו.
מה פתאום Micro Service בענן? פשוט תבנה Dockerfile ותפעיל docker compose.
למה אתם כל כך מסתבכים עם גיט? יותר קל לשמור גירסאות של הפרויקט בצד כל פעם שמעלים גירסה.
אל תוותרו על השאלות האלה. "למה כל כך מסובך" זו נקודת ההתחלה הטובה ביותר לכניסה לטכנולוגיה חדשה. הדבר החדש מסובך כי הוא צריך לטפל במקרים ושיטות עבודה איתם הדבר הישן לא מצליח להתמודד. רק כשנתחיל לראות את אותם מקרים שהדבר "הפשוט" לא פותר, אפשר יהיה להתחיל להבין את הדבר החדש.
האימרה המפורסמת של דונלד קנות' הולכת ככה-
“The real problem is that programmers have spent far too much time worrying about efficiency in the wrong places and at the wrong times; premature optimization is the root of all evil (or at least most of it) in programming.”
ואפשר להבין את הסיפור בשתי דרכים, שתיהן נכונות. המסר הברור הוא שעדיף לנו למקד את המאמצים שלנו בכתיבת תוכנה יעילה. בהתחלה אין לנו מושג איזה חלקים באמת יעבדו לאט, ולכן שווה לחכות עם האופטימיזציה עד שמשהו באמת נשבר.
המסר היותר עדין הוא שלפעמים דווקא הניסיון לחסוך עלול לסבך אותנו עוד יותר. לא רק במובן שנעבוד יותר קשה בבניית אותה מערכת, אלא גם שנקבל באגים שבלי אותו ניסיון לא היינו חולמים עליהם. ואת זה אנחנו מכירים: שומרים מידע ב Cache רק בשביל לגלות שהאפליקציה מציגה מידע לא עדכני למשתמש כי שכחנו לרענן את ה Cache; מוסיפים useCallback
או useMemo
בריאקט וטועים ברשימת התלויות, כך שהקומפוננטה לא מתעדכנת; בונים מערכת שתדע להתמודד עם עומס חריג ולהעלות קיבולת דינמית עם Micro Services ו Kubernetes רק בשביל לגלות שמרוב חלקים נעים אנחנו מאבדים הודעות ב Message Queue.
הדרך לבניית מוצר תוכנה שעובד היא איטית וארוכה. בנו את הפיצ'רים נכון ובצורה שמאפשרת שידרוג והחלפה בלי לשבור את כל המערכת. את האופטימיזציות והביצועים רצוי להשאיר לסוף.