קובצי שירות ממקורות שונים – התנסות באחזור זר

ג'ף פוזניק
ג'ף פוזניק

רקע

Service Workers מאפשרים למפתחי אינטרנט להגיב לבקשות רשת שנשלחות על ידי אפליקציות האינטרנט שלהם, וכך לאפשר להם להמשיך לעבוד גם במצב אופליין, להילחם ב-lie-fi וליישם אינטראקציות מורכבות של המטמון כמו לא פעיל בזמן אימות מחדש. אבל בעבר ה-Service Workers היו קשורים למקור ספציפי — כבעלים של אפליקציית אינטרנט, באחריותכם לכתוב ולפרוס קובץ שירות (service worker) כדי ליירט את כל הבקשות ברשת שאפליקציית האינטרנט שלכם שולחת. במודל הזה, כל קובץ שירות (service worker) אחראי לטפל אפילו בבקשות ממקורות שונים, למשל, לממשק API של צד שלישי או לגופני אינטרנט.

מה קורה אם לספק צד שלישי של ממשק API, של גופני אינטרנט או של שירות נפוץ אחר, היה את הכוח לפרוס קובץ שירות (service worker) משלו שקיבל הזדמנות לטפל בבקשות שנשלחו על ידי מקורות אחרים אל המקור? ספקים יכולים להטמיע לוגיקת רשת מותאמת אישית משלהם, ולנצל מופע יחיד של מטמון כדי לשמור את התשובות שלהם. כיום, הודות לאחזור זר, אפשר לפרוס סוג כזה של קובץ שירות (service worker) של צד שלישי.

הפריסה של קובץ שירות (service worker) שמטמיע אחזור זר יכולה להיות הגיונית עבור כל ספק של שירות שהגישה אליו מתבצעת דרך בקשות HTTPS מדפדפנים. פשוט חשוב על תרחישים שבהם תוכל לספק גרסה בלתי תלויה של השירות שלך, שבה דפדפנים יוכלו לנצל מטמון משאבים משותף. השירותים ששירות זה יכול להפיק תועלת כוללים, בין היתר:

  • ספקי API עם ממשק RESTful
  • ספקי גופן אינטרנט
  • ספקי נתוני ניתוח
  • ספקים לאירוח תמונות
  • רשתות כלליות להעברת תוכן

לדוגמה, נניח שאתם ספקים של שירותי ניתוח נתונים. פריסת קובץ שירות (service worker) של אחזור זר, מבטיחה שכל הבקשות לשירות שלך שנכשלו בזמן שהמשתמש לא מחובר לאינטרנט ממתינות בתור ו יופעלו מחדש ברגע שהקישוריות תחזור. ייתכן שלקוחות של שירות מסוים יטמיעו התנהגות דומה דרך קובצי שירות של צד ראשון, אבל דרישה מכל לקוח לכתוב לוגיקה מותאמת אישית לשירות שלכם לא ניתנת להתאמה כמו הסתמכות על קובץ שירות (service worker) משותף שאתם פורסים.

דרישות מוקדמות

אסימון גרסת מקור לניסיון

שליפה זרה עדיין נחשבת לניסוי. כדי למנוע פיתוח של העיצוב הזה בטרם עת לפני שהוא הוגדר במלואו ומוסכם על ידי ספקי הדפדפנים, הטמענו ב-Chrome 54 את העיצוב כגרסת מקור לניסיון. כל עוד האפשרות של אחזור זר עדיין ניסיונית, כדי להשתמש בתכונה החדשה הזו בשירות שאתם מארחים, תצטרכו לבקש אסימון שמשויך למקור הספציפי של השירות. צריך לכלול את האסימון ככותרת של תגובת HTTP בכל הבקשות ממקורות שונים של משאבים שאתם רוצים לטפל בהם באמצעות אחזור זר, וכן בתגובה למשאב ה-JavaScript של ה-Service Worker:

Origin-Trial: token_obtained_from_signup

