Работники службы Cross-Origin — экспериментируем с внешней выборкой

Фон

Сервис-воркеры дают веб-разработчикам возможность отвечать на сетевые запросы, сделанные их веб-приложениями, позволяя им продолжать работу даже в автономном режиме, бороться с ложью и реализовывать сложные взаимодействия с кэшем, такие как устаревшие при повторной проверке . Но сервис-воркеры исторически были привязаны к определенному источнику — как владелец веб-приложения вы обязаны написать и развернуть сервис-воркера для перехвата всех сетевых запросов, которые делает ваше веб-приложение. В этой модели каждый сервисный работник отвечает за обработку даже запросов из разных источников, например, к стороннему API или веб-шрифтам.

Что, если сторонний поставщик API, веб-шрифтов или другой широко используемой службы имел возможность развернуть своего собственного сервис-воркера, который получил бы возможность обрабатывать запросы, сделанные другими источниками к их источнику? Поставщики могут реализовать свою собственную сетевую логику и использовать единый авторитетный экземпляр кэша для хранения своих ответов. Теперь, благодаря внешней выборке , такой тип развертывания сторонних сервис-воркеров стал реальностью.

Развертывание сервис-воркера, реализующего внешнюю выборку, имеет смысл для любого поставщика сервиса, доступ к которому осуществляется через HTTPS-запросы от браузеров — просто подумайте о сценариях, в которых вы могли бы предоставить независимую от сети версию вашего сервиса, в которой браузеры могли бы воспользоваться преимуществами общий кэш ресурсов. Услуги, которые могут получить от этого выгоду, включают, помимо прочего:

  • Поставщики API с интерфейсами RESTful
  • Поставщики веб-шрифтов
  • Поставщики аналитики
  • Хостинг изображений
  • Общие сети доставки контента

Представьте, например, что вы поставщик аналитических услуг. Развернув стороннего исполнителя службы выборки, вы можете гарантировать, что все запросы к вашей службе, которые завершаются сбоем, пока пользователь находится в автономном режиме, будут поставлены в очередь и воспроизведены после восстановления подключения. Хотя клиенты службы могли реализовать аналогичное поведение с помощью сторонних сервис-воркеров, требование от каждого клиента писать индивидуальную логику для вашей службы не так масштабируемо, как использование общего внешнего работника службы выборки, который вы развертываете.

Предварительные условия

Токен пробной версии Origin

Иностранная выборка по-прежнему считается экспериментальной. Чтобы не допустить преждевременного внедрения этого дизайна до того, как он будет полностью указан и согласован поставщиками браузеров, он был реализован в Chrome 54 в виде пробной версии Origin . Пока внешняя выборка остается экспериментальной, чтобы использовать эту новую функцию со службой, которую вы размещаете, вам необходимо запросить токен , привязанный к конкретному источнику вашей службы. Токен должен быть включен в качестве заголовка ответа HTTP во все запросы между источниками для ресурсов, которые вы хотите обрабатывать посредством внешней выборки, а также в ответ для вашего ресурса JavaScript сервис-воркера:

Origin-Trial: token_obtained_from_signup

Пробная версия завершится в марте 2017 года. К этому моменту мы ожидаем, что определим все изменения, необходимые для стабилизации функции, и (надеемся) включим ее по умолчанию. Если к тому времени сторонняя выборка не будет включена по умолчанию, функциональность, связанная с существующими пробными токенами Origin, перестанет работать.

Чтобы облегчить экспериментирование с внешним получением перед регистрацией официального пробного токена Origin, вы можете обойти это требование в Chrome на локальном компьютере, перейдя по адресу chrome://flags/#enable-experimental-web-platform-features и включив параметр " Флаг «Экспериментальные возможности веб-платформы». Обратите внимание, что это необходимо сделать в каждом экземпляре Chrome, который вы хотите использовать в своих локальных экспериментах, тогда как с пробным токеном Origin эта функция будет доступна всем вашим пользователям Chrome.

HTTPS

Как и во всех развертываниях Service Worker, доступ к веб-серверу, который вы используете для обслуживания как ваших ресурсов, так и вашего сценария Service Worker , должен осуществляться через HTTPS . Кроме того, перехват внешней выборки применяется только к запросам, исходящим от страниц, размещенных в безопасных источниках, поэтому клиентам вашей службы необходимо использовать HTTPS, чтобы воспользоваться преимуществами вашей реализации внешней выборки.

