החל מגרסה 105 של Chromium, אפשר להתחיל בקשה לפני שכל הגוף זמין באמצעות Streams API.
תוכלו להשתמש בהם כדי:
- חימום השרת. במילים אחרות, אפשר להתחיל את הבקשה ברגע שהמשתמש מתמקד בשדה קלט טקסט, להסתיר את כל הכותרות ואז להמתין עד שהמשתמש ילחץ על 'שליחה' לפני שליחת הנתונים שהזין.
- שליחת נתונים שנוצרו בלקוח, כמו אודיו, וידאו או נתוני קלט, באופן הדרגתי.
- יוצרים מחדש את שקעי האינטרנט באמצעות HTTP/2 או HTTP/3.
אבל מכיוון שזו תכונה ברמה נמוכה בפלטפורמת האינטרנט, אל תתבססו רק על הרעיון שלי. אולי יש לכם תרחיש שימוש מרגש הרבה יותר להעברת בקשות בסטרימינג.
הדגמה (דמו)
כאן מוצג איך אפשר להעביר נתונים מהמשתמש לשרת בסטרימינג, ולשלוח נתונים בחזרה שאפשר לעבד בזמן אמת.
כן, אוקיי, זו לא הדוגמה הכי יצירתית, רק רציתי לשמור על פשטות, בסדר?
בכל מקרה, איך זה עובד?
בפרקים הקודמים של הרפתקאות האחזור המהנות
כבר זמן מה שידורי תגובות זמינים בכל הדפדפנים המודרניים. הם מאפשרים לכם לגשת לחלקים בתגובה כשהם מגיעים מהשרת:
const response = await fetch(url);
const reader = response.body.getReader();
while (true) {
const {value, done} = await reader.read();
if (done) break;
console.log('Received', value);
}
console.log('Response fully received');
כל value
הוא Uint8Array
בייטים.
מספר המערכים שתקבלו והגודל שלהם תלויים במהירות הרשת.
אם החיבור מהיר, תקבלו פחות 'קטעים' גדולים יותר של נתונים.
אם החיבור שלכם איטי, תקבלו יותר קטעים קטנים יותר.
אם רוצים להמיר את הבייטים לטקסט, אפשר להשתמש ב-TextDecoder
או ב-transform stream החדש יותר, אם דפדפני היעד תומכים בו:
const response = await fetch(url);
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
TextDecoderStream
הוא מקור נתונים לטראנספורמציה שמאחזר את כל קטעי ה-Uint8Array
האלה וממיר אותם למחרוזות.
זרמים הם כלי מצוין, כי אתם יכולים להתחיל לפעול על סמך הנתונים ברגע שהם מגיעים. לדוגמה, אם אתם מקבלים רשימה של 100 'תוצאות', אתם יכולים להציג את התוצאה הראשונה ברגע שאתם מקבלים אותה, במקום להמתין לקבלת כל 100 התוצאות.
אלה הנתונים של תגובות. הדבר החדש והמרגש שרציתי לדבר עליו הוא נתוני בקשות.
גופי בקשות בסטרימינג
לבקשות יכולים להיות גופים:
await fetch(url, {
method: 'POST',
body: requestBody,
});
בעבר, היה צריך להכין את כל גוף הבקשה לפני שאפשר היה להתחיל את הבקשה, אבל עכשיו ב-Chromium 105 אפשר לספק ReadableStream
משלכם של נתונים:
function wait(milliseconds) {
return new Promise(resolve => setTimeout(resolve, milliseconds));
}
const stream = new ReadableStream({
async start(controller) {
await wait(1000);
controller.enqueue('This ');
await wait(1000);
controller.enqueue('is ');
await wait(1000);
controller.enqueue('a ');
await wait(1000);
controller.enqueue('slow ');
await wait(1000);
controller.enqueue('request.');
controller.close();
},
}).pipeThrough(new TextEncoderStream());
fetch(url, {
method: 'POST',
headers: {'Content-Type': 'text/plain'},
body: stream,
duplex: 'half',
});
הקוד שלמעלה ישלח לשרת את המחרוזת "This is a slow request", מילה אחת בכל פעם, עם הפסקה של שנייה בין כל מילה.
כל מקטע של גוף הבקשה צריך להיות Uint8Array
בייטים, לכן אני משתמש ב-pipeThrough(new TextEncoderStream())
כדי לבצע את ההמרה בשבילי.
הגבלות
בקשות סטרימינג הן כלי חדש באינטרנט, ולכן הן כפופות למספר הגבלות:
Half duplex?
כדי לאפשר שימוש בסטרימינג בבקשה, צריך להגדיר את אפשרות הבקשה duplex
לערך 'half'
.
תכונה לא ידועה של HTTP (אם כי ההתנהגות הזו היא סטנדרטית או לא תלויה בשאלה למי שואלים) היא שאפשר להתחיל לקבל את התשובה בזמן שעדיין שולחים את הבקשה. עם זאת, הוא ידוע מעט מאוד, ולכן אין לו תמיכה טובה בשרתים ואף דפדפן לא תומך בו.
בדפדפנים, התגובה אף פעם לא זמינה עד שגוף הבקשה נשלח במלואו, גם אם השרת שולח תגובה מוקדם יותר. זה נכון לכל אחזור בדפדפן.
דפוס ברירת המחדל הזה נקרא 'half duplex'.
עם זאת, בהטמעות מסוימות, כמו fetch
ב-Deno, ברירת המחדל היא 'full duplex' לאחזור בסטרימינג, כלומר התגובה עשויה להיות זמינה לפני שהבקשה תושלם.
לכן, כדי לעקוף את בעיית התאימות הזו, צריך לציין את duplex: 'half'
בדפדפנים בבקשות שיש להן גוף של שידור.
בעתיד, יכול להיות ש-duplex: 'full'
יהיה נתמך בדפדפנים לבקשות סטרימינג ולבקשות ללא סטרימינג.
בינתיים, הפתרון הטוב ביותר אחרי תקשורת דו-כיוונית הוא לבצע אחזור אחד עם בקשת סטרימינג, ואז לבצע אחזור נוסף כדי לקבל את התשובה בסטרימינג. השרת יצטרך דרך כלשהי לשייך בין שתי הבקשות האלה, כמו מזהה בכתובת ה-URL. כך פועלת ההדגמה.
הפניות אוטומטיות מוגבלות
בחלק מהצורות של הפניה אוטומטית מסוג HTTP, הדפדפן צריך לשלוח מחדש את גוף הבקשה לכתובת URL אחרת. כדי לתמוך בכך, הדפדפן יצטרך לאגור את התוכן של הסטרימינג במטמון, מה שסותר את המטרה של העניין, ולכן הוא לא עושה זאת.
במקום זאת, אם הבקשה כוללת גוף סטרימינג והתגובה היא הפניה אוטומטית מסוג HTTP שאינה 303, האחזור יידחה וההפניה האוטומטית לא תוביל לאתר אחר.
מותר להשתמש בהפניות אוטומטיות מסוג 303, כי הן משנות את השיטה באופן מפורש ל-GET
ומבטלות את גוף הבקשה.
נדרש CORS והוא מפעיל בדיקת קדם-הפעלה
לבקשות סטרימינג יש גוף, אבל אין להן כותרת Content-Length
.
זוהי בקשה מסוג חדש, ולכן נדרש CORS, והבקשות האלה תמיד מפעילות בדיקת קדם-הפעלה.
אסור לשלוח בקשות no-cors
בסטרימינג.
לא פועל ב-HTTP/1.x
האחזור יידחה אם החיבור הוא HTTP/1.x.
הסיבה לכך היא שלפי כללי HTTP/1.1, גוף הבקשה והתגובה צריכים לשלוח כותרת Content-Length
כדי שהצד השני ידע כמה נתונים הוא יקבל, או לשנות את הפורמט של ההודעה כך שישתמש בקידוד בקטעים. בהצפנה מחולקת, הגוף מחולק לחלקים, לכל אחד מהם אורך תוכן משלו.
קידוד בקטעים נפוץ למדי כשמדובר בתגובות של HTTP/1.1, אבל נדיר מאוד כשמדובר בבקשות, ולכן הוא מהווה סיכון תאימות גדול מדי.
בעיות אפשריות
זו תכונה חדשה, שעדיין לא נעשה בה שימוש נרחב באינטרנט. אלה כמה בעיות שכדאי לשים לב אליהן:
אי-תאימות בצד השרת
שרתי אפליקציות מסוימים לא תומכים בבקשות סטרימינג, ובמקום זאת ממתינים לקבלת הבקשה המלאה לפני שהם מאפשרים לכם לראות ממנה משהו, מה שמבטל את היתרון של סטרימינג. במקום זאת, כדאי להשתמש בשרת אפליקציות שתומך בסטרימינג, כמו NodeJS או Deno.
אבל זה לא הכל! שרת האפליקציה, כמו NodeJS, בדרך כלל נמצא מאחורי שרת אחר, שנקרא בדרך כלל 'שרת קצה קדמי', שיכול להיות שנמצא מאחורי CDN. אם אחד מהם יחליט לשמור את הבקשה במטמון לפני שהוא מעביר אותה לשרת הבא בשרשרת, לא תהיה לכם יותר אפשרות ליהנות מהיתרונות של סטרימינג של בקשות.
אי-תאימות שלא בשליטתכם
מכיוון שהתכונה הזו פועלת רק דרך HTTPS, אין צורך לדאוג לגבי שרתי proxy ביניכם לבין המשתמש, אבל יכול להיות שהמשתמש מפעיל שרת proxy במחשב שלו. חלק מתוכנות ההגנה על האינטרנט עושות זאת כדי לאפשר להן לעקוב אחרי כל מה שקורה בין הדפדפן לרשת, ויכול להיות שבמקרים מסוימים התוכנה הזו מאחסנת במטמון גופי בקשות.
כדי להגן מפני מצב כזה, אפשר ליצור 'בדיקת תכונה' בדומה להדגמה שלמעלה, שבה מנסים להעביר נתונים מסוימים בסטרימינג בלי לסגור את הסטרימינג. אם השרת מקבל את הנתונים, הוא יכול להשיב באמצעות אחזור אחר. לאחר מכן, תוכלו לדעת שהלקוח תומך בבקשות סטרימינג מקצה לקצה.
זיהוי תכונות
const supportsRequestStreams = (() => {
let duplexAccessed = false;
const hasContentType = new Request('', {
body: new ReadableStream(),
method: 'POST',
get duplex() {
duplexAccessed = true;
return 'half';
},
}).headers.has('Content-Type');
return duplexAccessed && !hasContentType;
})();
if (supportsRequestStreams) {
// …
} else {
// …
}
רצינו להסביר איך פועל זיהוי התכונות:
אם הדפדפן לא תומך בסוג body
מסוים, הוא קורא ל-toString()
באובייקט ומשתמש בתוצאה כגוף.
לכן, אם הדפדפן לא תומך בסטרימינג של בקשות, גוף הבקשה הופך למחרוזת "[object ReadableStream]"
.
כשמשתמשים במחרוזת כגוף, המערכת מגדירה את הכותרת Content-Type
כ-text/plain;charset=UTF-8
.
לכן, אם הכותרת הזו מוגדרת, אנחנו יודעים שהדפדפן לא תומך בסטרימינג באובייקטים של בקשות, ואנחנו יכולים לצאת מוקדם.
ב-Safari יש תמיכה בסטרימינג באובייקטים של בקשות, אבל אין אפשרות להשתמש בהם עם fetch
, לכן נבדקת האפשרות duplex
, שאין בה תמיכה כרגע ב-Safari.
שימוש בזרמים שניתנים לכתיבה
לפעמים קל יותר לעבוד עם מקורות נתונים כשיש WritableStream
.
אפשר לעשות זאת באמצעות מקור נתונים מסוג 'זהות', שהוא זוג לקריאה/כתיבה שמקבל כל דבר שמוענק לקצה הכתיבה שלו, ושולח אותו לקצה הקריאה.
אפשר ליצור אחד מהם על ידי יצירת TransformStream
ללא ארגומנטים:
const {readable, writable} = new TransformStream();
const responsePromise = fetch(url, {
method: 'POST',
body: readable,
});
עכשיו, כל מה ששולחים לשידור הניתן לכתיבה יהיה חלק מהבקשה. כך תוכלו ליצור שילובים של שידורים. לדוגמה, הנה דוגמה פשוטה שבה הנתונים מאוחזרים מכתובת URL אחת, דחוסים ונשלחים לכתובת URL אחרת:
// Get from url1:
const response = await fetch(url1);
const {readable, writable} = new TransformStream();
// Compress the data from url1:
response.body.pipeThrough(new CompressionStream('gzip')).pipeTo(writable);
// Post to url2:
await fetch(url2, {
method: 'POST',
body: readable,
});
בדוגמה שלמעלה נעשה שימוש בזרמי דחיסה כדי לדחוס נתונים שרירותיים באמצעות gzip.