es
Feedback
ToCode

ToCode

Ir al canal en Telegram

טיפים קצרים למתכנתים מאת ינון פרק

Mostrar más
1 420
Suscriptores
Sin datos24 horas
+37 días
-430 días
Archivo de publicaciones
ToCode
1 421
We use the same configuration as Parcel to bundle this sandbox, you can find more
    info about Parcel 
    <a href="https://parceljs.org" target="_blank" rel="noopener noreferrer">here</a>.
  </div>
  `;  
}

window.addEventListener('pagehide', (ev) => {
  countdown = 10;
  document.getElementById("app").innerHTML = `${countdown}`
});

window.addEventListener('pageshow', (ev) => {
  setTimeout(tick, 0);
});

ToCode
1 421
# טיפים לעבודה יעילה עם bfcache אחד הטריקים של דפדפן כדי להציג עמודים מהר יותר נקרא bfcache. זה בעצם זיכרון מטמון בו הדפדפן שומר את העמוד ואת כל מצב ה JavaScript של העמוד ביציאה ממנו, כאילו העמוד עדיין פעיל באיזה "טאב" נסתר. בלחיצה על כפתור "אחורה" הדפדפן מושך מהמטמון את העמוד ואת כל זיכרון ה JavaScript כדי שמשתמשים יוכלו להמשיך לעבוד על העמוד במהירות ומאותה נקודה. בתור מתכנתים אנחנו שמחים על כל התנהגות של הדפדפן שמאיצה את האתרים שלנו, אבל גם צריכים להיות מודעים לכמה אתגרים ספציפיים סביב טעינת עמוד מ bfcache. ## המחשת השימוש ב bfcache היכנסו לקישור הבא: https://cdpn.io/pen/debug/JjegWao תוכלו לראות שם מספרים יורדים מ 9 עד 1, ואז במקום 0 יופיע טקסט ומתחת לטקסט יש לינק. לוחצים על הלינק ואז לוחצים "אחורה" בדפדפן, ותוכלו לראות שהדפדפן לא יריץ את המספרים אחורה שוב וישר יציג את הטקסט, בדיוק מהנקודה בה עצרנו. זה קוד העמוד אגב מוטמע כאן: <iframe height="300" style="width: 100%;" scrolling="no" title="Untitled" src="https://codepen.io/ynonp/embed/JjegWao?default-tab=html%2Cresult" frameborder="no" loading="lazy" allowtransparency="true" allowfullscreen="true"> See the Pen <a href="https://codepen.io/ynonp/pen/JjegWao"> Untitled</a> by Ynon Perek (<a href="https://codepen.io/ynonp">@ynonp</a>) on <a href="https://codepen.io">CodePen</a>. </iframe> ## איך יודעים אם העמוד נטען מה Cache דפדפנים מספקים לנו API כדי להבין איך העמוד נטען והאם הוא הגיע מה cache. האירוע pageshow יישלח ל window מיד אחרי load ובכל פעם שהעמוד נטען מה Cache. לאירוע זה יש מאפיין בשם persisted, ואם ערכו true אנחנו יודעים שהעמוד הגיע מה cache. בעזרת אירוע זה אני יכול להציג הודעה ספציפית למשתמש שהגיע אחרי לחיצה על כפתור "אחורה", כמו בדוגמה הבאה: https://cdpn.io/pen/debug/BaGXWGJ קוד הדוגמה:
let countdown = 10;

function tick() {
  countdown -= 1;
  if (countdown > 0) {
    document.getElementById("app").innerHTML = `${countdown}`;
    return setTimeout(tick, 1000);
  }

  document.getElementById("app").innerHTML = `
  <h1>Hello Vanilla!</h1>
  <div>
    We use the same configuration as Parcel to bundle this sandbox, you can find more
    info about Parcel 
    <a href="https://parceljs.org" target="_blank" rel="noopener noreferrer">here</a>.
  </div>
  `;
}

window.addEventListener('pageshow', (ev) => {
  console.dir(ev);
  if (ev.persisted) {
    // page loaded from cache after "back"
    document.getElementById("app").innerHTML = `Did you find what you were looking for?`
  }
});

setTimeout(tick, 0);
## איך לגרום לעמוד לא להיטען מה Cache יש מספר דברים שאנחנו יכולים לעשות בעמוד שימנעו מדפדפן לשמור את העמוד שלנו ב bfcache, ולכן רוב הזמן אלה דברים שנרצה להימנע מהם: 1. רוב הדפדפנים לא ישמרו את העמוד ב bfcache אם מוגדר קוד טיפול באירוע unload. 2. דפדפן פיירפוקס לא ישמור את העמוד ב bfcache אם מוגדר קוד טיפול באירוע beforeunload. 3. דפדפנים לא ישמרו את העמוד ב bfcache אם השרת שולח את הכותרת Cache-Control: no-store. 4. חלק מהדפדפנים לא ישמרו עמוד ב bfcache אם יש לו חיבורים פתוחים או בקשות רשת פתוחות (לדוגמה חיבור ל IndexDB, ממתין לתשובה של fetch, ממתין לתשובה של XMLHttpRequest או מחזיק WebSocket פתוח). אירוע בשם pagehide יישלח לעמוד שלכם בדיוק לפני שמשתמשים גולשים החוצה ממנו, ואתם יכולים להשתמש באירוע זה כדי לסגור חיבורים ולבטל בקשות פתוחות. בדוגמה האחרונה אני משתמש באירוע pagehide כדי להחזיר את שעון ה 10 שניות למצבו המקורי, ומתחיל להריץ את השעון אחורה באירוע pageshow במקום בטעינה של העמוד. בצורה כזאת העמוד עדיין נטען מה bfcache, אבל בניווט אחורה אליו משתמשים יצטרכו לחכות שוב 10 שניות עד שיראו את התוכן: https://cdpn.io/pen/debug/vYQoxMx הקוד:
let countdown = 10;

function tick() {
  countdown -= 1;
  if (countdown > 0) {
    document.getElementById("app").innerHTML = `${countdown}`;
    return setTimeout(tick, 1000);
  }

  document.getElementById("app").innerHTML = `
  <h1>Hello Vanilla!</h1>
  <div>

