ToCode
Відкрити в Telegram
1 420
Підписники
Немає даних24 години
Немає даних7 днів
-530 день
Архів дописів
1 420
פוטנציאל (טיפ לחיפוש עבודה)
לפעמים אני אחפש עבודה שבדיוק מתאימה לכישורים שלי. במצב כזה אני צריך לשכנע את המעסיק שאני מכיר את החומר ומסוגל לבצע את המטלות בצורה הטובה ביותר.
לעתים יותר קרובות אני אחפש עבודה שתקדם אותי, עבודה שאני עדיין לא יודע איך לבצע אבל אני חושב שאוכל לגדול לשם. במצב כזה האתגר הוא לשכנע את המעסיק שיש לי את הפוטנציאל להצליח לעשות את המעבר (ממתכת למנהל, מ QA למתכנת, ממתכנת לפרודקט, או כל מעבר אחר שאתם חולמים עליו). כן זה הכי קל לשכנע בתוך הארגון אבל אפשר גם לשכנע אנשים זרים. כמה דברים שהייתי מנסה במצב כזה-
1. להבליט בקורות חיים את הדברים הטובים שעשיתם בסטטוס הנוכחי או את הפרויקטים שביצעתם בהצלחה.
2. להתחיל את התהליך, אבל להיזהר עם הצגת תוצרים (מי שהיה 5 שנים מתכנת ורק התחיל לפני חודשיים קורס UI/UX אולי לא יצר עדיין את העיצובים הכי מטורפים).
3. להבליט שינויים מקצועיים קודמים שעשיתם, רצוי במכתב מקדים המצורף לקורות החיים.
4. להיות מוכנים לתקופת הכשרה או ירידה בשכר. לא לבוא בדרישות.
5. להתחיל בקטן - תוך כדי החיפוש שווה למצוא מקומות בתפקיד הנוכחי או אפילו בהתנדבות בהם אתם יכולים לעשות את הדבר שאתם רוצים לעשות. אותו מתכנת יוכל לבנות עיצובי UI/UX לאפליקציות דמיוניות או בהתנדבות. לא בטוח שהייתי מצרף אותם לקורות חיים אבל כן שיהיה משהו לדבר עליו בראיון.
הדבר החשוב בשכנוע הוא להראות שאתם מבינים את האתגר ושכולם מדברים באותה שפה. בתור מעסיק הייתי מפחד לתת צ'אנס למי שהתחיל לפני חודשיים קורס עיצוב אבל כבר מתנהג כמו מעצב-על. אבל אם הבן אדם מבין שהוא עדיין רחוק מהיעד אבל מפוקס בדרך לשם, ומראה שהוא עשה שינויים דומים בעבר ומוכן לאתגר, ואני מאוד צריך מעצב וכבר מיואש מלמצוא אחד עם ניסיון, יש סיכוי שאקח את הסיכון ואתן את ההזדמנות.
1 420
playerRef.current = YouTubePlayer(playerDivRef.current);
playerRef.current.loadVideoById(videoId);
}
}, [])
const changeVideoId = (e) => {
const videoId = e.target.value;
setVideoId(videoId);
const player = playerRef.current;
if (player) {
player.loadVideoById(videoId);
}
}
return (
<div>
<div>
<button onClick={() => playerRef.current.playVideo()}>Play</button>
<button onClick={() => playerRef.current.pauseVideo()}>Pasue</button>
<div>
<input type="text" value={videoId} onChange={changeVideoId} />
</div>
<div ref={playerDivRef}></div>
</div>
</div>
)
}
function Toggle() {
const [hidden, setHidden] = useState(false);
return (
<div>
<input type="checkbox" checked={hidden} onChange={() => setHidden(v => !v)} /> Hide
{hidden ? <></> : <YoutubePlayerComponent />}
</div>
)
}
export default Toggle1 420
playerRef.current.loadVideoById(videoId);
}
}, [])
אני לא אוהב את זה כי יש פה אפקט בלי פונקציית ניקוי וזה נראה כמו משהו שהולך להישבר בקרוב, אבל מאחר ו youtube-player מעדכן את ה DOM בלי לשאול אותנו זאת סוג של פשרה שאי אפשר להתחמק ממנה.
איך לשבור אותה אחרי ששילבנו
הסיבה המרכזית שאני לא אוהב את המנגנון היא ההנחה הנסתרת שהוא כולל - המעקף כולל הנחה סמויה לפיה אלמנט הנגן לעולם לא ישתנה כתוצאה משינויים בסטייט. זה הכרחי בשביל ש youtube-player תעבוד, שוב בגלל ש youtube-player משנה את ה DOM.
אם אנחנו מנסים לעדכן את הקומפוננטה וכן לשנות את ה DOM מתוך ריאקט יש התנגשות. שימו לב לגירסה השבורה הזאת:
import { useEffect, useRef, useState } from 'react'
import YouTubePlayer from 'youtube-player'
const YoutubePlayerComponent = () => {
const [videoId, setVideoId] = useState('efGw88Wlncw');
const [hidden, setHidden] = useState(false);
const playerDivRef = useRef(null);
const playerRef = useRef(null);
useEffect(() => {
if (!playerRef.current) {
playerRef.current = YouTubePlayer(playerDivRef.current);
playerRef.current.loadVideoById(videoId);
}
}, [])
const changeVideoId = (e) => {
const videoId = e.target.value;
setVideoId(videoId);
const player = playerRef.current;
if (player) {
player.loadVideoById(videoId);
}
}
return (
<div>
<div>
<input type="checkbox" checked={hidden} onChange={() => setHidden(v => !v)} /> Hide
<button onClick={() => playerRef.current.playVideo()}>Play</button>
<button onClick={() => playerRef.current.pauseVideo()}>Pasue</button>
<div>
<input type="text" value={videoId} onChange={changeVideoId} />
</div>
{hidden ? <></> : <div ref={playerDivRef}></div>}
</div>
</div>
)
}
export default YoutubePlayerComponent
הכפתורים עובדים אבל אם אני מנסה ללחוץ על hide הכל נעלם ומופיעה הודעת שגיאה בקונסול:
react-dom.development.js:12056 Uncaught DOMException: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.
at removeChild (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=782b4cde:8467:26)
at commitDeletionEffectsOnFiber (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=782b4cde:17506:21)
at commitDeletionEffects (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=782b4cde:17473:13)
at recursivelyTraverseMutationEffects (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=782b4cde:17670:17)
at commitMutationEffectsOnFiber (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=782b4cde:17733:15)
at recursivelyTraverseMutationEffects (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=782b4cde:17681:15)
at commitMutationEffectsOnFiber (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=782b4cde:17733:15)
at recursivelyTraverseMutationEffects (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=782b4cde:17681:15)
at commitMutationEffectsOnFiber (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=782b4cde:17695:15)
at recursivelyTraverseMutationEffects (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=782b4cde:17681:15)
ריאקט מנסה למחוק את הנגן אבל מגלה שזה לא הוא שהוסיף את הנגן ולכן מחליט להישבר. נו, ראינו את זה בא כש youtube-player החליטה לשנות את ה DOM אז קשה אפילו לכעוס עליו.
אם בכל זאת אנחנו מתעקשים להוסיף מנגנון של Toggle נצטרך לבנות אותו בתוך קומפוננטה חיצונית לנגן הוידאו, ואת זה ריאקט כבר יסכים לקבל:
import { useEffect, useRef, useState } from 'react'
import YouTubePlayer from 'youtube-player'
const YoutubePlayerComponent = () => {
const [videoId, setVideoId] = useState('efGw88Wlncw');
const playerDivRef = useRef(null);
const playerRef = useRef(null);
useEffect(() => {
if (!playerRef.current) {1 420
בואו נשבור קצת ריאקט
לריאקט יש כמה קונספטים פשוטים שרוב הזמן מצליחים להחזיק מערכת אבל לפעמים גם מביאים אותנו לקצוות של תסכול. זה תמיד קורה כי שברנו איזה כלל ריאקטי אין ספק, אבל בחיים קשה לעבוד רק לפי הכללים. בואו ניקח לדוגמה את הספריה youtube-player מ npm.
מה שבור בספריה
הספריה youtube-player נותנת ממשק קל לעבודה עם ה API של יוטיוב. אנחנו נותנים לה בבנאי אלמנט DOM והיא יוצרת בתוכו נגן יוטיוב עם אייפריים והכל. בשביל להשתמש בה (מתוך התיעוד שלהם) צריך רק לכתוב:
let player;
player = YouTubePlayer('video-player');
// 'loadVideoById' is queued until the player is ready to receive API calls.
player.loadVideoById('M7lc1UVf-VE');
// 'playVideo' is queue until the player is ready to received API calls and after 'loadVideoById' has been called.
player.playVideo();
הסוס הטרויאני כאן הוא שריאקט מאוד לא אוהב שמישהו אחר משנה לו את ה DOM, ועוד מעט הוא יתעורר ויתרגז.
ניסיון ראשון לשלב את הספריה עם ריאקט יכול להיראות כך-
import { useEffect, useRef, useState } from 'react'
import YouTubePlayer from 'youtube-player'
const YoutubePlayerComponent = () => {
const [videoId, setVideoId] = useState('efGw88Wlncw');
const playerDivRef = useRef(null);
const playerRef = useRef(null);
useEffect(() => {
playerRef.current = YouTubePlayer(playerDivRef.current);
playerRef.current.loadVideoById(videoId);
}, [])
const changeVideoId = (e) => {
const videoId = e.target.value;
setVideoId(videoId);
const player = playerRef.current;
if (player) {
player.loadVideoById(videoId);
}
}
return (
<div>
<div>
<button onClick={() => playerRef.current.playVideo()}>Play</button>
<button onClick={() => playerRef.current.pauseVideo()}>Pasue</button>
<div>
<input type="text" value={videoId} onChange={changeVideoId} />
</div>
<div ref={playerDivRef}></div>
</div>
</div>
)
}
export default YoutubePlayerComponent
יוצרים קומפוננטה, בעלייה של הקומפוננטה יוצרים את הנגן ואז כל פעם שמשתנה המזהה של הוידאו טוענים את הוידאו החדש דרך הנגן. אפשר גם להוסיף כפתורים של "נגן" ו"הפסק" בקוד הקומפוננטה.
הקוד נראה נכון אבל הוא לא עובד. הכפתורים לא מתפקדים.
קל לראות שהבעיה היא באפקט שיוצר את הנגן. בדרך כלל אפקטים צריכים לכלול קוד "ניקוי", כלומר האפקט הוא חיבור למשהו חיצוני ו useEffect צריכה להחזיר פונקציה שמבטלת את החיבור הזה. במקרה שלנו אין דרך לנקות את החיבור כי הספריה youtube-player לא מספקת כזו. היא כן מציעה פונקציה אסינכרונית בשם destroy, אבל האסינכרוניות של הפונקציה היא בעייתית. הפונקציה מסירה מה DOM את האלמנט של הנגן בצורה אסינכרונית, ולכן עלולה להסיר אותו הרבה אחרי שהאפקט בוטל. במלים אחרות אם אני מנסה להוסיף את קוד הניקוי הזה:
useEffect(() => {
playerRef.current = YouTubePlayer(playerDivRef.current);
playerRef.current.loadVideoById(videoId);
return () => {
playerRef.current.destroy();
}
}, [])
הקוד ייצור את ה iframe בשביל הנגן ואז מיד ימחק אותו.
מה שקורה כאן זה שבמצב פיתוח ריאקט מפעיל את האפקט ואז מריץ את קוד הניקוי שלו ואז מפעיל את האפקט שוב. זאת בדיקה של ריאקט שנועדה לוודא שאפשר לבטל ולהחזיר את האפקט בלי בעיה, אבל במקרה שלנו זה לא עובד כי המחיקה היא אסינכרונית. פונקציית destroy תופעל אחרי שריאקט מבטל את האפקט ומחזיר אותו ואז תמחק את האייפריים של הנגן השני. אותה התנהגות היא גם זאת שגרמה לכפתורים לא לעבוד, אבל מסיבה אחרת. כשריאקט מגיע להפעיל את האפקט פעם שניה הוא יוצר נגן חדש שדורס את הנגן הקודם. הנגן הראשון הוא זה שמתפקד אבל הכפתורים מפעילים את הפונקציות play ו pause של הנגן החדש שלא באמת מחובר לאייפריים.
איך בכל זאת לשלב אותה בפרויקט ריאקט
במצבים כאלה או אנחנו או ריאקט נצטרך להתפשר, ולצערי בעבודה עם ריאקט זה אנחנו שצריכים לוותר. הפיתרון העצוב הוא לבדוק בקוד האפקט אם הנגן כבר קיים, ואם כן לא ליצור חדש. זה נראה כך:
useEffect(() => {
if (!playerRef.current) {
playerRef.current = YouTubePlayer(playerDivRef.current);1 420
איך לזהות תירוצים לאי התקדמות מקצועית
יש המון סיבות טובות להתרחק מטכנולוגיה חדשה כמו למשל-
1. אני כבר ממש טוב ב X, בשביל מה לבזבז את הזמן על Y.
2. אין לי צורך בזה לעבודה שלי.
3. זה הכל הייפ, תכף ההתלהבות תיגמר וכולם יחזרו לטכנולוגיה שהיתה קודם.
4. אין מספיק אנשים בתעשייה שמשתמשים ב Y וחבל לי לבזבז את הזמן על משהו שלא יקדם אותי.
5. הייתי שמח ללמוד את Y אבל כרגע אנחנו בדיוק באמצע פרויקט, אחרי הגירסה אתחיל להסתכל על זה.
והמון פעמים הסיבות האלה ממש נכונות. יש תקופות בחיים שלא צריך ללמוד שום דבר חדש והדבר החשוב הוא להתקדם בפרויקט המדהים שאנחנו בונים. אם כל היום היינו לומדים טכנולוגיות חדשות לא היה זמן לעבוד.
הבעיה היא שלפעמים מה שנשמע כמו סיבה ממש טובה הוא בעצם תירוץ שמרגיש כמו סיבה. תירוץ שמתחזה כל כך טוב שאפילו הבן אדם שנותן אותו לא מצליח לשים לב שיש פה משהו חשוד.
טריק אחד שעובד בשבילי כדי לזהות מתי אני באמת עסוק מדי ומתי אני רק מתחמק הוא להאריך את חלון הזמן עליו אני מסתכל. במקום להסתכל על הדבר שאני צריך ללמוד עכשיו אני אסתכל אחורה ואשאל-
1. כמה טכנולוגיות חדשות למדתי בשנה האחרונה?
2. כמה כלים חדשים שילבתי בעבודה שלי בשנה האחרונה בצורה שתרמה לפרודוקטיביות?
3. האם יש לי רשימת טכנולוגיות ללימוד מעולם התוכן שלי שאני יודע שאני צריך לחקור? ואם כן האם הצלחתי לקצר אותה בשנה האחרונה?
כשמסתכלים על שנה אחורה במקום על מחר בבוקר יותר קל להבין את הסיטואציה. כמה מוצדקות שלא יהיו הסיבות שיש לי היום הן בטח לא היו שם לפני חודש, חודשיים או חצי שנה. יותר מזה, רק לשים לב שבשנה האחרונה לא לקחת אף צעד קדימה זה חוויה מטלטלת שיכולה לשנות סדרי עדיפויות. יש למוח שלנו יכולת מופלאה להעביר שנים על טייס אוטומטי. זיהוי המנגנון הזה בזמן הוא ההזדמנות שלנו לחיים שמחים יותר.
1 420
משחקים עם מקביליות בסקאלה (חלק 2)
בחלק הקודם של הפוסט כתבתי על pmap ואיך אפשר להשתמש בו כדי לחלק פעולה חישובית למספר תהליכונים כדי לשפר ביצועים. היום אני רוצה לדבר על עבודת IO, על המגבלה של Thread Pool במיקבול משימות הקשורות ל IO ועל הפיתרון עם Virtual Threads.
מה בעצם הבעיה
נתחיל עם הקוד הבא בסקאלה שמשתמש ב Thread Pool כדי להוריד במקביל מידע מ 50 עמודים באתר מסוים (כן הנתונים באתר מזויפים אבל זה לא משנה):
object futures {
given ExecutionContext = ExecutionContext.global
private def printUrlContent(url: String): Unit =
Using(Source.fromURL(url)) { reader =>
reader.getLines().mkString("\n")
}
println(s"Done: ${url}")
@main
def test(): Unit =
val s1 = System.nanoTime()
1.to(50)
.map {i => s"https://dummyjson.com/products/${i}" }
.map { url => Future { printUrlContent(url)} }
.foreach { f => Await.result(f, Duration.Inf) }
val s2 = System.nanoTime()
println(s"Took ${s2 - s1} ns")
}
אז בגדול אנחנו מחשבים 50 כתובות אינטרנט מאתר dummyjson.com, שולחים בקשת HTTP לכל כתובת ובודקים כמה זמן לקח למשוך את כל המידע.
הרצה של התוכנית מציגה תוצאה מעניינת - אנחנו רואים את הפלט נכתב למסך בקבוצות של URL-ים, כלומר מחכים קצת זמן, רואים הדפסה של מספר שורות ואז מחכים עוד קצת ועוד הדפסה. זה קורה בגלל המבנה של Thread Pool והשילוב עם עבודת IO. בסוג עבודה כזה מה שחוסם אותנו הוא הרשת ולא המחשב שלנו, ולכן כל התהליכונים עובדים על ניוטרל ובעצם מחכים למידע. הבעיה שה Thread Pool מוכן להריץ רק מספר מוגבל של תהליכונים ולכן צריך לחכות עד שתהליכון יסיים ויתפנה לפני שמתחילים להוריד את הנתיב הבא.
מעבר ל Virtual Threads
וכאן אנחנו מגלים את הפוטנציאל של Virtual Threads. בדרך כלל כל Thread תופס משאבים ולכן לא מומלץ ליצור יותר מדי מהם, בשלב מסוים הם יצרו עומס על מערכת ההפעלה ורק יאטו את התוכנית. תהליכון וירטואלי הוא מנגנון של ה JVM שלא יוצר Thread במערכת ההפעלה (זה נקרא fiber בהרבה מקומות אחרים) ולכן אפשר ליצור המון ממנו. נכון אם הפעולה שאנחנו מנסים לעשות היא משימה חישובית להוסיף עוד תהליכונים או תהליכונים וירטואליים לא ישפר את מצבנו, כי עדיין יהיה עומס על המעבד, אבל כשמדובר בפעולות IO ממילא המעבד לא עושה כלום והאיטיות מגיעה מהרשת ולא מהמחשב שלנו - ולכן תהליכונים וירטואליים יכולים לשנות את התמונה.
בסקאלה (וגם ב Java) יש קלאס בשם ExecutorService שאחראי על יצירת התהליכונים ולכן כל מה שצריך בשביל להתחיל לעבוד עם תהליכונים וירטואליים הוא לשנות את ה ExecutorService שלנו. עדכון התוכנית לעבודה עם תהליכונים וירטואליים הוא בסך הכל שינוי של השורה הראשונה ל:
given ExecutionContext = ExecutionContext.fromExecutorService(Executors.newVirtualThreadPerTaskExecutor())
והתוכנית המלאה:
object futures {
given ExecutionContext = ExecutionContext.fromExecutorService(Executors.newVirtualThreadPerTaskExecutor())
// given ExecutionContext = ExecutionContext.global
private def printUrlContent(url: String): Unit =
Using(Source.fromURL(url)) { reader =>
reader.getLines().mkString("\n")
}
println(s"Done: ${url}")
@main
def test(): Unit =
val s1 = System.nanoTime()
1.to(50)
.map {i => s"https://dummyjson.com/products/${i}" }
.map { url => Future { printUrlContent(url)} }
.foreach { f => Await.result(f, Duration.Inf) }
val s2 = System.nanoTime()
println(s"Took ${s2 - s1} ns")
}
גירסה זו של התוכנית רצה במחצית הזמן, ומה שיותר מעניין הוא שאין בה את החלוקה לקבוצות שאפיינה את העבודה עם Thread Pool. כל ההודעות נכתבות למסך יחד, בגלל שמראש יצרנו Thread וירטואלי לכל משימה.1 420
ביי ביי map בשביל לשנות ערכים במיקום מסוים ב JavaScript
אם אי פעם שמרתם מערך בסטייט של קומפוננטת ריאקט אתם וודאי זוכרים את הרגע שניסיתם לשנות רק ערך אחד במערך ולהעביר את התוצאה ל set, רק בשביל לגלות ש setState לא עושה כלום כי המערך עצמו לא השתנה. אחרי זה למדתם להשתמש ב map כדי ליצור מערך חדש עם שינוי אלמנט באינדקס מסוים-
const newItems = oldItems.map((item, index) =>
index === 1 ? 99 : item);
ואז הגיעה immer ושוב הצלחנו לכתוב קוד רגיל אבל ידענו בלב שזה לא בדיוק זה ושמנגנון הפרוקסים של immer עובד אומנם ברוב המקרים אבל בסוף תמיד מחכה איזה באג (לא בגלל immer ברור, אתם פשוט מחזיקים את זה לא נכון).
בקיצור קיטורים בצד לאחרונה JavaScript קיבלה בכל הדפדפנים תמיכה בפיתרון מובנה לשינוי מערכים לפי אינדקס ובלי map. זה נראה ככה-
const newItems = oldItems.with(1, 99);
נשים לב שאי אפשר לכתוב אחרי סוף המערך כך שזה נכשל:
[].with(99, 0)
בנוסף with שובר מערכים מרווחים, אבל אני לא בטוח כמה נזק זה עלול לגרום. כלומר הקוד הזה:
Array(10).forEach(() => console.log('1'))
והקוד הזה:
Array(10).with(0, 0).forEach(() => console.log('1'))
יעשו דברים שונים - הראשון לא ידפיס כלום, השני ידפיס 10 פעמים את ההודעה. אין תמיכה במספר אינדקסים לכתיבה לתוך מערכים מקוננים, אבל כן יש תמיכה באינדקסים שליליים לכתיבה מסוף המערך:
[1, 2, 3, 4, 5].with(-1, 10)1 420
היום למדתי - איפוס הגדרות postcss בפרויקט vite
לפעמים קשה להבין למה פרויקטי Front End לא מצליחים לעבוד טוב יחד, או שאולי הבעיה היא אצלנו המתכנתים שפשוט דוחפים עוד ועוד שטויות לפרויקט ובסוף מתפלאים כשמופיעות הודעות שגיאה מוזרות.
הסיפור שהיום לקח לי יותר מדי שעות מהחיים קשור ל postcss ולהתנהגות המוזרה שלו בתוך פרויקט vite - בהעדר הגדרה אחרת, postcss יחפש קובץ הגדרות במעלה עץ התיקיות עד לתיקיית הבית. בשביל הניסוי שמתי בתיקיית הבית קובץ בשם postcss.config.js עם התוכן הבא:
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
ואז יצרתי פרויקט vite חדש לגמרי בתיקייה:
/Users/ynonp/a/b/c/d/helloworld
הפעלתי npm run build בתיקיה וקיבלתי את הודעת השגיאה הבאה:
> helloworld@0.0.0 build
> tsc && vite build
vite v5.1.3 building for production...
transforming (1) index.htmlnode:internal/process/promises:289
triggerUncaughtException(err, true /* fromPromise */);
^
[Failed to load PostCSS config: Failed to load PostCSS config (searchPath: /Users/ynonp/a/b/c/d/helloworld): [Error] Loading PostCSS Plugin failed: Cannot find module 'tailwindcss'
Require stack:
- /Users/ynonp/postcss.config.js
(@/Users/ynonp/postcss.config.js)
Error: Loading PostCSS Plugin failed: Cannot find module 'tailwindcss'
Require stack:
- /Users/ynonp/postcss.config.js
(@/Users/ynonp/postcss.config.js)
at load (file:///Users/ynonp/a/b/c/d/helloworld/node_modules/vite/dist/node/chunks/dep-stQc5rCc.js:28883:11)
at file:///Users/ynonp/a/b/c/d/helloworld/node_modules/vite/dist/node/chunks/dep-stQc5rCc.js:28908:16
at Array.map (<anonymous>)
at plugins (file:///Users/ynonp/a/b/c/d/helloworld/node_modules/vite/dist/node/chunks/dep-stQc5rCc.js:28907:8)
at processResult (file:///Users/ynonp/a/b/c/d/helloworld/node_modules/vite/dist/node/chunks/dep-stQc5rCc.js:28977:14)
at file:///Users/ynonp/a/b/c/d/helloworld/node_modules/vite/dist/node/chunks/dep-stQc5rCc.js:29107:14]
Node.js v21.5.0
וכן זה לקח הרבה יותר זמן לגלות שהסיבה להודעה היא בעצם הקובץ postcss.config.js שנמצא בתיקיית הבית שלי.
פיתרון? די פשוט מסתבר אחרי שמבינים את הבעיה. יוצרים קובץ vite.config.js בתיקיית הפרויקט עם התוכן הבא והכל מסתדר:
import { defineConfig } from 'vite'
// https://vitejs.dev/config/
export default defineConfig({
css: {
postcss: {},
},
})
אז נכון אני לא אקבל את החצי יום שלי בחזרה, אבל אולי הפוסט הזה יחסוך לכם כמה שעות.1 420
עוד מחשבה על תבנית ה index.ts שמייצא הכל
בואו נדמיין פרויקט ריאקט שיש בו תיקייה בשם
src/components/HomePage ובתוכה ערימה של תיקיות וקובץ אחד בשם index.ts:
.
├── EmptyState
│ ├── EmptyState.tsx
│ └── index.ts
├── ErrorState
│ ├── ErrorState.tsx
│ ├── ErrorState.types.ts
│ └── index.ts
├── RecentArticles
│ ├── RecentArticles.tsx
│ ├── RecentArticles.types.ts
│ └── index.ts
├── RecentArticlesCard
│ ├── RecentArticleCard.tsx
│ ├── RecentArticleCard.types.ts
│ └── index.ts
├── RecentArticlesContent
│ ├── RecentArticlesContent.tsx
│ ├── RecentArticlesContent.types.ts
│ └── index.ts
└── index.ts
תוכן הקובץ index.ts יהיה:
export * from './EmptyState';
export * from './ErrorState';
export * from './RecentArticles';
export * from './RecentArticlesCard';
export * from './RecentArticlesContent';
למה שמישהו יכתוב ככה? מה היתרונות ומה החסרונות? והאם כדאי לנו גם להשתמש בתבנית זו בפרויקטים שלנו? (כן אני יודע רובכם כבר משתמשים. בגלל זה הפוסט)
למה כן לכתוב ככה
השימוש בקובץ אינדקס ראשי בתיקיה נועד להקל על אנשים שעושים import לקבצים מאותה תיקייה. קובץ אחר מאותו הפרויקט שמייבא קבצים יוכל לכתוב:
import { EmptyState, ErrorState, RecentArticles } from '@/components';
וזה נחמד כי עכשיו לא צריך לחשוב מאיזו תיקייה הגיעה כל קומפוננטה.
יותר מזה, אם הפרויקט שלי היה ספריה שאנשים אחרים עושים import לדברים מתוכה, אז הייתי יכול להעלות גירסה ולשנות מיקומים של קבצים ושמות שלהם בלי שאותם אנשים שמשתמשים בספריה שלי יצטרכו לעדכן את הקוד שלהם. זה כבר ניצחון רציני.
למה לא לכתוב ככה
בחזרה לשורת ה import שהצגתי בקטע הקודם-
import { EmptyState, ErrorState, RecentArticles } from '@/components';
מי שקורא את זה צריך להכיר לעומק את מבנה התיקיות בפרויקט (ולזכור את המבנה בעל פה), או לסמוך על IDE שידע לקרוא איפה כל דבר. לפעמים יש לנו גישה לכזה IDE אבל לפעמים אנחנו צריכים לקרוא קוד בסביבה פחות ידידותית ואז יותר קשה למצוא את הקומפוננטות.
יותר גרוע, קבצי index.ts דורשים תחזוקה - כל פעם שאני משנה שם של משהו בפרויקט או מזיז קובץ ממקום למקום אני צריך לתקן את ההפניות בקובץ האינדקס המתאים. אם הפרויקט הוא ספריה עם משתמשים חיצוניים זה לגמרי שווה את ההשקעה, אבל אם המשתמש היחיד בקומפוננטות הוא קוד הפרויקט עצמו אז לא הרווחנו כלום.1 420
איך ליצור דף Github Pages לפרויקט שלך
בין אם אתם בונים פרויקט בשביל העולם או רק בשביל קורות החיים, דף ווב לפרויקט הזה שמסביר מה הוא עושה יכול לעזור לאנשים למצוא אתכם, או לבוסים פוטנציאליים להתרשם מהעבודה שלכם. ואם כבר הפרויקט שמור על גיטהאב בואו נבנה לו גם דף מידע שיאוחסן באותו מקום, בתשתית של Github Pages.
איך ליצור דף Github Pages לפרויקט
אחרי שהעליתם את הפרויקט לגיטהאב הגיע הזמן ליצור עבורו דף מידע. הדף הוא פרויקט Front End רגיל שנשמר בתיקייה נפרדת בפרויקט, בדוגמה שלי אני קורא לתיקיה gh-pages. אני מנהל את הקבצים בדף המידע בענף נפרד, שגם לו אני קורא בשביל הנוחות gh-pages. כל פעם שנעלה שינוי לאותו ענף באופן אוטומטי גיטהאב יבנה את דף המידע ויעדכן אותו ברשת. הכתובת של דף המידע תהיה:
https://<user>.github.io/<repo>
כאשר user הוא שם המשתמש בגיטהאב שלכם ו repo הוא שם המאגר של הפרויקט. מוכנים? בואו ניכנס לתיקיית הפרויקט ונצא לדרך-
1. צרו את הענף gh-pages בו יישמר הדף:
$ git switch -c gh-pages
2. באותו ענף צרו תיקיה חדשה עבור הקבצים של הדף. אפשר ליצור את הפרויקט עם vite.
$ npm create vite@latest gh-pages
3. ערכו את הקבצים שבתיקייה כדי ליצור פרויקט צד-לקוח (אני בחרתי ריאקט וטייפסקריפט, אבל אפשר לבחור כל טכנולוגיית צד לקוח שתרצו). מה שחשוב שהפרויקט יוכל להיבנות עם npm build בלי שגיאות.
4. מעלים את העבודה שלנו לענף החדש במאגר:
$ git add .
$ git commit -m 'created gh page'
$ git push --set-upstream origin gh-pages
5. תקנו את ההגדרות - בקובץ vite.config.js או vite.config.ts יש לעדכן את מפתח ה base כדי שיתאים לדומיין אליו אתם הולכים להעלות את הפרויקט. אנחנו יוצרים דף גיטהאב לפרויקט קיים ובמקרה שלי שם המאגר הוא blog-to-telegram לכן אני אשתמש בשם הפרויקט בתור base. הקובץ אחרי השינוי נראה כך:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
base: '/blog-to-telegram/',
plugins: [react()],
})
6. צרו תיקיה חדשה בפרויקט (תחת התיקיה הראשית) בשם .github/workflows ובתוכה צרו קובץ באיזה שם שתבחרו עם סיומת yml, למשל לקובץ שלי קראתי deploy-gh-page.yml. תוכן הקובץ הוא:
* Simple workflow for deploying static content to GitHub Pages *
name: Deploy static content to Pages
on:
# Runs on pushes targeting the default branch
push:
branches: ['gh-pages']
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
* Sets the GITHUB_TOKEN permissions to allow deployment to GitHub Pages *
permissions:
contents: read
pages: write
id-token: write
* Allow one concurrent deployment *
concurrency:
group: 'pages'
cancel-in-progress: true
jobs:
# Single deploy job since we're just deploying
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
- name: Install dependencies
working-directory: ./gh-pages
run: npm install
- name: Build
working-directory: ./gh-pages
run: npm run build
- name: Setup Pages
uses: actions/configure-pages@v3
- name: Upload artifact
uses: actions/upload-pages-artifact@v2
with:
# Upload dist repository
path: './gh-pages/dist'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v2
7. דחפו את כל השינויים לפרויקט:
$ git add .
$ git commit -m 'add workflow'
$ git push
כעת הכנסו לדף הפרויקט שלכם בגיטהאב ובחרו בטאב Actions. לחצו על האקשן החדש שיצרתם ואם הכל עבד כמו שצריך אתם תראו לידה סימן של וי ירוק שמסמן שהיא בוצעה בהצלחה. לחצו על הוי הירוק ותגיעו למסך ההרצה ובו יש תיבה בשם deploy עם הנתיב לאן נשמר הדף שלכם. לחצו על הלינק כדי לראות את העמוד באוויר.
Вже доступно! Дослідження Telegram за 2025 — головні інсайти року 