תקופת הניסיון תסתיים במרץ 2017. בשלב הזה אנחנו מצפים שנבין את השינויים הנדרשים כדי לייצב את התכונה, ובתקווה שנפעיל אותה כברירת מחדל. אם שליפה זרה לא מופעלת כברירת מחדל עד אז, הפונקציונליות שקשורה לאסימונים הקיימים של תקופת הניסיון המקורית תפסיק לפעול.

כדי להקל על ביצוע ניסויים בשליפה זרה לפני הרישום לאסימון רשמי של גרסת מקור לניסיון, אפשר לעקוף את הדרישה ב-Chrome למחשב המקומי. לשם כך, צריך לעבור אל chrome://flags/#enable-experimental-web-platform-features ולהפעיל את הסימון 'תכונות ניסיוניות של פלטפורמת האינטרנט'. הערה: צריך לעשות זאת בכל מופע של Chrome שרוצים להשתמש בו בניסויים מקומיים. לעומת זאת, עם אסימון לגרסת המקור לניסיון, התכונה תהיה זמינה לכל משתמשי Chrome.

HTTPS

בדומה לכל הפריסות של Service Worker, צריך לגשת לשרת האינטרנט שבו אתם משתמשים כדי לשרת גם את המשאבים וגם את הסקריפט של Service Worker באמצעות HTTPS. בנוסף, יירוט של אחזור אחזור זר חל רק על בקשות שמקורן בדפים שמתארחים במקורות מאובטחים, כך שהלקוחות של השירות שלכם צריכים להשתמש ב-HTTPS כדי לנצל את הטמעת האחזור זר.

שימוש באחזור זר

ללא הדרישות המוקדמות, בואו נתעמק בפרטים הטכניים הדרושים להפעלת עובד שירות אחזור זר.

רישום קובץ השירות (service worker)

האתגר הראשון שסביר להניח שתיתקלו בו הוא איך לרשום את קובץ השירות (service worker). אם עבדתם בעבר עם קובצי שירות (service worker), סביר להניח שאתם מכירים את הדברים הבאים:

// You can't do this!
if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('service-worker.js');
}

קוד ה-JavaScript הזה לרישום של קובץ שירות (service worker) של צד ראשון הגיוני בהקשר של אפליקציית אינטרנט, שמופעל על ידי משתמש שמנווט לכתובת URL שבשליטתכם. אבל זו לא גישה מעשית לרישום קובץ שירות (service worker) של צד שלישי, כאשר הדפדפן היחיד לתקשורת עם השרת מבקש משאב משנה ספציפי ולא ניווט מלא. אם הדפדפן מבקש, למשל, תמונה משרת CDN שאתם מנהלים, לא תוכלו להוסיף את קטע ה-JavaScript בתחילת התגובה שלכם ולצפות שהוא יופעל. נדרשת שיטה אחרת לרישום קובץ השירות (service worker), שמחוץ להקשר הרגיל של JavaScript.

הפתרון מופיע ככותרת HTTP שהשרת יכול לכלול בכל תגובה:

Link: </service-worker.js>; rel="serviceworker"; scope="/"

נסביר את הכותרת לדוגמה לפי הרכיבים שלה, כשכל אחד מהם מופרד בתו ;.

  • חובה לציין </service-worker.js> ומשמש לציון הנתיב לקובץ ה-Service Worker (יש להחליף את /service-worker.js בנתיב המתאים לסקריפט). היא תואמת ישירות למחרוזת scriptURL שהייתה מועברת כפרמטר הראשון ל-navigator.serviceWorker.register(). הערך צריך להיות מוקף ב-<> תווים (כפי שנדרש במפרט הכותרת של Link). אם תצוין כתובת URL יחסית ולא מוחלטת, הוא יפורש כיחסי למיקום של התגובה.
  • גם המאפיין rel="serviceworker" הוא שדה חובה, ויש לכלול אותו ללא צורך בהתאמה אישית.
  • scope=/ היא הצהרת היקף אופציונלית, המקבילה למחרוזת options.scope שאפשר להעביר כפרמטר השני ל-navigator.serviceWorker.register(). בתרחישי שימוש רבים, אין לך בעיה להשתמש בהיקף ברירת המחדל, אז אפשר לא להגדיר את היקף ברירת המחדל הזה אלא אם כן ברור לך שהוא נחוץ. אותן המגבלות על ההיקף המקסימלי המותר, יחד עם היכולת להקל את ההגבלות האלה באמצעות הכותרת Service-Worker-Allowed, חלות על רישומי כותרות של Link.

