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

רקע

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

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

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

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

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

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

טוקן של גרסת מקור לניסיון

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

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

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

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

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

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

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

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

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

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

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

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

מידע נוסף

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

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