ToCode
1 421
# הטיפוס המינימלי שימו לב לפונקציה הבאה בפייתון שמקבלת אורך מינימלי ורשימה של מחרוזות, ומחזירה רק את המחרוזות שאורכן גדול יותר מהפרמטר הראשון:
def longer_than(n: int, items: list[str]) -> list[str]:
 return [I for I in items if len(i) > n]
המימוש מדויק אבל בדיקת הטיפוסים עלולה לבלבל. הפעלה כזאת עובדת מבחינת קוד, אבל זורקת שגיאה על טיפוסים:
print(longer_than(3, ((1, 2, 3), (2, 3, 4, 5, 6), range(10))))
וזה חבל כי המטרה של בדיקת טיפוסים היא לא לשבור קוד שעובד אלא לעזור לנו לכתוב קוד נכון. דרך קלה לשפר את המצב היא לעבור להשתמש בטיפוס המינימלי שאני צריך, כלומר במקום לקחת רשימה של מחרוזות אני אבקש Iterable של Sized:
def longer_than(n: int, items: Iterable[Sized]) -> list[Sized]:
 return [I for I in items if len(i) > n]


print(longer_than(3, ((1, 2, 3), (2, 3, 4, 5, 6), range(10))))
וזה כבר עובד מצוין מבחינת בדיקת הטיפוסים אבל עדיין יש בעיה עם ערך ההחזר. הקוד הבא זורק שגיאת טיפוסים, למרות שברור שהוא עובד:
long_items = longer_than(3, ((1, 2, 3), (2, 3, 4, 5, 6), range(10)))
print(long_items[0][0])
הבעיה היא ש long_items הוא Iterable של Sized (כי זה מה שהפונקציה מחזירה), ו Sized לא מאפשר גישה דרך אינדקס. האמת היא ש long_items הוא בכלל Iterable של tuple, ולכן הקוד כן עובד אבל מערכת הטיפוסים לא רואה את זה. הפיתרון כמו שאני מקווה שאתם כבר יודעים הוא TypeVar שמוכן לקבל כל טיפוס יותר ספציפי מ Sized, כלומר:
T = TypeVar('T', bound=Sized)  
def longer_than(n: int, items: Iterable[T]) -> list[T]:
 return [I for I in items if len(i) > n]

long_items1 = longer_than(3, ((1, 2, 3), (2, 3, 4, 5, 6), range(10)))
print(long_items1[0][0])  