בדיוק כמו רישום של קובץ שירות (service worker) 'מסורתי', גם שימוש בכותרת Link יתקין קובץ שירות (service worker) שישמש עבור הבקשה הבאה שתבוצע בהתאם להיקף הרשום. גוף התגובה שכולל את הכותרת המיוחדת ישמש כפי שהוא וזמין לדף באופן מיידי, בלי להמתין לסיום ההתקנה על ידי ה-Service Worker.

חשוב לזכור ששליפה זרה מוטמעת כרגע כגרסת מקור לניסיון, לכן לצד כותרת התגובה של הקישור צריך לכלול גם כותרת Origin-Trial תקינה. הקבוצה המינימלית של כותרות תגובה שצריך להוסיף כדי לרשום את קובץ השירות (service worker) של אחזור זר היא

Link: </service-worker.js>; rel="serviceworker"
Origin-Trial: token_obtained_from_signup

הרשמה לניפוי באגים

במהלך הפיתוח, סביר להניח שתרצו לוודא שהקובץ של שירות האחזור הזר מותקן ומעבד בקשות באופן תקין. יש כמה דברים שאפשר לבדוק בכלים למפתחים של Chrome כדי לוודא שהכל פועל כצפוי.

האם נשלחות כותרות התגובה המתאימות?

כדי לרשום את קובץ השירות הזר לאחזור, עליך להגדיר כותרת קישור בתגובה למשאב המתארח בדומיין שלך, כפי שתואר קודם לכן בפוסט הזה. במהלך תקופת הניסיון בגרסת המקור, ובהנחה שלא הגדרת את chrome://flags/#enable-experimental-web-platform-features, צריך להגדיר גם כותרת תגובה מסוג Origin-Trial. כדי לוודא ששרת האינטרנט מגדיר את הכותרות האלה, יש לבדוק את הרשומה בחלונית הרשת בכלי הפיתוח:

כותרות מוצגות בחלונית &#39;רשת&#39;.

האם עובד שירות אחזור זר רשום כראוי?

אפשר גם לבדוק את הרישום הבסיסי של קובץ השירות (service worker), כולל ההיקף שלו, ברשימה המלאה של ה-Service Workers בחלונית האפליקציות בכלי הפיתוח. חשוב לבחור באפשרות 'הצגת הכול', כי כברירת מחדל יוצגו רק קובצי שירות (service worker) של המקור הנוכחי.

קובץ השירות הזר של שירות האחזור בחלונית האפליקציות.

הגורם המטפל באירועי התקנה

עכשיו, לאחר שרשמת את קובץ השירות (service worker) של הצד השלישי, תהיה לו הזדמנות להגיב לאירועים של install ושל activate, בדיוק כמו שכל קובץ שירות אחר היה עושה. הוא יכול לנצל את האירועים האלה כדי, למשל, לאכלס קבצים שמורים במשאבים נדרשים במהלך האירוע install, או להסיר קבצים שמורים לא מעודכנים באירוע activate.

בנוסף לפעילויות רגילות של שמירת אירועים במטמון של install, יש שלב נוסף נדרש בתוך הגורם המטפל באירועים install של קובץ השירות (service worker) של הצד השלישי. הקוד שלך צריך לקרוא ל-registerForeignFetch(), כמו בדוגמה הבאה:

self.addEventListener('install', event => {
    event.registerForeignFetch({
    scopes: [self.registration.scope], // or some sub-scope
    origins: ['*'] // or ['https://example.com']
    });
});

