ToCode
Открыть в Telegram
1 420
Подписчики
Нет данных24 часа
+27 дней
-230 день
Архив постов
1 420
בואו ניקח את זה עוד צעד קדימה ונראה סכימה שמורכבת מכמה סוגים של ישויות, וכך גם נוכל לראות שאילתות יותר מתוחכמות. בדוגמה הבאה אני רוצה לבנות סכימה שתאפשר לאנשים להירשם כמנויים לפוסטים בבלוג. לכל פוסט יש את הפרטים שלו וגם רשימה של קטגוריות שהוא קשור אליהן, לכל מנוי יש אימייל ורשימה של קטגוריות בהן הוא מתעניין וגם רשימה של פוסטים שהוא כבר קיבל.
כל פעם שמנוי נרשם אני מכניס אותו למערכת ושומר את הקטגוריות עליהן הוא רוצה לקבל עדכונים. יהיה מעניין אותי להריץ שאילתות כמו "מי המנויים במערכת", "איזה פוסטים מנוי מסוים קיבל כבר ואיזה פוסטים מנוי מסוים לא קיבל עדיין כדי שאוכל לשלוח לו אותם.
לכן הסכימה, שזה אוסף כל סוגי העובדות שאני הולך להכניס למערכת, יכולה להיראות כך (הפעם אני כבר מדביק פה קוד Clojure):
(def schema
[
{:db/ident :post/published-at
:db/cardinality :db.cardinality/one
:db/valueType :db.type/instant}
{:db/ident :post/slug
:db/cardinality :db.cardinality/one
:db/valueType :db.type/string
:db/unique :db.unique/identity}
{:db/ident :post/title
:db/cardinality :db.cardinality/one
:db/valueType :db.type/string}
{:db/ident :post/content
:db/cardinality :db.cardinality/one
:db/valueType :db.type/string}
{:db/ident :post/categories
:db/cardinality :db.cardinality/many
:db/valueType :db.type/ref}
{:db/ident :category/javascript}
{:db/ident :category/clojure}
{:db/ident :category/rust}
{:db/ident :subscriber/email
:db/cardinality :db.cardinality/one
:db/valueType :db.type/string
:db/unique :db.unique/identity}
{:db/ident :subscriber/categories
:db/cardinality :db.cardinality/many
:db/valueType :db.type/ref}
{:db/ident :subscriber/received
:db/cardinality :db.cardinality/many
:db/valueType :db.type/ref}
])
במערכת יהיו שני סוגים של ישויות - פוסטים ומנויים. ה Attributes השונים מתאימים לסוגי הישויות, ולמרות שבסיס הנתונים לא אוכף את זה, בקוד יהיה לי יותר קל לעבוד עם המידע כשאני מקפיד לכתוב עובדות מהסוג הנכון. שימו לב ל cardinality, מנוי בבלוג יכול להיות רשום לכמה קטגוריות, ופוסט יכול להיות משויך לכמה קטגוריות, לכן בשני המקרים ציינתי את הערך :db.cardinality/many.
את רשימת הפוסטים אני יכול להגדיר כך:
(defn date [ddMMyyyy]
(.parse (java.text.SimpleDateFormat. "ddMMyyyy") ddMMyyyy))
(def posts [
{:post/title "first post"
:post/slug "first"
:post/published-at (date "01012023")
:post/categories [:category/clojure]}
{:post/title "second post"
:post/slug "second"
:post/published-at (date "02012023")
:post/categories [:category/clojure]}
{:post/title "rust"
:post/slug "rust"
:post/published-at (date "01012023")
:post/categories [:category/rust]}])
ורשימת מנויים ראשונית יכולה להיראות כך:
(def subscribers [
{:subscriber/email "clojure@demomail.com"
:subscriber/categories [:category/clojure]}
{:subscriber/email "rust@demomail.com"
:subscriber/categories [:category/rust]}
{:subscriber/email "all@demomail.com"
:subscriber/categories [:category/rust :category/clojure]}])
הפקודות הבאות שולחות את שלושת הרשימות לבסיס הנתונים:
@(d/transact conn schema)
@(d/transact conn posts)
@(d/transact conn subscribers)
אגב התחילית d נמצאת שם בגלל הדרך בה ייבאתי את datomic לתוכנית שהיא השורות:
(ns ynonp.blogmail
(:require [datomic.api :as d]))
ובשביל להתחבר לבסיס נתונים ולהריץ את הקוד אני יכול ליצור בסיס נתונים בזיכרון עם הפקודות:
(def db-uri "datomic:mem://blog")
(d/create-database db-uri)
(def conn (d/connect db-uri))
כולן יצטרכו להופיע בתוכנית לפני פקודת ה d/transact.1 420
# קטנה על שאילתות ב Datomic
בשנת 2012 ריק היקי, היוצר של Clojure, שיחרר בסיס נתונים חדש בשם Datomic במטרה לפתור שתי בעיות מרכזיות של בסיסי נתונים רלציוניים "רגילים": הראשונה היא דריסת מידע ישן בפעולות Update ו Delete והשניה היא הסכימה הקשיחה שמכריחה לשים כל דבר בתוך טבלה. הפיתרון שלו היה חדשני ב 2012 ונשאר מעניין גם היום - בואו נראה איך זה עובד.
## שמירת עובדות במקום שורות
רעיון ראשון של דטומיק שצריך להבין הוא שבסיס נתונים שומר "עובדות" על העולם ברגע מסוים ולא שורות בטבלה. עובדה נשמרת לנצח ותישאר תמיד נכונה, בגלל שהיא רלוונטית לרגע בו היא נאמרה. ברור שבעתיד יכולה להתגלות עובדה חדשה שתשנה משהו שידענו, אבל זה לא אומר שעובדות ישנות נמחקות. תמיד אפשר להיכנס לבסיס הנתונים ברגע ישן יותר בזמן ולראות את העובדות עד לאותו רגע.
אם בסיס הנתונים שומר עובדות השאלה הבאה שצריך לברר היא מה זה עובדה. וגם פה התשובה לא מסובכת, ומורכבת משני מושגים בסיסיים יותר. מושג ראשון הוא "ישות", שזה משהו שאפשר להגיד עליו דברים, ומושג שני נקרא Attribute (לא בטוח איך לתרגם את זה לעברית), שזה משהו שאתה יכול להגיד על ישויות.
בדוגמה פשוטה אם אני רוצה לשמור מידע לגבי מחירים של מוצרים אז אני יכול לחשוב על מוצר בתור ישות, ועל המחיר בתור עובדה. ואז יהיו לי עובדות כמו:
1. קילו עגבניות עולה 12 ש"ח.
2. לחמניה עולה 3 ש"ח.
3. קילו תפוחים ירוקים עולה 14 ש"ח
בשביל לתאר את זה ב Datomic אני צריך להשתמש בסכימה, אבל הסכימה של דטומיק היא לא מבנה טבלאי קשיח שאומר שיש טבלת מוצרים ובה יש עמודת "שם המוצר" ועמודת "מחיר המוצר", אלא הסכימה היא אוסף העובדות האפשריות שאפשר להגיד על דברים בעולם, או אוסף ה Attributes. בדוגמה שלנו הסכימה תהיה:
[
{:db/ident :product/name
:db/cardinality :db.cardinality/one
:db/unique :db.unique/identity
:db/valueType :db.type/string}
{:db/ident :product/price
:db/cardinality :db.cardinality/one
:db/valueType :db.type/long}
]
וזה אומר שיש שני דברים בעולם שאפשר להגיד על "ישויות", האחד הוא שיש ישויות שיש להן "שם מוצר", ואם אתה ישות כזאת אז אפשר לזהות אותך לפי שם המוצר, כלומר לעולם לא יהיו שתי ישויות שונות במערכת שיש להן אותו שם מוצר. הדבר השני זה שיש ישויות שיש להן מחיר. המילה cardinality אומרת שגם "מחיר" וגם "שם" זה משהו שיש רק אחד ממנו לישות. בהמשך נראה דברים שיש "הרבה" מהם לכל ישות.
פקודת הכנסה לבסיס נתונים של העובדות שכתבתי מקודם יכולה להיראות בקלוז'ר בערך כך:
[[:db/add "e1", :product/name "1kg tomatoes"]
[:db/add "e1", :product/price 12]
[:db/add "e2", :product/name "bun"]
[:db/add "e2", :product/price 3]
[:db/add "e3", :product/name "1kg green apples"]
[:db/add "e3", :product/price 14]]
ושימו לב איך כל מה שיש לי פה זו רשימה של "עובדות", ומה שמחבר בין העובדות זה מזהה חסר חשיבות שאני בחרתי - e1 עבור העגבניות, e2 ללחמניה ו e3 לתפוחים. המזהה הזה יימחק ברגע שהמידע יישלח לבסיס הנתונים והוא רק שם זמני שעוזר לבסיס הנתונים לחבר בין שתי העובדות שקשורות לכל ישות, השם שלה והמחיר שלה.
למרות ש Attribute בדרך כלל כתוב בשתי מילים המופרדות ביניהן עם לוכסן, אל תתבלבלו לחשוב שהמילה הראשונה "מתאימה" לטבלה או לסוג מסוים של ישויות. לבסיס הנתונים לא אכפת איזה Attributes תשימו על איזה ישות. החלוקה לשתי מילים היא רק בשבילנו ועוזרת לנו לחשוב בצורה מדויקת על המערכת שלנו.
## שאילתות
חלק גדול מהכח של דטומיק הוא בשפת שאילתות חדשה, לא מבוססת SQL, שאיתה אנחנו מחפשים מידע לגבי העובדות השמורות במערכת. שפת השאילתות מבוססת על שפה שנקראת Datalog. אני לא רוצה להיכנס להסבר מלא על השפה כי זה יהפוך את הפוסט להרבה יותר מדי ארוך, אבל במקום בואו נראה כמה דוגמאות רק בשביל להרגיש את המבנה. בדוגמת המידע שהצגתי קודם הקוד הבא הוא שאילתת datalog שמחזירה את שמות כל המוצרים במערכת:
[:find ?name
:where [_ :product/name ?name]]
בתרגום לעברית: כל מה שמתחיל בסימן שאלה הוא משתנה, וכל בלוק where מקבל שלשה שמורכבת מ"ישות", Attribute וערך. השלשה שבדוגמה אומרת שלא משנה לי מה הישות, אבל אני רוצה שיהיה ל Attribute בשם :product/name ואת הערך שלו אני שומר במשתנה name, שגם חוזר מהשאילתה.
## דוגמה לסכימה אמיתית1 420
# למה אי אפשר לייבא ES Module ממודול CommonJS ?
אם תנסו לשלב קוד Node.JS שמשתמש ב ESM עם קוד שמשתמש ב require (מה שנקרא CommonJS), תגלו שהשילוב עובד ב 3 מתוך 4 אפשרויות:
## מה עובד ומה לא
1. מודול ES יכול לעשות import כדי לייבא קוד ממודול CommonJS
2. מודול ES לעשות import כדי לייבא קוד ממודול ES אחר
3. מודול CommonJS יכול לעשות import כדי לייבא קוד ממודול CommonJS אחר
4. מודול CommonJS שינסה לעשות require למשהו שמיוצא ממודול ES יקבל שגיאה.
בשביל לראות את זה ניצור פרויקט חדש, ניצור קובץ בשם utils.mjs עם התוכן הבא:
export function twice(x) {
return x * 2;
}
וקובץ בשם main.js עם התוכן הבא:
const { twice } = require('./utils.mjs');
console.log(twice(10));
נפעיל עם:
$ node main.js
ונקבל את השגיאה:
node:internal/modules/cjs/loader:1087
throw new ERR_REQUIRE_ESM(filename, true);
^
Error [ERR_REQUIRE_ESM]: require() of ES Module /Users/ynonp/tmp/node/modules/utils.mjs not supported.
Instead change the require of /Users/ynonp/tmp/node/modules/utils.mjs to a dynamic import() which is available in all CommonJS modules.
at Object.<anonymous> (/Users/ynonp/tmp/node/modules/main.js:1:19) {
code: 'ERR_REQUIRE_ESM'
}
Node.js v18.14.0
## למה טעינה כזו לא נתמכת?
אז הסיפור הוא כנראה לא שמישהו ב node.js התעצל, אלא שסט הפיצ'רים של ES Modules קצת שונה מזה של CommonJS. ההבדלים המרכזיים הם:
1. ב ES Module אנחנו יודעים רק מלהסתכל על הקוד איזה שמות מיוצאים ממנו. ב CommonJS חייבים להריץ כדי לראות מה יהיו הערכים על אוביקט ה exports.
2. ב ES Module אני יכול לכתוב await מחוץ לכל פונקציה, מה שיגרום לטעינה אסינכרונית של המודול. ב CommonJS הטעינה תמיד סינכרונית.
בקישור הזה יש דיון מאוד מעניין על האפשרות להוסיף await למודולי CommonJS ולמה היא כנראה תשבור הכל:
https://github.com/nodejs/node/issues/21267
## מה בכל זאת אפשר לעשות
אם הבעיה היא התמיכה המובלעת בקוד אסינכרוני, אז הפיתרון הפשוט הוא להפוך את המובלע למפורש. וזה מה שגם עובד ב Node.JS. הפונקציה import שמחזירה Promise למודול (נקראת גם Dynamic Import) מאפשרת לנו לייבא קובץ ES module מתוך קובץ CommonJS. אני מעדכן את הקוד ב main.js לקוד הבא:
async function main() {
const { twice } = await import('./utils.mjs');
console.log(twice(10));
}
main();
וה import מצליח לטעון את המודול בלי שום שינוי בקוד המודול utils.mjs.1 420
# כן, לשכפל
הבעיה הגדולה בשכפול קוד היא שכשצריך לתקן משהו אנחנו צריכים לתקן בהמון מקומות במקביל. אם עד עכשיו היו לי באתר 3 נגני וידאו, ועכשיו אני מוסיף נגן רביעי אני צריך לעדכן את כל הדפים שמציגים נגן וידאו. אם אני שומר את "קוד הנגן" במקום אחד ורק טוען אותו מכל שאר הדפים, אז חסכתי לי עבודה כשצריך לשנות את הלוגיקה.
אבל הבעיה הגדולה בקוד גנרי היא שקשה להתמודד עם שינויים שלא צריכים להשפיע על כל הדפים. בדוגמה של נגן וידאו קל לראות שלא משנה אם אני שם את הנגן בדף "שיעור בקורס" או בדף "הקלטה מוובינר" אני רוצה לראות את הוידאו באותו אופן. האמת הפשוטה הזאת מסתבכת ככל שמלבישים יותר לוגיקה על הקוד הגנרי.
אם נישאר בדוגמה של נגן הוידאו שלי, אז אולי אני ארצה להוסיף מנגנון ששומר איפה הייתם בוידאו כדי שפעם הבאה שתגיעו לאתר תוכלו להמשיך לצפות מאותה נקודה, אבל בעוד שפיצ'ר כזה נשמע מדליק עבור שיעורים בקורס, הוא הרבה פחות חשוב כשצופים בהקלטה מוובינר.
תעשו Zoom Out מהדוגמה ונוכל לראות את הבעיה - כששני דברים תמיד מתואמים בלוגיקה זה באמת עוזר ליצור אבסטרקציה ולכתוב את הקוד רק פעם אחת. כששני דברים משתנים בצורות שונות והיום הם במקרה מתנהגים דומה אבל מחר כבר יתנהגו אחרת, החיבור ביניהם רק מסבך.
לכן בכל המקרים הבאים אני מעדיף לשכפל קוד ולא להתאמץ ולכתוב פונקציה אחת שעושה הכל:
1. כשאני רק בונה פיצ'ר חדש, ועדיין לא בטוח מה הוא צריך לכלול.
2. בקוד בדיקות - שכפול קוד בדיקה נותן את הגמישות לשנות תמיד רק את הבדיקות שמושפעות משינוי בפיצ'ר מסוים, בלי לדאוג ששברתי דברים לא קשורים.
3. סקריפטים שאני כותב למשימה מסוימת (כמו פרסום הבלוג הזה לטלגרם). כתיבה של הסקריפט מאפס אפילו אם חלק ממנו זה שכפול של קוד קיים עדיין שווה את המאמץ, כי ככה אני יודע שכל תיקון שלא יהיה בו לא ישבור שום דבר אחר במערכת.
4. כשכל אבסטרקציה שאני מנסה לבנות לא ממש "מסתדרת" ומשאירה המון פינות ומקרי קצה.
כלל אצבע טוב הוא לכתוב קוד חדש בגישת השכפול, ולאחד קטעי קוד בשלב ה Refactoring, כשאנחנו בטוחים בתאימות הפיצ'רים בין שני המנגנונים.
1 420
# תיקון של שורה (עם ובלי בדיקות)
## אני בפרויקט שאין בו בדיקות
״נו ברור שזה לא עבד אבל התיקון ממש פשוט. שניה מתקן ומעלה גירסה״
עשר דקות מאוחר יותר-
״אף אחד לא יכול להשתמש באתר?? איך זה יכול להיות? שניה בודק בלוגים״,
״אה כן ברור איך לא ראיתי את זה... רגע מתקן״
שלושה חודשים מאוחר יותר-
״מה זאת אומרת התקלה חזרה? כבר תיקנו את זה שלוש פעמים. טוב לפחות זה תיקון שאנחנו מכירים״
## אני בפרויקט שיש בו בדיקות
״נו ברור שזה לא עבד אבל התיקון ממש פשוט. ייקח לי כמה שעות לכתוב בדיקה אוטומטית לתרחיש שמצאת אבל עד הערב תהיה גירסה חדשה.
5 שעות מאוחר יותר-
״שומע הגירסה החדשה באוויר. תבדקו רק שהכל עלה כמו שצריך והבאג נעלם״
שלושה חודשים מאוחר יותר, בעקבות כישלון בדיקה אוטומטית-
״איזה קטע שכחתי לגמרי מהתרחיש הזה בזמן הריפקטורינג. טוב שדאגתי לכתוב בדיקה כדי לא להעלות גירסה שבורה״.
1 420
# מה אפשר לעשות עם ES Modules ב Node.JS
גירסאות עדכניות של Node.JS כבר תומכות בצורה מובנית ב ES Modules, ואפילו מציעות כמה שיפורים על פני עבודה בדפדפן. בואו נראה כמה טכניקות מרכזיות של עבודה עם ES Modules ב Node.JS.
## הגדרת פרויקט שמשתמש במודולים
עבור פרויקט חדש אתם יכולים להגדיר ב package.json את הפרויקט בתור "מודול", ואז תקבלו תמיכה בכתיב ה import/export בצורה אוטומטית בכל הקבצים בפרויקט. המפתח הרלוונטי נקרא type והערך שלו צריך להיות module. זה קובץ package.json לדוגמה:
{
"name": "demo1",
"version": "1.0.0",
"description": "",
"type": "module",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
שימו לב למפתח type. עכשיו אם אני שם את הקובץ הזה בתיקיית פרויקט Node.JS אני יכול לכתוב בפרויקט קבצים שישתמשו בכתיב ה import/export שאני מכיר מדפדפן. לדוגמה הקובץ utils.js:
export function twice(x) {
return x * 2;
}
ולידו הקובץ index.js:
import { twice } from './utils.js';
console.log(twice(10));
## הגדרת קובץ ספציפי כמודול בפרויקט רגיל
ומה אם יש לכם פרויקט קיים שכבר כתוב בתחביר CommonJS (זה עם ה require שאנחנו מכירים ואוהבים)? אין בעיה - בכל פרויקט Node.JS אפשר להוסיף קבצים עם סיומת mjs ואליהם node יתייחס בתור קבצי מודולים.
לדוגמה אם יש לכם פרויקט Node רגיל עם package.json שלא מגדיר לפרויקט type, ובפרויקט יש קובץ utils.js עם התוכן הבא:
exports.twice = x => x * 2;
תוכלו להגדיר קובץ index.mjs שישתמש ב import כדי להגיע לפונקציה ולהפעיל אותה:
import { twice } from './utils.js';
console.log(twice(10));
בנוסף אם קובץ ה CommonJS הרגיל מעדכן את module.exports ישירות, אפשר לייבא את ה module.exports המלא באמצעות default import. לדוגמה הקובץ user.js מגדיר מחלקה עבור משתמש:
module.exports = class User {
constructor(id, name) {
this.id = id;
this.name = name;
}
sayHi() {
console.log(`Hi! my name is ${this.name}`);
}
}
אז קובץ index.mjs יוכל לטעון אותו ולהפעיל את הפונקציות:
import User from './user.js';
const user = new User(1, 'ynon');
user.sayHi();
סך הכל השימוש ב ES Modules ב Node הוא פיצ'ר חשוב בעולם שעובר יותר ויותר להסתמך עליהם, ועוזר ליצור אחידות בין קוד צד שרת לקוד צד לקוח. השילוב המובנה עם קוד קיים מאפשר לעבור בהדרגה לכתיב ה import, בזמן שמתאים לפרויקט שלכם.1 420
# איך להפוך למתכנתי פייתון טובים יותר
נתחיל במה שלא עובד (וזה קצת מפתיע). אפשר לדמיין שבשביל להיות מתכנתי פייתון טובים יותר עלינו ללמוד כמה שיותר APIs של פייתון. למשל אם נלמד גם לכתוב ממשקים גרפיים, גם לקבל מידע מהרשת, גם לנתח מידע וגם לבנות בינה מלאכותית אז כל API חדש יהפוך אותנו למתכנתי פייתון טובים יותר.
אבל זה לא ממש עובד.
הדבר היחיד שאנחנו מקבלים מללמוד כל חודש API חדש לגמרי הוא סחרחורת. אנחנו מקבלים את התחושה שהטכנולוגיה רצה מהר מדי, שאי אפשר אף פעם לדעת הכל, שלא משנה כמה אני אלמד תמיד אצטרך "להתחיל מחדש" ושהעולם הזה לא בשבילי. אנחנו כל פעם משקיעים בדברים שלא נשארים, ואחרי חודש או שנה מגלים שחזרנו לנקודת ההתחלה.
דרך טובה יותר להשתפר בתור מפתחי פייתון, או כל טכנולוגיה אחרת, היא להשקיע בדברים שנשארים. בפייתון זה יהיה:
1. להבין איך פייתון מבינה ומריצה את התוכנית שלי.
2. להבין איך להשתמש ב Type Hints בצורה שתייצר ערך ותהפוך את הקוד לקל יותר לקריאה.
3. להבין איך לעשות דברים במקביל, באמצעות Threads, Processes או async.
4. להבין מה זה תכנות מונחה עצמים ואיך למדל באמצעותו בעיות.
5. להבין איך לכתוב בדיקות בצורה יעילה ומהירה.
6. להבין מה משפיע על זמן הריצה של תוכנית ואיך למדוד ולשפר זמני ריצה.
7. להכיר את המבנים היותר בסיסיים של פייתון כמו Decorator ו Metaclass ואיזה תפקיד הם משחקים בספריות קוד מרכזיות.
8. להבין את הקשר בין Python ל C. אחרי זה איך לכתוב הרחבות ב C לפייתון, ואיזה חבילות פייתון מרכזיות משתמשות בהרחבות C כדי לשפר ביצועים.
כשיש ספק, השקיעו בדברים שנשארים.
1 420
# ריאקט זהירות! מידע גלובאלי בין בדיקות
נתבונן בקומפוננטה הבאה מתוך דף הפתיחה של swr:
import useSWR from 'swr'
function Profile () {
const { data, error, isLoading } = useSWR('/api/user/123', fetcher)
if (error) return <div>failed to load</div>
if (isLoading) return <div>loading...</div>
// render data
return <div>hello {data.name}!</div>
}
ונכתוב תוכנית בדיקה שמשנה את fetch כדי להחזיר אוביקט שלנו, בשביל שאפשר יהיה לבדוק את הקומפוננטה גם בלי לצאת לרשת:
import { render, screen } from '@testing-library/react';
import Profile from './Profile;
test('test one', async () => {
jest.spyOn(global, 'fetch').mockImplementation(url => Promise.resolve({
json: () => Promise.resolve({ name: 'bug'})
}));
render(<Profile />);
expect(await screen.findByText(/hello bug/));
});
קודם כל תשמחו לשמוע שהתוכנית עובדת. עכשיו קראו את הקוד שוב. רואים את הבעיה?
## מה נשבר
בשביל לראות את הקוד נשבר צריך רק להוסיף עוד בדיקה:
test('test two', async () => {
jest.spyOn(global, 'fetch').mockImplementation(url => Promise.resolve({
json: () => Promise.resolve({ name: 'oh no'})
}));
render(<Profile />);
expect(await screen.findByText(/hello oh no/));
});
הפעם הבדיקה השניה כבר נכשלת. קריאה בלוג תספר לכם שהכישלון קרה בגלל שעל המסך מופיעה ההודעה hello bug, שהתקבלה בתשובה הקודמת של ה API. ומה שיותר גרוע, כשתנסו לדלג על הבדיקה הראשונה הבדיקה השניה תתחיל לעבוד.
מה?
כשיש לנו בדיקות שכל אחת לבד מצליחה אבל יחד נכשלות זה רמז מאוד עבה למידע גלובאלי שמשותף לשתי הבדיקות. בדוגמה כאן המידע הגלובאלי הוא ה Cache של swr. ברגע ש swr מושך פעם אחת את התשובה מהשרת עבור URL מסוים, הוא זוכר את התשובה ומשתמש בה גם לבדיקות הבאות. באופן רגיל ה Cache נשמר בתור מפה גלובאלית שמשותפת לכל הבדיקות בתוכנית.
הפיתרון הקל במקרה הזה הוא להשתמש ב Cache נפרד לכל בדיקה באמצעות הצגת הקומפוננטה בתוך SWRConfig שונה עם Cache משלו כמו שמוצע בדיון הזה. במקרה הכללי יותר חשוב לשים לב כשאנחנו כותבים בדיקות למידע הגלובאלי שמשפיע על הבדיקות שלנו ולנקות אותו כך שכל בדיקה תוכל באמת לרוץ בצורה עצמאית.1 420
# ריילס היום למדתי: לא מעבירים מזהים למודל ביצירה או עדכון
את הטיפ הבא מצאתי במאמר הזה של חברת Betterment ומיד התחברתי אז אני משתף גם פה.
נתחיל עם קוד ריילס הבא עבור controller:
class Documents::AttachmentsController < ApplicationController
def create
AttachmentLink.new(create_params.merge(document: document)).save!
end
private
def create_params
params.permit(:attachment_id, :caption)
end
def document
current_user.documents.find(params[:document_id])
end
end
הקוד מאפשר להצמיד Attachments למסמך דרך הפונקציה create. הפונקציה מקבלת מהדפדפן מזהה של "קובץ מצורף" ומזהה של "מסמך" ויוצרת AttachmentLink שזה אוביקט חיבור בין השניים.
קחו רגע לקרוא את הקוד ונסו לחשוב מה שבור בו.
## הבעיה בקוד: מאיפה מגיע Attachment
ראיתם את זה? הפונקציה הפרטית document טוענת את ה Document מתוך כל המסמכים של המשתמש, וכך מוודאת שאנחנו מנסים להצמיד קובץ מצורף למסמך שלנו.
לעומתה מזהה הקובץ המצורף מועבר בתור id פנימה למודל. טעינת אוביקט ה Attachment מתבצעת אוטומטית בתוך הפונקציה new ולכן תוקף יכול להעביר כל מזהה שירצה וכך לצרף כל Attachment למסמך שלו.
בעצם האחריות המרכזית של ה Controller בריילס היא לוודא שכל המודלים שייטענו מבסיס הנתונים הם מודלים שלמשתמש הנוכחי יש גישה אליהם, ולכן זה הקונטרולר שחייב לטעון את כל המודלים ולוודא הרשאות. אם נעביר מזהים פנימה לפונקציה new אז היא עלולה לגשת למודלים שלא קשורים למשתמש הנוכחי.
ברגע שמבינים את זה התיקון וגם המניעה פשוטים מאוד. ככה זה נראה בקוד:
class Documents::AttachmentsController < ApplicationController
def create
AttachmentLink.new(attach_params).save!
end
private
def create_params
params.permit(:attachment_id, :caption)
end
def attach_params
{
document: document,
attachment: attachment,
caption: create_params[:caption]
}
end
def attachment
current_user.attachments.find(create_params[:attachment_id])
end
def document
current_user.documents.find(params[:document_id])
end
end
החברים ב Betterment גם יצרו כללי Rubocop שיעזרו לכם לוודא שאתם לא מעבירים מזהים של מודלים לתוך פונקציות העדכון והיצירה של ריילס. אפשר למצוא את הכללים בקישור הזה ואם אתם כותבים בריילס שווה לשלב אותם גם ביישומים שלכם:
https://github.com/betterment/betterlint/1 420
# חדש באתר: מיני קורס בדיקות בריאקט
זה לא פשוט לכתוב בדיקות, במיוחד בדיקות לקוד צד לקוח.
הרבה אנשים חושבים שבדיקות לוקחות להם זמן והם מעדיפים להשקיע את הזמן הזה בבניית פיצ׳רים חדשים. הם שוכחים שהדבר הכי מרגיז במערכות זה כשמעלים גירסה חדשה וכל הדברים הישנים מפסיקים לעבוד.
אנשים אחרים חושבים שבדיקות End To End הן חשובות, אבל על בדיקות יחידה אפשר לוותר. הם נכוו מספיק פעמים מבדיקות יחידה לא יעילות שלא מצאו את הבאגים האמיתיים במערכת.
ויש גם את מי שבטוחים שבדיקות זה רעיון ממש מצוין אבל ספציפית המערכת שלהם מסובכת מדי וכרגע יש יותר מדי באגים. יום אחד כשדברים קצת יתייצבו הם ישמחו לכתוב בדיקות.
אם גם אתם חלק מאחת הקבוצות ברשימה שלמעלה תרגישו חופשי להמשיך הלאה, הפוסט הזה לא בשבילכם.
מצד שני אם אתם היום מרגישים לא בטוחים לגבי הקוד שלכם. אם אתם מבינים שאי אפשר להמשיך כך כשכל ריפקטור קטן שובר אינסוף דברים. אם גם לכם נמאס לעבור בין הדפדפן ל VS Code כל הזמן בשביל לבדוק שהקוד שאתם כותבים עובד כמו שצריך, אז תשמחו לשמוע שהעליתי היום מיני קורס חדש במטרה לעזור לכם להתחיל לכתוב בדיקות יחידה או לשפר את רמת הבדיקות שאתם כותבים.
הקורס כולל 6 סרטי וידאו באורך כולל של קצת פחות משעה, ועוסק בנושאים:
1. היכרות עם react-testing-library, איך נראית תוכנית בדיקה ואיך לא לבזבז זמן בכתיבת תוכניות הבדיקה.
2. בדיקת טיפול באירועים עם user-event.
3. בדיקת קומפוננטות המושפעות מזמן, גם בצורה אסינכרונית וגם באמצעות שעונים מזויפים.
4. בדיקת תקשורת באמצעות mock לפונקציית fetch ו Best Practices סביב בדיקות כאלה.
5. בדיקת קומפוננטות-בתוך-קומפוננטות באמצעות Jest Spies.
6. בדיקת יישומים המשתמשים ב Redux.
השיעורים מעשיים, כל שיעור מודגם על קומפוננטות אמיתיות שכתובות ב React ו TypeScript וייתן לכם בסיס טוב לבניית בדיקות למערכות שלכם.
אם יש לכם מנוי לאתר יכולים כבר להיכנס ולצפות בקישור:
https://www.tocode.co.il/boosters/7
ואם עדיין אין לכם מנוי היום הוא הזדמנות מצוינת להירשם. פשוט לחצו על הקישור לקורס כדי להגיע לדף ההרשמה.
1 420
promise.status = 'rejected';
promise.reason = reason;
},
);
throw promise;
}
}
הפונקציה מקבלת Promise ומשתמשת ב throw כדי לברוח מהפונקציה שקראה לה אם ה Promise עדיין לא מוכנה. קומפוננטת Suspense תתפוס את ה Promise ותציג את ה Fallback Content, ואוטומטית כשה Promise יסתיים תרנדר מחדש את הקומפוננטה הפנימית.
החלק האחרון בדוגמה הוא הפונקציה fetchData. אחסוך לכם את המימוש כי הוא לא חשוב, זאת פשוט פונקציה אסינכרונית שמקבלת מידע מהשרת.
## מפל בקשות
אספקט נוסף של Suspense שקצת עובר מתחת לרדאר הוא היחס לבעיה שנקראת מפל בקשות. הבעיה בגדול עשויה להיגרם כשיש לנו קומפוננטות מקוננות שכל אחת מהן צריכה לקבל מידע אחר מהשרת. נדמיין משהו בסגנון:
1. קומפוננטה של טופס החלפת סיסמה במסך "החשבון שלי", שבשביל להיטען צריכה לקבל מהשרת את פרטי המשתמש הנוכחי.
2. בתוכה קומפוננטה של Captcha שצריכה לקבל משרת אחר את הציור של האותיות המוזרות כדי לוודא שהמשתמש אכן בן אדם.
במצב רגיל של קומפוננטה בתוך קומפוננטה, רק אחרי שקונפוננטת הטופס תסיים לטעון ולהתרנדר על המסך אפשר יהיה להמשיך למשוך את המידע מקומפוננטת ה Captcha.
בחלק מדפי התיעוד על Suspense הם מדברים על הבעיה הזו, באחרים לא. נשים לב ששימוש נאיבי ב Suspense לא יפתור את בעיית המפל כי ה throw שיעצור את הרינדור של קומפוננטת הטופס ימנע רינדור של קומפוננטת ה Captcha הפנימית יותר. כן אפשר לארגן אחרת את עץ הקומפוננטות ולמשוך את המידע בקומפוננטה ברמה גבוהה יותר או מתוך ספריית ניהול הסטייט כדי לצאת מזה, אבל בכל מקרה Suspense יהיה רק חלק קטן מהפיתרון כאן.
## מה אפשר לעשות במקום Suspense?
אם נסכם היתרון הגדול של Suspense הוא ביכולת לטפל במספר קומפוננטות שטוענות מידע ולהציג קומפוננטת Loading מסודרת אחת שמרכזת את כל הטעינות. לפני Suspense הפיתרון הטוב ביותר למצב הזה הוא לבצע את כל הטעינות מהקומפוננטה הראשית ולהעביר את המידע שנטען לילדים כשהוא מוכן, משהו בסגנון הזה:
export default function ArtistPage({ artist }) {
const { data, isLoading } = useSWR(`/${artist.id}/albums`);
return (
<>
<h1>{artist.name}</h1>
{isLoading ? <Loading /> : <Albums data={albums} />}
</>
);
}
היתרון בגישה כזאת הוא שאנחנו מקבלים אוטומטית הפרדה בין הקומפוננטות של הלוגיקה (אלה ששולחות בקשות רשת) לקומפוננטות פשוטות יותר של תצוגה שנמצאות בתוכן. החיסרון הוא שהכתיבה מאוד ריכוזית וזה יכול להיות מבלבל כשצריך לשנות נתיבים או כשיש לוגיקה יותר משמעותית סביב הבקשה, לדוגמה אם Albums צריך לקחת את המידע שמגיע מהשרת ולעשות עליו איזושהי מניפולציה לפני שיכול להציג אותו.
## אז לשכתב?
מצד אחד ל Suspense יש קלף מנצח בשרוול - הוא באמת מאפשר הרבה יותר בקלות לבנות דף שמורכב מהרבה קומפוננטות, שכל אחת מהן מושכת מידע מהשרת וכולן יחד מציגות מסך טעינה אחד.
מצד שני ה Use Case הזה הרבה פחות נפוץ ממה שמדמיינים. בהרבה מערכות יש לי נתיב אחד בשרת שמחזיר את כל המידע של "דף" מסוים, ואז אני משתמש ב useSWR (או מקבילות שלו) רק בקומפוננטה הראשית של הדף ומפזר את המידע שקיבלתי בין הקומפוננטות. בעוד הרבה מערכות כן יהיו לי קומפוננטות קטנות שטוענות את עצמן, אבל כשכל קומפוננטה יודעת להציג את עצמה גם במצב "טעינה", למשל במערכת שמציגה על המסך כמה גרפים, אז הגיוני לראות את הגרפים נטענים בצורה עצמאית אחד אחרי השני.
אז כן כדאי להכיר את Suspense, כדאי להשתמש בו כשצריך, אבל אל תעצרו עכשיו הכל בשביל לשכתב את כל קוד התקשורת שלכם עם Suspense. ברוב המערכות אפשר להסתדר טוב מאוד גם בלעדיו.
Уже доступно! Исследование Telegram 2025 — ключевые инсайты года 
