ToCode
Kanalga Telegram’da o‘tish
טיפים קצרים למתכנתים מאת ינון פרק
Ko'proq ko'rsatish1 419
Obunachilar
-224 soatlar
-17 kunlar
-130 kunlar
Postlar arxiv
1 419
הבעיה עם עצות גרועות
הבעיה עם עצות גרועות היא שהן גרועות. טוב תכלס זאת הבעיה היותר קלה איתן.
עצות גרועות באמת הן כאלה שקשה לזהות שהן גרועות. וזאת בעיה הרבה יותר גדולה, כי רוב הזמן (בבעיות מעניינות) גם לעצות טובות לוקח זמן לעבוד. עצות גרועות נראות כמו עצות טובות, אבל עד שאתה מבין שעבדו עליך זה כבר הרבה יותר קשה לתקן. הנה כמה מאפיינים של עצות גרועות שמתחפשות לטובות:
1. עצה גרועה תשתמש בז'ארגון לא מובן ותנצל פערי ידע בין היועץ למקבל העצה.
2. עצה גרועה הרבה פעמים כן תפתור בעיה, פשוט לא את הבעיה שלך (זה מה שגורם לה להיות מבלבלת).
3. עצה גרועה תתמקד בתוצאה ולא בתהליך, ולא תאפשר לך להבין בזמן שאתה בדרך הלא נכונה.
בואו נחבר את זה רגע לדוגמה. נניח שיש לי בעיה של איטיות ב DB ואני לא מבין למה שליפות מסוימות חוזרות לאט. אני מצלצל ליועץ מומחה לבסיסי נתונים והוא מתחיל להסביר לי על אינדקסים וכמה הם חשובים, וגם מוסיף המון מידע טכני על סוגי האינדקסים ובסוף משכנע אותי לארגן מחדש את הטבלאות והאינדקסים כי רק ככה בעיית הביצועים תיפטר. הוא יוכל גם להפנות אותי ללקוחות אחרים שלו שעשו תהליך דומה ובסוף קיבלו שאילתות מהירות יותר. הפיתרון באמת פותר בעיה אבל לא בטוח שאת הבעיה שלי.
יועץ טוב יותר ייכנס איתי לקוד שלי ויראה לי איך לנתח מה בסיס נתונים עושה כשמריצים שליפה, איזה אינדקסים נמצאים בשימוש ואיזה לא, ומתוך זה יראה לי ממש מה גורם לאיטיות. הדגש פה הוא לתת לי את הכלים להבין את הבעיה ולהתמודד איתה, ולעזור לי כשאני נתקע, במקום לתת לי פיתרון שעבד במקומות אחרים אבל לא בטוח שיעבוד עבורי (ואפילו אם יעבוד לא בטוח אם ימשיך לעבוד עבורי בפעם הבאה שאתקל בבעיה).
הבעיה עם עצות גרועות היא שעצות גרועות הן פשוט עצות טובות שיצאו מקונטקסט.
1 419
באמת צריך לדעת לענות על השאלה הזאת?
יש המון בעיות שאפשר לפתור גם בלי להבין כל שורת קוד בפרויקט.
ואפילו לא מעט בעיות שאפשר לפתור בלי להבין את שורות הקוד הרלוונטיות לבעיה.
ויש מספיק שאלות שיהיה מאוד מעניין ולחלוטין לא מועיל לגלות את התשובה להן. כמה עמוק שווה לצלול בקוד של ספריה חיצונית רק בשביל למצוא באג בקוד שלה, כשאפשר במקום לשדרג גירסה של הספריה החיצונית ולהתקדם עם הפרויקט? לא חבל על הזמן שלנו?
איך נדע כמה עמוק צריך לחפור בתקלה מסוימת כדי להבין אותה עד הסוף? זה תלוי בפרויקט אבל ככלל זה סוג השאלות שלא הייתי מוותר על למצוא להן תשובה במערכות Full Stack:
1. מה מחובר למה? מאיפה מגיע המידע ולאן הוא הולך? איפה המידע נשמר?
2. למה אוביקט מסוים נשלח ברשת? למה תוכן מסוים חוזר? איזה חלק בתוכנה אחראי לכל חלק ב HTML?
3. מה קורה כשאני לוחץ על כפתור או מבצע אירוע אחר? איזה חלקים בקוד לוקחים חלק בטיפול באירוע.
4. מה הספריות החיצוניות בהן אני משתמש? מה תפקיד כל ספריה? באיזה גירסה כל ספריה? מתי היא שודרגה לאחרונה? מה הגירסה העדכנית ביותר שלה? מי מתחזק את הספריה החיצונית הזאת?
5. איזה שרתים יש לי במערכת? מה קורה אם אחד מהם מפסיק לעבוד? מה התוכנות שרצות על כל שרת? מה תפקיד כל תוכנה?
עכשיו אתם- מתחזקים מערכת Full Stack? על איזה שאלות לא תוותרו לעצמכם ותתאמצו למצוא את התשובות?
1 419
עשר שנים של פרונטאנד
אפאל שה כתב על הפרויקטים המובילים ב JavaScript לפני 10 שנים ואיפה הם היום. פוסט מרתק ומלא נוסטלגיה ואני ממליץ לקרוא אותו. בקצרה הוא מחלק את הפרויקטים לנושאים הבאים:
1. פרונטאנד
2. בקאנד
3. באנדלרים
4. מריצי משימות
5. כלי בדיקות
אני לא יודע למה הוא בחר בחלוקה הזאת, אבל כן רוצה להוסיף על הניתוח המעולה שלו כמה מילים על מה השתנה בכל נושא.
פרונטאנד
לפני 10 שנים SPA היו הטרנד הגדול. מאז למדנו שהם לא כאלה טובים כי לא לכולם יש JavaScript ומנועי חיפוש לא הכי מרוצים מהם ובכלל למדנו לאהוב את הדפדפן. התוצאה של השיעור הזה היתה סיבוב חוזר של המטוטלת לכיוון של פריימוורקים של Full Stack. היום מאוד נפוץ למצוא את צוות הפרונטאנד מתחזק יישום next.js.
מעניין לשים לב שלמרות שהפריימוורקים האלה רצים בצד שרת הם לא מחליפים את קוד צד השרת הרגיל. בהרבה חברות אני רואה תהליך של מעבר מיישום SPA שכתוב ב JavaScript בלבד ומתחבר ל API, ליישום Next.js או רמיקס שמושך מידע מ API מבצע רינדור בצד שרת ומחזיר קוד ריאקט ללקוח. צוות בקאנד ממשיך לתחזק את מכונת "צד השרת" וצוות הפרונט אנד הפך בעצם לצוות Full Stack ובונה קוד ב Next.js.
לאן זה הולך? מבחינת פריימוורק אין ספק שריאקט היא השורדת החזקה והוותיקה ביותר. מצד אחד קשה לראות את הסוף של ריאקט באקוסיסטם הנוכחי ומצד שני דברים כאלה מגיעים במכה אחת. ריאקט היא היום מסובכת מדי ורק הופכת יותר מסובכת עם כל גירסה חדשה, ודברים כאלה לא מחזיקים לאורך זמן.
בקאנד
למרות הרבה ניסיונות לחדש בתחום הזה בסוף אנשים חוזרים לאקספרס כי אנחנו מעדיפים את המוכר במיוחד כשהוא לא מספיק גרוע. גרף קיו אל לא הצליח לאיים על המודל של REST וגם לא דברים כמו tRPC. בסוף יש משהו נוח ויפה בשרת שפשוט מחזיר JSON-ים.
לאן זה הולך? קשה לדעת. אולי נתחיל לכתוב שרתים ב Rust, אולי Go יתחזק שוב, ואולי נישאר עם אקספרס עוד 10 שנים.
באנדלרים
וואו כמה השתנה פה וכמה עוד הולך להשתנות. לפני עשר שנים RequireJS עוד היה דבר, וובפאק היה חדש ומהפכני אבל יותר מהכלים הסיפור הגדול של הבאנדלרים זה השינויים בפלטפורמה. לפני 10 שנים היינו צריכים להמציא מנגנונים שיאפשרו לכתוב JavaScript בקבצים שונים ואז לחבר את כל הקבצים האלה לקובץ JS אחד לשלוח לדפדפן כי דפדפנים לא יכלו להוריד הרבה קבצים במקביל מאותו דומיין. מאז ES Modules הפך לסטנדרט ודפדפנים (וגם node.js) יודעים לקרוא פקודות import ו export באופן טבעי, ה JavaScript נהיה הרבה יותר גדול ופתאום לחבר הכל לקובץ אחד זה רעיון טפשי כי הקובץ גדול מדי ומאט את זמן הטעינה של העמוד.
היום המטרה העיקרית של באנדלרים היא לתמוך ב FrontEnd Frameworks שאנחנו בוחרים, לדוגמה לקמפל את ה JSX של ריאקט או את קבצי ה Vue או ה TypeScript לקוד JavaScript שדפדפן יוכל להבין. קשה לראות אותם נעלמים אבל ככל שהם צריכים לעשות פחות הם הופכים להרבה פחות מעניינים.
לאן זה הולך? בעתיד הנראה לעין יהיו פרויקטים שיחליפו את הבאנלדר ב Import Maps. כן לאט בהתחלה ואז יותר מהם. לדעתי גם נתחיל לראות Front End Frameworks שתומכות ב Import Maps ולא דורשות Build Step ויהיה מי שיתעניין בזה (DHH כבר מדבר על גישה כזאת בריילס, אני חושב שיהיו עוד). מצד שני זה לא מה שיחסל את ריאקט וחבריו ולכן באנדלרים ימשיכו ללוות אותנו לפחות בתור עוד רכיב בתוך ה Full Stack Framework שנבחר.
מריצי משימות
כן פעם היו דברים בשם grunt ו gulp וכן השתמשנו בהם כדי להריץ את הבאנדלר. ואז הבאנדלר נהיה מספיק טוב ועברנו להשתמש ב npm כך שלא היה בהם יותר צורך. יש אנשים שחוזרים ל Makefile-ים, יש את ג'אסט שגם כתבתי עליו ואני חושב שהוא אחלה אבל יותר מהכל יש npm scripts ו Github Actions.
לאן זה הולך? קשה לי לראות את התחום של מריצי משימות מתרומם שוב. זה היה רעיון יפה לתקופה אבל כבר אין בו צורך.
כלי בדיקות
לפני 10 שנים מוקה וג'סמין היו בחירות פופולריות. היום שתיהן עדיין עובדות אבל רבים מעדיפים את Jest, אבל האמת העצובה היא שלפני 10 שנים לא כתבנו המון בדיקות והיום אנחנו עדיין לא כותבים המון בדיקות, כך שפחות משנה הכלי.
לאן זה הולך? אולי AI יתחיל לכתוב בדיקות בשבילנו. כנראה שלא.
1 419
ומגלה שכל לחיצה על כל משבצת גורמת לקומפוננטה להתעדכן. בגלל שהקומפוננטה גדולה עדכון שלה אומר להריץ מחדש את שתי הלולאות.
בנו קומפוננטה בשם Square שאחראית על הצגת תוכן משבצת בודדת, וקומפוננטה נוספת בשם Player שאחראית על הצגת השחקן הנוכחי - וגם בשתיהן הוסיפו את קטע הקוד שמקשיב ל updated ומדפיס כשיש עדכון לקומפוננטה.
עכשיו מצאו דרך לעדכן את הקוד כך שלחיצה על משבצת תגרום לעדכון רק של הקומפוננטות Square ו Player אבל לא לעדכון של הקומפוננטה הראשית TicTacToeGame.
1 419
דוגמת ויו: משחק איקס עיגול
לפני כמה שנים כתבתי מדריך על Vue ואחד הדברים שאז לא עבדו מספיק טוב היה מנגנון הריאקטיביות. רוב הזמן זה היה בסדר אבל היה יחסית קל להגיע לקצוות מבלבלים. לאחרונה אני מרענן את החומרים על Vue לקראת קורס שאני מעביר בנושא ושמח לגלות שהריאקטיביות עובדת הרבה יותר טוב היום. הנה דוגמה קצרה לפיתוח משחק איקס עיגול ריאקטיבי ב Vue, עם כל הפינוקים של הפרדה בין קובץ לוגיקה לקומפוננטות.
לוגיקת המשחק
הדרך הסטנדרטית ב Vue לתקשר בין לוגיקה לקומפוננטות היא ריאקטיביות. הקסם של Vue (בניגוד לריאקט), הוא שאפשר ליצור משתנים ריאקטיביים בכל מקום ולא רק בתוך קומפוננטות וכך אפשר לבנות לוגיקה שכוללת State מחוץ לקומפוננטות. בדוגמה שלנו זה הקוד של משחק איקס עיגול שמחזיק סטייט ריאקטיבי:
import {ref, Ref} from 'vue';
export default class ReactiveTicTacToe {
board: Ref<Array<Array<'O'|'X'|'.'>>>;
winner: Ref<undefined|'O'|'X'>;
currentPlayer: Ref<'O'|'X'>;
constructor() {
this.board = ref([
['.', '.', '.'],
['.', '.', '.'],
['.', '.', '.']
]);
this.currentPlayer = ref('X');
this.winner = ref();
}
play(row: number, column: number) {
if ((this.winner.value) || (this.board.value[row][column] !== '.')) {
return;
}
this.board.value[row][column] = this.currentPlayer.value;
if (this.checkWinner(this.currentPlayer.value)) {
this.winner.value = this.currentPlayer.value;
}
this.currentPlayer.value = this.nextPlayer();
}
nextPlayer() {
if (this.currentPlayer.value === 'X') {
return 'O'
} else {
return 'X'
}
}
checkWinner(player: 'X' | 'O') {
const winningCoordinates = [
[[0, 0], [0, 1], [0, 2]],
[[1, 0], [1, 1], [1, 2]],
[[2, 0], [2, 1], [2, 2]],
[[0, 0], [1, 0], [2, 0]],
[[0, 1], [1, 1], [2, 1]],
[[0, 2], [1, 2], [2, 2]],
[[0, 0], [1, 1], [2, 2]],
[[0, 2], [1, 1], [2, 0]]
]
if (winningCoordinates.some(triplet =>
triplet.every(([row, column]) => (
this.board.value[row][column] === player
))
)) {
return true;
}
return false;
}
}
זה קוד שקל לעבוד איתו, קל לבדוק אותו בבדיקות יחידה או לשלב אותו יחד עם קבצי לוגיקה אחרים. אפשר להגיד שקוד כזה היה כל מה שתמיד רציתי מ mobx, אבל דברים שם אף פעם לא עבדו מספיק טוב.
הקומפוננטה
ויו מעודד כתיבת לוגיקה פשוטה של קומפוננטה בתוך סקריפט האיתחול שלה, אבל כמובן שדברים יותר מורכבים עדיף לכתוב בקובץ נפרד. אחרי שכתבנו את קובץ הלוגיקה נוכל להשתמש בו מתוך קומפוננטה באופן הבא:
<script setup lang="ts">
import ReactiveTicTacToe from '../game/logic';
const {game} = defineProps<{game: ReactiveTicTacToe}>()
</script>
<template>
<div>
<h1 v-if="game.winner.value">Bravo! {{ game.winner }} won</h1>
<h1 v-else>Next player = {{ game.currentPlayer }}</h1>
<div v-for="(_, row) in 3" :style="{display: 'flex'}">
<div v-for="(_, column) in 3" :style="{flex: 1, cursor: 'pointer'}">
<div
@click="game.play(row, column)"
>
{{ game.board.value[row][column] }}
</div>
</div>
</div>
</div>
</template>
ובטח שכיף להשתמש בקיצורי הדרך של vue במיוחד באפשרות של לולאה על מספר בתוך ה v-for ובאפשרות לרשום ב @click משהו שנראה כמו הפעלה של פונקציה ו vue כבר יהפוך את זה לפונקציה.
לסיום בשביל להשתמש במשחק נוכל להפעיל אותו באופן הבא:
<script setup lang="ts">
import TicTacToeGame from './components/TicTacToeGame.vue';
import ReactiveTicTacToe from './game/logic';
</script>
<template>
<TicTacToeGame :game="new ReactiveTicTacToe()"/>
<TicTacToeGame :game="new ReactiveTicTacToe()"/>
<TicTacToeGame :game="new ReactiveTicTacToe()"/>
</template>
עכשיו אתם
ולסיום תרגיל לאלה מכם שרוצים לצלול קצת יותר לעומק הריאקטיביות ב Vue. אני מוסיף את הקוד הבא ל script setup של הקומפוננטה TicTacToeGame.vue:
onUpdated(() => {
console.count(\TicTacToeGame::updated\);
})1 419
איזה סוג של מכוער
כשאנחנו אומרים שמצאנו פיתרון מכוער לבעיה אנחנו מתכוונים שאנחנו לא מאוד מרוצים מהפיתרון, אבל עוד לא אמרנו מה בדיוק אנחנו לא אוהבים. כל אחת מהאפשרויות האלה יכולות לקבל את התווית "פיתרון מכוער":
1. מצאתי באינטרנט דרך שפותרת את הבעיה והעתקתי בלי להבין.
2. כתבתי קוד שפותר את הבעיה בכמה מקומות שונים, אבל לא הצלחתי למצוא דרך לאחד אותם למקום אחד.
3. היו לי כמה רעיונות איך לפתור את הבעיה ובחרתי בפיתרון שפוגע בביצועים של המערכת כי היה יותר קל לממש אותו.
4. הפיתרון שלי מטפל רק ב 90% מהמקרים.
5. פתרתי את הבעיה בצורה שיש סיכוי טוב שתישבר כשיהיו עדכונים לפלטפורמה או לספריות אחרות.
6. פתרתי את הבעיה בצורה שתפריע לכל הוספת פיצ'ר חדש למערכת.
7. פתרתי את הבעיה בצורה שמוסיפה אילוץ חדש למערכת שלא היה שם קודם.
כשאנחנו ברורים לגבי הבעיות בפיתרונות שלנו הרבה יותר קל לנו להתקדם לפיתרונות טובים יותר.
1 419
<meta name="description" content="Generated by create next app" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<div
className={\${styles.page} ${geistSans.variable} ${geistMono.variable}\}
>
<main className={styles.main}>
<p>{props.one}</p>
<p>{props.two}</p>
</main>
</div>
</>
);
}
הפעם אלה הצ'אנקים שנוצרו בבנייה:
ynonp@Ynons-MacBook-Air ~/tmp/perf (main*?) $ ls -l .next/static/chunks
total 816
-rw-r--r-- 1 ynonp staff 181258 נוב 4 19:46 framework-88d0cc4abe8a5763.js
-rw-r--r-- 1 ynonp staff 113610 נוב 4 19:46 main-22f485a16e76a198.js
drwxr-xr-x 5 ynonp staff 160 נוב 4 19:46 pages
-rw-r--r-- 1 ynonp staff 112594 נוב 4 19:46 polyfills-42372ed130431b0a.js
-rw-r--r-- 1 ynonp staff 1459 נוב 4 19:46 webpack-59041051e025bed2.js
ynonp@Ynons-MacBook-Air ~/tmp/perf (main*?) $ ls -l .next/static/chunks/pages
total 24
-rw-r--r-- 1 ynonp staff 400 נוב 4 19:46 _app-f45d290ef4eb0435.js
-rw-r--r-- 1 ynonp staff 232 נוב 4 19:46 _error-8c2b6ff87cd513a2.js
-rw-r--r-- 1 ynonp staff 1480 נוב 4 19:46 index-f051a589b07bc05e.js
הפעם אף קובץ בתיקיית chunks לא כולל את ספריית moment או את ספריית lodash. ספריות אלה נטענות ונמצאות בשימוש רק בצד השרת, והגודל הכולל של כל ה JavaScript שנשלח לדפדפן הוא כ 20% פחות.
שורה תחתונה בעבודה עם next גם בגירסת ה Pages Router אנחנו יכולים להוריד משמעותית את כמות ה JavaScript שנשלחת לדפדפן באמצעות העברת הקוד לצד שרת, או להשתמש נכון יותר ב Cache באמצעות העברת הספריות לקבצים של הדפים. הקובץ app צריך להיות ממש המוצא האחרון כיוון שקוד שאני כותב בו יישלח לכל הדפים במרוכז.1 419
טיפ נקסט: שימו לב ל import-ים
ב next.js בגירסה ה Page Router יש לנו שלוש אפשרויות לטעון ספריות חיצוניות, ואני חושש שההבדל ביניהן לא תמיד מספיק ברור.
שימוש בספריה חיצונית בקובץ נפרד
דרך ראשונה היא לקרוא ל import מתוך קובץ של Page, לדוגמה בפרויקט next חדש אני כותב בקובץ index.tsx את הקוד הבא:
import _ from 'lodash';
import moment from 'moment';
// export const getServerSideProps = (async () => {
// const _ = (await import('lodash')).default;
// return { props: { name: foo(), number: _.random(100) } }
// });
export default function Home(props: any) {
return (
<>
<Head>
<title>Create Next App</title>
<meta name="description" content="Generated by create next app" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<div
className={\${styles.page} ${geistSans.variable} ${geistMono.variable}\}
>
<main className={styles.main}>
<p>Hello {_.head([1, 2, 3])}</p>
<p>{moment("20111031", "YYYYMMDD").fromNow()}</p>
</main>
</div>
</>
);
}
בבנייה של הקוד עם npm run build אני מקבל בתיקיית chunks את:
$ ls -1 .next/static/chunks
29107295-e679c2f4ae05922d.js
75fc9c18-25d3a9414ec027b7.js
framework-88d0cc4abe8a5763.js
main-22f485a16e76a198.js
pages
polyfills-42372ed130431b0a.js
webpack-69db33922baa0ba7.js
שימו לב לשני הקבצים ששמותיהם הם מספרים, אלה הספריות lodash ו moment. הם נכנסו לפרויקט בתור קבצים נפרדים, וזה נחמד כי דפדפן יכול לשמור כל אחד מהם בנפרד ב cache, ולטעון אותם רק מהדפים שבאמת צריכים אותם.
שימוש בספריה חיצונית מתוך app
בפרויקט next יש גם קובץ כללי בשם _app.tsx. ספריות שאנחנו טוענים ממנו יאוחדו לקובץ אחד שייטען בכל אחד מהדפים ביישום. אני מחזיר את index.tsx למצבו ההתחלתי ומעדכן הפעם את _app.tsx לתוכן הבא:
import "@/styles/globals.css";
import type { AppProps } from "next/app";
import _ from 'lodash';
import moment from 'moment';
export default function App({ Component, pageProps }: AppProps) {
return <div>
<p>Hello {_.head([1, 2, 3])}</p>
<p>{moment("20111031", "YYYYMMDD").fromNow()}</p>
<Component {...pageProps} />;
</div>
}
מבחינת העמוד דברים נראים אותו דבר יש לי את אותם טקסטים בדיוק (כן במקום אחר אבל זה לא חשוב כרגע), אבל מה שכן חשוב זה ה JavaScript שנוצר:
$ ls -l .next/static/chunks
total 816
-rw-r--r-- 1 ynonp staff 181258 נוב 4 19:42 framework-88d0cc4abe8a5763.js
-rw-r--r-- 1 ynonp staff 113610 נוב 4 19:42 main-22f485a16e76a198.js
drwxr-xr-x 5 ynonp staff 160 נוב 4 19:42 pages
-rw-r--r-- 1 ynonp staff 112594 נוב 4 19:42 polyfills-42372ed130431b0a.js
-rw-r--r-- 1 ynonp staff 1553 נוב 4 19:42 webpack-69db33922baa0ba7.js
הפעם אין צ'אנקים עבור הספריות החיצוניות ובמקומים בתוך תיקיית pages אני מקבל:
$ ls -l .next/static/chunks/pages
total 272
-rw-r--r-- 1 ynonp staff 129509 נוב 4 19:42 _app-852f41acee199ae3.js
-rw-r--r-- 1 ynonp staff 232 נוב 4 19:42 _error-8c2b6ff87cd513a2.js
-rw-r--r-- 1 ynonp staff 1385 נוב 4 19:42 index-de2181fd4b2adca1.js
כלומר קובץ אחד בשם _app-852f41acee199ae3.js שמכיל את שתי הספריות. הקובץ הזה ייטען בכל אחד מהדפים ביישום ודפדפן לא יוכל לשמור אותו ב cache, כי כל שינוי באחד המרכיבים שלו יגרום ליצירה מחדש של כל הקובץ.
שימוש בספריה חיצונית רק מתוך קוד צד שרת
אופציה שלישית היא לטעון את הספריה החיצונית ולהשתמש בה רק בפונקציה getServerSideProps מתוך אחד הדפים. אני מחזיר את _app.tsx לצורתו המקורית וחוזר ל index.tsx ועכשיו זה יהיה תוכנו:
import _ from 'lodash';
import moment from 'moment';
export const getServerSideProps = (async () => {
return {
props: {
one: _.head([1, 2, 3]),
two: moment("20111031", "YYYYMMDD").fromNow(),
}
}
});
export default function Home(props: any) {
return (
<>
<Head>
<title>Create Next App</title>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 שלנו.