ToCode
رفتن به کانال در Telegram
1 419
مشترکین
-224 ساعت
-17 روز
-130 روز
آرشیو پست ها
1 419
TextField(controller: minutesController, onChanged: (String newValue) {
setTime(newValue, 60);
}),
Text("Hours: "),
TextField(controller: hoursController, onChanged: (String newValue) {
setTime(newValue, 3600);
}),
],
),
),
);
}
}
נשים לב לפרטים:
1. קוד flutter מורכב מ Wdigets. בקוד הדוגמה יש ווידג'ט ראשי בשם MyApp ובתוכו ווידג'ט משני שנקרא MyHomePage.
2. ווידג'ט יכול להיות Stateless או Stateful - כשהשאלה היא האם הוא משתנה במהלך ריצת התוכנית. פלאטר ידאג לעדכן את הממשק של Stateful Widget כל פעם שהסטייט משתנה.
3. הסטייט נשמר במחלקה נפרדת והוא איפה שכל הלוגיקה יושבת. במערכת אמיתית בטח הייתי שומר אותו בקובץ אחר, ובדוגמה שלנו הקלאס נקרא _MyHomePageState.
4. בגלל שדארט היא שפה מונחית עצמים וכל ה API של פלאטר מבוסס על מחלקות, גם הסטייט הוא מחלקה בעצמו. קצת מבלבל של Stateless Widget יש פונקציית build שבונה אותו, אבל ל Stateful Widget יש רק פונקציית createStaet ולסטייט שלו יש פונקציית build.
5. נדלג על המתודה setTime ועל dispose כדי להגיד כמה מילים על build: הפונקציה מחזירה את הייצוג של ה Widget ותיקרא כל פעם שהסטייט ישתנה. שימו לב לסידור של הילדים בתוך אוביקט Column מה שאומר שכולם יופיעו אחד מתחת לשני. חוץ מ Column יש עוד כמה דרכים לארגן ווידג'טים על המסך וזה אחד הרעיונות הנחמדים של פלאטר - כי בעצם כל העיצוב מובנה בתוך הקוד.
6. הפונקציה האנונימית שעוברת בתור ערך של הפרמטר onChanged ב TextField תיקרא כל פעם שמשתמש מעדכן את השדה. כל שדה משתמש ב setTime עם ערך בסיס אחר וזה מה שגורם לעדכון שני השדות האחרים בכל עדכון. המשתנה השני controller הוא שמאפשר לי לשנות את הערך בתיבה בצורה תכנותית.
7. לא מקובל לשמור Reference לאלמנטים עצמם שיצרנו בתוך ה Build. הדרך המרכזית לתקשר עם ה Widgets שאנחנו יוצרים היא להשתמש במשתני סטייט כשיוצרים אותם (כי אז build ייקרא שוב כשמשתנים אלו יתעדכנו). במקרה של שדות טקסט זה הקונטרולר.
8. לסיום הפונקציה setTime שעושה את כל העבודה מספרת יותר על Dart מאשר על פלאטר: הקוד כולו בתוך try/catch כי double.parse עלול לזרוק Exception אם הערך לא נראה כמו מספר; לכל משתנה יש טיפוס; בחלק מהמקומות בקוד שלי אפשר היה לוותר על המילה String לפני משתנה כי dart יכול להבין את זה לבד.
9. למרות כל מה שאמרתי על סטייט אנחנו רואים כאן השמה ישירה לתוך שדה מידע בקונטרולר שגורם לעדכון הקונטרולר והפעלה מחדש של ה Build, כל זה בלי לעבור דרך פונקציית setState של פלאטר. באופן כללי אני מוצא את ה API של פלאטר קצת לא עקבי אבל אולי יש לזה סיבות טובות שפשוט עדיין לא הבנתי.
סך הכל ולמרות שהקוד עובד אני חייב להודות שעבור אפליקציית הדוגמה שבחרתי פלאטר ו dart נראו לי מסורבלים מדי. ההמרות בין מספרים למחרוזות והטיפול ב Exceptions הפכו את הקוד ליותר ארוך ממה שאני רגיל לראות בעולם ה Web. מצד שני האפליקציה רצה בתור Desktop App על כל מערכות ההפעלה ובתור Web App בדפדפן.
קוד האפליקציה המלא זמין בגיטהאב בקישור:
https://github.com/ynonp/hello-flutter-world.1 419
# שלום Flutter World
עם גירסה 2 של פלאטר הגיעה כניסה לעולם ה Desktop ושיתוף פעולה עם קנוניקל ששם את פלאטר בתור סביבת הפיתוח ברירת המחדל של Ubuntu. פלאטר הפכה לאלטרנטיבה מהירה לפיתוח ממשקים עבור אפליקציות מובייל, ווב ו Desktop ויהיה מעניין לראות עד כמה המהלך האחרון באמת ישפיע על מתכנתים לבחור בה.
בשביל להקל עליכם את הכניסה (או ההתלבטות אם כדאי) הנה סיור זריז בפלאטר דרך פיתוח אפליקציה לא מסובכת.
## התקנות וכו
הוראות התקנה מפורטות נמצאות באתר שלהם. מכל המערכות ההתקנה הכי קלה היא דווקא על לינוקס - יש פשוט קובץ snap להתקנה, אבל גם על חלונות ומק ההתקנה לא אמורה לקחת יותר מכמה דקות של העתקת שורות מהמדריך. כאן נמצאים מדריכי ההתקנה לכל שלושת מערכות ההפעלה:
https://flutter.dev/docs/get-started/install.
אחרי זה יש גם דף שמסביר איך לקנפג את עורך הטקסט ל-4 עורכים פופולריים (אימקס, VS Code, IntelliJ ו Android Studio):
https://flutter.dev/docs/get-started/editor?tab=vscode
ועם קצת עזרה מגוגל תמצאו בקלות הוראות התקנה לכל עורך טקסט אחר שאתם אוהבים.
אחרי ההתקנה פלאטר מגיעה עם Project Generator ובשביל ליצור תבנית של פרויקט חדש נכנסים לתיקיה ריקה ומפעילים:
$ flutter create .
## מבנה התוכנית
יצרן הפרויקטים בנה לנו תיקיה ספציפית לכל מערכת הפעלה (עם השמות android, ios, linux, macos, windows ו web) ותיקיה בשם lib בה נמצא הקוד המשותף. בתוך התיקיות הספציפיות נכתוב קוד ספציפי לכל מערכת הפעלה - לדוגמה קוד שחושף איזשהו Native API, ובתוך תיקיית lib יהיה הקוד המשותף לכל המערכות.
בשביל הדוגמה אני רוצה לבנות אפליקציית Desktop פשוטה להמרת יחידות זמן: האפליקציה מראה 3 תיבות טקסט, אחת בשביל שניות, השניה בשביל הדקות והשלישית בשביל השעות. שינוי הערך בכל תיבה יגרום לעדכון הערך בשתי התיבות האחרות בהתאמה. כלומר אם כתבתי 7200 בתיבת השניות יופיע לי 2 בתיבת השעות ו 120 בתיבת הדקות. אם כתבתי 1 בתיבת השעות אוטומטית יופיע 3600 בתיבת השניות ו 60 בתיבת הדקות.
כל הקוד הרלוונטי נכנס ב 90 שורות בקובץ lib/main.dart. הנה הדבקה שלו ואחר כך כמה הערות שיעזרו להבנה:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final secondsController = TextEditingController();
final minutesController = TextEditingController();
final hoursController = TextEditingController();
setTime(String value, int factor) {
try {
double seconds = double.parse(value) * factor;
String secondsText = seconds.toStringAsFixed(2);
String hoursText = (seconds / 3600).toStringAsFixed(2);
String minutesText = (seconds / 60).toStringAsFixed(2);
if (factor != 1) {
secondsController.text = secondsText;
}
if (factor != 60) {
minutesController.text = minutesText;
}
if (factor != 3600) {
hoursController.text = hoursText;
}
} on FormatException { }
}
@override
void dispose() {
secondsController.dispose();
minutesController.dispose();
hoursController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text("Seconds: "),
TextField(controller: secondsController, onChanged: (String newValue) {
setTime(newValue, 1);
}),
Text("Minutes: "),1 419
# טיפ וים: השלמה אוטומטית עם CoC
כן אני יודע שהשלמה אוטומטית רק פוגעת לנו בזיכרון ומתכנתים אמיתיים כותבים בלי צבעים ובלי השלמות, אבל אם בכל זאת אתם מרגישים צורך להתפנק או עובדים עם שפת תכנות חדשה ורוצים לקצר זמנים יש שתי סיבות שגורמות ל coc.vim לעבוד יותר טוב מכל פלאגין אחר שניסיתי.
הסיבה הראשונה היא ניצול היכולת האסינכרונית של וים8 או ניאו-וים כדי לספק מנגנון השלמה אוטומטית שלא מאט בכלל את העורך ולא עוצר "לחשוב" באמצע שכותבים קוד. כן יש יותר עומס על הזיכרון וזה בגלל הסיבה השניה.
הסיבה השניה היא מנגנון שנקרא Language Server Protocol או בקיצור lsp. הרעיון הוא ליצור מנגנון סטנדרטי של השלמה אוטומטית לעורכי טקסט כך ש"שרת" ההשלמה האוטומטית יהיה חיצוני לעורך ושני הרכיבים יתקשרו באמצעות פרוטוקול הודעות. coc מתקשר עם שרת שפות כדי להציע את ההשלמה האוטומטית אז אם יש לנו שרת שפות טוב ומתוחזק גם הצעות ההשלמה יהיו טובות. הרצת שרת שפות מתוך העורך לוקחת זיכרון ולפחות אצלי על המכונה צריכת הזיכרון של vim היתה גבוהה משמעותית כשהוספתי את coc בחלק מהשפות. בדף הזה יש רשימת של כל שרתי השפות הידועים לאדם:
https://microsoft.github.io/language-server-protocol/implementors/servers/.
בשפות שניסיתי בינתיים ההשלמה היתה מדויקת ומהירה ולכן אם אתם בעניין השלמות אוטומטיות אני מאוד ממליץ להתקין את התוסף. הוראות התקנה תמצאו בדף התוסף כאן:
https://github.com/neoclide/coc.nvim.
אחרי ההתקנה יש להפעיל מתוך vim פקודות להתקנת שרת השפות (או שרתי השפות) שאתם צריכים, למשל בשביל לעבוד עם typescript הייתי צריך להפעיל מתוך וים את:
:CocInstall coc-tsserver
ואלה השורות שהוספתי ל vimrc כדי לקבל התנהגות יותר אינטואיטיבית:
let g:lsc_auto_map = v:true
nmap <silent> gd <Plug>(coc-definition)
nmap <silent> gy <Plug>(coc-type-definition)
nmap <silent> gi <Plug>(coc-implementation)
nmap <silent> gr <Plug>(coc-references)
inoremap <silent><expr> <c-@> coc#refresh()
inoremap <silent><expr> <cr> pumvisible() ? coc#_select_confirm()
\: "\<C-g>u\<CR>\<c-r>=coc#on_enter()\<CR>"
" Use K to show documentation in preview window.
nnoremap <silent> K :call <SID>show_documentation()<CR>
function! s:show_documentation()
if (index(['vim','help'], &filetype) >= 0)
execute 'h '.expand('<cword>')
elseif (coc#rpc#ready())
call CocActionAsync('doHover')
else
execute '!' . &keywordprg . " " . expand('<cword>')
endif
endfunction
" Symbol renaming.
nmap <leader>rn <Plug>(coc-rename)
" Formatting selected code.
xmap <leader>f <Plug>(coc-format-selected)
nmap <leader>f <Plug>(coc-format-selected)
" Add `:Format` command to format current buffer.
command! -nargs=0 Format :call CocAction('format')1 419
# באג ריפורט: מחיקה מרובה וחיפוש
פעמיים בתקופה האחרונה נתקלתי באותו באג בשתי מערכות שונות ובשני המקרים עם תוצאה הרסנית, וזה לבד מצדיק פוסט קטן בתקווה שיעזור גם לכן להימנע מבעיות דומות.
## מה קרה
במסך רשימת פריטים במערכת היתה לנו תיבת חיפוש וגם אפשרות לביצוע פעולה (למשל מחיקה) על כל הפריטים. בעזרת תיבת החיפוש משתמשים יכלו לקבל רק פריטים שמתאימים למילת חיפוש מסוימת. ברגע שמשתמש סינן חלק מהפריטים בתיבת החיפוש ואז לחץ "בחר הכל" לצורך ביצוע פעולה על כל הפריטים בתוצאות החיפוש, המערכת "שכחה" שאנחנו בתוך חיפוש וביצעה את הפעולה על כל הפריטים.
## למה זה קרה
בשביל לממש ביצוע פעולה על מספר פריטים מרשימה, אם כל הפריטים מופיעים בקלאיינט, אפשר לשלוח לשרת את רשימת כל הפריטים (או מזהי הפריטים) עליהם רוצים לבצע את הפעולה. במקרה שלנו, ובדומה לממשק בג'ימייל ובמערכות נוספות, הרשימה כוללת המון המון פריטים מחולקים לעמודים. אפשרות "בחר הכל" אמורה לסמן את כל הפריטים מכל הדפים, כולל אלה שעכשיו אין עליהם מידע בקלאיינט.
הפיתרון שנבחר היה לשלוח פרמטר מיוחד שמציין שהפעולה שאנחנו מבצעים צריכה לרוץ על כל הפריטים כולל אלה שהקלאיינט לא רואה כרגע כי הם נמצאים בדפים אחרים.
הבעיה שלא היה תיאום בין פרמטר זה לבין פרמטר החיפוש: בקשה אחת סיננה את הפריטים לפי מילת חיפוש מסוימת, ובקשה אחריה ביצעה "מחיקה מרובה" ומחקה את כל הפריטים, בלי שהשרת יוכל להבין ש"כל הפריטים" אמורים היו להיות רק "כל הפריטים שמתאימים לחיפוש".
## איך כדאי למנוע בעיות דומות בעתיד
המקור האמיתי לבעיה הוא מימוש לא נכון של מנגנון החיפוש. אם ניקח דוגמה מ Rails אפשר לדמיין קונטרולר בשם ItemsController עם פעולת index שמציגה את כל הפריטים ומטפלת בחיפוש:
class ItemsController < ApplicationController
def index
@items = Item.where(owner: current_user)
if params[:search]
@items = @items.where("name like '%?%'", params[:search])
end
end
end
ופה הבעיה - חיפוש הוא לא "עוד דרך" להציג תוצאות אלא הוא שינוי Context, הוא ממש משפיע על כל הפעולות על המסך שעכשיו צריכות להיות מושפעות מרשימת התוצאות.
גישה טובה יותר לכתיבת קוד החיפוש היא לבנות אותו בתור Controller נפרד עם פעולת index משלו:
class ItemsController < ApplicationController
def index
@items = Item.where(owner: current_user)
end
end
class ItemsSearchController < ApplicationController
def index
@items = Item.where(owner: current_user).where("name like '%?%'", params[:search])
end
end
בגישה כזאת כל הכפתורים ב View יכולים לשלוח אותנו ל Actions מתאימים ב Controller שלהם, ואז כפתור של Delete All היה מגיע ל ItemsSearchController#delete_all וזורק שגיאה כי הפעולה לא מומשה עדיין, במקום למחוק את כל הפריטים בלי קשר לתוצאות החיפוש.1 419
# שבע המשימות הבאות
איך ללמוד Java? איך מתחילים ללמוד React? איך אני הופך למתכנת Node.JS? איך אני לומד להיות מתכנת אוטומציה? כל שאלה כאן היא שאלה ענקית כי היא מסתכלת על יעד רחוק שיש המון דרכים להגיע אליו, ועל כזה שאין מסלול ברור אליו.
אם אני מחליט לקחת את "איך בתור בודק ידני אני עובר ללמוד לכתוב בדיקות אוטומציה למערכות ווב" בתור דוגמה, אני מגלה שאני צריך:
1. ללמוד איך בנויה אפליקציית Web (כולל HTML, CSS וקצת JavaScript)
2. ללמוד איך עובד קוד צד שרת ומה הקשר בין Front End ל Back End
3. ללמוד על פרוטוקולי תקשורת ובמיוחד HTTP
4. ללמוד שפת תכנות כלשהי איתה אכתוב את הבדיקות
5. ללמוד איך להשתמש ב Selenium ולכתוב בדיקות בשפת התכנות שבחרתי
וכל סעיף פה ברשימה נפתח לתת רשימה נפרדת משלו - והכי גרוע, האורך של כל רשימה משתנה לפי הדברים שאני היום יודע והזמן שיש לי. אז בדוגמה של בדיקות אוטומטיות יכול להיות שאני אצטרך להשקיע יותר זמן בהכרת המבנה של Web Applications, ומישהו אחר יצטרך להשקיע את עיקר הזמן בלימוד Python ומישהו שלישי בכלל יסתבך עם ה Selenium.
דרך טובה להתחיל ללמוד נושא חדש היא לא להסתכל על הנושאים אלא לבנות רשימה (או למצוא רשימה מוכנה או להיעזר בחבר לצורך הבניה) שמכילה 7 מטלות ספציפיות שיקדמו אתכם ליעד. דברים כמו-
1. לכתוב ולהריץ תוכנית Hello World בשפת פייתון.
2. לכתוב תוכנית בשפת Python שמדפיסה את כל המספרים הזוגיים.
3. לכתוב דף אינטרנט שמציג מידע עליי.
4. לכתוב תוכנית פייתון שמושכת מידע משרת מרוחק.
5. לכתוב תוכנית פייתון שמפעילה דפדפן ובודקת מה הטקסט שמופיע בכותרת של הדף.
6. להריץ את התוכנית שכתבתי בצורה אוטומטית בתור משימה מתוזמנת.
7. לקבל אימייל מהתוכנית בסוף ההרצה אם הטקסט הוא לא מה שחשבתי.
אלה 7 משימות שבתור איש QA אני יכול להבין בכל משימה בדיוק מה אני צריך והן נותנות לי משהו להתעסק איתו בשבוע הקרוב. סיכוי טוב שאצליח לבצע חלק מהמשימות שבחרתי, וכמעט בטוח שיהיו כאן משימות "גדולות" עליי, כלומר דברים שחשבתי שאני יכול לעשות אבל אז אני בא לכתוב אותם ומגלה שהם הרבה יותר מסובכים. זה בסדר גמור. מה שחשוב זה שבסוף העבודה על הרשימה יהיה לי מספיק חומר למחשבה לרשימת שבע המשימות הבאות.
כשבוחרים יעד רחוק אי אפשר לראות את סוף הדרך - אנחנו רואים רק עד כמה שהפנס שלנו מגיע. ככל שנתקדם יותר עמוק לתוך המערה נוכל לראות יותר דברים ולמצוא רשימות יותר מעניינות. וכן, בסוף גם ללמוד את הדבר ההוא שבגללו נכנסנו למערה, אם הוא עדיין יהיה מעניין בעוד שלושה או ארבעה חודשים.
1 419
# הסוד הכי שמור בהייטק
יש לי חבר שעובד כבר שנים בהייטק ועד לאחרונה תמיד היה מאוד מתוסכל: הוא אחד המתכנתים המהירים שפגשתי ובעיקרון היה יכול לסיים כל יום עבודה בחצי מהזמן של יום עבודה רגיל, אבל "בשביל מה?" הוא אומר לי מדי פעם, "היית צריך לראות איך מסתכלים עליי אם אני מעז יום אחד לצאת קודם".
וחברה אחרת שמה לב שלא משנה מה היא עושה היא אף פעם לא מצליחה לעמוד בזמנים. זה נראה כאילו תמיד יש יותר עבודה ממה שהיא מספיקה לעשות, לא משנה כמה היא מתאמצת. יומיים בשבוע היא נשארת לעבוד לתוך הלילה לרוקן את הערימה, ולמחרת רשימת המשימות כאילו מתמלאת מעצמה.
למרות ששני המקרים נראים הפוכים, לשני החברים שלי יש בעיה דומה - שניהם מרגישים שאין להם שליטה על הזמן שלהם. שניהם "לא יכולים" לקחת שעה ביום ללמוד טכנולוגיות חדשות כי הבוס מחליט מה מותר ומה אסור.
והסוד? מסתבר שבתקופת הקורונה שניהם גילו שיש להם הרבה יותר שליטה ממה שהם חשבו, וכנראה תמיד היתה. החבר שעובד מהר למד לנצל את הזמן שהוא מרוויח, והחברה שעובדת לאט למדה להתעלם מחלק מההודעות הנכנסות. שניהם בילו בשנה האחרונה הרבה יותר זמן עם המשפחה ממה שאי פעם חשבו שאפשרי.
בואו לא ניתן לסוד הזה להישכח- גם כשאנחנו מתקרבים לחזרה למשרד לפחות לחלק מהשבוע. יש לכם הרבה יותר שליטה על הזמן שלכם ממה שתמיד היה נדמה, וכמו שאפשר לנצל אותה כדי לבלות יותר זמן משפחה, אפשר יהיה (כשהילדים חוזרים למסגרות) לנצל אותה כדי ללמוד טכנולוגיות חדשות, להתחיל לכתוב בדיקות אוטומטיות ולבנות תהליכי עבודה טובים יותר בחברה שלכם.
1 419
## חיבור לריאקט
כל העבודה הזאת מתחברת ל React בצורה ממש פשוטה באמצעות hook בשם useMachine. פונקציה זו מחזירה לנו שני משתנים: הראשון נקרא current ומייצג את הסטייט הנוכחי, והשני נקרא send ובעזרתו נוכל לשלוח אירועים למכונה. לדוגמה בשביל לשלוח אירוע PLAY למכונה שיתחיל להציג מחדש את רצף הצבעים כל מה שאני צריך לעשות הוא:
send("PLAY");
את המשתנים sequence ו currentIndex מתוך הזיכרון של המכונה אני קורא דרך משתנה context של הסטייט הנוכחי:
const [current, send] = useMachine(machine);
const { sequence, currentIndex } = current.context;
שם הסטייט הנוכחי הוא current.value והאירוע שבגללו הגעתי לסטייט הנוכחי הוא current.event. אתם מוזמנים לראות את הקוד המלא של החיבור לריאקט בקודסנדבוקס בקישור https://codesandbox.io/s/wonderful-worker-pyozo.
## רגע, זה לא דומה קצת ל redux?
בעבודה עם xstate יש הרבה דברים שמזכירים את Redux:
1. הלוגיקה נשמרת בקובץ נפרד ואנחנו ניגשים אליה דרך Hook של הספריה, מה שמשחרר את ריאקט להתמקד רק בקוד התצוגה
2. הלוגיקה עצמה מקבלת "אירועים" ומנהלת מצב פנימי לפי האירועים שנכנסו למכונה.
3. אנחנו מאוד מוגבלים בתקשורת עם הלוגיקה ויכולים רק "לשלוח" אירועים כדי לגרום למכונה לעשות דברים.
4. אירועים מסוימים שנשלח יגרמו ל Side Effects (כלומר ליצירת "פעולות"). בקוד הדוגמה שלנו אלה היו שעונים ובמקומות מתוחכמים יותר נוכל למצוא בקשות רשת. ב Redux היינו משתמשים ב Middlewares כדי להגדיר איזה אירועים שנכנסים יפעילו Side Effects.
5. עדכון המידע קורה רק דרך הכלים של xstate, והמידע כולו הוא Immutable, בדומה ל Redux שם עדכון המידע קורה באמצעות החזרת סטייט מעודכן מה Reducer.
הדימיון קיים כי גם Redux הוא סוג של מכונת מצבים ומספק פונקציונאליות מאוד דומה. ההבדל המרכזי הוא בגישה הדקלרטיבית של xstate שמגדיר את מעברי המצבים באמצעות JSON ולא באמצעות קוד, ולכן קל יותר לכתוב כלים אוטומטיים לויזואליזציה של מעברי מצבים אלה. כמו כן הכתיב המפורש ב JSON אומר שכל מעבר מצב הוא מכוון, כך שיהיו פחות באגים של הגעה למצב לא חוקי. ואולי ההבדל המשמעותי ביותר הוא ש Redux מעודד אותנו לשמור את כל הסטייט של היישום ב Store יחיד, ולעומתו XState מעודד הגדרת מספר מכונות מצבים עבור חלקים שונים ביישום.1 419
entry: actions.send("TICK", { delay: 200, id: "autoplayTimer" }),
on: {
TICK: {
target: "autoplay"
}
}
},
idle: {
entry: actions.cancel("autoplayTimer")
},
flash: {
entry: actions.send("TICK", { delay: 200 }),
on: {
TICK: "idle"
}
}
}
});
בואו נפרק את הקוד הזה לקטעים כדי להבין מה קורה שם.
כדי ליצור מכונת מצבים אנחנו מעבירים אוביקט מידע לפונקציה createMachine של xstate. השדות הראשונים באוביקט הם שדות מידע כללי ומגדירים את מזהה המכונה, את המצב ההתחלתי שלה ואת הזיכרון עם הערכים הראשונים שייכתבו אליו:
id: "simon",
initial: "autoplay",
context: {
sequence: [1, 1, 2, 3],
currentIndex: 0
},
המפתח states מתאר את כל המצבים והמעברים ביניהם. שימו לב למפתחות בלבד בתוכו: autoplay, pause, idle, flash. כל מה שיש לנו במשחק בעצם מסתכם ב-4 מצבים.
מצב idle הוא המצב הפשוט של "מחכה לקלט". המכונה כאן לא עושה כלום וריאקט יצטרך להציג את כל הצבעים מכובים. הנה הקוד שמתאר אותו:
idle: {
entry: actions.cancel("autoplayTimer")
},
המצבים היותר מעניינים הם autoplay ו pause. מצבים אלה מתארים את מנגנון הצגת רצף הצבעים, כאשר במצב autoplay תהיה לנו מנורה דולקת ב GUI, ובמצב pause כל הנורות יהיו כבויות לצורך הפסקה קצרה בין הצגת הצבעים. האירוע TICK הוא שמעביר בין המצבים:
autoplay: {
entry: actions.send("TICK", { delay: 1000, id: "autoplayTimer" }),
on: {
TICK: {
target: "pause",
actions: assign({
currentIndex: (context, event) => context.currentIndex + 1
})
}
},
always: [
{
target: "idle",
cond: ({ currentIndex, sequence }) => currentIndex >= sequence.length
}
]
},
pause: {
entry: actions.send("TICK", { delay: 200, id: "autoplayTimer" }),
on: {
TICK: {
target: "autoplay"
}
}
},
ובואו נקרא את זה לאט:
1. במצב autoplay אם התקבל אירוע TICK יש לעבור למצב pause, וגם להפעיל "פעולה פנימית" של עדכון הזיכרון - קידום האינדקס ברצף ב-1.
2. בלוק always במצב autoplay אומר שאם נכנסתי למצב זה אבל התנאי מתקיים, כלומר האינדקס הנוכחי גדול או שווה למספר הצבעים ברצף, אז צריך לעבור אוטומטית למצב idle.
3. במצב pause אם התקבל אירוע TICK יש לעבור למצב autoplay.
נשארנו עם המפתח entry: מפתח זה מתאר פעולות פנימיות שהמכונה מייצרת בכניסה למצב המתאים. בקוד שלי בכניסה למצב autoplay אני מתחיל שעון של שניה אחריו יישלח בצורה אוטומטית אירוע TICK; בכניסה למצב pause שוב יופעל שעון של 200 מילי שניות בסיומו יישלח אירוע TICK. הפעולות הפנימיות של המכונה מאפשרות לי לטפל בקוד שקשור לזמן בצורה נוחה.
## מימוש פידבק למשתמש אחרי לחיצה
בכל רגע נתון וכתוצאה מפעולות בממשק המשתמש יכולים להיכנס למכונה שלי אירועים חדשים: משתמש שמבקש עכשיו לראות מההתחלה את רצף האורות ישלח אירוע PLAY למכונה, ומשתמש שמבקש להתחיל ללחוץ על הכפתורים הצבעוניים בעצמו יפעיל את אירוע FLASH.
בגלל שאירועים אלה יכולים להגיע בכל מצב וצריכים טיפול מיידי לא משנה מתי הם הגיעו, ובגלל שהטיפול בהם זהה בלי תלות במצב בו היינו כשהם התקבלו, כתבתי את קוד הטיפול בשני האירועים במקום מרכזי במפתח on של המכונה כולה:
on: {
PLAY: {
target: "autoplay",
actions: [
actions.cancel("autoplayTimer"),
assign({ currentIndex: (context, event) => 0 })
]
},
FLASH: {
target: "flash",
actions: [
actions.cancel("autoplayTimer"),
assign({
currentIndex: (context, event) => 0
})
]
}
},
אנחנו כבר יודעים לקרוא את זה:
1. אם התקבל אירוע PLAY, לא משנה באיזה מצב היינו, יש לעבור למצב autoplay, לבטל את השעון autoplayTimer ולאפס את משתנה currentIndex.
2. אם התקבל אירוע FLASH, לא משנה באיזה מצב היינו, יש לעבור למצב flash ושוב לבטל את השעון autoplayTimer ולאפס את משתנה currentIndex.
ולסיום מצב flash עצמו נראה כך:
flash: {
entry: actions.send("TICK", { delay: 200 }),
on: {
TICK: "idle"
}
}1 419
# סיור קצר ב XState לצורך פיתוח משחק סיימון
לפני כמה ימים כתבתי כאן על פיתוח משחק סיימון ב React. בפוסט הצבעתי על הקושי בכתיבת קומפוננטות ריאקט הכוללות אלמנט של זמן או מעבר מורכב בין מצבים. במקרה של סיימון היו לנו את המצבים הבאים:
1. הקומפוננטה יכולה להיות במהלך הצגת רצף הצבעים האקראי שהמחשב בחר
2. במהלך הצגת צבע מהרצף יכול להיות שמשתמש ילחץ על אחד הכפתורים ואז נפסיק להציג את הרצף וניתן למשתמש להקליד את הרצף שלדעתו הוא הנכון
3. אפשרות שניה במהלך הצגת צבע מהרצף היא שאף אחד לא נוגע ואז צריך להמשיך להציג את הצבע הבא
4. אבל אם אנחנו מציגים את הצבע האחרון ברצף צריך לעצור ולחכות שמשתמש יתחיל להקליד את הרצף שלו או ילחץ על כפתור "הצג שוב"
5. בנוסף במהלך הצגת הצבעים מהרצף האקראי ובין כל שני צבעים צריך לכבות את כל המנורות לכמה מאיות שניה כדי לתת למשתמש להבדיל בין הצבעים.
אם פספסתם את אותו פוסט אתם מוזמנים לחזור אליו ולראות את המימוש שכתבתי בריאקט למעברי המצבים האלה. אם אתם זוכרים מה הלך שם ומחפשים גישה נוספת לכתיבת קומפוננטות מסוג זה הגעתם למקום הנכון. בפוסט זה אציג את ספריית xstate למימוש מכונת מצבים בסביבת JavaScript ונראה איך היא תעזור לנו בכתיבת משחק סיימון.
## עקרונות מרכזיים של xstate שהשתמשתי בהם בכתיבת המשחק
הספריה xstate היא ספריה למימוש מכונות מצבים ב JavaScript. מכונת מצבים היא מערכת שמכילה מספר "מצבים" והגדרה למעברים ביניהם. בבניית מכונת מצבים אנחנו מגדירים מה המצבים השונים, מה יכול לקרות בכל מצב ולאיזה מצב צריך לעבור בעקבות כל אירוע. לפעמים (כמו ב xstate) למכונת מצבים יש גם "זיכרון" שזה אוביקט מידע שהמצבים השונים יכולים לקרוא ממנו ומעברי מצבים מסוימים יכולים לעדכן אותו.
ב xstate מעברי מצבים הם תוצאה של "אירועים". האירועים יכולים להגיע מבחוץ או כתוצאה מ"פעולות" שמכונת המצבים עצמה מייצרת.
לדוגמה בואו נדבר על ניגון רצף הצבעים האקראי-
נוכל להגדיר מצב "מציג צבע", מצב "הפסקה בין צבעים" ומצב "idle", ובנוסף להגדיר אוביקט זיכרון למכונה שכולל את רצף הצבעים האקראי ואת האינדקס שכרגע אנחנו מציגים מתוך הרצף. קוד חיצוני שיבקש להתחיל להציג את רצף הצבעים ישלח אירוע PLAY ובתגובה אנחנו נאפס את האינדקס וניכנס למצב "מציג צבע".
באופן אוטומטי בכניסה למצב "מציג צבע" נשים שעון שבעוד שניה יעבור אותנו למצב "הפסקה בין צבעים". בכניסה למצב "הפסקה בין צבעים" נשים שעון שעוד 200 מילי שניות יעביר אותנו חזרה למצב "מציג צבע" וכך עד שסיימנו להציג את כל הצבעים כי אז נעבור חזרה למצב idle.
ספריית xstate תעזור לי לקחת את התיאור המילולי ולכתוב אותו בקוד כך שריאקט לא יצטרך להתעסק עם הלוגיקה העסקית של המשחק והקומפוננטה תוכל להתמקד רק בלוגיקה החזותית של הצגת המידע על המסך. את הלוגיקה שאני כותב ב xstate גם הרבה יותר קל לבדוק או להרחיב בהשוואה לקוד שתקוע בתוך קומפוננטה.
מצד שני ובגיזרת החסרונות - הספריה כוללת הרבה מושגים ורעיונות חדשים, דורשת זמן לימוד ולפחות מהרושם שלי לא תמיד קל לדבג את כל הנתיבים בקוד.
## מימוש ניגון רצף הצבעים עם הפסקות בין הצבעים
נעבור לקוד. תחילה אדביק כאן את קוד המכונה המלא ולאחר מכן אדביק קטע קטע מתוכו עם הסברים:
const { actions, createMachine, assign } = require("xstate");
// Stateless machine definition
// machine.transition(...) is a pure function used by the interpreter.
export default createMachine({
id: "simon",
initial: "autoplay",
context: {
sequence: [1, 1, 2, 3],
currentIndex: 0
},
on: {
PLAY: {
target: "autoplay",
actions: [
actions.cancel("autoplayTimer"),
assign({ currentIndex: (context, event) => 0 })
]
},
FLASH: {
target: "flash",
actions: [
actions.cancel("autoplayTimer"),
assign({
currentIndex: (context, event) => 0
})
]
}
},
states: {
autoplay: {
entry: actions.send("TICK", { delay: 1000, id: "autoplayTimer" }),
on: {
TICK: {
target: "pause",
actions: assign({
currentIndex: (context, event) => context.currentIndex + 1
})
}
},
always: [
{
target: "idle",
cond: ({ currentIndex, sequence }) => currentIndex >= sequence.length
}
]
},
pause: {1 419
# טיפ גיט: תחרות קומיטים שבועית
אחת הפקודות הפחות מוכרות ב git תוכל לעזור לכם כשתחזרו למשרד, או לפחות לאלה מכם שמחזיקים במשרד מסך גדול ומחפשים סטטיסטיקות להציג עליו. אני מדבר על הפקודה git shortlog שמראה לנו את הקומיטים האחרונים לפי הכותב. פלט לדוגמה בהפעלה ללא מתגים עשוי להיראות כך:
$ git shortlog
ynonp (4)
do stuff
add more stuff
fix the tests of stuff
break some stuff
johndoe (2)
break stuff
fix stuff
בעוד ש log רגיל מתמקד ברצף הקומיטים, ה shortlog מתמקד במתכנתים מאחורי הקומיטים, או לפחות מארגן את הקומיטים לפי מי שכתב אותם.
והתחרות שלנו? הפקודה הבאה מציגה רק את המספרים (בלי פרטי הקומיטים עצמם) של קומיטים מהשבוע האחרון בכל הענפים:
git shortlog -s --branches --since="1 week ago"
ואם אתם מקרינים את זה על המסך הגדול תוכלו להשתמש ב watch כדי לרענן את הפלט פעם ביום בצורה אוטומטית:
watch -n 86400 'git shortlog -s --branches --since="1 week ago"'1 419
ואת קוד המשחק המלא עם האפקט שמציג את רצף הצבעים אפשר לראות בקודפן הבא: https://codesandbox.io/s/elated-minsky-3uuko?file=/src/App.js:362-799
## קליטת רצף צבעים מהמשתמש
עברנו את החלק הקשה בבניית המשחק אבל עדיין צריך להוסיף את הטיפול בקלט מהמשתמש כדי שיהיה כיף לשחק. עיקר הטיפול הוא תוספת של משתנה State מערכי בשם currentSequence שמייצג את הצבעים שמשתמש לחץ עליהם.
כל פעם שמשתנה זה מתעדכן אני אבדוק אם יש התאמה באמצעות אפקט, ואם יש אוכל להציג הודעת Bravo ולאפס את כל המשתנים כדי להתחיל סיבוב חדש.
כל פעם שמשתמש לוחץ על אחד ה div-ים אני מוסיף את הערך המתאים של ה div ל currentSequence.
ובשביל שמשתמשים ידעו שהלחיצה נקלטה הוספתי מנגנון flash קצר של הכפתורים: משתנה State בשם flash מייצג את הערך של הכפתור שעכשיו צריך להציג אור לזמן קצר; כל פעם שמעדכנים אותו מתחיל useEffect שסופר 200 מילי שניות עד לכיבוי אוטומטי ועדכון הערך ל-0.
קוד המשחק המלא יחד עם הפונקציה שמגרילה רצפים נראה כך:
import React, { useState, useEffect } from "react";
import _ from "lodash";
import "./styles.css";
const colors = ["red", "blue", "green", "yellow"];
const sequenceLength = 4;
function randomSequence(len) {
const seq = new Array(len)
.fill(0)
.map((x) => Math.floor(Math.random() * colors.length) + 1);
const zeros = new Array(len).fill(0);
return _.flatMap(_.zip(seq, zeros));
}
function Game(props) {
const [secretSequence, setSecretSequence] = useState(
randomSequence(sequenceLength)
);
const [index, setIndex] = useState(0);
const [currentSequence, setCurrentSequence] = useState([]);
const [flash, setFlash] = useState(0);
useEffect(() => {
if (flash === 0) return;
const timer = setTimeout(() => {
setFlash(0);
}, 200);
return () => {
clearTimeout(timer);
};
}, [flash]);
useEffect(() => {
if (
_.isEqual(currentSequence.slice(-1 * sequenceLength * 2), secretSequence)
) {
alert("Bravo!");
setCurrentSequence([]);
setSecretSequence(randomSequence(sequenceLength));
setIndex(0);
}
}, [currentSequence, secretSequence]);
useEffect(() => {
if (index >= secretSequence.length) return;
const timer = setTimeout(
() => {
setIndex((i) => i + 1);
},
index % 2 === 0 ? 600 : 200
);
return () => {
clearTimeout(timer);
};
}, [index, secretSequence]);
function cx(squareIndex) {
return `
colored-box
${colors[squareIndex - 1]}
${secretSequence[index] === squareIndex ? "active" : ""}
${flash === squareIndex ? "active" : ""}
`;
}
function clicked(n) {
setCurrentSequence((s) => [...s, n, 0]);
setFlash(n);
}
return (
<div>
<div className="game">
<div className={cx(1)} onClick={() => clicked(1)}>
1
</div>
<div className={cx(2)} onClick={() => clicked(2)}>
2
</div>
<div className={cx(3)} onClick={() => clicked(3)}>
3
</div>
<div className={cx(4)} onClick={() => clicked(4)}>
4
</div>
</div>
<div>
<button onClick={() => setIndex(0)}>Play Again</button>
</div>
</div>
);
}
export default function App() {
return (
<div className="App">
<Game />
</div>
);
}
ואתם מוזמנים לשחק בו או לעדכן את הקוד בקישור: https://codesandbox.io/s/epic-bell-pcf9z?file=/src/App.js.1 419
# בואו נכתוב משחק סיימון ב React כדי ללמוד לעבוד עם שעונים
אחד האתגרים בכתיב הקומפוננטות של ריאקט הוא יצירת קומפוננטות המושפעות מזמן. ברוב הפעמים זה הגיוני, כי לא היית רוצה שפתאום דברים ישתנו על המסך בלי שמשתמש עשה איזושהי פעולה - זה רק מבלבל. ועדיין, כשאנחנו כן רוצים לכתוב קוד שקשור לזמן כדאי שנדע איך לגשת לזה בלי להתבלבל.
## מהו משחק סיימון
משחק סיימון הוא משחק זיכרון פשוט הכולל 4 כפתורים צבעוניים. הכפתורים נדלקים בסדר מסוים (כל כפתור לפי הצבע שלו) ואחרי הצגת הרצף המשתמש צריך ללחוץ על הכפתורים בדיוק באותו הסדר בו הם נדלקו אוטומטית קודם. ככל שמתקדמים במשחק הרצפים הופכים ארוכים יותר וכך קשה יותר לזכור אותם. בחלק מהגירסאות אפשר להוסיף צלילים במקום אורות או גם וגם.
## הצגת הממשק
בגלל שאנחנו ב Web נוכל לייצג כל כפתור בתור div וסך הכל נציג 4 מהם על המסך. דיב שיש לו את הקלאס active יוצג בצבע שלו, ואלה בלי הקלאס active יוצגו שקופים. כשמשתמש ילחץ על div נוכל להוסיף את הקלאס active לכמה מאיות שניה כדי להראות אפקט של לחיצה, וכשמציגים את רצף האורות פשוט נוסיף ונוריד את הקלאס active ל div-ים לפי הסדר.
קוד ה CSS יספיק בשביל הממשק:
.colored-box {
width: 100px;
height: 100px;
vertical-align: top;
border: 1px solid;
display: inline-block;
line-height: 100px;
text-align: center;
font-size: 1.5rem;
}
.red {
background: red;
}
.yellow {
background: yellow;
}
.blue {
background: blue;
}
.green {
background: green;
}
.colored-box:not(.active) {
background: transparent;
}
ואיתו הקוד הבא עבור ה React:
import React, { useState } from "react";
import "./styles.css";
const colors = ["red", "blue", "green", "yellow"];
function randomSequence() {
return [1, 2, 1, 1];
}
function Game(props) {
const [currentSequence, setCurrentSequence] = useState(randomSequence());
const [index, setIndex] = useState(0);
function cx(squareIndex) {
return `colored-box ${colors[squareIndex - 1]} ${
index === squareIndex ? "active" : ""
} `;
}
return (
<div className="game">
<div className={cx(1)}>1</div>
<div className={cx(2)}>2</div>
<div className={cx(3)}>3</div>
<div className={cx(4)}>4</div>
</div>
);
}
export default function App() {
return (
<div className="App">
<Game />
</div>
);
}
אם בשביל הדוגמה נוסיף את הקלאס active לאחד ה div-ים נוכל לראות שהוא ייצבע בצבע המתאים לפי מערך הצבעים שהגדרתי בתחילת הקובץ.
אפשר לראות שבשביל להתחיל את המשחק אני לא באמת צריך רצף אקראי ולכן הפונקציה randomSequence מחזירה מערך קבוע. אחרי שהכל יעבוד אוכל להחליף אותה לפונקציה שמייצרת מערך אקראי.
אפשר לראות את הקוד לייב בקודסנדבוקס בקישור הבא: https://codesandbox.io/s/delicate-darkness-crrq1?file=/src/App.js
## מימוש הצגת רצף צבעים
האתגר הראשון האמיתי שלנו במשחק סיימון הוא הצגת רצף הצבעים. בריאקט אנחנו משתמשים ב useEffect כדי לתת לרכיב יכולת "להזיז את עצמו". הרעיון בגדול:
1. אני מגדיר משתנה סטייט בשם index שמייצג את האינדקס ברצף שעכשיו צריך להציג.
2. אני מגדיר אפקט שכל פעם שמשתנה זה מתעדכן באופן אוטומטי בעוד שניה הוא יקודם שוב.
כדי שהמימוש יראה טוב נרצה להוסיף עוד כמה טריקים:
1. אני מוסיף ברצף את הערך 0 בין כל שני צבעים כדי לייצר אפקט של מנורה שנדלקת ונכבית לפני שהמנורה הבאה נדלקת.
2. האפקט ישתמש בזמן המתנה שונה למספרים זוגיים (המייצגים צבעים אמיתיים) ולמספרים אי זוגיים (המייצגים את ההפוגה בין המנורות).
3. חשוב לזכור לבטל את ה timeout בקוד ביטול האפקט, כדי שכפתור Play Again יעבוד כמו שצריך ויבטל את הניגון האוטומטי הקודם.
סך הכל הרעיונות האלה מתנקזים לאפקט הבא:
useEffect(() => {
if (index >= secretSequence.length) return;
const timer = setTimeout(
() => {
setIndex((i) => i + 1);
},
index % 2 === 0 ? 600 : 200
);
return () => {
clearTimeout(timer);
};
}, [index, secretSequence]);
function cx(squareIndex) {
return `colored-box ${colors[squareIndex - 1]} ${
secretSequence[index] === squareIndex ? "active" : ""
} `;
}
اکنون در دسترس! پژوهش تلگرام ۲۰۲۵ — مهمترین بینشهای سال 