long_items2 = longer_than(3, ['abc', 'abcdefg', 'as'])
print(long_items2[0].capitalize())
וזה מביא אותנו לטיפ של היום - בכתיבת פונקציה בפייתון נסו לכתוב את הטיפוס של הקלט בצורה הכללית ביותר שאפשר, ואת הטיפוס של ערך ההחזר בצורה הספציפית ביותר שאפשר. שילוב כזה הוא המפתח לבניית פונקציות שיהיה קל להשתמש בהן בלי שהמשתמשים יצטרכו להתעצבן על שגיאות טיפוסים בקוד שעובד.

ToCode
1 421
# בעידן של ChatGPT, הכל זה התלבטויות בין אפשרויות עובדות ״איך את בונה תיבה עם השלמה אוטומטית שתהיה נגישה?״ ״איך אתה בונה Checkbox שישתמש בתמונה מותאמת אישית וגם יאפשר ניווט מקלדת?״ ״איך תבני גלריית תמונות ב HTML/CSS אם כל התמונות בגדלים שונים?״ ״איך תציג על המסך תמונה עם אפקט Scale כשעוברים עליה עם העכבר?״ כל אלה שאלות שפעם אפשר היה להשתמש בהן בראיונות עבודה, אבל היום הפכו ללא רלוונטיות. אם מתכנת יכתוב לי היום שהוא לא הצליח לבנות Checkbox שיש בו גם תמונה וגם ניווט מקלדת זה יישמע מאוד מוזר. ״למה אתה שואל אותי? יש Chat GPT״. היכולת של Chat GPT או בינג (או Copilot במידת מיומנות פחותה) ליצור תבניות התחלה וקטעי קוד קטנים שעובדים צריכה לשנות את כל הגישה שלנו לעבודה. אי אפשר להישאר אדישים.

ToCode
1 421
# מה עבד לך? אם תשאלו חברים שעושים משהו שגם אתם רוצים לעשות איך הם עשו את זה, לכל אחד תהיה תשובה אחרת - חבר אחד למד ספרדית דרך טיול בדרום אמריקה, חברה אחרת למדה עם ספר שממש ממליצה עליו, עוד חבר לומד באינטרנט עם מורה ואחר דרך צפיה בסדרות וסרטים מדובבים. וככל שיש יותר אפשרויות כך יש יותר שילובים, אחד לומד עם אפליקציה וספרים ואחרת עם מוזיקה וטיולים. בניסיון למצוא את ״הנוסחה המושלמת״ ללימודים (הנוסחה של בית ספר, הנוסחה של ״מורה מלמד כולם מקשיבים״, הנוסחה של מבחנים ועבודות, של ציונים ואם לא תצליח לא תתקבל לאוניברסיטה), קל לראות שהנוסחה המושלמת לא עובדת להרבה מאוד אנשים. קל לראות שבעצם אין נוסחה מושלמת. לא ללימודי שפה ולא ללימודי תכנות. המטרה של ללכת לחפש אנשים שעשו את השינוי שאתם רוצים לעשות ולשאול ״מה עבד לך?״ היא לא להעתיק שיטות, כי השיטה שעבדה להם לא תעבוד לנו. יותר נכון שהמטרה היא אינטגרציה, לקחת חלקים מכל שיטה ולבנות שיטה חדשה שתעבוד לכם. ומיטיבי לכת ידעו לשים לב שגם את הקריטריונים של ״מה עבד לך״ אנחנו בוחרים. שאפשר ללמוד תכנות כדי לבנות פרויקט צד או לקחת קצת עבודות פרילאנס או לעבוד בגוגל או לקרוא ולכתוב מאמרים מקצועיים או לפתור שאלות בפרויקט אוילר. כל מטרה באה עם קריטריונים שונים, שעוזרים לנו להבין אם אנחנו בכיוון. שיטת לימוד שתעזור לך להתקבל לעבודה בגוגל היא לא אותה שיטה שתעזור לך לבנות פרויקט צד שיככב באינדיהאקרס. ״מה עבד לך?. אולי חלק מזה יעבוד גם לי (ואולי לא, וזה גם בסדר).״