Использование внешней выборки

Ознакомившись с предварительными условиями, давайте углубимся в технические детали, необходимые для запуска и запуска стороннего исполнителя службы выборки.

Регистрация вашего сервис-воркера

Первая проблема, с которой вы, вероятно, столкнетесь, — это как зарегистрировать своего сервис-воркера. Если вы раньше работали с сервисными работниками, вы, вероятно, знакомы со следующим:

// 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.js соответствующим путем к вашему скрипту). Это напрямую соответствует строке scriptURL , которая в противном случае была бы передана в качестве первого параметра в navigator.serviceWorker.register() . Значение должно быть заключено в символы <> (как того требует спецификация заголовка Link ), и если указан относительный, а не абсолютный URL-адрес, он будет интерпретироваться как относительный к местоположению ответа .
  • rel="serviceworker" также является обязательным и должен быть включен без необходимости настройки.
  • scope=/ это необязательное объявление области действия, эквивалентное строке options.scope , которую вы можете передать в качестве второго параметра в navigator.serviceWorker.register() . Во многих случаях использования вы можете использовать область по умолчанию , поэтому не стесняйтесь оставлять ее, если вы не уверены, что она вам нужна. Те же ограничения в отношении максимально допустимой области действия, а также возможность ослабить эти ограничения с помощью заголовка Service-Worker-Allowed применяются к регистрации Link .

Как и при «традиционной» регистрации сервис-воркера, использование заголовка Link приведет к установке сервис-воркера, который будет использоваться для следующего запроса, сделанного к зарегистрированной области. Тело ответа, включающее специальный заголовок, будет использоваться как есть и сразу же доступно для страницы, не дожидаясь завершения установки внешним сервисным работником.

Помните, что внешняя выборка в настоящее время реализована как пробная версия Origin , поэтому вместе с заголовком ответа на ссылку вам также необходимо включить действительный заголовок Origin-Trial . Минимальный набор заголовков ответа, который необходимо добавить для регистрации вашего внешнего работника службы выборки:

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

Отладка регистрации

Во время разработки вы, вероятно, захотите убедиться, что ваш внешний работник службы выборки правильно установлен и обрабатывает запросы. Есть несколько вещей, которые вы можете проверить в инструментах разработчика Chrome, чтобы убедиться, что все работает должным образом.

Отправляются ли правильные заголовки ответов?

Чтобы зарегистрировать работника службы внешней выборки, вам необходимо установить заголовок Link в ответе на ресурс, размещенный в вашем домене, как описано ранее в этом посте. В течение пробного периода Origin и при условии, что у вас не установлен chrome://flags/#enable-experimental-web-platform-features , вам также необходимо установить заголовок ответа Origin-Trial . Вы можете убедиться, что ваш веб-сервер устанавливает эти заголовки, просмотрев запись на панели «Сеть» DevTools:

Заголовки отображаются на панели «Сеть».

Правильно ли зарегистрирован работник службы Foreign Fetch?

Вы также можете подтвердить регистрацию базового сервис-воркера, включая его область, просмотрев полный список сервис-воркеров на панели приложений DevTools. Обязательно выберите опцию «Показать все», поскольку по умолчанию вы увидите только сервис-воркеров текущего источника.

Работник службы внешней выборки на панели «Приложения».

Обработчик событий установки

Теперь, когда вы зарегистрировали своего стороннего сервисного работника, он получит возможность реагировать на события install и activate , как это сделал бы любой другой сервисный работник. Он может использовать эти события, например, для заполнения кешей необходимыми ресурсами во время события install или удаления устаревших кешей в событии activate .

Помимо обычных действий по кэшированию событий install , в обработчике событий install стороннего сервис-воркера требуется дополнительный шаг. Ваш код должен вызвать registerForeignFetch() , как в следующем примере:

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

Существует два варианта конфигурации, оба обязательны:

  • scopes принимает массив из одной или нескольких строк, каждая из которых представляет область для запросов, которые запускают событие foreignfetch . Но подождите , вы можете подумать: я уже определил область действия во время регистрации сервис-воркера! Это правда, и эта общая область по-прежнему актуальна — каждая область, которую вы здесь указываете, должна быть либо равна общей области действия сервис-воркера, либо являться ее подобластью. Дополнительные ограничения области позволяют вам развернуть универсальный сервис-воркер, который может обрабатывать как собственные события fetch (для запросов, сделанных с вашего собственного сайта), так и сторонние события 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

