ToCode
Ir al canal en Telegram
1 419
Suscriptores
Sin datos24 horas
Sin datos7 días
+130 días
Archivo de publicaciones
1 419
<input type="text" value={text} onChange={handleChange} />
<button onClick={reset}>Reset</button>
<button onClick={deleteLastChar}>Delete Last Char</button>
<button onClick={deleteTwoChars}>Delete Two Chars</button>
</div>
</div>
);
}
הניסיון לחסוך את ה render של useEffect נראה כמו תבנית הגיונית, אבל מסתבר שהוא עובד נגד ההגיון של ריאקט. אותו קוד עם useEffect היה כבר כמעט עובד:
export default function App() {
const [text, setText] = React.useState("");
const [resetCount, setResetCount] = React.useState(0);
React.useEffect(() => {
if (text === "") {
setResetCount((c) => c + 1);
}
}, [text]);
function handleChange(e) {
setText(e.target.value);
}
function reset() {
setText("");
}
function deleteLastChar() {
setText((t) => t.slice(0, -1));
}
function deleteTwoChars() {
deleteLastChar();
deleteLastChar();
}
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<div>
<p>reset count = {resetCount}</p>
<input type="text" value={text} onChange={handleChange} />
<button onClick={reset}>Reset</button>
<button onClick={deleteLastChar}>Delete Last Char</button>
<button onClick={deleteTwoChars}>Delete Two Chars</button>
</div>
</div>
);
}
ולמה כמעט? כי עכשיו אנחנו מקבלים הפעלה אחת מיותרת של האפקט בפעם הראשונה שהקומפוננטה מופיעה על המסך, כלומר הערך של resetCount מתחיל מאחד. הדרך המקובלת להתעלם מהאפקט הראשון היא להוסיף משתנה ref:
export default function App() {
const [text, setText] = React.useState("");
const [resetCount, setResetCount] = React.useState(0);
const isFirstEffect = React.useRef(true);
React.useEffect(() => {
// skip the first time
if (isFirstEffect.current) {
isFirstEffect.current = false;
return;
}
if (text === "") {
setResetCount((c) => c + 1);
}
}, [text]);
function handleChange(e) {
setText(e.target.value);
}
function reset() {
setText("");
}
function deleteLastChar() {
setText((t) => t.slice(0, -1));
}
function deleteTwoChars() {
deleteLastChar();
deleteLastChar();
}
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<div>
<p>reset count = {resetCount}</p>
<input type="text" value={text} onChange={handleChange} />
<button onClick={reset}>Reset</button>
<button onClick={deleteLastChar}>Delete Last Char</button>
<button onClick={deleteTwoChars}>Delete Two Chars</button>
</div>
</div>
);
}
מוזמנים לראות את הגירסה האחרונה עובדת בקודסנדבוקס בקישור הבא:
https://codesandbox.io/s/affectionate-lamport-ddrc3y?file=/src/App.js.1 419
# איך לירות לעצמך ברגל בריאקט
בתכנות לירות לעצמך ברגל זה מה שקורה כשאתה משתמש כל כך לא נכון בשפה שאתה עושה לעצמך נזק. בריאקט זה קורה לי בכל מה שקשור לעדכון סטייט ובמיוחד בהעברת הערך הלא נכון לפונקציית ה setState.
ניקח קומפוננטה לדוגמה שמציגה תיבת טקסט וסופרת כמה פעמים הטקסט בתיבה מתאפס, כלומר כל פעם שמישהו מוחק את כל הטקסט מהתיבה ערך המונה עולה ב-1. יודעים מה, ובשביל שיהיה מעניין, ניתן גם כפתור שבלחיצה עליו הטקסט מתאפס:
export default function App() {
const [text, setText] = React.useState("");
const [resetCount, setResetCount] = React.useState(0);
function handleChange(e) {
setText(e.target.value);
if (e.target.value === "") {
setResetCount(resetCount + 1);
}
}
function reset() {
setText("");
setResetCount(resetCount + 1);
}
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<div>
<p>reset count = {resetCount}</p>
<input type="text" value={text} onChange={handleChange} />
<button onClick={reset}>Reset</button>
</div>
</div>
);
}
שימו לב לקוד הכפול בשתי הפונקציות handleChange ו reset. יש שתי דרכים קלות לכתוב אותו בצורה יותר גנרית:
1. אפשר לכתוב פונקציה אחת ששתי הפונקציות יקראו לה, והיא גם תפעיל את setText וגם תבדוק אם הטקסט ריק ותעלה את המונה.
2. אפשר להשתמש ב useEffect שיבדוק אם הטקסט התרוקן ויעדכן את המונה.
היתרון בגישה הראשונה הוא ביצועים. במקום לשנות את הטקסט, לקרוא ל render, לגלות שהטקסט התעדכן ואז לשנות את המונה ושוב לקרוא ל render, אנחנו עושים את הכל בפעם אחת. הקוד אחרי עידכון נראה כך:
export default function App() {
const [text, _setText] = React.useState("");
const [resetCount, setResetCount] = React.useState(0);
function setText(newText) {
_setText(newText);
if (text !== "" && newText === "") {
setResetCount(resetCount + 1);
}
}
function handleChange(e) {
setText(e.target.value);
}
function reset() {
setText("");
}
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<div>
<p>reset count = {resetCount}</p>
<input type="text" value={text} onChange={handleChange} />
<button onClick={reset}>Reset</button>
</div>
</div>
);
}
עובד? עובד. אבל במקום לקבל פיתרון גנרי, הטמנו בקוד מוקש.
כי עכשיו בואו נוסיף כפתור שמוחק את התו האחרון מהטקסט. ביום רגיל אני יודע שבקריאה ל setter שתלוי בערך הקודם כדאי לי להעביר ל setter פונקציה, אבל הפעם כבר יש לי פונקציית setText שיודעת לטפל בקשר בין שני משתני הסטייט, ואני רוצה להמשיך להשתמש בה, אז אני עלול לקחת את הצעד הראשון לכיוון האבדון:
function deleteLastChar() {
setText(text.slice(0, -1));
}
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<div>
<p>reset count = {resetCount}</p>
<input type="text" value={text} onChange={handleChange} />
<button onClick={reset}>Reset</button>
<button onClick={deleteLastChar}>Delete Last Char</button>
</div>
</div>
);
ולמה צעד לכיוון האבדון? כי הקוד עדיין עובד אבל המוקשים רק מתרבים. אם עכשיו אני רוצה להוסיף כפתור למחיקת שני תווים - שכמובן ישתמש בפונקציה שכבר יש לי - אז הקוד יפסיק לעבוד:
export default function App() {
const [text, _setText] = React.useState("");
const [resetCount, setResetCount] = React.useState(0);
function setText(newText) {
_setText(newText);
if (text !== "" && newText === "") {
setResetCount(resetCount + 1);
}
}
function handleChange(e) {
setText(e.target.value);
}
function reset() {
setText("");
}
function deleteLastChar() {
setText(text.slice(0, -1));
}
function deleteTwoChars() {
deleteLastChar();
deleteLastChar();
}
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<div>
<p>reset count = {resetCount}</p>1 419
# התער של אוקאם
התער של אוקאם הוא עיקרון פילוסופי שטוען שכאשר קיימים הסברים שונים לאותה תופעה יש להעדיף את הפשוט מביניהם, שבו המספר המועט ביותר של מושגים וחוקים.
גם אם בעולם המדעי או האמיתי לא תמיד ברור איך ליישם עיקרון זה (או אם בכלל כדאי), בעבודה על קוד פיתרונות פשוטים הם יותר קלים לתחזוקה ויותר עמידים לטווח הרחוק. אם לדוגמה יש לי בעיה עם קומפוננטת UI מסוימת אני אנסה לפתור אותה בשלבים הבאים:
1. אולי בתוך אותה הספריה אני יכול להעביר פרמטר שיגרום לקומפוננטה להתנהג כמו שאני רוצה.
2. אולי אני יכול לדרוס חלק מהמראה של הקומפוננטה באמצעות שינוי CSS.
3. אולי אני יכול בקוד שלי לבנות קומפוננטה חדשה מתוך הקומפוננטה הקיימת, שמרחיבה אותה בצורה שאני רוצה.
4. אולי אני יכול ליצור Fork לספריה ולהוסיף לקומפוננטה מבפנים את המנגנון שאני צריך (בתקווה בלי לשבור שום דבר אחר).
5. אולי אני יכול להשתמש בקומפוננטה מקבילה מספריית UI אחרת.
6. אולי אני יכול לשנות את כל היישום שלי כך שישתמש בספריית UI אחרת שבה הקומפוננטה מתנהגת כמו שאני רוצה.
הרבה פעמים הפיתרון הפשוט - אלה שכאן בראש הרשימה - דורש יותר מחשבה ודיוק מאשר פיתרונות מורכבים יותר, ולפעמים כתיבה של כמה שורות CSS שיפתרו בעיה יכולה לקחת משמעותית יותר זמן מאשר לעשות פשוט fork לספריה ולתקן את הבעיה שם. הייתרון של הפיתרון הפשוט הוא לא זמן העבודה הקצר יותר ל Delivery, אלא היותו יציב יותר וקל יותר לתחזוקה בטווח הארוך.
1 419
# פירוק מוקשים
אם אני עובר בבית ליד חתיכת לגו קטנה שזרוקה על הריצפה אני מיד ארים אותה. במקרה הטוב אחזיר אותה לקופסה, אבל בכל מקרה גם אם אני לא בטוח איפה הקופסה אני אעדיף לשים אותה על שולחן או משהו - כי אני יודע שאחרת אדרוך עליה בטעות כשאקום לילדים בלילה.
את אותו הגיון הרבה יותר קשה ליישם בקוד.
בכל מערכת אנחנו נתקלים באינסוף קטעי קוד עם מוקשים: משהו שנראה כמו באג (אבל בכל זאת עובד ולא ברור איך), פונקציה לא מתועדת, מנגנון שאין לו בדיקות, קוד מסורבל, ארוך ולא מספיק ברור, או אפילו תקלת אבטחה שאי אפשר לנצל אותה.
וכל פעם שרואים כזה מוקש האינסטינקט הראשון הוא להתרחק. אתה מספר לעצמך סיפורים כמו "זה לא הזמן עכשיו לתקן את זה", "אולי מישהו כתב את זה ככה בכוונה", "אם זה עובד לא נוגעים", "אני לא מבין מספיק טוב מה הקוד צריך לעשות ופוחד לשבור". בגלל שאנחנו לא יודעים איפה המקום הנכון של חתיכת הלגו, אנחנו משאירים אותה על הריצפה.
קוד טוב הוא קוד חי - ובשביל שקוד יהיה חי צריך להרגיש נוח לשנות אותו. גם אם בהתחלה תגלו שה"תיקונים" שלכם רק שוברים יותר דברים, לאט לאט וככל שתתרגלו לפרק מוקשים גם התיקונים שלכם ישתפרו וגם תרגישו הרבה יותר בנוח לעבוד על כל המערכת.
1 419
# טיפ קוד ריוויו: התמקדו בדבר אחד
הרבה פעמים כשחברים או תלמידים הגיעו להראות לי קטע קוד מסוים הסתכלתי בטעות קודם כל על עצמי, והתיחסתי לזה כמו משחק: האם אני מסוגל למצוא את כל הבעיות בקוד? האם יהיו לי רעיונות מספיק טובים לתיקון או שיפור? או לפעמים "בואו נמצא את כל הדרכים בהן אפשר לשפר את הקוד".
חשבתי גם שאנשים מעדיפים לקבל פידבק כמה שיותר מפורט, ושהם כבר יארגנו לעצמם את הזמן וההתמודדות עם אותו פידבק. כמו מוכר תפוחים שמסיים את העבודה שלו אחרי שהוא נותן לך את התפוחים. זאת לא בעיה שלו אם אתה יודע או לא יודע איך מכינים עוגה.
הבעיה עם הדרך הזאת היא ההתמקדות באינטרקציה אחת והמחשבה שהאינטרקציה תהיה חד-פעמית. זה לא תמיד בריא בקונטקסט של קורס, וזה מאוד לא בריא בקונטקסט של מנטורינג או עבודה עם מתכנתים צעירים אצלכם בחברה.
רעיון שאני משחק איתו לאחרונה ובינתיים עובד לא רע הוא להסתכל על Code Review בתור תהליך ולהתמקד כל פעם בנקודה אחת. זה אומר שאם אתם יושבים לעבור על קוד של חבר ורואים בו גם שימוש באלגוריתם לא מספיק יעיל, גם חוסר עקביות בשמות משתנים, גם חלוקה לא מספיק טובה לפונקציות, גם בעיית אבטחה וגם שאותו חבר שכח לכתוב בדיקות - אתם לא תגידו את כל הדברים ב Code Review. במקום זה, תבחרו דבר אחד שאפשר לתקן ותגיבו רק עליו. בפעם הבאה תמשיכו לדבר על אותו דבר עד שהבעיה הזאת נפתרת בכל קוד עתידי שהחבר הצעיר שלכם כותב, ורק אז תמשיכו לדבר על הדבר הבא.
האם זה יפתור את כל בעיות העולם? כנראה שלא. אבל יכול להיות שגישה כזאת תשפר בקצת את הדינמיקה אצלכם בחברה ותגרום לאנשים לרצות יותר לשמוע את הדעה שלכם.
(ומצד שני גם יכול להיות שלא. אני רק משחק עם הרעיון הזה כמה שבועות. אם אתם גם מנסים או ניסיתם משהו דומה אשמח לשמוע איך הלך לכם בתגובות).
1 419
# דברים שהיו קשים ועכשיו קלים ואנחנו
ג׳וליה אבנס פירסמה אתמול פוסט מוצלח על דברים שפעם היו קשים ועכשיו קלים. אלה עיקרי הדברים עם כמה תוספות שלי:
תעודות SSL (טירוף)
זיהוי תמונה (הבדיחה הזאת למשל כבר לא רלוונטית)
להרים שרת בענן
פיתוח ממשק משתמש חוצה פלטפורמות
העברת קוד כולל כל התלויות ממכונה למכונה (היוש דוקר)
הקמת תשתית CI/CD מלאה בחינם (היוש גיטהאב)
יישומי ווב בזמן אמת (זוכרים כשהצ'ט בג'ימייל היה נראה כמו הברקה טכנולוגית?)
זירו דאונטיים דיפלוימנטס
למרכז דברים ב CSS
תמיכה בדפדפנים ישנים (כן היה פעם דבר שנקרא IE)
מה שמעניין לראות כאן זה איך השינויים הטכנולוגיים האלה אומנם שינו את המוצרים שאנחנו בונים, אבל לא שינו את העבודה שלנו. זה לא שעכשיו מתכנתים יכולים לשבת בים והקוד כותב את עצמו. אנחנו עדיין פותרים את אותם באגים, מתמודדים עם אותם דדליינים ועם אותם דמואים שמתרסקים תמיד ברגע הלא נכון.
ואולי יש בזה משהו מעודד. כן, יהיו עוד אינסוף טכנולוגיות חדשות שנצטרך ללמוד, אבל הניסיון שאנחנו צוברים הוא בעל ערך. העבודה האמיתית - שהיא היכולת להתמודד עם אתגרים, ללמוד, לפתור בעיות חכמות בדרכים יצירתיות - זה לא הולך לשום מקום. רק הכלים ישתנו.
1 419
# מה האילוצים שלי
כשאנחנו מסתכלים על קטע קוד בפעם הראשונה ולפני שמתחילים אפילו לשאול "איך הוא פותר בעיה מסוימת", צריך לוודא שאנחנו מסכימים על המטרות.
קוד שצריך להיכנס לפריימווק צריך לספק אמינות וביצועים ברמה הרבה יותר גבוהה מקוד אפליקטיבי רגיל. מצד שני לא יקרה כלום אם הוא יהיה קצת יותר קשה להבנה או קשה לתחזוקה, כי ממילא תדירות השינוי שם נמוכה.
קוד שאני כותב בדוגמה בפוסט בבלוג צריך להיות קצר, מדויק ולהעביר את המסר שרציתי להעביר בפוסט. לא יקרה כלום אם האלגוריתם לא יהיה הכי יעיל.
או במחשבה על בדיקות - יש בדיקות יחידה שהמטרה שלהן היא לתת פידבק כמה שיותר מהיר כדי שהרצת הבדיקות תהיה מיידית ולא תפריע לתהליך הפיתוח. ויש בדיקות אינטגרציה או בדיקות מערכת בהן אני מוכן להתפשר על מהירות הריצה אבל חשוב לי לקבל תוצאה כמה שיותר אמינה, כדי לא להעביר לייצור מערכת עם באג.
(וזאת הבעיה המרכזית שלי עם העתקת קטעי קוד מסטאק אוברפלו. מי שכתב אותם לא יודע מה האילוצים שלי ועם איזה פשרות אני מוכן לחיות).
במקום להביא את שיטת העבודה שלכם לכל קוד שאתם מגיעים לתחזק, עיצרו קודם לשאול - מה האילוצים של אותו קוד, איך האילוצים האלה באים לידי ביטוי בקוד שכבר קיים, ורק אחרי זה הציעו תיקון שייקח בחשבון את אותם האילוצים.
1 419
# שכתוב דוגמת תקשורת ב ClojureScript שכתבתי לפני שנתיים
בפוסט ארבע תבניות לקומפוננטות React מתורגמות ל ClojureScript ו Reagent הראיתי איך לעבוד עם ריאייג'נט ולכתוב קומפוננטות ריאקט ב clojurescript. הדוגמה האחרונה באותו פוסט, וזו שבגללה שמרתי מרחק מריאייג'נט בשנתיים האחרונות היתה דוגמה לתקשורת ajax. היא היתה שונה מקוד שאני רגיל לכתוב בריאקט, ואולי בגללי ואולי בגלל ריאייג'נט עבדה פחות טוב מקוד ג'אווהסקריפט מקביל שהייתי כותב. הקושי העיקרי שלי בתרגום הקוד מ JavaScript לריאייג'נט היתה חוסר התמיכה ב Hooks, ובפרט useEffect שהיה לי מאוד חסר.
שנתיים עברו ו Hooks קיבלו תמיכה מלאה ומפנקת בריאייג'נט וזאת היתה בשבילי הזדמנות לכתוב מחדש את הדוגמה ההיא בקוד שיראה הרבה יותר ידידותי למתכנתי JavaScript. אז בלי יותר מדי הקדמות בואו נראה את שתי הגירסאות.
## גירסה ישנה שלא עבדה מלפני שנתיים
בפוסט שכתבתי לפני שנתיים הראיתי את הגירסה הבאה שלא עבדה:
(defn pokemon-character [{id :id}]
(r/with-let [
data (r/atom {})
active (r/atom true)
request (go
(let [response (<! (http/get
(str "https://pokeapi.co/api/v2/pokemon/" @id)
{:with-credentials? false}))]
(if (true? @active) (reset! data (:body response)) nil)))
]
[:div
[:p "ID: " @id]
[:p "Name: " (str (get @data :name))]]))
הבעיה איתה היא שהפקודה with-let קורית רק פעם אחת, ולכן כשה id ישתנה לא תצא בקשת תקשורת חדשה. בעיה קטנה נוספת היא השימוש ב request שזו ספריית תקשורת של cljs, במקום בקוד התקשורת המוכר של הדפדפן.
## גירסה שעובדת היום
עם התמיכה ב hooks ושיפור החיבור בין clojurescript לדפדפן הצלחתי לשכתב את הקוד ולהגיע לגירסה הבאה שאני די אוהב:
(defn use-pokemon [id]
(let [[data set-data] (react/useState {})
active? (r/atom true)]
(react/useEffect (fn []
(go
(let [response (<p! (js/fetch
(str "https://pokeapi.co/api/v2/pokemon/" id)))
data (<p! (.json response))]
(if @active? (set-data data))))
(fn []
(reset! active? false)))
(array id))
data))
בואו נראה מה יש פה:
1. שימוש ב useState כדי לשמור את המידע שחוזר מהשרת, עובד בדיוק כמו ב JavaScript.
2. המשתנה active? מוגדר בתור אטום כדי שאפשר יהיה לשנות את הערך שלו. בדרך כלל משתנים ב Clojure הם Immutable, אבל פה באמת הייתי צריך Mutable Data בשביל לאפשר ביטול של האפקט לפי הממשק של useEffect.
3. הקריאה ל useEffect נראית בדיוק כמו ב JavaScript. הפרמטר הראשון הוא פונקציית האפקט, שמחזירה פונקציית ביטול, והפרמטר השני הוא מערך התלויות.
4. קוד התקשורת משתמש ב fetch API הרגיל של הדפדפן. המאקרו go הוא המקבילה הקלוז'רסקריפטית ל async והמאקרו "קטן מ p ואז סימן קריאה" הוא המקבילה הקלוז'רסקריפטית ל await.
וזה בסך הכל יפה כי עכשיו אני יכול להשתמש בפונקציה use-pokemon בתוך קומפוננטה באופן הבא:
(defn pokemon [id]
(let [
data (use-pokemon id)
]
[:div
[:p "ID: " (.stringify js/JSON id)]
[:p "Name " (aget data "name")]]))
והכל עובד והקומפוננטה נשארת נקיה וממוקדת.
מוזמנים לראות את הקוד המלא עם כל ה require-ים בגיסט: https://gist.github.com/ynonp/57c429315d282a82b145a0b94db92cb71 419
1. הפקודה
r/atom יוצרת אטום, שזה דבר ששומר מידע שיכול להשתנות. רוב המידע בקלוז'ר הוא Immutable, ובדרך כלל יהיה לנו אטום גדול או מספר אטומים כדי להחזיק את המידע המשתנה של היישום. אפשר לחשוב על האטום כמו Store ברידאקס או מובאקס.
2. האטום המרכזי אצלי בתוכנית הוא רשימת הקלפים. יש גם אטום שמטפל ב timeout כדי שבלחיצה על קלף אחרי שכבר יש שניים גלויים נקצר את ה timeout ומיד נסתיר את מה שגלוי.
3. הפונקציה click-on, שכנראה היתה צריכה להיקרא handle-click, היא נקודת הכניסה לקוד והיא נקראת כשמשתמש לוחץ על קלף. היא מקבלת את ה id של הקלף, בודקת מה מצב העניינים במשחק ומעדכנת את מאפייני הקלפים. היא נעזרת בפונקציות העזר האחרות שכתובות בקובץ.
בקובץ ה UI עדכנתי את הקוד ל home-page כך שיציג את רשימת הקלפים ואחרי השינוי הוא נראה כך:
(defn home-page []
(fn []
[:span.main
[:h1 "Welcome to my-reagent-app"]
[:ul {:class "cards"}
(for [item @cards/cards]
^{:key (:id item)}
[:li {
:class [(if (:visible item) "visible" "hidden")
(when (:found item) "found")]
:on-click #(cards/click-on (:id item))
} (:value item)])]]))
אני מאוד אהבתי את הכתיב של reagent. אני חושב שהוא הרבה יותר נקי וקל לתחזוקה בהשוואה ל jsx. כל קומפוננטה מיוצגת על ידי רשימה שמתחילה ב keyword כלשהו, אחר כך hash-map של מאפיינים ובסוף הטקסט או הילדים של האלמנט.
## מה הלאה
אתם יכולים למצוא את משחק הזיכרון המלא שלי בגיטהאב בקישור: https://github.com/ynonp/cljs-memory-game.
ואם אתם רוצים לדעת יותר על ריאייג'נט שווה לקרוא את המדריך הקצר שלהם בקישור:
https://reagent-project.github.io/.1 419
# משחק זיכרון ב ClojureScript
בפעם הקודמת ששיחקתי עם קלוז'רסקריפט וריאייג'נט ירדתי ממנו בגלל שלא היתה תמיכה ב Hooks. מאז הבנתי שהוסיפו תמיכה (למרות שלא ניסיתי עדיין), ורציתי לנצל קצת זמן פנוי כדי להעיף מבט נוסף בכלים. התוצאה היתה משחק זיכרון שעבד לא רע בכלל ואפילו היה נעים לפתח. בואו נראה את הקוד.
## איך מתחילים
בשביל לקבל סטארטר לכתיבת אפליקציית ריאייג'נט (שזה הריאקט של קלוז'רסקריפט) אתם צריכים להתקין כלי בשם lein מכאן:
https://leiningen.org/
ואחר כך מהמסוף כותבים:
$ lein new reagent my-reagent-app
זה נותן תבנית לפרויקט עם מספר דפים. קובץ הממשק נקרא src/cljs/my_reagent_app/core.cljs. בהתחלה יש בו הרבה תוכן לא רלוונטי ואפשר למחוק את הרוב ולהישאר עם הקוד הזה בשביל לקבל יישום פשוט:
(ns my-reagent-app.core
(:require
[reagent.core :as reagent :refer [atom]]
[reagent.dom :as rdom]
[my-reagent-app.util :as util]
[my-reagent-app.lib.cards :as cards]))
(defn home-page []
(fn []
[:span.main
[:h1 "Hello World"]]))
(defn mount-root []
(rdom/render [home-page] (.getElementById js/document "app")))
(defn init! []
(mount-root))
מתיקיית הפרויקט שנוצר מפעילים:
$ npm install
$ npx shadow-cljs watch app
ואז אפשר להיכנס בדפדפן לפורט 3000 על localhost כדי לראות את הדף הראשון והפשוט מהאפליקציה שבסך הכל כותב Hello World.
## קוד למשחק זיכרון
אחרי שהיתה לי את התבנית המשכתי לכתוב את קוד הקלוז'ר למשחק ושמרתי אותו בקובץ src/cljs/my_reagent_app/lib/cards.cljs.
משחק זיכרון כולל חבילה של קלפים, כך שכל קלף הוא אוביקט עם המפתחות id, value, found ו visible. המשמעות של value היא המספר שכתוב על הקלף (במשחק זיכרון יש זוגות של קלפים עם אותו מספר), המפתח visible מתאר האם עכשיו הקלף גלוי או מוחבא, המפתח found מתאר האם מצאתי את הקלף הזה ואת הזוג שלו והמפתח id כולל מזהה ייחודי לקלף.
כל הקלפים מתחילים מוסתרים, לחיצה על קלף מציגה אותו, לחיצה על קלף נוסף מציגה גם אותו, ואז אם שני הקלפים זהים הם יישארו גלויים וה found של שניהם יוגדר ל true, ואם הם שונים אז שניהם יוסתרו חזרה אחרי שתי שניות.
זה הקוד בקלוז'ר למשחק:
(ns my-reagent-app.lib.cards
(:require [reagent.core :as r]))
(def cards (r/atom (shuffle
(map-indexed (fn [id v]
{
:id id
:value v
:found false
:visible false})
(flatten (take 2 (repeat (range 5))))))))
(def timeout (r/atom nil))
(defn show [id]
(fn [item]
(if (== (:id item) id)
(conj item {:visible true})
item)))
(defn new-turn [cards]
(map #(conj % {:visible false}) cards))
(defn reveal [card1 card2]
(fn [cards]
(map #(if (or
(== (:id %) (:id card1))
(== (:id %) (:id card2)))
(conj % {:found true :visible false})
%) cards)))
(defn check-pairs [id]
(let [other (first (filter :visible @cards))
this (first (filter #(== (:id %) id) @cards))]
(if (== (:value other) (:value this))
(swap! cards (reveal other this))
(do
(swap! cards #(map (show id) %))
(reset! timeout (js/setTimeout #(swap! cards new-turn) 2000))))))
(defn click-on [id]
(if (== 2 (count (filter :visible @cards))) (do
(js/clearTimeout @timeout)
(swap! cards new-turn)))
(let [card (first (filter #(== (:id %) id) @cards))
in-turn (some :visible @cards)]
(cond
in-turn (check-pairs id)
(:found card) false
(:visible card) false
:else (swap! cards #(map (show id) %)))))
אז נכון זה רק 54 שורות אבל בכל זאת אם לא מכירים קלוז'ר חלק מהקוד יכול לבלבל. אלה עיקרי הדברים ובקצרה:1 419
# ארגון מחדש
בואו נניח שאתם צריכים לכתוב קוד שקורא url-ים של דוקרהאב ומדפיס את שם המחבר ושם הריפו מה url. כלומר אתם תתנו לו את המחרוזת:
https://hub.docker.com/r/bitnami/postgresql
והקוד יחזיר את המילון:
{ 'author': 'bitnami', 'repo': 'postgresql' }
יודעים מה? עזבו, הנה חסכתי לכם את המאמץ, זאת הפונקציה:
import re
import sys
def parse_dockerhub_url(url: str):
res = re.search(r'https://hub.docker.com/r/([\w%-]+)/([\w%-]+)', url)
if res:
author, repo = res.groups()
return { 'author': author, 'repo': repo }
print(parse_dockerhub_url(sys.argv[1]))
כולם מרוצים הקוד הגיע לפרודקשן ואז לקוח גדול מתקשר להתלונן על באג. מסתבר שאצלם משתמשים הרבה ב Official Images, שזה אימג'ים שה url שלהם נראה ככה:
https://hub.docker.com/_/ubuntu
הקוד שלי לא מזהה את המבנה כי אני מחפש את ה /r/ לפני שם הריפו, ולכן הם לא מצליחים להזין את הנתיבים שלהם בתוכנה.
מה עושים?
הרבה מתכנתים שמקבלים משימה כזאת מחפשים את הדרך המהירה ביותר לפיתרון. במקרה שלנו הכי קל לשכפל את הקוד בפונקציה ולשנות את התנאי:
import re
import sys
def parse_dockerhub_url(url: str):
res = re.search(r'https://hub.docker.com/r/([\w%-]+)/([\w%-]+)', url)
if res:
author, repo = res.groups()
return { 'author': author, 'repo': repo }
res = re.search(r'https://hub.docker.com/_/([\w%-]+)', url)
if res:
author, repo = ["Official Image", res.group(1)]
return { 'author': author, 'repo': repo }
print(parse_dockerhub_url(sys.argv[1]))
זה עובד ואפשר להמשיך לתקלה הבאה.
כן?
רגע.
אומנם תיקנתם את הבאג אבל נראה לי ששברתם את הפונקציה.
בגירסה הקודמת, אם מישהו היה רוצה לשנות את המפתחות במילון שהפונקציה מחזירה היה עליו לשנות רק במקום אחד. בגירסה החדשה אם במקום author נרצה לכתוב owner נצטרך לשנות בשני מקומות. ומה שיותר גרוע, שיצרתם כאן תבנית. הבן אדם הבא שיצטרך להוסיף עוד url ששכחתי לטפל בו גם יעתיק את הקוד ורק יהפוך את שינוי שמות המפתחות לעוד יותר קשה.
אני מבין לגמרי את הדחיפות לתקן את הבאג. אני מבין גם שבזמן שהלקוח צלצל להתלונן על התקלה הזאת עבדתם על 5 דברים אחרים, אולי יותר חשובים. אני מבין שקשה לתקן קוד וגם לארגן אותו מחדש באותו PR, ושאולי כרגע אין לכם רעיון איך לתקן את זה בצורה שתשמר את הרוח של הפונקציה המקורית.
ועם כל זה אני חושב שיש פה רק שתי אפשרויות שיעבדו לטווח הרחוק:
1. אפשר להחליט שאנחנו מארגנים מחדש את הקוד כשאנחנו מתקנים באגים, ואז "תיקון" כמו שכתבתי קודם בכלל לא נכנס למערכת.
2. אפשר להחליט שאנחנו מכניסים תיקונים מהירים, אבל מקדישים כמה שעות בשבוע כדי לתקן את התיקונים, ולהחזיר אותם לרוח המקורית של הקוד.
קוד הופך להרגל. אף אחד לא יצא ל"פרויקט ריפקטורינג" בשביל לתקן כפל קוד של שתי שורות, אבל הדברים האלה מצטברים ולאורך זמן קשה לתקן אותם.
נ.ב. יש לכם המון רעיונות איך לארגן מחדש את ה"תיקון"? אל תשמרו אותם בלב - עופו על עצמכם ושתפו בתגובות. אני בכוונה לא משתף את הרעיונות שלי כדי לא לקלקל.
¡Ya disponible! Investigación de Telegram 2025 — los principales insights del año 