יש שתי אפשרויות הגדרה, שתיהן נדרשות:

  • הפונקציה scopes מקבלת מערך של מחרוזת אחת או יותר, וכל אחת מהן מייצגת היקף של בקשות שיפעילו אירוע foreignfetch. אבל רגע, אתם עשויים לחשוב, כבר הגדרתי היקף במהלך הרישום של קובץ השירות (service worker!) זה נכון, וההיקף הכולל עדיין רלוונטי – כל היקף שתציינו כאן חייב להיות שווה להיקף הכולל של קובץ השירות (service worker), או לתת לו תת-היקף. ההגבלות הנוספות על היקף ההרשאות מאפשרות לך לפרוס קובץ שירות (service worker) לכל מטרה שיכול לטפל גם באירועי fetch של צד ראשון (לבקשות הנשלחות מהאתר שלך) וגם באירועי foreignfetch של צד שלישי (לבקשות שנשלחות מדומיינים אחרים), ומבהירות שרק קבוצת משנה של ההיקף הגדול יותר יכולה להפעיל את foreignfetch. בפועל, אם פורסים קובץ שירות (service worker) שמיועד לטיפול רק באירועים של foreignfetch של צד שלישי, כדאי להשתמש בהיקף יחיד ומפורש ששווה להיקף הכולל של קובץ השירות. זה מה שנעשה בדוגמה שלמעלה, עם הערך self.registration.scope.
  • origins גם לוקח מערך של מחרוזת אחת או יותר, ומאפשר להגביל את ה-handler של foreignfetch כך שיגיב רק לבקשות מדומיינים ספציפיים. לדוגמה, אם מאשרים באופן מפורש את 'https://example.com', בקשה שנשלחת מדף שמתארח ב-https://example.com/path/to/page.html אל משאב שמוגש בהיקף האחזור זר שלך תפעיל את ה-handler שלך לאחזור זר, אבל בקשות שיבוצעו מ-https://random-domain.com/path/to/page.html לא יפעילו את ה-handler שלך. אלא אם יש סיבה ספציפית להפעיל את הלוגיקה של אחזור נתונים זרים רק לקבוצת משנה של מקורות מרוחקים, אפשר פשוט לציין את '*' כערך היחיד במערך, וכל המקורות מותרים.

הגורם המטפל באירועים של שליפה זרה

עכשיו, אחרי שהתקנתם את קובץ השירות (service worker) של הצד השלישי, והקובץ הוגדר באמצעות registerForeignFetch(), הוא יקבל הזדמנות ליירט בקשות למשאבי משנה ממקורות שונים לשרת שלכם שתואמות לאחזור זר.

בקובץ שירות (service worker) מסורתי של צד ראשון, כל בקשה מפעילה אירוע fetch של-Service Worker הייתה הזדמנות להגיב. קובץ השירות (service worker) של הצד השלישי מקבל הזדמנות לטפל באירוע קצת שונה בשם foreignfetch. בעיקרון, שני האירועים דומים למדי, והם מאפשרים לך לבדוק את הבקשה הנכנסת, ואם ברצונך להגיב לה באמצעות respondWith():

self.addEventListener('foreignfetch', event => {
    // Assume that requestLogic() is a custom function that takes
    // a Request and returns a Promise which resolves with a Response.
    event.respondWith(
    requestLogic(event.request).then(response => {
        return {
        response: response,
        // Omit to origin to return an opaque response.
        // With this set, the client will receive a CORS response.
        origin: event.origin,
        // Omit headers unless you need additional header filtering.
        // With this set, only Content-Type will be exposed.
        headers: ['Content-Type']
        };
    })
    );
});