Теперь, когда вы установили стороннего сервис-воркера и настроили его с помощью registerForeignFetch() , он получит возможность перехватывать запросы подресурсов между источниками на ваш сервер, которые попадают в область внешней выборки.

В традиционном сервисном работнике каждый запрос запускал событие fetch , на которое ваш сервисный работник имел возможность отреагировать. Нашему стороннему сервисному работнику предоставляется возможность обработать немного другое событие, названное 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 ! Вы можете создать свой ответ с помощью цепочки обещаний и передать эту цепочку в качестве параметра в respondWith() функции foreignfetch , но цепочка должна разрешиться с помощью объекта, который содержит свойство response , установленное для объекта Response . Вы можете увидеть демонстрацию этого в примере кода выше.
  • origin не является обязательным и используется для определения того, является ли возвращаемый ответ непрозрачным . Если вы пропустите этот параметр, ответ будет непрозрачным, и клиент будет иметь ограниченный доступ к телу и заголовкам ответа. Если запрос был сделан в mode: 'cors' , то возврат непрозрачного ответа будет рассматриваться как ошибка. Однако если вы укажете строковое значение, равное источнику удаленного клиента (которое можно получить через event.origin ), вы явно соглашаетесь предоставить клиенту ответ с поддержкой CORS.
  • headers также не являются обязательными и полезны только в том случае, если вы также указываете origin и возвращаете ответ CORS. По умолчанию в ваш ответ будут включены только заголовки из списка заголовков ответа, внесенного в список безопасных CORS . Если вам нужно дополнительно отфильтровать возвращаемые данные, headers принимает список из одного или нескольких имен заголовков и будет использовать его в качестве списка разрешенных заголовков, которые следует отображать в ответе. Это позволяет вам подписаться на CORS, при этом предотвращая раскрытие потенциально конфиденциальных заголовков ответов непосредственно удаленному клиенту.

Важно отметить, что при запуске обработчика foreignfetch он имеет доступ ко всем учетным данным и внешним полномочиям источника, на котором размещен сервис-воркер . Как разработчик, развертывающий стороннего сервис-воркера с поддержкой выборки, вы несете ответственность за то, чтобы не допустить утечки каких-либо привилегированных данных ответа, которые в противном случае были бы недоступны благодаря этим учетным данным. Требование согласия на получение ответов 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 в стороннем обработчике службы получают первую возможность ответить на все запросы, сделанные веб-приложением, даже если есть сторонний обработчик службы с включенной 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.
});

Если сторонний обработчик fetch вызывает event.respondWith() но не использует fetch() для запроса ресурса в вашей внешней области выборки, то ваш внешний работник службы выборки не получит возможности обработать запрос.

Клиенты, у которых нет собственного сервис-воркера

Все клиенты, отправляющие запросы к сторонней службе, могут получить выгоду, если служба развернет стороннего работника службы выборки, даже если они еще не используют своего собственного работника службы. Клиентам не нужно делать ничего особенного, чтобы согласиться на использование стороннего исполнителя службы выборки, если они используют браузер, который его поддерживает. Это означает, что при развертывании внешнего исполнителя службы выборки ваша пользовательская логика запросов и общий кеш немедленно принесут пользу многим клиентам вашей службы, без каких-либо дальнейших действий.

Собираем все вместе: где клиенты ждут ответа

Принимая во внимание приведенную выше информацию, мы можем составить иерархию источников, которые клиент будет использовать для поиска ответа на запрос из разных источников.

  1. Обработчик fetch стороннего сервисного работника (если присутствует)
  2. Обработчик foreignfetch стороннего сервисного работника (если присутствует и только для запросов между источниками)
  3. HTTP-кеш браузера (если существует свежий ответ)
  4. Сеть

Браузер начинается сверху и, в зависимости от реализации сервис-воркера, продолжает движение вниз по списку, пока не найдет источник ответа.

Узнать больше

Будьте в курсе

Реализация в Chrome зарубежной пробной версии Origin может быть изменена по мере рассмотрения отзывов разработчиков. Мы будем поддерживать этот пост в актуальном состоянии посредством встроенных изменений и будем отмечать конкретные изменения ниже по мере их возникновения. Мы также будем делиться информацией об основных изменениях через аккаунт @chromiumdev в Твиттере.