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

רקע

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

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

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

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

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

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

אסימון המקור לניסיון

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

Origin-Trial: token_obtained_from_signup

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

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

HTTPS

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

שימוש ב-Foreign Fetch

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

האם קובץ השירות (service worker) של אחזור נתונים מחוץ לאתר רשום כראוי?

אפשר גם לבדוק את רשימת שירותי העבודה המלאה בחלונית האפליקציות של DevTools כדי לוודא את הרישום הבסיסי של שירות העבודה, כולל ההיקף שלו. חשוב לבחור באפשרות 'הצגת הכול', כי כברירת מחדל יוצגו רק שירותי ה-Workers של המקור הנוכחי.

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

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

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

גורם הטיפול באירוע foreignfetch

אחרי שהטמעתם את ה-Service Worker של הצד השלישי והוא הוגדר דרך registerForeignFetch(), יש לו הזדמנות ליירט בקשות למשאבי משנה ממקורות שונים בשרת שעומדות בהיקף של אחזור זר.

ב-Service Worker מסורתי של צד ראשון, כל בקשה תפעיל אירוע fetch שלעובד השירות שלכם הייתה הזדמנות להגיב. ל-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, צריך להעביר ל-respondWith() של ForeignFetchEvent את Promise שמתמסר לאובייקט עם מאפיינים ספציפיים:

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

חשוב לציין שכאשר הטיפול foreignfetch מופעל, יש לו גישה לכל פרטי הכניסה והסמכות הסביבה של המקור שמארח את ה-service worker. כמפתח שפורס קובץ שירות (service worker) שמאפשר אחזור זר, באחריותך לוודא שלא תדלפו נתוני תגובה בעלי הרשאות שלא היו זמינים בדרך אחרת באמצעות פרטי הכניסה האלה. דרישה להבעת הסכמה לתגובות CORS היא צעד אחד להגבלת חשיפת מידע לא מכוונת, אבל כמפתחים אתם יכולים לשלוח בקשות fetch() באופן מפורש בתוך הטיפול של 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}))
    );
});

שיקולים לגבי הלקוח

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

לקוחות שיש להם שירות משלהם של צד ראשון

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

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

בתוך שירות צד ראשון, שימוש ב-fetch() כדי לאחזר משאבים ממקורות שונים יפעיל את שירות הצד השלישי המתאים לאחזור. כלומר, קוד כמו הקוד הבא יכול להשתמש במטפל 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));
});

באופן דומה, אם יש לכם מודולים לטיפול באחזור מצד ראשון, אבל הם לא קוראים ל-event.respondWith() כשהם מטפלים בבקשות למשאב שלכם ממקורות שונים, הבקשה תעבור באופן אוטומטי לטיפול של המודול 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() כדי לבקש משאב במסגרת היקף אחזור הזר שלכם, ה-handler של שירות אחזור הזרים לא יקבל הזדמנות לטפל בבקשה.

לקוחות שאין להם שירות משלהם

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

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

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

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

הדפדפן מתחיל מלמעלה למטה, ובהתאם לאופן ההטמעה של Service Worker, הוא ימשיך ברשימה עד שהוא ימצא את מקור התגובה.

מידע נוסף

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

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