למרות הדמיון הרעיוני, יש כמה הבדלים בפועל בעת קריאה ל-respondWith() באמצעות ForeignFetchEvent. במקום רק לספק Response (או Promise שמסתיים ב-Response) ל-respondWith(), כמו שעושים עם FetchEvent, צריך להעביר Promise שנקבע עם אובייקט עם מאפיינים ספציפיים ל-respondWith() של ForeignFetchEvent:

  • חובה להגדיר את response, וצריך להגדיר אותו לאובייקט Response שיוחזר ללקוח שהגיש את הבקשה. אם תספקו כל דבר מלבד Response חוקי, בקשת הלקוח תסתיים בשגיאת רשת. שלא כמו בקריאה ל-respondWith() בתוך handler של אירועים של fetch, צריך לספק כאן Response, ולא Promise שנסיים באמצעות Response! אפשר ליצור את התשובה דרך שרשרת הבטחה ולהעביר את השרשרת כפרמטר ל-respondWith() של foreignfetch, אבל השרשרת צריכה להגיע עם אובייקט שמכיל את המאפיין response שמוגדר לאובייקט Response. אפשר לראות הדגמה של זה בדוגמת הקוד שלמעלה.
  • origin היא אופציונלית, ומשמשת כדי לקבוע אם התשובה שתוחזר אטומה או לא. אם לא בוחרים באפשרות הזו, התשובה תהיה אטומה וללקוח תהיה גישה מוגבלת לגוף התגובה ולכותרות שלה. אם הבקשה נשלחה באמצעות mode: 'cors', החזרת תגובה אטומה תטופל כשגיאה. עם זאת, אם מציינים ערך מחרוזת ששווה למקור של הלקוח המרוחק (שניתן לקבל דרך event.origin), מביעים הסכמה מפורשת לספק ללקוח תשובה התומכת ב-CORS.
  • גם headers הוא אופציונלי, והוא שימושי רק אם מציינים גם את הערך origin ומחזירים תגובת CORS. כברירת מחדל, התשובה שלך תכלול רק כותרות ברשימת כותרות התגובה שברשימת ההיתרים של CORS. אם צריך לסנן עוד יותר את מה שיוחזר, הכותרות יקבלו רשימה של שם כותרת אחד או יותר, והיא תשתמש בשם זה כרשימת ההיתרים של הכותרות שיש לחשוף בתגובה. כך תוכלו להביע הסכמה ל-CORS ועדיין למנוע חשיפה של כותרות תגובה שעשויות להיות רגישות ישירות ללקוח המרוחק.

חשוב לציין שכשה-handler של foreignfetch פועל, יש לו גישה לכל פרטי הכניסה ולסמכות הסביבה של המקור שמארח את קובץ השירות (service worker). כמפתחים שפורסים קובץ שירות (service worker) זר התומך באחזור, באחריותך לוודא שאינך מדליף נתוני תגובה מורשים שלא היו זמינים באמצעות פרטי הכניסה האלה. דרישה להביע הסכמה לקבלת תגובות CORS היא שלב אחד להגבלת חשיפה לא מכוונת, אבל כמפתחים אתם יכולים לשלוח בקשות fetch() באופן מפורש בתוך ה-handler של foreignfetch, שלא משתמשות בפרטי הכניסה המשתמעים באמצעות:

self.addEventListener('foreignfetch', event => {
    // The new Request will have credentials omitted by default.
    const noCredentialsRequest = new Request(event.request.url);
    event.respondWith(
    // Replace with your own request logic as appropriate.
    fetch(noCredentialsRequest)
        .catch(() => caches.match(noCredentialsRequest))
        .then(response => ({response}))
    );
});

שיקולים הקשורים ללקוח

קיימים מספר שיקולים נוספים שמשפיעים על האופן שבו קובץ שירות אחזור זר מטפל בבקשות מלקוחות השירות שלך.

לקוחות שיש להם קובץ שירות (service worker) של צד ראשון משלהם

ייתכן שללקוחות מסוימים בשירות שלכם כבר יש קובץ שירות (service worker) של צד ראשון שמטפל בבקשות שמגיעות מאפליקציית האינטרנט שלהם. מה המשמעות מבחינת קובץ השירות (service worker) של הצד השלישי שלכם?