ToCode
1 421
# היום למדתי: npm isntall שגיאת כתיב מקרית בהתקנת חבילות הובילה אותי לקובץ cmd-list.js במאגר npm/cli, ומשם לקטע הבא מתוך אוביקט aliases:
in: 'install',
ins: 'install',
inst: 'install',
insta: 'install',
instal: 'install',
isnt: 'install',
isnta: 'install',
isntal: 'install',
isntall: 'install',
מה קורה כאן? אז הכי אחורה שהצלחתי להגיע עם זה היה ה PR הזה שהוסיף את ה alias בשביל לא להרגיז את אלכס סקסטון. אותו אלכס אגב פירסם באיזה גיסט מאותה תקופה את הלוג הבא מעבודה עם npm עם הכותרת How Dare You:
$ npm isntall
npm ERR! Darwin 14.0.0
npm ERR! argv "node" "/Users/alexsexton/.nave/installed/0.10.33/bin/npm" "isntall"
npm ERR! node v0.10.33
npm ERR! npm  v2.1.7
npm ERR! code MODULE_NOT_FOUND

npm ERR! Cannot find module '/Users/alexsexton/.nave/installed/0.10.33/lib/node_modules/npm/lib/isntall.js'
npm ERR!
npm ERR! If you need help, you may report this error at:
npm ERR!     <http://github.com/npm/npm/issues>

npm ERR! Please include the following file with any support request:
npm ERR!     /Users/alexsexton/stripe/dashboard-ui/npm-debug.log
לימים נוספו עוד alias-ים נוספים ל install ולפקודות רבות נוספות, כשההסבר הרשמי לכולם (מתוך הדיון כאן) הוא: > npm is not here to punish people for spelling mistakes. npm is here to install your packages, even if you type "isntall". גישה חיובית ולא מאוד נפוצה.

ToCode
1 421
המבנה טוב כיוון שהוא מחבר בין משהו שהפונקציה צריכה - המשתמש - לבין שורת ההגדרה של הפונקציה. אי אפשר היה להיכנס לקוד הטיפול בנתיב בלי שעברנו בקוד שקודם מפענח את ה User מתוך הבקשה. נשווה את זה לריילס שם כל נתיב אחראי על טעינת המידע ובדיקת המשתמש:
def show
    @post = Post.find(params[:id])
    authorize! :read, @post
    render plain: "Welcome #{current_user.name}"
end
או ל Express שם אפשר להשתמש ב Middlewares בשורת הגדרת הפונקציה:
app.get('/post/:id', authorize_user, (req, res) => {
    const user = res.locals.user;
    res.send(`Welcome! ${user.name}`);
});
אז בריילס המצב הכי גרוע כי הכי קל לשכוח לקרוא ל authorize או לקרוא לו עם פרמטרים לא נכונים (קרה לי יותר מדי פעמים), אבל גם ב Express המצב בעייתי כי אנחנו מתבססים על תיאום בין המידלוור authorize_user שתכתוב את המידע לשדה ב req.locals באותו שם בו הנתיב מצפה למצוא אותו. בין שלושת האפשרויות הפיתרון של רוקט הוא הטוב ביותר מאחר ואי אפשר "לשכוח" לבקש את ה User (אם תשכחו פשוט לא תקבלו את המידע על המשתמש), ולא צריך לתאם שמות בין מספר מקומות שונים בקוד.

ToCode
1 421
# מה למדתי מ Rocket על ניהול תלויות רוקט היא ספריית פיתוח ווב ל Rust. למפתח המרכזי שלה קוראים סרחיו בניטז והוא מפתח את רוקט מאז 2016. אם אתם כמוני, כלומר אנשים שנלחצים מפרויקטי צד של בן אדם אחד שעדיין נמצאים בגירסה 0.5 אז אולי לא תרצו לבחור את רוקט בתור תשתית לפרויקט הבא שלכם, וזה בסדר. אבל אפילו אם זה המצב הספריה עדיין מעניינת ומציגה מבט רענן על פיתוח ווב בתוך שפה עם הגדרת טיפוסים סטטית. דוגמת ה Hello World שלהם נראית כך:
#[macro_use] extern crate rocket;

