ToCode
Open in Telegram
1 419
Subscribers
No data24 hours
-27 days
-530 days
Posts Archive
1 419
לפעמים כפל קוד דווקא יכול להיות יותר קריא
בואו נראה דוגמה פשוטה מ vue לצמצום כפל קוד ואז ננסה לראות מה דעתנו על השינוי. אני מתחיל עם משתנה בשם seconds ושני משתנים מחושבים בשם minutes ו hours:
const seconds = ref(0);
const minutes = computed(() => {
get() { return seconds / 60},
set(minutes) { seconds.value = minutes * 60}
});
const hours = computed(() => {
get() { return hours / 3600},
set(hours) {seconds.value = hours * 3600}
})
ואז אני שם לב ש minutes ו hours בעצם חושבו באותה צורה אז אני מארגן מחדש את הקוד:
function deriveTime(start: Ref<number>, factor: number) {
return computed({
get() { return Number((start.value / factor).toFixed(2)) },
set(newValue) { start.value = newValue * factor }
})
}
const seconds = ref(0);
const minutes = deriveTime(seconds, 60);
const hours = deriveTime(seconds, 3600);
יותר מזה, מאחר ו deriveTime לא באמת קשורה לקומפוננטה אני יכול להעביר אותה לקובץ אחר ואז יש לי רק:
const seconds = ref(0);
const minutes = deriveTime(seconds, 60);
const hours = deriveTime(seconds, 3600);
והשאלה - איזה API טוב יותר? כמה מחשבות:
1. הפונקציה deriveTime משנה התנהגות. בזה שהיא מחביאה קריאה ל computed היא מחזיקה בתוכה מטען סמנטי משמעותי. כל מי שיקרא את הקוד יצטרך לדעת שפונקציה זו מחזירה ערך מחושב ושומרת על ריאקטיביות, ושאת הערך המחושב הזה אפשר גם לשנות.
2. במילים אחרות החוזה של פונקציית deriveTime עם הקוד החיצוני כולל יותר התחייבויות ממה שרואים ברשימת הפרמטרים.
3. אם הפונקציה נמצאת בשימוש בהרבה מקומות בקוד המערכת יש הגיון להוסיף לה את המטען הסמנטי הזה. ממילא כל מי שיקרא את הקוד צפוי להכיר אותה. ככל שהיא פחות נמצאת בשימוש היא עלולה לבלבל.
4. פיתרון טוב יהיה להוסיף תיעוד באתר הקריאה שמסביר מה קורה פה, משהו כזה:
const seconds = ref(0);
// create reactive modifiable computed refs for other time units:
const minutes = deriveTime(seconds, 60);
const hours = deriveTime(seconds, 3600);
מצד שני ככל שההסבר בתיעוד יותר ארוך אולי עדיף להישאר עם הקוד המקורי.
פיתרון ביניים נוסף שיכול לעזור כאן הוא להעביר רק את יצירת האוביקט לפונקציה אבל להשאיר את הקריאה ל computed באתר הקריאה כלומר:
const seconds = ref(0);
const minutes = computed(deriveTime(seconds, 60));
const hours = computed(deriveTime(seconds, 3600));
כתיבה כזאת משאירה את "מילת המפתח" computed במקום בולט וכך אנחנו יודעים מה אנחנו יוצרים. עדיין גם גירסה זאת מסתירה את העובדה שהאוביקט כולל גם getter וגם setter ולכן אפשר לשנות את הדקות והשעות.
הוצאת קוד לפונקציה כדי למנוע חזרה היא הרבה פעמים רעיון טוב. היא מאפשרת תחזוקה טובה יותר של הקוד ותיקון בעיות רק במקום אחד. מצד שני כשאנחנו מסתירים קריאה לפונקציות ליבה של הפריימוורק בתוך פונקציית עזר שלנו התוצאה עלולה להיות קוד מבלבל יותר ורק להקשות על התחזוקה.
מה דעתכם? במצב כזה הייתם משתמשים בפונקציית עזר, נשארים עם הקוד הכפול או בוחרים פיתרון אחר?1 419
משחקים עם Embeddings
חוץ משיחות, ChatGPT גם יודע לחשב משהו שנקרא Embedding לכל טקסט שניתן לו. אמבדינג זה בעצם וקטור של מספרים שמייצג איפה במוח של ChatGPT הטקסט הזה יושב, כי אפשר לדמיין את המוח של ChatGPT בתור מרחב רב מימדי של טקסטים. בעזרת וקטור כזה אפשר לפעמים למצוא דברים שקשורים אחד לשני, כי הם כנראה יהיו "קרובים" באותו מרחב רב מימדי.
בפוסט היום הלכתי לנסות לחשב Embeddings ולראות אם זה יעזור לי לזהות פוסטים מהבלוג שקשורים לפוסט מסוים. המסקנה אינה חד משמעית אבל אני מקווה שהדרך תהיה מעניינת.
איך מחשבים Embedding
הרעיון הראשון והוא מאוד פשוט זה שבשביל לחשב Embedding רק צריך לשלוח טקסט למודל שפה גדול, במקרה שלנו ל ChatGPT. אני ברובי אז לקחתי ספריה שנקראת ruby-openai והשתמשתי בקוד שנראה ככה:
response = client.embeddings(
parameters: {
model: "text-embedding-3-large",
input: "The food was delicious and the waiter..."
}
)
puts response.dig("data", 0, "embedding")
בשביל לחשב Embeddings על פוסטים הפעלתי את הקוד הבא על 100 הפוסטים האחרונים שפרסמתי:
sources = blogPost.last(100).map(&:markdown_source)
embeddings2 = sources.map do |s|
client.embeddings(parameters: {model: "text-embedding-3-large", input: s })
end
ואחרי המתנה קצרה קיבלתי מערך של 100 אמבדינגס עבור 100 פוסטים אחרונים מהבלוג.
מרחק בין Embeddings
ראיתי ברשת כל מיני דרכים לחשב מרחקים בין Embeddings אבל נשמע שהפופולרית ביותר למצוא דברים שקשורים אחד לשני נקראת cosine similarity. הלכתי ל ChatGPT וביקשתי ממנו שיכתוב לי פונקציה ברובי לחשב את זה בין שני וקטורים:
def cosine_similarity(vec1, vec2)
dot_product = vec1.zip(vec2).map { |a, b| a * b }.sum
magnitude1 = Math.sqrt(vec1.map { |v| v**2 }.sum)
magnitude2 = Math.sqrt(vec2.map { |v| v**2 }.sum)
dot_product / (magnitude1 * magnitude2)
end
התוצאות
עכשיו נשאר רק לשחק עם המספרים. לדוגמה לקחתי את הפוסט האחרון שפירסמתי (ניסוי כתיבת Directives בריאקט) וחיפשתי איזה Embeddings היו יחסית קרובים אליו, כלומר במרחק גדול מ 0.5:
distances = embeddings2.map {|i| cosine_similarity(embeddings2[-1]["data"][0]["embedding"], i["data"][0]["embedding"])
}
distances.each_index.select {|i| distances[i] > 0.5 }
התוצאה כללה די הרבה פוסטים:
1. איך עובד פרויקט Rails עם ESBuild
2. אז OpenAI עברו לרמיקס
3. חמש דקות עם flet
4. ניסוי דינו - מה אפשר לעשות היום עם Deno ו next.js
5. ניסוי ריילס: משחק איקס עיגול
6. השתמשו באיזה ספריות שתרצו
7. טיפ LLM - איך זה עובד ב X ?
8. ריאקט או Vue - הדגמה דרך הוק קצר
9. ואז זה נגמר
10. ניסוי React - ואם היו Directives?
אם מחפשים רק פוסטים שהמרחק שלהם גדול מ 0.6 מקבלים רק את הפוסט המקורי ואת "ריאקט או Vue - הדגמה דרך הוק קצר". שזה יותר הגיוני כי מדובר על שני פוסטים שמשווים בין ריאקט ל Vue מזוויות שונות.
אחרי זה לקחתי את הפוסט "ניסוי דינו - מה אפשר לעשות היום עם Deno ו next.js" וחיפשתי באותה שיטה פוסטים קשורים. הגעתי ל:
1. ה Killer Feature החדש של node.js (או: האם זה הסוף של דינו?)
2. הונו הוא כל מה שרציתי מ express
3. אז OpenAI עברו לרמיקס
4. דוגמת דינו: שמירת תמונות מויקיפדיה
שזה כבר די מוצלח.
הצעד הבא
עכשיו שהתשתית במקום אפילו אם לא מושלמת אני מקווה לקחת את זה צעד נוסף קדימה ולהתחיל לחשב Embeddings של פוסטים כשאני מעלה אותם לאתר ולשמור ב DB, וגם להריץ חישוב מרוכז על כל הפוסטים שכבר פירסמתי (לפחות בשנה האחרונה). אחרי שזה יהיה במקום אפשר יהיה להוסיף קופסת "פוסטים קשורים". עדיין לא ברור אם זה יעבוד, אבל הניסוי היום הראה שיש סיכוי.1 419
ניסוי React - ואם היו Directives?
אחד המנגנונים החמודים של Vue הוא ה Directives שמאפשרים לכתוב פחות קוד בקוד התבנית ולקחת חלקים של קוד שמשפיעים על ה DOM כדי להשתמש בהם בכמה מקומות. בשביל המשחק בואו ננסה לבנות משהו דומה בריאקט.
מה זה Directives
אחת התכונות שאנשים אוהבים ב Vue היא ה Directives. בקצרה זאת היכולת לכתוב מאפיין בתוך אלמנט ולקבל התנהגות מסוימת עבור המאפיין הזה. לדוגמה:
<p v-if="seen">Now you see me</p>
המאפיין v-if הוא מאפיין מיוחד מסוג זה. הוא כולל "קוד" שרץ כל פעם שצריכים לרנדר את האלמנט והקוד הזה יכול לשנות את האלמנט או אפילו להוציא אותו לגמרי מהעץ. בדוגמה של v-if אם המשתנה seen הוא אמיתי האלמנט יופיע ואם הוא "שקר" אז האלמנט לא יופיע. הם עושים המון דברים עם Directives ב Vue אבל מה שחשוב לקחת מכאן זה:
1. דירקטיבס מאפשרים להשפיע על קומפוננטה מבחוץ, בלי קשר לקוד של הקומפוננטה עצמה (לא הייתי צריך לגעת בקוד של p בשביל להיות מסוגל להוסיף לו v-if).
2. אפשר להוסיף דירקטיבס לכל קומפוננטה.
3. דירקטיב הוא חלק מקוד הקומפוננטה שלא כתוב בתוכה ומתווסף או מוסר בצורה של הרכבה.
חלומות בריאקט
אז הלכתי לנסות לכתוב משהו דומה בריאקט, לפחות את הרעיון. התחלתי מהקוד הבא לקומפוננטה הראשית App:
function App() {
const [count, setCount] = useState(0)
return (
<>
<div>
<Text v-if={count % 2 == 0}></Text>
<Text v-highlight={true} v-if={count % 2 == 0}></Text>
<button onClick={() => setCount(c => c+1)}>{count}</button>
</div>
</>
)
}
וכבר פה אני מבין את המגבלות שלי. אין לי איך להשפיע על קומפוננטות כמו p או div או כל קומפוננטה מובנית אחרת בריאקט. הדברים היחידים שאני יכול להשפיע עליהם הם קומפוננטות שאני בונה, ולכן בשביל לנסות לכתוב Directives יצרתי קומפוננטה בשם Text שמציגה טקסט.
אני גם יודע שבשביל להוסיף תמיכה ל Directives צריך להיכנס לקוד הקומפוננטה, אבל אני לא רוצה לעשות את זה לכל קומפוננטה במערכת לכן יצרתי פונקציה שעוטפת הגדרה של קומפוננטות:
const Text = directify((props) => {
return <p>Hello World</p>
});
כל עוד אני מגדיר קומפוננטות עם הפונקציה directify אפשר יהיה להוסיף להן Directives.
קוד הפונקציה הוא די פשוט כי זה רק פוסט בבלוג, כמובן שאם תרצו לקחת את הרעיון הזה למנגנון אמיתי תצטרכו לחשוב קצת יותר לעומק על סדר פעולות ומתי מפעילים את קוד ה Directive. בינתיים כתבתי את זה:
export const Directives = {};
export function directify(c) {
return (props) => {
let el = c(props);
for (const [directiveName, directive] of Object.entries(Directives)) {
if (directiveName in props) {
if (el) {
el = directive(el, props[directiveName]);
} else return el;
}
}
return el;
}
}
הפונקציה מקבלת קומפוננטה, מפעילה אותה ומעבירה את התוצאה ל Directives. ליד הפונקציה יש אובייקט גלובאלי של Directives.
ה Directives עצמן באובייקט הגלובאלי נראות כך:
Directives['v-if'] = (el, value) => {
if (value) {
return el;
} else {
return false;
}
}
Directives['v-highlight'] = (el, value) => {
return cloneElement(el, {
style: {
...(el.props.style),
backgroundColor: 'yellow',
}
})
}
אז כן זה עובד, אבל צריך עוד הרבה עבודה. אפילו בדוגמה הקטנה של ה v-if צריך להבין ש v-if אמור לרוץ לפני שמרנדרים את האלמנט ואם הוא מחזיר false אז לא צריך לרנדר את האלמנט בכלל. מצד שני v-highlight עובד יחסית בסדר. אני לא בטוח איך לדמיין את v-for אבל זה יכול להיות תרגיל מעניין.
החיסרון הכי גדול הוא שאי אפשר להוסיף Directives בדרך הזאת לאלמנטים המובנים בריאקט, לכן נצטרך גם ליצור גירסה "תומכת Directives" של כל הקומפוננטות המובנות.
ספריה מעניינת שהלכה בכיוון הזה היא react-directive. שם הם התמקדו בהוספת תמיכה ב Directives לאלמנט div יחיד ומימוש ה Directives המובנים בלי לבנות תשתית להרחבה ל Directives שלנו.1 419
ואז זה נגמר
מישהו ברדיט שאל- יש לנו אלפי בדיקות אנזים לפרויקט ריאקט ואני רואה שאנזים כבר לא נתמך ורק בגלל הבדיקות אנחנו תקועים עם גירסה ישנה של ריאקט. מה עושים?
ואותה שאלה חוזרת כל הזמן ובאינסוף גירסאות ... מה עושים עם אפליקציית אנגולר1? מה יקרה עם אפליקציית ה next.js שלי במעבר ל App Router? מה עושים עם כל הקלאסים כשיצאו React Hooks? מה עושים עם אתר הפלאש? הסילברלייט? ה Java Applet?
ובדיקות זה עוד החלק הקל. לא נעים אבל אפשר למחוק את כל הבדיקות ולהתחיל לכתוב מחדש ב React Testing Framework. לפעמים צריך לעדכן קוד ולפעמים לכתוב מוצר חדש מאפס.
יש שיגידו ששינויים כאלה הם הסיבה לכתוב Micro Services. אני לא בגישה הזאת. שינויים דרסטיים (פריימוורק שלם לא עובד יותר) אינם פתאומיים, וכמעט תמיד אפשר לשכתב דברים בהדרגה או רק חלק מהמוצר. בסוף אנחנו חיים בעולם דינמי וכדאי לזכור החדשים נוצרו מסיבה - הרבה פעמים כתיבה מחדש של אותו קוד עם הכלים החדשים תהיה הרבה יותר מהירה מהכתיבה בפעם הראשונה, גם בגלל שאנחנו יודעים יותר טוב מה לעשות וגם בגלל שהכלים החדשים פשוטים וטובים יותר.
1 419
ריאקט או Vue - הדגמה דרך הוק קצר
הרבה אנשים מדברים על עקומת הלמידה של ריאקט בהשוואה ל Vue ויש בזה משהו. נכון לריאקט 18 (כלומר לפני ש Suspense יהיה דבר גדול ולפני use והבעיות שלו), האתגר הכי גדול עם ריאקט היה useEffect. בואו נראה דוגמה קצרה של אפקט בהשוואה בין שתי הספריות כדי להבין קצת את הרוח והייחודיות של כל אחת.
ריאקט: הקשבה לתנועת עכבר
בדוגמה אני כותב Hook שמקשיב לתנועת עכבר כדי להציג את המיקום הנוכחי של העכבר באמצע המסך. זה הקוד של ה Hook והקומפוננטה שמשתמשת בו, בשביל הפשטות באותו קובץ:
import { useEffect, useState } from 'react'
import './App.css'
function useMousePosition() {
const [position, setPosition] = useState([0, 0]);
useEffect(() => {
function handleMove(ev: MouseEvent) {
setPosition([ev.clientX, ev.clientY]);
}
window.addEventListener('mousemove', handleMove);
return () => {
window.removeEventListener('mousemove', handleMove);
}
}, [])
return position;
}
function App() {
const position = useMousePosition();
return (
<h1>Position: {position[0]}, {position[1]}</h1>
)
}
export default App
מבחינת הקומפוננטה השימוש ב Custom Hook נראה מאוד פשוט, והקומפוננטה כוללת בסך הכל את שורת הגדרת המשתנה ואת התבנית. בזכות טייפסקריפט אנחנו יודעים ש position מורכב משני מספרים, וגם ב Hook-ים יותר מסובכים טייפסקריפט יכול לעזור לזכור מה בדיוק חוזר מהפונקציה.
מבחינת קוד ה Hook יש פה קצת יותר אתגר בגלל המבנה של useState, useEffect והקשר ביניהם:
1. פונקציית useState מחזירה משתנה ופונקציית עדכון לאותו משתנה. במקרה של מערכים זה מבלבל וצריך לזכור שאי אפשר פשוט לשנות ערך באחד התאים של המערך, אלא תמיד לקרוא לפונקציית העדכון.
2. יותר מבלבל הוא הממשק של useEffect, בגלל שהוא מחולק ל-3 - הפונקציה הראשונה נקראת כדי ליצור את האפקט, הפונקציה השנייה (שחוזרת מ useEffect) צריכה לנקות את האפקט ומערך התלויות שקובע מתי ליצור מחדש את האפקט. כל אחד מהחלקים הכרחי אבל ניתן להשמטה וכל חלק ששוכחים לכתוב יכול להביא לתוצאות מוזרות.
גירסת Vue
אותה דוגמה בגירסת Vue לא נראית יותר מדי שונה, וזו התחלה טובה:
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
function useMouse() {
const position = ref([0, 0])
function update(event: MouseEvent) {
position.value = [event.clientX, event.clientY];
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return position;
}
const position = useMouse();
</script>
<template>
<h1>{{ position[0] }}, {{ position[1] }}</h1>
</template>
הקומפוננטה מגדירה את position בתור ref במקום בתור useState, והעדכון שלו מתבצע על ידי השמה למאפיין .value. גם כאן אי אפשר לשנות רק תא אחד במערך וזה יכול להיות מבלבל.
השינוי היותר גדול הוא הממשק של ה Hook - הפעם במקום להשתמש בפונקציה אחת אנחנו קוראים ל-2, הפונקציה onMounted ו onUnmounted, ופה גם המפתח להבין את ההבדלים בין הספריות. בעוד שריאקט הכניסה את המילה Effect בתור אוסף של 3 חלקים, ב Vue הלכו על גישה יותר Low Level בה אנחנו פשוט מגדירים איזה קוד להפעיל מתי. הייתרון בגישה הוא הפשטות ועקומת הלימוד היותר קלה, כי לא צריך לחשוב מה זה אפקט ומתי טוב או לא טוב להשתמש בו, אבל החיסרון הוא שיש יותר מקום לטעויות כשצריך לחשוב למשל באיזה מצבים רוצים ליצור מחדש את ה Event Handler. הקשר בין onMounted ל onUpdated יכול להיות מסורבל לכתיבה ולכן בריאקט עברו ממודל של Lifecycle למודל של אפקטים, אבל מצד שני המודל החדש בריאקט יצר עקומת לימוד הרבה יותר משמעותית ובפרספקטיבה של השנים שעברו מאז יצירת המודל אני לא בטוח שזה היה רעיון טוב.
מה דעתכם? איזה מהגישות אהבתם יותר? ואיזה מאפיינים ייחודיים של כל ספריה אתם אוהבים?1 419
טיפ CSS: כותרות דביקות לטבלה
הערך sticky של מאפיין position ב CSS יכול לחסוך הרבה כאב ראש כשרוצים לבנות טבלה עם כותרות דביקות - כלומר טבלה עם פסי גלילה בה שורת הכותרת נשארת תמיד למעלה (כמו באקסל) כשגוללים למטה כדי שאפשר יהיה לראות את שמות העמודות, ובאותו אופן העמודה הראשונה תישאר תמיד על המסך כשנגלול הצידה. זה הקוד בקצרה:
thead {
position: sticky;
top: 0;
z-index: 2;
}
th:first-child, td:first-child {
position: sticky;
left: 0;
}
td:first-child {
background-color: white;
}
th {
background-color: #f2f2f2;
}
בואו נעבור על כל המאפיינים:
1. המאפיין position: sticky גורם לאלמנט "להידבק" למיקום מסוים אם גלילה תגרום לו לצאת מהמסך. זה בדיוק האפקט שאנחנו רוצים לכותרות של הטבלה. ל thead הגדרתי שיידבק לחלק העליון בעזרת top וכל td שהוא ילד ראשון, כלומר כל העמודה הראשונה, נדבקת לצד שמאל.
2. הגדרת z-index על thead קבעה ששורת הכותרת העליונה תופיע מעל עמודת הכותרת וכך שם העמודה הראשונה לא מוסתר בגלילה למטה על ידי הערכים של העמודה.
3. אלמנטים שמוגדרים בתור sticky יופיעו "מעל" אלמנטים אחרים ולכן צריכים הגדרת צבע רקע כי אחרת נוכל לראות את הטקסט של השורות כשהן עוברות "מתחת" לשורת הכותרת או של העמודות כשהן עוברות "מתחת" לעמודת הכותרת.
העליתי לקודפן את הקוד המלא עם כל ה CSS וה HTML של הטבלה מוזמנים לשחק עם זה בקישור:
https://codepen.io/ynonp/pen/zYgWKOM
או מוטמע כאן:
<iframe height="400" style="width: 100%;" scrolling="no" title="Untitled" src="https://codepen.io/ynonp/embed/zYgWKOM?default-tab=html%2Cresult" frameborder="no" loading="lazy" allowtransparency="true" allowfullscreen="true">
See the Pen <a href="https://codepen.io/ynonp/pen/zYgWKOM">
Untitled</a> by Ynon Perek (<a href="https://codepen.io/ynonp">@ynonp</a>)
on <a href="https://codepen.io">CodePen</a>.
</iframe>1 419
סטראבה
כואב לשים לב שהרבה בעיות אבטחת מידע הן בעצם בעיות תיאום ציפיות. היה מספיק שסטראבה היתה מציגה פופ-אפ לפני שיתוף מסלול שאומר משהו כמו-
> אתה עומד לשתף מידע על מסלול ריצה שביצעת עם האינטרנט. שים לב: כל בן אדם אחר שרץ את אותו מסלול יוכל לראות את הפרטים שלך ואת כל המסלולים האחרים בהם רצת. האם אתה בטוח שאתה רוצה להמשיך?
ואולי בעצם איש ה Product שהיה מנסח כזאת הודעה היה מבין ששיתוף כזה הוא שבור וכולל בעיית אבטחה מובנית וכך היה נחסך מאיתנו פיצ'ר ריגול מיותר.
1 419
טיפ לדיבוג בעיות קונפיגורציה של Apache
אני יודע אתם כבר לא משתמשים ב Apache, יש לכם nginx או שאתם שמים את הפרויקטים שלכם על vercel או AWS ומי בכלל מתחזק שרתים בימינו. נו, אני צוחק. המון אנשים מתחזקים שרתים, ובהרבה מהם רץ עדיין Apache. ואחת הבעיות עם Apache היא שהודעות השגיאה שלו לא תמיד קיימות או לא מדויקות.
אז אם לדוגמה יש לי קובץ קונפיגורציה ל Site עם טעות, לפעמים אפאצ'י יסרב לעלות. לפעמים הוא יספר פרטים על הטעות ולפעמים הוא פשוט יעביר את התנועה שנשלחה ל Site הזה ל Site אחר עם קונפיגורציה יותר גנרית.
דרך אחת לקבל יותר אינפורמציה על Apache היא הפקודה:
apache2ctl -S. הפלט מהשרת שלי נראה בערך כך:
AH00548: NameVirtualHost has no effect and will be removed in the next release /etc/apache2/sites-enabled/002-tocode.co.il.conf:1
VirtualHost configuration:
*:443 is a NameVirtualHost
default server tocode.co.il (/etc/apache2/sites-enabled/002-tocode.co.il.conf:8)
port 443 namevhost tocode.co.il (/etc/apache2/sites-enabled/002-tocode.co.il.conf:8)
port 443 namevhost www.tocode.co.il (/etc/apache2/sites-enabled/002-tocode.co.il.conf:27)
*:80 is a NameVirtualHost
default server tocode.co.il (/etc/apache2/sites-enabled/002-tocode.co.il.conf:3)
port 80 namevhost tocode.co.il (/etc/apache2/sites-enabled/002-tocode.co.il.conf:3)
וזה מראה לנו איזה Host-ים יש לנו על השרת, באיזה שמות מטפל כל Host והכי חשוב באיזה קובץ קונפיגורציה כל Host הוגדר. המשך הפלט מראה עוד נתוני debug שלי היו פחות חשובים. נסו את זה על השרתים שלכם וספרו אם למדתם משהו חדש או מצאתם משהו מיותר.1 419
מבחינת קוד העבודה עם דינו היתה מאוד פשוטה בזכות התמיכה בסטנדרטים המתקדמים של פיתוח רשת, כלומר אפשר להשתמש ב fetch כדי למשוך מידע מ URL מרוחק, Promise.all כדי לחכות לכמה בקשות רשת במקביל ויש להם פונקציה בשם
Deno.writeFile כדי לכתוב לקובץ. בגדול מבחינת קוד אין שם שום דבר ש Chat GPT לא יכל היה לכתוב. בעיה אחת שכן היתה לי עם הקוד היא הלולאה:
for (let i=0; i < names.length; i++) {
const fixed = await modifyImage(buffers[i], 512, 512);
await Deno.writeFile(path.join(dir, names[i]), fixed);
}
הלולאה מפעילה את modifyImage בצורה סדרתית תמונה אחרי תמונה. כשניסיתי להפעיל את הפונקציה במקביל על כל התמונות עם Promise.all קיבלתי תוצאות שגויות. לא הצלחתי להבין למה.
בעיה נוספת עם הקוד היתה ש Deno לא הצליח לפענח את קובץ ה index.d.ts של ספריית wikipedia מ npm, ולכן אי אפשר היה להשתמש בספריה מקובץ TypeScript ועברתי ל JavaScript בקובץ הלוגיקה.
דוקר
הקובץ האחרון הוא ה Dockerfile וגם פה יש כבר פיתרון טוב לדינו זה נראה ככה:
FROM denoland/deno:2.0.3
* The port that your application listens to. *
EXPOSE 8000
WORKDIR /app
RUN bash -c "mkdir -p /app/files && chown -R deno /app/files"
* Prefer not to run as root. *
USER deno
* These steps will be re-run upon each file change in your working directory: *
COPY . .
* Compile the main app so that it doesn't need to be compiled each startup/entry. *
RUN deno cache main.ts
CMD ["run", "-A", "main.ts"]
האימג' deno מ Denoland היא אימג' בסיסי להרצת תוכנית דינו וצריך להוסיף עליה רק את הקבצים שלנו ולהריץ.
בעיה אחת שהיתה לי עם ה Dockerfile הזה היא שהתקנת התלויות קורית בשורה:
RUN deno cache main.ts
מה שאומר שבשביל להתקין את התלויות אני צריך להעתיק לאימג' את כל הקבצים של הפרויקט ולכן קורה רק אחרי העתקתם. בדוקר זה אומר שאי אפשר להשתמש בתלויות בתור שכבה נפרדת ולכן כל שינוי בקוד בבנייה הבאה הוא יצטרך להתקין מחדש את כל התלויות. ב node אני מזכיר שהיה לנו את:
COPY package*.json ./
RUN npm install
לפני העתקת שאר הפרויקט וכך שינויים בקוד נכנסו בשכבה עליונה יותר מהתלויות. אני חושב שיש דרך לעשות משהו דומה גם ב Deno אבל עדיין לא מצאתי.
שורה תחתונה העבודה עם דינו לא היתה רעה ויש הרבה דברים שעבדו לא פחות טוב ממה שהייתי מקבל עם node.js, אבל עדיין קשה להתיחס לזה כקפיצת מדרגה. האקוסיסטם עדיין מבולגן והתמיכה במודולים מ npm חלקית, כולל תמיכה חלקית בהגדרות הטיפוסים ששם מה שיכול להוביל לקושי בפרויקטים גדולים יותר.
האתגר הבא מבחינת דינו נשאר לדעתי החיבור ל npm - או על ידי שיפור התמיכה במודולים מ npm והפיכתם ל First Class Citizens באקוסיסטם או על ידי יבוא מאוד מאסיבי של מודולים מ npm ל JSR.1 419
דוגמת דינו: שמירת תמונות מויקיפדיה
כבר כמה ימים שאני משחק עם קוד כדי להוריד תמונות מויקיפדיה בשביל בוט מילון שאני כותב, וכמו הרבה פעמים בפרויקטי צד אני אוהב לבחור טכנולוגיות שמעניינות אותי כדי לראות איך זה עובד בעולם האמיתי. הפעם לקחתי את דינו ורציתי לראות אם אני יכול להשתמש ב Deno2 לפרויקט ובעיקר מה יהיו הקשיים. סיכום הניסוי בפוסט כאן.
מה בונים
בשביל לשלוח תמונה דרך טלגרם היא צריכה להיות לא גדולה מדי אבל הבעיה שתמונות בויקיפדיה יכולות להגיע בכל גודל. לכן נבנה מיקרו סרביס ב Deno שמקבל מאמר מויקיפדיה (שם הנושא והשפה), מושך את כל התמונות, מקטין אותן ושומר לתיקייה מקומית. אחרי זה אפשר יהיה לקחת את התמונות מהסרביס במקום מויקיפדיה כדי לקבל את הגירסה הקטנה שגם מתאימה לטלגרם.
אפשר לראות את הקוד המלא בגיטהאב:
https://github.com/ynonp/wikipedia-image-fetcher
הקובץ main.ts
פריימוורק ה Web שבחרתי נקרא Hono. כתבתי עליו כאן וכבר הרבה זמן שאני רוצה לבנות עוד דברים בעזרתו. הונו מהיר וקל לשימוש ואני מרגיש שהוא גירסת אקספרס שמתאימה ל 2024. הנתיב הראשון שאני מכניס לפרויקטים פשוט מחזיר שהכל בסדר:
app.get('/up', (c) => {
return c.json({ok: true})
})
וזה יעזור אם יום אחד נרצה לשלב את הפרויקט באיזה מערכת Zero Downtime Deployment כדי שהיא תדע שהכל עלה כמו שצריך, אבל גם באופן כללי בשבילנו שנדע שדברים עובדים.
הנתיב הבא הוא היותר מעניין. הוא מקבל שפה ונושא, בודק אם יש לי כבר תמונות על הנושא הזה. אם אין תמונות הוא יוריד אותן ואם יש הוא רק יחזיר את רשימת התמונות:
app.get('/images', async (c) => {
const { topic, lang } = c.req.query();
if (await lib.hasSavedImages(lang, topic)) {
const images = await lib.getSavedImages(lang, topic);
return c.json(images);
} else {
const images = await lib.downloadWikipediaImages(lang, topic);
return c.json(images);
}
})
ב hono אין בעיה לכתוב קוד טיפול בנתיב אסינכרוני ולהחזיר json. הדבר היחיד שהייתי צריך להתרגל אליו זה שיש פרמטר אחד לפונקציות הטיפול במקום שני פרמטרים כמו באקספרס.
הונו עובד ב Deno אבל גם ב node.js ו bun. יש לו ביצועים מצוינים ובינתיים אין לי שום תלונות לגביו. אנחנו מפעילים את השרת עם:
Deno.serve(app.fetch)
שמירת תמונות ושינוי גודל
הקובץ השני בפרויקט נקרא wikipedia_fetcher.js ושם כתבתי את כל הלוגיקה, ושם גם דברים התחילו להסתבך.
בעיה ראשונה שהיתה לי עם דינו היא שדינו יודע למשוך ספריות מ-3 מאגרים: יש לו את המאגר הרשמי שנקרא JSR, שם אין כמעט חבילות אבל נראה שה Toolchain של דינו עובד איתו הכי טוב. יש מאגר רשמי ישן יותר שנקרא Deno land, שם יש יותר חבילות אבל זה לא נתמך מכל הכלים ויש לו תמיכה בספריות מ npm שם הכלים עובדים די טוב אבל הספריות עצמן לא תמיד נתמכות במלואן. וכך יוצא שמצד אחד אי אפשר להגביל את עצמנו רק ל JSR כי אין שם מספיק חבילות אבל עבודה עם מאגרים אחרים יוצרת בלאגן בפרויקט.
ככה מתחיל קובץ הלוגיקה בפרויקט שלי, ושוב אני מזכיר זה פרויקט ממש קטן שכתבתי בכמה שעות:
import * as fs from "@std/fs";
import * as path from "jsr:@std/path";
import wiki from "wikipedia";
import {basename} from "https://deno.land/std@0.224.0/url/mod.ts";
import {
ImageMagick,
initialize,
MagickGeometry,
} from "https://deno.land/x/imagemagick_deno@0.0.31/mod.ts";
הקובץ כולל ספריות מ JSR, ספריות מ Deno land ועוד ספריות שהוספתי בעזרת פקודת deno add ולכן נשמרו בתוך הקובץ deno.json ברשימת ה import-ים:
"imports": {
"@std/fs": "jsr:@std/fs@^1.0.5",
"hono": "jsr:@hono/hono@^4.6.6",
"wikipedia": "npm:wikipedia@^2.1.2"
},
עכשיו ברור שהבעיה המרכזית פה היא שאני לא מספיק מסודר ופשוט זורק ספריות פנימה לפי קוד הדוגמה באותה ספריה. אבל אולי אם האקוסיסטם היה יותר מאורגן גם אני הייתי משקיע יותר.
Available now! Telegram Research 2025 — the year's key insights 