fetch הגורמים המטפלים בקובץ שירות של צד ראשון מקבלים את ההזדמנות הראשונה להגיב לכל הבקשות שנשלחות על ידי אפליקציית האינטרנט, גם אם יש קובץ שירות(service worker) של צד שלישי שמופעל בו foreignfetch עם היקף שמכסה את הבקשה. אבל לקוחות עם ספקי שירות של צד ראשון עדיין יכולים ליהנות מהיתרון של קובץ השירות (service worker) של הצד הראשון שלך!

בתוך קובץ שירות (service worker) של צד ראשון, שימוש ב-fetch() לאחזור משאבים ממקורות שונים יפעיל את קובץ השירות המתאים לאחזור זר. המשמעות היא שקוד כמו זה יכול להשתמש ב-handler של foreignfetch:

// Inside a client's first-party service-worker.js:
self.addEventListener('fetch', event => {
    // If event.request is under your foreign fetch service worker's
    // scope, this will trigger your foreignfetch handler.
    event.respondWith(fetch(event.request));
});

באופן דומה, אם יש רכיבי handler של אחזור מאינטראקציה ישירה, אבל הם לא מבצעים קריאה ל-event.respondWith() במהלך הטיפול בבקשות למשאב ממקורות שונים, הבקשה תעבור באופן אוטומטי ל-handler של foreignfetch:

// Inside a client's first-party service-worker.js:
self.addEventListener('fetch', event => {
    if (event.request.mode === 'same-origin') {
    event.respondWith(localRequestLogic(event.request));
    }

    // Since event.respondWith() isn't called for cross-origin requests,
    // any foreignfetch handlers scoped to the request will get a chance
    // to provide a response.
});

אם handler של צד ראשון ב-fetch קורא ל-event.respondWith() אבל לא משתמש ב-fetch() כדי לבקש משאב שנמצא בהיקף השליפה הזרה שלכם, לעובד שירות האחזור הזר לא תהיה אפשרות לטפל בבקשה.

לקוחות שאין להם קובץ שירות (service worker) משלהם

כל הלקוחות שמגישים בקשות לשירות צד שלישי יכולים להפיק תועלת כאשר השירות פורס קובץ שירות (service worker) לאחזור זר, גם אם הם עדיין לא משתמשים ב-Service Worker שלו. אין משהו ספציפי שהלקוחות צריכים לעשות כדי להצטרף לשימוש ב-Service Worker זר, כל עוד הם משתמשים בדפדפן שתומך בכך. פירוש הדבר הוא שכאשר פורסים קובץ שירות אחזור זר, לוגיקת הבקשות המותאמת אישית והמטמון המשותף יועילו לרבים מלקוחות השירות שלכם באופן מיידי, בלי שהם יבצעו פעולות נוספות.

סיכום של כל המידע הזה: המקום שבו הלקוחות מחפשים תגובה

על סמך המידע שלמעלה, אנחנו יכולים ליצור היררכיה של מקורות שבהם הלקוח ישתמש כדי למצוא תשובה לבקשה ממקורות שונים.

  1. handler של fetch של קובץ שירות של צד ראשון (אם יש כזה)
  2. handler של foreignfetch של צד שלישי (אם יש כזה ורק לבקשות ממקורות שונים)
  3. מטמון ה-HTTP של הדפדפן (אם קיימת תגובה חדשה)
  4. הרשת

הדפדפן מתחיל מלמעלה, ובהתאם להטמעת של קובץ השירות (service worker) ממשיך ברשימה עד שמוצאים מקור לתגובה.

מידע נוסף

רוצה לקבל עדכונים וטיפים?

ההטמעה של גרסת המקור לניסיון ב-Chrome של אחזור זר עשויה להשתנות בהתאם למשוב שאנחנו מקבלים ממפתחים. נעדכן את הפוסט הזה באמצעות שינויים מוטבעים, ונרשום את השינויים הספציפיים בהמשך כאשר יתרחשו. בנוסף, נשתף מידע על שינויים משמעותיים דרך חשבון ה-Twitter @chromiumdev.