#[get("/hello/<name>/<age>")]
fn hello(name: &str, age: u8) -> String {
    format!("Hello, {} year old named {}!", age, name)
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![hello])
}
והיא מייצרת נתיב עם שני משתנים, name ו age שבגלישה אליו יחזיר הודעת טקסט עם הערכים. זה לבד נראה אחלה ועושה חשק להמשיך ללמוד, אבל הנושא של הפוסט היום הוא דווקא לא רוקט אלא פיצ'ר ספציפי של רוקט שנקרא Request Guards. הרעיון מאוד מזכיר את ה Middlewares של אקספרס ואומר שכשיש לי בקשה אז לפני שאני מגיע לקוד הטיפול בנתיב קודם צריך לעבור דרך ה"שומרים" שיקחו מידע מהבקשה ויאתחלו משתנים מסוימים, שאחרי זה עוברים כפרמטרים לנתיב. הדוגמה הבאה ממחישה את הקונספט לא רע:
#[get("/sensitive")]
fn sensitive(key: ApiKey) { /* .. */ }
בהנחה שיש לנו טיפוס בשם ApiKey והטיפוס מממש התנהגות בשם FromRequest, אז לפני שרוקט תעביר את הטיפול לנתיב שהגדרנו היא תנסה "ליצור" ApiKey מהבקשה. הדוגמה הבאה מציגה פונקציה יותר מפורטת שתיקרא רק אם משתמש במערכת מנסה לגשת לנתיב:
/// This route is chosen if the request guard for User passes (e.g. logged in).
#[get("/")]
fn home(user: User, _cookies: Cookies) -> Markup {
    html! {
        head {
            title {"Welcome | Auth0 Rocket Example"}
            link rel="stylesheet" href="static/css/style.css";
        }
        body{
            h1 {"Guarded Route"}
            div {p {
                "You logged in successfully."
            }}
            div {p {
                "Email: " (user.email)
            }}
            div {p {
                a class="login" href="/profile" {"Another private route"}
            }}
        }
    }
}
כשהקוד של User הוא:
#[derive(Debug, Serialize, Deserialize)]
struct User {
    user_id: String,
    email: String,
}

impl<'a, 'r> FromRequest<'a, 'r> for User {
    type Error = ();
    fn from_request(request: &'a Request<'r>) -> Outcome<User, ()> {
        let session_id: Option<String> = request
            .cookies()
            .get("session")
            .and_then(|cookie| cookie.value().parse().ok());
        match session_id {
            None => {
                println!("no session id");
                rocket::Outcome::Forward(())
            }
            Some(session_id) => {
                println!("session id: {}", session_id);
                let db = State::<DB>::from_request(request).unwrap();
                let session_key = make_key!("sessions/", session_id);
                match db.get(&session_key.0) {
                    Ok(Some(sess)) => {
                        let sess: Session =
                            deserialize(&sess).expect("could not deserialize session");
                        if sess.expired() {
                            return rocket::Outcome::Forward(());
                        }
                        let user_key = make_key!("users/", sess.user_id);
                        match db.get(&user_key.0) {
                            Ok(Some(user)) => {
                                let user: User =
                                    deserialize(&user).expect("could not deserialize user");
                                rocket::Outcome::Success(user)
                            }
                            _ => rocket::Outcome::Forward(()),
                        }
                    }
                    _ => rocket::Outcome::Forward(()),
                }
            }
        }
    }
}

ToCode
1 421
בסגירה אנחנו צריכים לסגור את תהליכון הרקע ובשביל זה הגדרתי פונקציית טיפול באירוע סגירה:
def closeEvent(self, event):
    self.finder_thread.exit(0)
פיתרון כזה יכול לתת מענה טוב לכל תוכנית גרפית שצריכה לבצע עבודה ברקע. נכון אין להם עדיין תמיכה טובה ב async/await, אבל אני מקווה שבעתיד גם לזה יימצא פיתרון.

