ToCode
Open in Telegram
1 419
Subscribers
No data24 hours
No data7 days
+130 days
Posts Archive
1 419
# מה האורך של דגל ישראל ב 2022
למרות שעשינו התקדמות משמעותית בכל מה שקשור לטיפול ביוניקוד בשפות תכנות, פוסט שעלה לאחרונה לרדיט הזכיר לי שלא הכל ורוד ושגם ב 2022 רוב שפות התכנות שנעבוד איתן ידרשו טיפול מיוחד בשביל לחשב נכון אורך של מחרוזת. הנה סקירה זריזה שעשיתי בעקבות אותו פוסט כדי להבין מה האורך של דגל ישראל בשפות תכנות שונות שאני מכיר.
## ג'אווהסקריפט
אז מילה של הקדמה - כשאני שואל על דגל ישראל אני בעצם מדבר על האימוג'י של הדגל שאתם מכירים מהווטסאפ. ב HTML אפשר להדפיס אותו בעמוד עם הקוד:
<span>🇮🇱</span>
כמו הרבה דגלים הוא מורכב משני Unicode Codepoints, הראשון מייצג את "סימן אזורי I" והשני "סימן אזורי L". בני אדם שרואים את האימוג'י הזה מתיחסים אליו בתור תו אחד, ובשפה מקצועית זה נקרא Grapheme אחד. הרבה אימוג'ים אחרים (שאינם דגלים) מיוצגים על ידי Codepoint יחיד, ומה שמעניין בדגלים זה שהם מראים לנו את ההבדל בין Codepoints ל Graphemes. קודפוינט ביוניקוד זה פשוט תו יוניקודי יחיד, וגרפמה זה מה שאנחנו כבני אדם תופסים בתור תו אחד.
שפת התכנות הראשונה שרציתי לבדוק איך היא מטפלת בגרפמות היא ג'אווהסקריפט פשוט כי קוד JavaScript נראה לי כמו משהו שבסבירות גבוהה יפגוש אימוג'ים. אז כתבתי את התוכנית הבאה:
console.log("🇮🇱".length);
וניסיתי להריץ אותה גם ב node וגם בכרום. התוצאה בשני המקומות היתה זהה - המספר 4.
כשניסיתי לשאול את גוגל אם יש דרך לספור גרפמות ב JavaScript הוא לא ידע להמליץ לי על דרך מובנית בשפה, אבל כן שלח אותי לספריה הזאת:
https://github.com/orling/grapheme-splitter שנראה שיודעת לספור כמו שצריך.
## פייתון
בפייתון המצב קצת יותר טוב. התוכנית שכתבתי נראית כך:
print(len("🇮🇱"))
והפעם התוצאה היתה 2. עדיין לא מדויק אבל לפחות קצת יותר קרוב לאורך האמיתי.
גם כאן גוגל לא ידע להציע לי דרך נוחה ומובנית בשפה לספור גרפמות, ובמקום שלח אותי לספריה הזאת https://pypi.org/project/grapheme/ שנראה שמצליחה להתמודד עם האתגר.
## רובי
גם ברובי הניסיון הראשון שלי לקבל את האורך החזיר 2:
puts "🇮🇱".length
אבל הפעם גוגל כבר ידע לספר שאני פשוט לא מפעיל את הפונקציה הנכונה, והדרך האמיתית לספור גרפמות במחרוזת ברובי היא פשוט:
puts "🇮🇱".grapheme_clusters.length
וזה כבר מחזיר את התוצאה הנכונה - כלומר 1.
## פרל
בשביל לסיים את סבב "שפות הסקריפטים" חשבתי לנסות לראות מה המצב בפרל. נכון, מזמן לא כתבתי בה, אבל זה כמו לרכב על אופניים או משהו. בקיצור הגירסה הראשונה שכתבתי בפרל המשיכה את המסורת של פייתון ורובי והחזירה 2:
use v5.30;
use utf8;
say(length("🇮🇱"));
אבל בניגוד לפייתון, בפרל מודול הביטויים הרגולאריים מספיק משוכלל כדי לספור גרפמות עם התו המיוחד \X (להגנת פייתון צריך להגיד שיש ספריה חיצונית בשם regex שכן מספקת את ההתנהגות הזאת גם שם). בכל מקרה הקוד הזה בפרל כבר הדפיס את התוצאה הנכונה:
my $str = "🇮🇱";
my $count = 0;
while ($str =~ /\X/g) { $count++ }
say($count);
קצת יותר ארוך מרובי אבל עדיין מספיק טוב בעיניי.
## אליקסיר
שתי השפות האחרונות לסקירה הן אלה שעושות את הדבר הנכון מההתחלה. באליקסיר, הפקודה String.length מחזירה את האורך בגרפמות, ולכן התוכנית הבאה מדפיסה 1:
String.length("🇮🇱")
|> IO.inspect
## סוויפט
ואותו המשחק קורה בסוויפט, כשהפונקציה count הרגילה לחישוב אורך מדפיסה את הדבר הנכון בלי לעשות עניינים:
print("🇮🇱".count)
אם נסכם - גם ב 2022 הרבה שפות תכנות פופולריות מכריחות אתכם להתקין ספריה חיצונית או להשתמש בקוד מיוחד כדי לספור כמו שצריך כמה תווים יש במחרוזת. קשה לי לראות את זה משתנה בעתיד הקרוב מטעמי תאימות אחורה ולכן האחריות בידיים שלנו. שימו לב באיזו שפה אתם עובדים ואיזה טיפול מיוחד נדרש בעבודה עם מחרוזות.1 419
https://github.com/axios/axios/issues/1227.
מעבר ל Fetch יאפשר להפעיל את הפרמטר maxRedirects גם בדפדפן.
2. כרגע אין בדיקות ל adapter של xhr. הספריה MSW מאפשרת לבנות שרת http קטן מתוך הדפדפן. באמצעותה אפשר לבנות בדיקות לקוד התקשורת שירוצו גם בדפדפן וגם ב Node.JS, ויבדקו את שני ה adapters במכה אחת.
3. כרגע כל adapter מכיל את קוד התקשורת המלא שהוא צריך, מה שגורם לכפל קוד ביניהם לדוגמה בטיפול בביטול בקשה. אפשר לארגן אחרת את הקוד כדי לבטל את כפל הקוד הזה.
עד לפה סקירת הארכיטקטורה המרכזית של axios. ברור שיש עוד הרבה קוד לחזור אליו בספריה הזו, ואני מקווה שסקירה זו נתנה לכם את הכלים להתמצא טוב יותר בספריה ולהמשיך לצלול למנגנון שלה שמעניינים אתכם.
1 419
config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
}
}
והקטע הזה מטפל בביטול בקשה בקובץ ה adapter השני, הקובץ adapters/http.js:
if (config.cancelToken || config.signal) {
// Handle cancellation
// eslint-disable-next-line func-names
onCanceled = function(cancel) {
if (req.aborted) return;
req.abort();
reject(!cancel || (cancel && cancel.type) ? new Cancel('canceled') : cancel);
};
config.cancelToken && config.cancelToken.subscribe(onCanceled);
if (config.signal) {
config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
}
}
ארגון מחדש של הקוד בשני ה adapters היה יכול לחסוך חלק גדול מהכפילות, ואולי לעשות חיים יותר קלים למי שיבוא לבנות adapter חדש.
## ארגז החול ותיקיית הדוגמאות
שתי התיקיות הבאות הן כבר לא סטנדרטיות בפרויקטי קוד פתוח. התיקייה examples כוללת קובץ בשם server.js, שאם תריצו אותו תקבלו שרת שיודע להגיש 6 דוגמאות שונות (מסודרות יפה בתוך תיקיות בתוך examples), כל דוגמה מספרת משהו על איך להשתמש ב axios. לדוגמה הקובץ examples/get/index.html מראה לנו איך להשתמש ב axios כדי לקבל ב Ajax רשימת פריטים מהשרת ולשתול אותה בתוך קובץ html:
<!doctype html>
<html>
<head>
<title>axios - get example</title>
<link rel="stylesheet" type="text/css" href="//maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"/>
</head>
<body class="container">
<h1>axios.get</h1>
<ul id="people" class="list-unstyled"></ul>
<script src="/axios.min.js"></script>
<script>
axios.get('/get/server')
.then(function (response) {
document.getElementById('people').innerHTML = response.data.map(function (person) {
return (
'<li class="row">' +
'<img src="https://avatars.githubusercontent.com/u/' + person.avatar + '?s=50" class="col-md-1"/>' +
'<div class="col-md-3">' +
'<strong>' + person.name + '</strong>' +
'<div>Github: <a href="https://github.com/' + person.github + '" target="_blank">' + person.github + '</a></div>' +
'<div>Twitter: <a href="https://twitter.com/' + person.twitter + '" target="_blank">' + person.twitter + '</a></div>' +
'</div>' +
'</li><br/>'
);
}).join('');
})
.catch(function (err) {
document.getElementById('people').innerHTML = '<li class="text-danger">' + err.message + '</li>';
});
</script>
</body>
</html>
השימוש העיקרי בתיקיית הדוגמאות לדעתי היה אמור להיות לאפשר לאנשים לפתוח את הקוד מתוך אתר עריכת קוד אונליין כמו gitpod (ככה לפחות הם מספרים בתיעוד) ואז בלחיצה אחת אפשר לראות את axios בפעולה בתוך קובץ HTML עם קוד צד שרת לידה. במציאות בשביל לגשת לתיקיית הדוגמאות בגיטפוד צריך להיכנס לקישור גיטפוד שלהם כאן:
https://gitpod.io/#https://github.com/axios/axios/blob/master/examples/server.js
לחכות שהפרויקט ייבנה ואז ללחוץ על הכפתור של חץ-קטן-בתוך-ריבוע כדי לפתוח את התוצאה בחלון חדש, אחרת קובץ ה server.js שלהם מחזיר שגיאת 404.
בתיקיית sandbox אנחנו יכולים למצוא קובץ HTML ושרת שמגיש אותו שמאפשר הרצה של axios מתוך טופס.
לדעתי שתי התיקיות examples ו sandbox לא מועילות במיוחד והיה אפשר לוותר עליהן.
## רעיונות לתרומות קוד
אם הגעתם עד כאן בקריאה אתם כבר מבינים דבר או שניים על איך עובדת הספריה axios. אני רוצה לסיים את הסקירה בכמה הצעות שאתם יכולים לממש בשביל ללמוד עוד על הספריה ובסוף גם לשפר אותה:
1. כרגע יש לאקסיוס adapters עבור XMLHttpRequest ועבור Node.JS. דפדפנים כבר כוללים API חדש שנקרא Fetch API שמאפשר עוד כמה יכולות בנוסף ל xhr. אפשר להוסיף Adapter עבור Fetch API.
תוספת כזאת יכולה לתת עוד יכולות לספריה, למשל היום הקוד של xhr לא תומך בהגבלת מספר ה Redirects כמו שמופיע ב Issue הזה:1 419
axios.Axios = Axios;חוץ ממנה אין הרבה לוגיקה בקובץ, ולכן התחנה הבאה שלי היא הקובץ
code/Axios שם מוגדרת המחלקה Axios. זה כבר קובץ הרבה יותר מעניין שכמעט מגיע ל 150 שורות. הפונקציה המרכזית בו נקראת request. היא קצת ארוכה אז אני מדביק כאן רק את החלק שלה שקשור לשליחת בקשה:
var chain = [dispatchRequest, undefined];
Array.prototype.unshift.apply(chain, requestInterceptorChain);
chain = chain.concat(responseInterceptorChain);
promise = Promise.resolve(config);
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
אז יש פה שלוש שורות ראשונות קצת מבלבלות כשהם מכינים מערך בשם chain, אבל מה שחשוב הוא הפונקציה dispatchRequest שאחראית על שליחת הבקשה בפועל. הבעיה שההגדרה שלה עדיין לא כאן. אני ממשיך לקובץ dispatchRequest.js באותה תיקיה וגם שם מוצא פונקציה ראשית קצת ארוכה עם הקוד הראשי הבא:
var adapter = config.adapter || defaults.adapter;
return adapter(config).then(function onAdapterResolution(response) {
throwIfCancellationRequested(config);
// Transform response data
response.data = transformData.call(
config,
response.data,
response.headers,
config.transformResponse
);
return response;
}, function onAdapterRejection(reason) {
אוקיי אז בשביל לשלוח בקשה אנחנו פונים לפונקציה בשם adapter, מעבירים את פרטי הבקשה ומקביל בחזרה אוביקט response שימשיך לעבור טרנספורמציות לפני שיוחזר לקוד שקרא לאקסיוס. הטרנספורמציות הן כנראה פיענוח ה JSON ודברים נוספים בסגנון, אבל מי זה ה adapter?
בגלל שהוא מוגדר בתוך config ולא מיובא מבחוץ אני קצת תקוע. לכן אני מפעיל חיפוש על כל הקבצים בתיקיה:
~/tmp/axios/lib ╱ master !5 ack -l adapter
core/README.md
core/dispatchRequest.js
core/mergeConfig.js
adapters/README.md
defaults.js
ואחרי חיטוט מהיר בכולם מגיע ל defaults.js ושם לפונקציה:
function getDefaultAdapter() {
var adapter;
if (typeof XMLHttpRequest !== 'undefined') {
// For browsers use XHR adapter
adapter = require('./adapters/xhr');
} else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
// For node use HTTP adapter
adapter = require('./adapters/http');
}
return adapter;
}
אוקיי עכשיו אני מבין! אז הם בעצם מסתכלים אם אני בדפדפן (כלומר אם יש לי XMLHttpRequest) כדי להשתמש ב adapter בשם xhr, שכנראה עוטף את XMLHttpRequest, או אם אני ב node ואז ילכו על adapter בשם http, שכנראה נותן עטיפה דומה למנגנון התקשורת המובנה של node.
## תיקיית adapters
כבר ראינו שבתיקיית test/unit/adapters יש קובץ בשם http.js שבודק את קוד התקשורת, ועכשיו אנחנו רואים שיש לו גם מקבילה בתיקיית המקור: הקובץ lib/adapters/http.js. קובץ זה עוטף את מנגנון התקשורת הפנימי של node למבנה של Axios Adapter.
החבר השני בתיקיה הוא הקובץ xhr.js. לקובץ זה אין קובץ בדיקות, וזה הגיוני. אם אני עושה Mocking לכל קוד התקשורת בדפדפן, אז אני לא באמת יכול לבדוק את הקוד שאחראי על התקשורת. זה באמת היה נכון בעבר, אבל היום כבר אפשר להשתמש בספריה כמו Mock Service Worker כדי לבנות מיני שרת ווב בתוך הדפדפן ולבדוק חלק גדול מקוד התקשורת.
מבחינת הקוד בקובץ lib/adapters/xhr.js אין הפתעות גדולות. אנחנו מוצאים בתחילת הפונקציה את:
var request = new XMLHttpRequest();
ובהמשך הגדרת כל המאפיינים של אותה request עד הסיום עם:
request.send(requestData);
מה שכן כדאי לשים לב בבחירה לבנות שני קבצים שונים עבור התקשורת הוא כפל הקוד שיש בין שני הקבצים. הקטע הבא מטפל בביטול בקשה בקובץ xhr.js:
if (config.cancelToken || config.signal) {
// Handle cancellation
// eslint-disable-next-line func-names
onCanceled = function(cancel) {
if (!request) {
return;
}
reject(!cancel || (cancel && cancel.type) ? new Cancel('canceled') : cancel);
request.abort();
request = null;
};
config.cancelToken && config.cancelToken.subscribe(onCanceled);
if (config.signal) {1 419
אני מאוד אהבתי את סגנון הבדיקה המדויק - יוצרים אוביקט, פותחים שרת שיחזיר אותו עם Content-Type של application/json, ואז בודקים שכשנפנה לאותו שרת באמת נקבל את האוביקט מפוענח. בגלל שהבדיקות האלה רצות בצד שרת מאוד קל להרים שרת כדי לבדוק את כל התקשורת. בצד הדפדפן הם יצטרכו לעבוד קצת יותר קשה בשביל לקבל תוצאה דומה.
## בדיקות קארמה
תיקיית הבדיקות השניה,
specs, היא גם הגדולה יותר וכנראה שכאן הושקעו עיקר המאמצים בכתיבת הבדיקות. יש בדיקות מכל הסוגים לדוגמה הקובץ test/specs/core/buildFullPath.spec.js בודק פונקציה טהורה אחת בשם buildFullPath:
var buildFullPath = require('../../../lib/core/buildFullPath');
describe('helpers::buildFullPath', function () {
it('should combine URLs when the requestedURL is relative', function () {
expect(buildFullPath('https://api.github.com', '/users')).toBe('https://api.github.com/users');
});
it('should return the requestedURL when it is absolute', function () {
expect(buildFullPath('https://api.github.com', 'https://api.example.com/users')).toBe('https://api.example.com/users');
});
it('should not combine URLs when the baseURL is not configured', function () {
expect(buildFullPath(undefined, '/users')).toBe('/users');
});
it('should combine URLs when the baseURL and requestedURL are relative', function () {
expect(buildFullPath('/api', '/users')).toBe('/api/users');
});
});
כשמגיעים לבדיקות התקשורת עצמה הבדיקה משתמשת ב Mocks כדי לדמות את התקשורת ולוודא שהקוד שולח בקשות ומפענח תשובות כמו שצריך. לדוגמה הבדיקה הבאה מתוך הקובץ test/specs/instance.spec.js:
beforeEach(function () {
jasmine.Ajax.install();
});
afterEach(function () {
jasmine.Ajax.uninstall();
});
it('should make an http request with url instead of baseURL', function (done) {
var instance = axios.create({
url: 'https://api.example.com'
});
instance('/foo');
getAjaxRequest().then(function (request) {
expect(request.url).toBe('/foo');
done();
});
});
הבדיקה משתמשת במנגנון הריגול של jasmine כדי לתפוס בקשות Ajax ולהחליף אותן ב Mocks שאפשר יהיה לתשאל.
עכשיו גם יהיה זמן טוב להציץ לתיקיית lib כדי לראות את מבנה תיקיות קבצי המקור, ונוכל לשים לב שמבנה תיקיית הבדיקה דומה מאוד למבנה תיקיית המקור. התיקיות core, cancel ו helpers נמצאות שלושתן גם בתיקיית המקור וגם בתיקיית הבדיקות. גם מבחינת הקבצים יש דמיון, לדוגמה הקובץ test/specs/defaults.spec.js כנראה בודק את הקוד שנמצא בקובץ lib/defaults.js. ההתאמה אינה אחד לאחד ויש הרבה קבצי בדיקות שאין להן קובץ מקור מתאים, וגם להיפך הרבה קבצי מקור בלי קובץ בדיקות מתאים.
היכולת לשנות קצת קוד ואז להריץ את הבדיקות כדי לראות שלא שברנו כלום היא קריטית בפרויקט קוד פתוח שרוצה לקבל תרומות ממתכנתים בכל העולם. כשאני מסתכל על פרויקט כמו axios הבדיקות עוזרות לי להרגיש בטוח לפני שאני מוסיף פיצ'ר ומייצר Pull Request, כי הן מורידות את הסיכוי שאשבור משהו בלי לשים לב.
## איך אקסיוס מוציא בקשות http
אם הבדיקות מחולקות לקוד Node.JS וקוד דפדפן - כנראה שגם הקוד עצמו של אקסיוס בנוי כך. ואחרי שאנחנו יודעים להריץ את הבדיקות ולבנות את הפרויקט אנחנו במקום טוב להיכנס לתיקיית lib עצמה כדי לענות על השאלה הכי מעניינת לגבי אקסיוס - איך הוא שולח בקשות?
כשאקסיוס רק נולד עדיין לא היה fetch API בדפדפנים, וכשרצינו להוציא בקשות רשת היה צריך להכיר תחביר קצת מסורבל של XMLHttpRequest - היית צריך ליצור אחד, להגיד לו לאן ללכת ואיזה פונקציה הוא צריך להפעיל כשיסיים ולשלוח אותו לדרכו. רוב המתכנתים השתמשו ב jQuery כדי לעטוף את XMLHttpRequest בתחביר יותר ידידותי, ובעצם כשמאט זברינסקי נכנס לסצינה הוא מבין שאם אתה משתמש בריאקט אז אתה לא צריך jQuery, ואנשים השאירו את jQuery רק בשביל אותה עטיפה קטנה ל XMLHttpRequest. בעצם ב jQuery יכולת לכתוב:
$.get('/some/url', function(data) {
console.log(data);
});
וזה פשוט עבד ופנה ל /some/url, הביא ממנו מידע בדרך כלל בצורת JSON ואז המשיך לבצע את הפונקציה בפרמטר השני עם המידע שקיבל.
נתחיל את המסלול שלנו באקסיוס בקובץ הראשי של הספריה lib/axios.js ושם אני מוצא את השורה:
// Expose Axios class to allow class inheritance1 419
כלומר מוקה מריץ את הבדיקות בתיקיית
test/unit וקארמה מריץ את הבדיקות בתיקיית test/specs. קארמה משתמש בדפדפנים ולכן בודק את החלק של הדפדפן בקוד של אקסיוס, ומוקה רץ בתוך node.js ולכן בודק את ההרצות מתוך יישום node.
לפני הכניסה לתיקיות הבדיקות ננסה לבנות את הפרויקט כדי לראות אם החשש שלי מ dist היה מוצדק. אני מפעיל:
$ npm run build
> axios@0.25.0 build
> NODE_ENV=production grunt build
Running "clean:dist" (clean) task
>> 5 paths cleaned.
Running "webpack" task
Hash: d004b5ae618258baef57
Version: webpack 4.46.0 / grunt-webpack 4.0.3
Time: 275ms
Built at: 01/28/2022 10:40:45 AM
Asset Size Chunks Chunk Names
axios.js 62.3 KiB main [emitted] main
axios.map 67.6 KiB main [emitted] [dev] main
Entrypoint main = axios.js axios.map
[./index.js] 40 bytes {main} [built]
[./lib/adapters/xhr.js] 6.79 KiB {main} [built]
[./lib/axios.js] 1.52 KiB {main} [built]
[./lib/cancel/Cancel.js] 385 bytes {main} [built]
[./lib/cancel/CancelToken.js] 2.41 KiB {main} [built]
[./lib/cancel/isCancel.js] 102 bytes {main} [built]
[./lib/core/Axios.js] 4.14 KiB {main} [built]
[./lib/core/InterceptorManager.js] 1.33 KiB {main} [built]
[./lib/core/mergeConfig.js] 3.12 KiB {main} [built]
[./lib/defaults.js] 3.5 KiB {main} [built]
[./lib/env/data.js] 43 bytes {main} [built]
[./lib/helpers/bind.js] 256 bytes {main} [built]
[./lib/helpers/isAxiosError.js] 373 bytes {main} [built]
[./lib/helpers/spread.js] 564 bytes {main} [built]
[./lib/utils.js] 8.65 KiB {main} [built]
+ 14 hidden modules
Hash: 298941b4c4e5b7c2fd49
Version: webpack 4.46.0 / grunt-webpack 4.0.3
Time: 196ms
Built at: 01/28/2022 10:40:45 AM
Asset Size Chunks Chunk Names
axios.min.js 17.3 KiB 0 [emitted] main
axios.min.map 79.3 KiB 0 [emitted] [dev] main
Entrypoint main = axios.min.js axios.min.map
[0] ./lib/utils.js 8.65 KiB {0} [built]
[1] ./lib/defaults.js 3.5 KiB {0} [built]
[2] ./lib/cancel/Cancel.js 385 bytes {0} [built]
[3] ./lib/helpers/bind.js 256 bytes {0} [built]
[4] ./lib/helpers/buildURL.js 1.61 KiB {0} [built]
[5] ./lib/core/enhanceError.js 1.11 KiB {0} [built]
[8] ./lib/cancel/isCancel.js 102 bytes {0} [built]
[9] ./lib/core/mergeConfig.js 3.12 KiB {0} [built]
[10] ./lib/env/data.js 43 bytes {0} [built]
[11] ./index.js 40 bytes {0} [built]
[12] ./lib/axios.js 1.52 KiB {0} [built]
[13] ./lib/core/Axios.js 4.14 KiB {0} [built]
[26] ./lib/cancel/CancelToken.js 2.41 KiB {0} [built]
[27] ./lib/helpers/spread.js 564 bytes {0} [built]
[28] ./lib/helpers/isAxiosError.js 373 bytes {0} [built]
+ 14 hidden modules
Done.
עושה רושם שזה הצליח. ועכשיו נשווה עם מה ששמור בגיט:
~/tmp/axios ╱ master !5 PAGER=cat git diff --name-only -- dist
dist/axios.js
dist/axios.map
dist/axios.min.js
dist/axios.min.map
ואנחנו מגיעים לבעיה הראשונה עם מבנה התיקיות: אם שמים בריפו תיקיית dist היא חייבת להתאים למה שאני אקבל מבניית הפרויקט.
## בדיקות מוקה
נמשיך לתיקיות הבדיקות ותיקיית הבדיקות הראשונה שאני רוצה לקרוא היא התיקיה test/unit. אני כבר יודע שאלה בדיקות שרצות בתוך node.js בלבד באמצעות מוקה. הקובץ המרכזי כאן הוא unit/adapters/http.js, קובץ שכולל מעל אלף שורות של בדיקות. הנה בדיקה אחת לדוגמה ממנו:
it('should allow passing JSON', function (done) {
var data = {
firstName: 'Fred',
lastName: 'Flintstone',
emailAddr: 'fred@example.com'
};
server = http.createServer(function (req, res) {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(data));
}).listen(4444, function () {
axios.get('http://localhost:4444/').then(function (res) {
assert.deepEqual(res.data, data);
done();
});
});
});1 419
# קריאה מודרכת בקוד של Axios
אקסיוס החלה כפרויקט צד של מתכנת בשם מאט זבריסקי. הוא בדיוק גילה את ריאקט ומאוד התלהב ואז באיזה ערב אחרי כנס JavaScript הוא נשאר במלון והתחיל לכתוב ספריית http לריאקט. מהר מאוד הוא הבין שהספריה שלו לא באמת קשורה רק לריאקט ו axios הפכה לספריית תקשורת גנרית לדפדפנים ול Node.JS.
היום מאט כבר כמה שנים עובד באפל ומי שמתחזק את Axios הוא מתכנת אחר בשם ג'ייסון סיימן. אקסיוס מקבל תרומות קוד ממאות מתכנתים בעולם ועומד על מעל 24 מיליון הורדות בשבוע ב npm.
בפוסט היום אסתכל על הארכיטקטורה ומבנה הפרויקט וגם אחשוב על רעיונות לתרומות קוד אפשריות.
## מבנה התיקיות
כל הקוד של הפרויקט יושב בתיקיה בשם lib, אבל לפני שניכנס אליה מעניין לראות מה יש בכל התיקיות האחרות בריפו:
1. הספריה הראשונה שקופצת מול העיניים היא test. אני אוהב פרויקטים עם בדיקות כי הבדיקות הן צעד ביניים בין התיעוד לקוד עצמו. הן גם מספרות לא מעט על הקוד, וגם מאפשרות להריץ את הקוד בסביבה מבוקרת.
2. לאקסיוס, חוץ מתיקיית הבדיקות הרגילה test יש עוד שתי תיקיות בשם sandbox ו examples. מישהו מאוד התאמץ לעשות חיים קלים למתכנתים אחרים שרוצים להצטרף ולשחק עם הקוד - וזה עוד סימן טוב.
3. הספריה dist קצת מטרידה. אנחנו מסתכלים על ריפו של קוד מקור, ו dist מכילה תוצאה של בניה. למה צריך גם את dist שם? למה לא לתת לאנשים לבנות לבד? ומה אם אני אבנה והתוצאה לא תהיה זהה למה ששמור ב dist? למה לא לבנות עם Github Action ולהעלות את התוצאה ל Release בגיטהאב?
4. התיקיה הנסתרת
.github כוללת קבצים לאינטרקציה עם גיטהאב. יש שם תבנית לפתיחת Issue ויותר מעניין הקובץ .github/workflows/ci.yml שמכיל את ההוראות ל Github Action שמריץ את הבדיקות.
אחרי סקירת עץ התיקיות הצעד המתבקש הבא הוא לנסות להריץ את הבדיקות ולנסות לבנות את הפרויקט כדי לראות קודם כל שכל הבדיקות עוברות ושהבניה של גירסה נקיה מגיטהאב תייצר בדיוק את מה שכבר קיים בתיקיית dist.
אני מפעיל:
$ npm install
כדי להתקין את כל התלויות, ופותח את package.json כדי לראות איזה סקריפטים מחכים שם:
"scripts": {
"test": "grunt test && dtslint",
"start": "node ./sandbox/server.js",
"build": "NODE_ENV=production grunt build",
"preversion": "grunt version && npm test",
"version": "npm run build && git add -A dist && git add CHANGELOG.md bower.json package.json",
"postversion": "git push && git push --tags",
"examples": "node ./examples/server.js",
"coveralls": "cat coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js",
"fix": "eslint --fix lib/**/*.js"
},
ושוב אני רואה שהפרויקט מאוד ידידותי למתכנתים חדשים. נכון הם משתמשים ב grunt אבל אולי נצליח להתחמק מקריאת ה Gruntfile. הרצת הבדיקות עם:
$ npm run test
עובדת ואני שם לב שמריצה שני סוגים של בדיקות - הבלוק הראשון הוא בלוק של בדיקות mocha שרצות בתוך node.js, והבלוק השני הוא בלוק של בדיקות karma שרצות בתוך דפדפנים. הוא אפילו כותב לי איזה דפדפנים הוא הריץ:
28 01 2022 10:34:29.443:INFO [karma-server]: Karma v6.3.11 server started at http://localhost:9876/
28 01 2022 10:34:29.444:INFO [launcher]: Launching browsers FirefoxHeadless, ChromeHeadless with concurrency unlimited
והנה הרגע שחששתי ממנו - בגלל שיש שני סוגי בדיקות ושני כלים שונים שמריצים אותן, ואני רוצה לדעת איזה בדיקות קשורות לאיזה כלי, אני נאלץ לפתוח את ה Gruntfile. הבלוק שמעניין אותנו ממנו הוא:
karma: {
options: {
configFile: 'karma.conf.js'
},
single: {
singleRun: true
},
continuous: {
singleRun: false
}
},
mochaTest: {
test: {
src: ['test/unit/**/*.js']
},
options: {
timeout: 30000
}
},
אוקיי אז מוקה מריץ את הבדיקות בתיקיית test/unit וקארמה מריץ ... אה יש לו גם קובץ קונפיגורציה משלו. נמשיך לפתוח את karma.conf.js ושם נמצא את הבלוק:
preprocessors: {
'test/specs/__helpers.js': ['webpack', 'sourcemap'],
'test/specs/**/*.spec.js': ['webpack', 'sourcemap']
},1 419
# כל הבדיקות שבעולם
אם כתבת קומפוננטת ריאקט וקובץ בדיקות עבורה, ועדיין יש באגים, זה אומר ש-
1. לא כתבת בדיקות מספיק טובות?
2. לא כתבת מספיק בדיקות?
3. יש באגים שבדיקות לא יכולות לאתר?
4. כתיבת בדיקות לא שווה את הזמן שלך?
אף תשובה כאן היא לא אמת מוחלטת. אלה בסך הכל 4 גישות. מי שבוחר בגישה הראשונה ימשיך לשפר את הבדיקות עם כל באג עד שילמד לכתוב בדיקות טובות יותר; מי שבוחר בגישה השניה כותב עוד ועוד בדיקות עד שיצליח לתפוס את כל המקרים; מי שלוקח את הגישה השלישית יעבור מהר מבדיקה ב jest לבדיקה בדפדפן אמיתי או אפילו בפרודקשן; ומי שבוחר את הגישה הרביעית כמעט ולא ישקיע מראש זמן בכתיבת בדיקות או בבניית התשתית.
וכן, אם התשובה שלכם לא מובילה לכתיבת הקוד שאתם רוצים מותר לנסות גם גישה אחרת.
1 419
# היום למדתי: משתנים גלובאליים ב node.js
פיצ'ר שאף פעם לא חשבתי שאצטרך ב Node.JS הוא המשתנה המיוחד global. כמו window בדפדפן, global הוא האוביקט ש node הולך אליו כשפניתם למשתנה אבל node לא מצליח למצוא את המשתנה הזה בשום מקום. על global אנחנו נוכל למצוא את הדברים הגלובאליים של נוד כמו setTimeout, clearTimeout וכמובן גם את global עצמו.
בעזרת global אנחנו יכולים לשתול משתנה גלובאלי בקובץ אחר. לדוגמה נניח שיש לי קובץ בשם b.js עם התוכן הבא:
console.log(foo);
אז ברור שאם ננסה להריץ ישירות את b.js נקבל שגיאה כי foo אינו משתנה מוגדר. אבל במקום להריץ את b.js אני יכול לכתוב קובץ חדש בשם a.js לדוגמה עם התוכן הבא:
global.foo = 10;
const b = require('./b.js');
ועכשיו כשאני מריץ את a.js הוא יטען את b.js ו b כבר ימצא את המשתנה foo כי הוא הוגדר על global בקובץ a.
נ.ב. מתי נרצה להשתמש ב global אתם שואלים? בגדול אף פעם. בקטן דמיינו שאתם צריכים להריץ בדיקות עם react-testing-library בתוך סביבת node.js, למשל ב jest או mocha. דמיינו גם שהיישום שלכם טוען איזה קובץ שמותאם לריצה בדפדפן כי הוא משתמש באיזשהו אוביקט גלובאלי כמו window, fetch או IntersectionObserver (כן יש דבר כזה). עכשיו בשביל להריץ את הבדיקה על היישום שלכם אתם צריכים שאותו קובץ יצליח למצוא את ה window או ה fetch הגלובאליים.
הספריה node-window-polyfill מספקת דוגמה לשימוש כזה ב global כדי לייצר משתנה window שכולם יכירו. הנה סניפט ממנה שממחיש את הנקודה:
var globalObject = global;
var registerWindow = function () {
globalObject.window = globalObject.window || {};
exports.registerWindowProperties();
};
אחרי import לספריה זו אפשר להתחיל לטעון קבצי קוד שנכתבו לדפדפן ומחפשים אוביקט window, כי הם ימצאו את האוביקט עם כל המאפיינים שהם ציפו להם.1 419
# שני שימושים הגיוניים ל git stash
הרבה זמן לא הבנתי את git stash. הרי למה לזרוק את כל השינויים שלי ל stash כשאני יכול לעשות להם קומיט ל branch זמני חדש? מה אני מרוויח מלשים אותם במקום שבו רק יהיה לי יותר קשה למצוא אותם?
אז אם גם אתם לא מהמשתמשים הכבדים של גיט סטאש הנה שני מצבים שבהם כן כדאי להכיר אותו ולהשתמש בו-
## אם בפרויקט מוגדר commit hook
אני יודע, קומיט הוקס הם חשובים כדי לוודא שהקוד עובד כמו שצריך או כל מיני דברים כאלה, אבל לפעמים הם יכולים להיות מאוד מציקים - למשל אם יש לי קומיט הוק שמוודא שהקוד עובר eslint, ואצלי הקוד עדיין לא עובר eslint אבל בכל זאת צריך לשים אותו בצד, אז אני כבר לא יכול לעשות קומיט ל branch זמני וללכת לעשות דברים אחרים כי ה commit hook יחסום אותי.
במצבים כאלה stash הוא פיתרון מעולה כי הוא מדלג על pre-commit hook. אנחנו זורקים את השינויים ל stash עם:
$ git stash -u
מקבלים תיקיית עבודה נקיה, יכולים לתקן באג או לעבוד על פיצ'ר אחר, וכשרוצים לחזור לאיפה שהיינו מפעילים:
$ git stash pop --index
המתג -u גורם לשליחת כל הקבצים ל stash כולל אלה שעדיין לא התווספו לגיט, והמתג --index אחרי pop גורם לשיחזור גם של ה Staging Area כך שאם עשיתם add לכמה קבצים אז אחרי החזרה מה stash לא תצטרכו לעשות להם add שוב.
## אם יש לי קובץ גדול בתיקיה
אחת הבעיות עם קומיטים היא שהם נשמרים לנצח. אז אם לקחתי כמה שינויים ויצרתי מהם קומיט חדש בענף חדש (זמני תכף אמחק אותו), ואני ממזג את הענף החדש ל main כדי להמשיך לעבוד, אז כל הקבצים שהיו באיחסון ה"זמני" יישארו במאגר הגיט לנצח.
במילים אחרות נניח שסטטוס נותן לי את הסיטואציה הבאה:
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: src/App.js
Untracked files:
(use "git add <file>..." to include in what will be committed)
buggy-data.db
הקובץ buggy-data.db מייצג dump של בסיס הנתונים עם מידע שגורם לבאג במערכת שלי ואני מנסה לתקן אותו. זה קובץ בינארי גדול ואני צריך אותו רק בשביל הבדיקות והתיקונים. באמצע העבודה אני צריך לעבור לעבוד על פיצ'ר או באג אחר.
אם אני עכשיו יוצר קומיט חדש בענף חדש לצורך התיקון ומכניס אליו גם את buggy-data.db, קובץ זה יידחף לשרת המרכזי בפעם הבאה שאעשה push ויישמר במאגר הגיט לנצח. אם אני מוותר על buggy-data.db בקומיט הזמני ומשאיר אותו פשוט בתיקיית העבודה הוא עלול להפריע לי בתיקון הבאג בענף החדש. אני יכול להכניס אותו ל .gitignore אבל לא בטוח שאני רוצה לשנות את gitignore בשביל משהו כל כך זמני.
סטאש שוב מסתמן בתור הפיתרון הקל במצב הזה. מפעילים:
$ git stash -u
והקובץ הגדול נעלם לסטאש יחד עם השינוי שהתחלתי לעשות. הפעלה עתידית של git push לא תשלח את הקובץ הגדול למאגר המרכזי ובהפעלה עתידית של gc הוא יימחק מתיקיית .git, או אם רוצים מיד אחרי ה git stash pop למחוק אותו אפשר להריץ:
git -c gc.reflogExpire=0 -c gc.reflogExpireUnreachable=0 -c gc.rerereresolved=0 -c gc.rerereunresolved=0 -c gc.pruneExpire=now gc
מכירים עוד מקרים שבהם git stash הוא הפיתרון הטוב ביותר? מוזמנים לשתף בתגובות.1 419
אחרי שבדקנו את כל החלקים הגנריים ביישום אנחנו יכולים להמשיך לבדיקת מסלולי קוד אמיתיים, וכאן כדאי להתמקד במסלולי קוד שדורשים הכנה מיוחדת או שקשים במיוחד לבדיקה. זיכרו שאנשי ה QA שלכם יודעים להשתמש בכלי בדיקות אוטומטיים כמו סלניום. מה שהם לא יודעים זה איך הקוד בנוי ומה עלול לשבור אותו, ולכן אם אתם יודעים שמנגנון מסוים משתמש ב Cookies אז אתם יכולים לכתוב בדיקה שמגדירה מראש ערכים מסוימים לאותם Cookies, אפילו ערכים לא הגיוניים, ולראות שהודעות השגיאה מוצגות כמו שצריך. אנשי ה QA לעולם לא יגיעו למסלול הזה כי הם בודקים רק דברים שמשתמשים יכולים להגיע אליהם בעבודה רגילה עם המערכת.
או אם אתם יודעים שהקוד שלכם מבצע דיאלוג עם קוד צד שרת אתם יכולים להשתמש ב mock כדי לגרום לשרת להחזיר קלטים מסוימים שלכם שאתם יודעים שיש איתם בעיה או לדחוף איטיות מזויפת לפני התשובה האמיתית מהשרת, ולוודא שהקומפוננטות שלכם מטפלות כמו שצריך בבעיות. אין טעם לבזבז את הזמן על דברים שאנשי ה QA ימצאו, אבל יש המון דברים יצירתיים שמשתמשים לעולם לא יגיעו אליהם במהלך עבודה תקין אבל הם כן יקרו מדי פעם וכדאי להיות מוכנים.
בדיקות נוספות ששווה להוסיף בקטגוריה זו אלה בדיקות שדורשות הרבה Data שאולי לא קיים בסביבת הבדיקות. אם בניתם קומפוננטה של טבלה תוכלו לבדוק איך היא מתנהגת אם צריך להציג עשרות אלפי שורות, אפילו שלרוב המשתמשים שלכם יהיו רק כמה מאות שורות.
1 419
# ארבעה סוגים של בדיקות שכדאי לכתוב
בשבוע שעבר הצעתי כאן כמה רעיונות לסוגי בדיקות שאתם יכולים לכתוב, ובתגובה לאותו פוסט כמה קוראים רצו לדעת לא רק איזה בדיקות אפשר לכתוב, אלא איזה בדיקות כדאי לכתוב. אז הנה הרשימה שלי של ארבעה סוגי בדיקות ליישומי ריאקט שנותנות הכי הרבה ערך למפתחים שכותבים אותן:
## בדיקת Utility Functions גנריות
כל פונקציה טהורה שאנחנו עושים לה export מאיזשהו קובץ תרוויח מכתיבת סט בדיקות מקיף בנוסף לתיעוד (ולפעמים אפילו במקומו). מבחינת העלות כתיבת בדיקות לפונקציה טהורה זה ממש פשוט כי היא מושפעת רק מהקלטים שהיא מקבלת; מבחינת התועלת פונקציה שעושים לה export זו פונקציה שמשתמשים בה מכמה מקומות במערכת וככל שיעבור הזמן ישתמשו בה מיותר מקומות. אלה בדיוק הפונקציות ששורדות הכי הרבה זמן בקוד, ושהכי מפחיד לשנות אותן כי אנחנו לא יודעים איזה Use Case מוזר אנחנו שוברים. סט בדיקות משמעותי על פונקציות כאלה יבטיח לנו שכל שינוי לוקח בחשבון את כל המקרים החשובים שהיו בעבר. מגדילי ראש גם יקפידו להוסיף בדיקות כל פעם שעושים שינוי בכזאת פונקציה, כדי לוודא ששום שינוי עתידי לא ידרוס את השינוי או התיקון שאנחנו מכניסים.
דוגמה טובה היא הספריה lodash שכוללת בנוסף לכל קבצי המקור גם תיקיית test שמכילה קובץ בדיקה שמתאים לכל קובץ מקור. רצוי לראות את הבדיקות שלהם כאן:
https://github.com/lodash/lodash/tree/master/test.
## בדיקת קומפוננטות גנריות
כשהמערכת מספיק גדולה לאט לאט יהיו לנו רכיבי UI שגם הם סוג של Utility Functions - לדוגמה רכיב של Dropdown, רכיב של שורה בטופס או אולי איזה Spinner מיוחד שמתאים בדיוק לעיצוב שלנו. בדיקה לקומפוננטות יותר קשה לכתוב מאשר בדיקה לפונקציה טהורה, אבל מאחר והקומפוננטות הן גנריות ומשתמשים בהן בהרבה מקומות במערכת הבדיקה שלהן עדיין שווה את ההשקעה.
כדאי לבדוק כמה שיותר קלטים וערכים שונים ל props של הקומפוננטה, ואם היא מסתכלת גם על context אז להעביר כמה שיותר אפשרויות לערכים שם. המטרה שלכם היא לבנות חומה סביב הממשק והפיצ'רים הנוכחיים כך שכשמשהו יישבר אתם תדעו לפני ה QA.
## בדיקת Custom Hooks
אנחנו עדיין באזור הגנרי שמשתמשים בו בהרבה מקומות במערכת אבל עכשיו המחיר עולה בעוד מדרגה. נכון, יש Custom Hooks מאוד פשוטים שכוללים רק לוגיקה וזה מהמם אם יש לכם כאלה, אבל הרבה Custom Hooks כוללים אינטרקציה עם רכיבים חיצוניים ושימוש באפקטים.
קחו לדוגמה את useCookies הוק שמאפשר לקומפוננטה לקרוא מידע מ Cookies. יש להם קובץ בדיקה די מקיף כאן: https://github.com/reactivestack/cookies/blob/master/packages/react-cookie/src/__tests__/useCookies-test.js. הנה דוגמה לבדיקה ממנו:
it('update when a cookie change', () => {
const cookies = new Cookies();
const node = document.createElement('div');
const toRender = (
<CookiesProvider cookies={cookies}>
<TestComponent />
</CookiesProvider>
);
act(() => {
cookies.set('test', 'big fat cat Pacman');
ReactDOM.render(toRender, node);
});
expect(node.innerHTML).toContain('big fat cat Pacman');
act(() => {
cookies.set('test', 'mean lean cat Suki');
ReactDOM.render(toRender, node);
});
expect(node.innerHTML).toContain('mean lean cat Suki');
});
זאת כבר בדיקה שיותר קשה לכתוב מאשר בדיקת קומפוננטה רגילה, וכמובן יותר קשה לכתוב מאשר בדיקה של Pure Function. למרות הקושי בדיקת Custom Hooks נותנת ערך כי זה משהו שמשתמשים בו בהרבה מקומות במערכת וכשיש עליו סט מקיף של בדיקות יחידה אנחנו מרגישים יותר בנוח לשנות אותו.
## מסלולי קוד שקשה להגיע אליהם אחרת1 419
> make the third argument work
מסתבר שבגירסה קודמת של הפונקציה זה הופיע כך:
ch || (ch = ' ');
רואים את הבעיה? אם עדיין לא תשמחו לדעת ש Steve Mao הוסיף גם בדיקה כדי להראות איפה בדיוק הקוד הקודם נכשל:
assert.strictEqual(leftpad(1, 2, 0), '01');
עכשיו זה ברור! ערך ברירת המחדל היה בשימוש בכל מצב בו ch היה false, וזה כולל את המצב בו הוא קיבל את הערך 0. השינוי של סטיב מאו הוא שאיפשר לפונקציה לטפל גם בריפוד מספרי ולקבל את הערך 0 בתור תו הריפוד משמאל.
סך הכל הגירסה העדכנית של הפונקציה היא מאוד מעניינת וכוללת גם טיפול במקרי קצה מבחינת פרמטרים, גם אופטימיזציה למקרים נפוצים באמצעות cache וגם אלגוריתם חכם שמצליח לעשות את אותה עבודה בפחות זמן מעבד. חבל רק שזמן קצר אחרי שפורסמה אותה פונקציה נכנסה בתור תוספת מובנית לדפדפנים וכעת אין צורך להשתמש בה כספריה חיצונית.
Available now! Telegram Research 2025 — the year's key insights 
