מגרסה Chromium 105, אפשר להתחיל בקשה לפני שכל הגוף זמין באמצעות 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
או בזרם ההמרה החדש יותר אם דפדפני היעד תומכים בו:
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, ברירת המחדל הייתה 'דופלקס מלא' לאחזור נתונים בסטרימינג, כלומר התשובה יכולה להיות זמינה לפני שהבקשה מסתיימת.
לכן, כדי לעקוף את בעיית התאימות הזו, בדפדפנים צריך לציין את duplex: 'half'
בבקשות שיש להן גוף של סטרימינג.
בעתיד, יכול להיות שיהיה אפשר להשתמש ב-duplex: 'full'
בדפדפנים לסטרימינג ולבקשות שלא קשורות לסטרימינג.
בינתיים, הפתרון הכי טוב לתקשורת דו-כיוונית הוא לבצע אחזור אחד עם בקשת סטרימינג, ואז לבצע אחזור נוסף כדי לקבל את תגובת הסטרימינג. השרת צריך דרך כלשהי לשייך בין שתי הבקשות האלה, כמו מזהה בכתובת ה-URL. כך פועלת ההדגמה.
הפניות מחדש מוגבלות
חלק מההפניות האוטומטיות של HTTP מחייבות את הדפדפן לשלוח מחדש את גוף הבקשה לכתובת URL אחרת. כדי לתמוך בזה, הדפדפן יצטרך לשמור בזיכרון המטמון את התוכן של הסטרימינג, וזה קצת מפספס את המטרה, אז הוא לא עושה את זה.
במקום זאת, אם הבקשה כוללת גוף של סטרימינג והתגובה היא הפניה מחדש של HTTP שאינה 303, הפעולה fetch תידחה וההפניה מחדש לא תתבצע.
מותר להשתמש בהפניות אוטומטיות מסוג 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.