ToCode
1 421
# הרצת משימות ברקע עם Qt בפייתון בכתיבת תוכניות גרפיות עם Qt ב Python נוכל לשים לב שפעולות ארוכות ״תוקעות״ את התוכנית. לדוגמה נדמיין תוכנית המציגה תיבת חיפוש לקבצים, כפתור ורשימה ובלחיצה על הכפתור התוכנית מחפשת קבצים שמתאימים לשם הקובץ שבתיבה וממלאת את הרשימה בשמות הקבצים שנמצאו. אם נריץ את התוכנית תוך שימוש ב Thread אחד, בלחיצה על הכפתור פייתון יתחיל לחפש קבצים ולא יהיה זמין לטפל באירועים אחרים הקשורים לממשק המשתמש - למשל הגדלה או הקטנה של החלון, או אפילו שינויי עיצוב קטנים כשסמן העכבר עובר על הכפתור. התנהגות כזאת תיתן למשתמשים הרגשה שהתוכנית ״תקועה״, למרות שההיפך הוא הנכון והתוכנית עובדת מאוד קשה. יותר מזה, רק אחרי שיימצאו כל הקבצים ולולאת החיפוש תסתיים נראה שינוי בממשק המשתמש ולכן ברגע אחד יתווספו לרשימה כל הקבצים שמצאנו. חווית משתמש טובה יותר תאפשר למשתמשים להמשיך לעבוד עם הממשק ותוסיף את התוצאות שהיא מוצאת אחת-אחת בזמן בו התוכנית מוצאת אותן. בארכיטקטורה של Qt יש שתי דרכים מרכזיות להגיע לתוצאה זו: 1. אפשר לכתוב את לולאת החיפוש עם ״הפסקות״, כך שכל פעם שמוצאים קובץ שמתאים למילת החיפוש נעצור לנשום ונבקש מ Qt לטפל באירועים גרפיים שאולי הצטברו. זה ייתן חווית משתמש טובה יותר ויפתור את הבעיה בתוכנית חיפוש הקבצים, אבל לא תמיד אפשרי במקרה הכללי כי לא תמיד יש מקום טוב לעצירות כאלה. 2. פיתרון יותר כללי יהיה לפתוח Thread נוסף ולבצע את הלולאה (במקרה שלנו לולאת חיפוש) מתוך ה Thread הנוסף. התקשורת בין תהליכון ראשי ב Qt לבין תהליכון נוסף שרץ ברקע מתבצעת במנגנון הסיגנלים הרגיל של Qt - התהליכון הראשי שולח סיגנל ותהליכון הרקע קורא את הסיגנל הזה ומתחיל לעבוד. כל פעם שתהליכון החיפוש מזהה תוצאה חדשה הוא ישלח סיגנל והתהליכון הראשי יוסיף פריט נוסף לתיבת התוצאות. קוד? ברור, ומספיק קובץ אחד:
from PySide6.QtCore import *
from PySide6.QtGui import *
from PySide6.QtWidgets import *
from pathlib import Path
from time import sleep

class Finder(QObject):
    path_found = Signal(str)

    def __init__(self, parent=None):
        super().__init__(parent)

    def search(self, text):
        results = Path('.').rglob(text)

        for result in results:
            self.path_found.emit(str(result.absolute()))
            sleep(0.01)



class Ui(QWidget):
    start_search = Signal(str)

    def __init__(self, parent=None):
        super().__init__(parent)
        self.results = QListWidget()
        self.searchbox = QLineEdit()
        self.search_button = QPushButton("Search")
        self.finder = Finder()
        self.finder_thread = QThread(self)
        self.finder.moveToThread(self.finder_thread)
        self.finder_thread.start()

        self.main_layout = QVBoxLayout(self)
        self.top_layout = QHBoxLayout()
        self.main_layout.addLayout(self.top_layout)

        self.top_layout.addWidget(self.searchbox)
        self.top_layout.addWidget(self.search_button)
        self.main_layout.addWidget(self.results)

        self.search_button.clicked.connect(self.search)

        self.start_search.connect(self.finder.search)
        self.finder.path_found.connect(self.results.addItem)

    def search(self):
        self.results.clear()
        self.start_search.emit(self.searchbox.text())

    def closeEvent(self, event):
        self.finder_thread.exit(0)



app = QApplication()
w = Ui()
w.show()
app.exec()
אז מה היה לנו כאן? 1. האוביקט Finder אחראי על חיפוש הקבצים. הוא יורש מ QObject כי ככה Qt דורש. הפונקציה moveToThread שלו שולחת אותו לעבוד ב Thread אחר. 2. ה Thread אגב הוא Thread של Qt ולא של פייתון. נוצר באמצעות QThread(). 3. ה Widget הראשי מגדיר סיגנל כדי לאותת ל Finder שצריך להתחיל את החיפוש, וה Finder מגדיר סיגנל כדי לאותת ל Widget הראשי שהוא מצא קובץ. 4. השורות שמחברות את הסיגנלים לקודי הטיפול מופעלות ב init של ה Widget והן:
self.start_search.connect(self.finder.search)
self.finder.path_found.connect(self.results.addItem)
כל השאר זה קסם אוטומטי של Qt - כשהסיגנל start_search נשלח ה Finder מתחיל לחפש ברקע, וכל תוצאה שהוא מצא נשלחת דרך סיגנל לפונקציה addItem של תיבת התוצאות.

ToCode
1 421
# הפרויקט הראשון מה עושים כשאין עדיין תיק עבודות? מה עושים כשרוצים להיכנס להייטק, אבל אף אחד לא מדבר איתך (ובצדק) כי הם לא מזהים את הערך עבורם, כי הם (עדיין) לא רואים? האתגר כאן הוא כפול כי צריך גם לבנות תיק עבודות, גם שהפרויקטים יהיו מספיק טובים וגם להספיק לעשות את הכל לפני שהכסף ייגמר - ואת כל זה צריך לעשות עוד לפני שנכנסת לעבודה ולפני שיש לך את הכלים והידע להצליח. מאתגר, אבל לא בלתי אפשרי. כיוון אחד שעובד הוא לבחור פרויקט אחד ולבנות אותו בצורה פתוחה תוך כדי חיפוש עבודה, ותוך הדגשת המסגרת. כלומר: 1. בונים קודם דף נחיתה שמספר על הפרויקט. תניחו שהכל כבר כתוב ועובד ואפילו תוכלו לשים Screenshots מהפרויקט הדמיוני (מנועי בינה מלאכותית ישמחו לייצר לכם כמה). 2. בונים פרויקט בגיטהאב עם קובץ readme מושקע שמספר על הפרויקט. 3. את הפיתוח מתחילים עם תשתיות של בדיקות ו Deployment אוטומטי דרך Github Actions. 4. משקיעים בהודעות קומיט (מישהו אמר gitpoet?) עכשיו אפשר להתחיל לעבוד - תוך כדי חיפוש העבודה אתם ממשיכים להשקיע לפחות חצי מהזמן שלכם בפיתוח הפרויקט שבחרתם. בקורות חיים מופיע קישור לדף הנחיתה של הפרויקט כדי להרשים את המגייסים, ובזמן הראיונות אתם מספרים על האתגרים איתם אתם מתמודדים במהלך הפיתוח. נכון הפרויקט עדיין לא עובד ואין שם המון קוד, אבל כשהכיוון ברור והתשתית הבסיסית טובה אפשר לראות את הפוטנציאל גם בפרויקט אחד שעדיין נמצא בהתחלה.

ToCode
1 421
# ואולי Icon Fonts לא היו רעיון כזה טוב במאי 2012 דייב גאנדי פירסם את ספריית Font Awesome שכתב, שאיפשרה למתכנתים לבנות אייקונים באמצעות אותיות בגופן. בגלל שהיה יותר קל לעצב טקסט מאשר לעצב תמונות, אפשר היה באמצעות CSS לשנות את הצבעים והגדלים של האייקונים וכך להתאים אותם לכל עיצוב. היום כבר אפשר להשתמש ב SVG כדי להשיג בדיוק את אותו אפקט, אבל האקים שנוצרו ב 2012 לא מתים בקלות. וכך לא מזמן חברה ביקשה שאעזור לה לפתור בעיה באתר. מסתבר שמשתמשים שנעזרו בקוראי מסך כדי לגלוש באתר שמעו אותיות מוזרות ולא קשורות בין הטקסטים הרגילים. זה נשמע כאילו קורא המסך פתאום ממציא טקסטים שלא מופיעים בטקסט. מהר מאוד שמנו לב שהאותיות המוזרות מופיעות בדיוק איפה שבאתר מופיעים אייקונים, ושהאתר משתמש ב Icon Font. ברגע שחיברנו את הנקודות גם הפיתרון היה קל - או כמו שכותבים באתר של Font Awesome פשוט צריך להסתיר את האייקונים מקורא המסך עם:
<span class="fa-solid fa-envelope" aria-hidden="true" />
אז כן גם אייקונים בגופן יכולים להיות נגישים. חשוב רק לשים לב ולזכור את הטיפול המיוחד בהם כשמשתמשים בהם באתר.