Введите сервисных работников. Через сервисных работников все рамки и код приложения для вывода HTML-представления могут быть предварительно сдвилены в браузере, что ускоряет как первую значимую краску, так и время взаимодействия. В этой статье я поделюсь своим опытом с реализацией сервисных работников для PoP, SPA сайт, который работает на WordPress, с целью ускорения времени загрузки и предоставления автономных возможностей.
Большая часть кода из объяснения ниже может быть повторно использована для создания решения для традиционных (т.е. не-SPA) WordPress веб-сайтов, тоже. Кто-нибудь хочет реализовать плагин?
Определение особенностей приложения
В дополнение к подходящим для веб-сайта SPA WordPress, реализация услуг ниже была разработана для поддержки следующих функций:
- загрузка активов из внешних источников, таких как сеть доставки контента (CDN);
- многоязычная (i18n) поддержка;
- несколько просмотров;
- выбор времени выполнения стратегии кэширования, на основе URL-ПО-URL.
Исходя из этого, мы будем принимать следующие проектные решения.
Сначала загрузите оболочку приложения (или Аппеллы)
Если архитектура SPA поддерживает его, сначала загрузите приложение (т.е. минимальный HTML, CSS и JavaScript, чтобы включить пользовательский интерфейс), под https://www.mydomain.com/appshell/
. После загрузки приложение динамически запрашивает содержимое с сервера через API. Поскольку все активы аппликции могут быть предварительно сдвилены с помощью сервисных работников, рама веб-сайта загружается немедленно, ускоряя первую значимую краску. В этом сценарии используется стратегиякэширования «отсечку кэширования сети».
Следите за конфликтами! Например, WordPress выводит код, который не должен быть кэширован и использован навсегда, например, nonces, который обычно истекает через 24 часа. Приложение, которое работники службы будут кэшировать в браузере в течение более 24 часов, необходимо иметь дело с nonces должным образом.
Извлекайте ресурсы WordPress добавленные через wp’enqueue-script и wp’enqueue
Поскольку веб-сайт WordPress загружает свои ресурсы JavaScript и CSS через wp_enqueue_script
wp_enqueue_style
крючки, соответственно, мы можем удобно извлечь эти ресурсы и добавить их в список precache.
Хотя выполнение этой стратегии сокращает усилия по составлению списка ресурсов для предварительного вывода (некоторые файлы по-прежнему должны быть добавлены вручную, как мы увидим позже), это означает, что service-workers.js
файл должен быть сгенерирован динамически, во время выполнения. Это очень хорошо подходит решение, чтобы WordPress плагины подключить в поколение service-workers.js
файла, как объясняется в следующем пункте.
В самом деле, я бы утверждать, что нет другого пути, кроме как подключиться к этим функциям, потому что для создания списка вручную (т.е. поиск и перечисление всех ресурсов, загруженных всеми плагинами и темой) слишком хлопотно процесса, и с помощью других инструментов для создания JavaScript и CSS-файлы, такие как Service Worker Precache,на самом деле не будут работать в этом контексте по двум основным причинам:
- Сервисный работник Precache работает, сканируя файлы в указанной папке и фильтруя их с помощью подстановочных знаков. Тем не менее, файлы, которые WordPress судов с гораздо больше, чем фактические из них, необходимых для применения, так что мы, скорее всего, precaching много избыточных файлов.
- WordPress прикрепляет номер версии к запрашиваемому файлу, который варьируется от файла к файлу, например:
https://www.mydomain.com/wp-includes/js/utils.min.js?ver=4.6.1
https://www.mydomain.com/wp-includes/js/jquery/jquery-migrate.min.js?ver=1.4.1
https://www.mydomain.com/wp-includes/js/jquery/jquery.js?ver=1.12.4
Работники службы перехватывают каждый запрос на основе полного пути запроса ресурса, включая параметры, такие как номер версии, в данном случае. Поскольку инструмент Precache работника службы не знает о номере версии, он не сможет правильно создать необходимый список.
Разрешить плагины крючок в поколение сервис-workers.js
Добавление крючков в нашу функциональность позволяет нам расширить функциональность сервисных работников. Например, сторонние плагины могут подключиться к списку предварительно кэшированных ресурсов, чтобы добавить свои собственные ресурсы, или указать, какую стратегию кэширования использовать в зависимости от их шаблона URL, среди других.
Кэширование внешних ресурсов: Определите список доменов и проверите ресурсы для precache Originate из любого из них.
Всякий раз, когда ресурс исходит из домена веб-сайта, он всегда может быть обработан с помощью работников службы. Всякий раз, когда нет, он все еще может быть извлечен, но мы должны использовать no-cors
режим получения. Этот тип запроса приведет к непрозрачному ответу, поэтому мы не сможем проверить, был ли запрос успешным; однако, мы все еще можем precache эти ресурсы и позволить веб-сайт, чтобы быть browsable в автономном режиме.
Поддержка нескольких представлений
Предположим, что URL-адрес содержит параметр, указывающий, какое представление использовать для визуализации веб-сайта, например:
- Представление по умолчанию:
https://www.mydomain.com/
(?view=default
) - Воспламеняемый вид:
https://www.mydomain.com/?view=embed
- Печатная вид:
https://www.mydomain.com/?view=print
Несколько аповов могут быть предварительно кэшированы, каждая из которых представляет представление:
https://www.mydomain.com/appshell/?view=default
https://www.mydomain.com/appshell/?view=embed
https://www.mydomain.com/appshell/?view=print
Затем, при загрузке веб-сайта, мы извлекаем значение view
параметра из URL и загружаем соответствующую оболочку во время выполнения.
поддержка i18n
Мы предполагаем, что языковой код является частью URL, как это:https://www.mydomain.com/language-code/path/to/the/page/
Одним из возможных подходов было бы разработать веб-сайт, чтобы обеспечить различные service-workers.js
файлы для каждого языка, каждый из которых будет использоваться в соответствии с его соответствующей языковой области: service-worker-en.js
файл для области en/
английского языка, service-worker-es.js
для сферы для es/
испанского языка, и так далее. Однако при доступе к общим ресурсам, таким как файлы JavaScript и CSS, расположенные в папке, возникают wp-content/
конфликты. Эти ресурсы одинаковы для всех языков; их URL-адреса не содержат никакой информации о языке. Добавление еще одного service-workers.js
файла для решения всех неязыковых областей добавит нежелательной сложности.
Более простой подход был бы использовать тот же метод, как выше для визуализации нескольких представлений: Зарегистрируйте уникальный service-workers.js
файл, который уже содержит всю информацию для всех языков, и принять решение о времени выполнения, какой язык использовать путем извлечения языка код из запрошенного URL-
Для поддержки всех функций, описанных до сих пор, скажем, для трех представлений (по умолчанию, встраиваемый и печатный вид) и двух языков (английский и испанский), нам необходимо создать следующие приложения:
https://www.mydomain.com/en/appshell/?view=default
https://www.mydomain.com/en/appshell/?view=embed
https://www.mydomain.com/en/appshell/?view=print
https://www.mydomain.com/es/appshell/?view=default
https://www.mydomain.com/es/appshell/?view=embed
https://www.mydomain.com/es/appshell/?view=print
Используйте различные стратегии кэширования
Приложение должно иметь возможность выбирать из нескольких стратегий кэширования для поддержки различных моделей поведения на разных страницах. Вот несколько возможных сценариев:
- Страница может быть извлечена из кэша большую часть времени, но в некоторых случаях она должна быть извлечена из сети (например, для просмотра публикации после его редактирования).
- Страница может иметь состояние пользователя и, как таковая, не может быть кэширована (например, «Изменить свою учетную запись», «Мои сообщения»).
- Страницу можно запросить в фоновом режиме, чтобы принести дополнительные данные (например, ленивые комментарии к публикации)
Вот стратегии кэширования и когда использовать каждый из них:
-
Кэш, возвращаясь к сети
-
Статические активы (Файлы JavaScript и CSS, изображения и т.д.)
Статическое содержимое никогда не будет обновляться: JavaScript и CSS файлы имеют номер версии, и загрузка одного и того же изображения во второй раз в медиа-менеджер WordPress изменит имя файла изображения. Таким образом, кэшированные статичные активы не станут устаревшими. -
Аппшелули
Мы хотим, чтобы приложение загрузилось немедленно, поэтому мы извлекаем его из кэша. Если приложение обновлено и приложение изменяется, то изменяя номер версии, установится новая версия работника службы и загрузит последнюю версию приложения.
-
Статические активы (Файлы JavaScript и CSS, изображения и т.д.)
-
Кэш, затем сеть
-
Общее содержание
Получая содержимое из кэша для его немедленного отображения, мы также отправляем запрос на получение содержимого с сервера. Мы сравниваем две версии с помощью заголовка ETag,и если содержимое изменилось, мы кэшаем ответ сервера (т.е. наиболее актуальные из двух), а затем показать сообщение пользователю: «Эта страница была обновлена, пожалуйста, нажмите здесь, чтобы обновить его.»
-
Общее содержание
-
Только сеть
-
Принудительное содержание, чтобы быть в курсе
Содержимое, которое обычно использует стратегию «кэш, то сеть» может быть вынужден оставить на использовании стратегии «только сеть», искусственно добавив параметрsw-strategy=networkfirst
илиsw-networkfirst=true
в запрашиваемый URL. Этот параметр может быть удален до отправки запроса на сервер. -
Содержимое с состоянием пользователя
Мы не хотим кэшировать любой контент с состоянием пользователя, из-за безопасности. (Мы можем удалить кэш состояния пользователя, когда пользователь вырегистрируется, но реализация этого является более сложной.)
-
Принудительное содержание, чтобы быть в курсе
-
Сеть, возвращаясь к кэшу
-
Ленивый нагруженный контент
Содержимое лениво загружается, когда пользователь не увидит его сразу, что позволяет содержимому, которое пользователь видит, немедленно загружать ся. (Например, сообщение будет немедленно загружаться, а его комментарии будут загружены ленивыми, поскольку они отображаются в нижней части страницы.) Поскольку это не будет видно сразу, извлечение этого содержимого прямо из кэша также не является необходимым; вместо этого всегда старайтесь получить самую актуальную версию с сервера.
-
Ленивый нагруженный контент
Игнорировать определенные данные при генерации заголовка ETag для стратегии «Кэш- затем сеть»
ETag может быть сгенерирован с помощью функции хэша; очень крошечные изменения в входе будет производить совершенно другой выход. Таким образом, значения, которые не считаются важными и которые могут стать устаревшими, не должны учитываться при генерации ETag. В противном случае, пользователь может быть предложено с сообщением «Эта страница была обновлена, пожалуйста, нажмите здесь, чтобы обновить его» для каждого крошечного изменения, такие как счетчик комментариев происходит от 5 до 6.
Реализации
Весь приведенный ниже код, слегка адаптированный для этой статьи, можно найти в репозитории GitHub. Источники для sw-template.js и sw.php файлов, упомянутых ниже, также доступны. Кроме того, доступен пример генерируемого файла service-workers.js.
Создание файла service-workers.js на Runtime
Мы решили автоматически извлечь все файлы JavaScript и CSS, которые должны быть использованы приложением, добавленные через и wp_enqueue_script
wp_enqueue_style
функции, для того, чтобы экспортировать эти ресурсы в список Precache работника службы. Это означает, что service-workers.js
файл будет сгенерирован во время выполнения.
Когда и как он должен быть создан? Запуск действия для создания файла admin-ajax.php
через (например, вызов https://www.mydomain.com/wp-admin/admin-ajax.php?action=create-sw
) не будет работать, потому что это будет загружать WordPress’admin области. Вместо этого нам нужно загрузить все файлы JavaScript и CSS с переднего конца, которые, безусловно, будут отличаться.
Решение заключается в создании частной страницы на веб-сайте (скрытые от просмотра), доступ через https://www.mydomain.com/create-sw/
, который будет выполнять функциональность для создания service-workers.js
файла. Создание файла должно происходить в самом конце выполнения запроса, так что все файлы JavaScript и CSS будут к тому времени включены в очередь:
function generate_sw_shortcode($atts) {
add_action('wp_footer', 'generate_sw_files', PHP_INT_MAX);
}
add_shortcode('generate_sw', 'generate_sw_shortcode');
Сгенерированный файл может быть помещен в корень веб-сайта (т.е. рядом) wp-config.php
для предоставления ему /
сферы охвата. Тем не менее, размещение файлов в корневой папке веб-сайта не всегда целесообразно, например, для безопасности (корневая папка должна иметь очень ограничительные разрешения на запись) и для обслуживания (если service-workers.js
бы они были созданы плагином, и этот был отключен, потому что его папка была переименована, после чего service-workers.js
файл может никогда не быть удален).
К счастью, есть и другая возможность. Мы можем поместить service-workers.js
файл в любой каталог, например, wp-content/sw/
и добавить .htaccess
файл, который предоставляет доступ к корневой области:
function generate_sw_files() {
$dir = WP_CONTENT_DIR."/sw/"
// Create the directory structure
if (!file_exists($dir)) {
@mkdir($dir, 0755, true);
}
// Generate Service Worker .js file
save_file($dir.'service-workers.js', get_sw_contents());
// Generate the file to register the Service Worker
save_file($dir.'sw-registrar.js', get_sw_registrar_contents());
// Generate the .htaccess file to allow access to the scope (/)
save_file($dir.'.htaccess', get_sw_htaccess_contents());
}
function get_sw_registrar_contents() {
return '
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/wp-content/sw/service-workers.js", {
scope: "/"
});
}
';
}
function get_sw_htaccess_contents() {
return '
<FilesMatch "service-workers.js$">
Header set Service-Worker-Allowed: /
</FilesMatch>
';
}
function save_file($file, $contents) {
// Open the file, write content and close it
$handle = fopen($file, "wb");
$numbytes = fwrite($handle, $contents);
fclose($handle);
return $file;
}
По соображениям безопасности мы можем добавить некоторую проверку function generate_sw_files()
перед выполнением:
- предоставить действительный ключ доступа в качестве параметра.
- чтобы убедиться, что он может быть запрошен только с одного и того же сервера.
if ($_REQUEST['accesskey'] != $ACCESS_KEY) {
die;
}
if (!in_array(getenv('HTTP_CLIENT_IP'), array('localhost', '127.0.0.1', '::1'))) {
die;
}
Чтобы запросить https://www.mydomain.com/create-sw/
с сервера и сделать это с одного сервера, мы хотели бы выполнить:
wget -q https://www.mydomain.com/create-sw/?accesskey=…
Из массива серверов, стоящих за балансером нагрузки, или из стека серверов в облаке с помощью автоматического масштабирования, мы не можем wget URL
выполнить, потому что мы не знаем, какой сервер будет обслуживать запрос. Вместо этого мы можем непосредственно выполнить процесс PHP, php-cgi
используя:
cd /var/www/html/
sudo SCRIPT_FILENAME=index.php SCRIPT_NAME=/index.php REMOTE_ADDR=127.0.0.1 REDIRECT_STATUS=200 SERVER_PROTOCOL=HTTP/1.1 REQUEST_METHOD=GET HTTPS=on SERVER_NAME=www.mydomain.com HTTP_HOST=www.mydomain.com SERVER_PORT=80 REQUEST_URI=/create-sw/ QUERY_STRING="accesskey=…" php-cgi > /dev/null
Если вам неудобно иметь эту страницу на производственном сервере, этот процесс также может быть запущен в промежуточной среде, если он имеет точно такую же конфигурацию, как производственный сервер (т.е. база данных должна иметь те же данные; все константы в wp-config.php
должны иметь те же значения; URL-адрес для доступа к веб-сайту на промежуточном сервере должен быть таким же, как и сам веб-сайт и т.д.). Затем вновь созданный service-workers.js
файл должен быть скопирован с постановочных на производственные серверы во время развертывания веб-сайта.
Содержимое сервис-работников.js
Создание service-workers.js
функции PHP означает, что мы можем предоставить шаблон этого файла, который будет декларировать, какие переменные ему нужны, и логику работника службы. Затем, во время выполнения, переменные будут заменены фактическими значениями. Мы также можем удобно добавлять крючки, чтобы плагины добавляли свои собственные требуемые значения. (Дополнительные переменные конфигурации будут добавлены позже в этой статье).
function get_sw_contents() {
// $sw_template has the path to the service-worker template
$sw_template = dirname(__FILE__).'/assets/sw-template.js';
$contents = file_get_contents($sw_template);
foreach (get_sw_configuration() as $key => $replacement) {
$value = json_encode($replacement);
$contents = str_replace($key, $value, $contents);
}
return $contents;
}
function get_sw_configuration() {
$configuration = array();
$configuration['$version'] = get_sw_version();
…
return $configuration;
}
var config = {
version: $version,
…
};
Внедрение типов ресурсов, который является способом разделения активов на группы, позволяет реализовать различные поведения в логике работника службы. Нам понадобятся следующие типы ресурсов:
-
Html
Производится только при первой загрузке сайта. После этого весь контент динамически запрашивается с помощью API приложения, ответ которого находится в формате JSON -
Json
API для получения и публикации контента -
Статический
Любые активы, такие как JavaScript, CSS, PDF, изображение и т.д.
Типы ресурсов могут быть использованы для кэширования ресурсов, но это необязательно (это только делает логику более управляемой). Они необходимы для:
- выбор соответствующей стратегии кэширования (для статического, кэш-первых и для JSON, в первую очередь);
- определение путей не перехватывать (что-нибудь под
wp-content/
Для JSON, но не для статических, или что-нибудь окончание.php
для статических, для динамически генерируемых изображений, но не для JSON).
function get_sw_resourcetypes() {
return array('static', 'json', 'html');
}
function getResourceType(request) {
var acceptHeader = request.headers.get('Accept');
var resourceType = 'static';
if (acceptHeader.indexOf('text/html') !== -1) {
resourceType = 'html';
}
else if (acceptHeader.indexOf('application/json') !== -1) {
resourceType = 'json';
}
return resourceType;
}
Мы определим, для каких шаблонов URL мы не хотим, чтобы работник службы перехватил запрос. Список ресурсов, чтобы исключить первоначально пуст, просто содержащие крючок, чтобы придать все значения.
-
$excludedFullPaths
Полные пути, чтобы исключить. -
$excludedPartialPaths
Пути, чтобы исключить, появляясь после домашнего URL (например,articles
будет исключать,https://www.mydomain.com/articles/
но неhttps://www.mydomain.com/posts/articles/
). Частичные пути полезны, когда URL содержит языковую информацию (например,https://www.mydomain.com/en/articles/
), поэтому один путь исключает эту страницу для всех языков (в этом случае домашний URLhttps://www.mydomain.com/en/
будет). Подробнее об этом позже.
function get_sw_configuration() {
…
$resourceTypes = get_sw_resourcetypes();
$configuration['$excludedFullPaths'] = $configuration['$excludedPartialPaths'] = array();
foreach ($resourceTypes as $resourceType) {
$configuration['$excludedFullPaths'][$resourceType] = apply_filters('PoP_ServiceWorkers_Job_Fetch:exclude:full', array(), $resourceType);
$configuration['$excludedPartialPaths'][$resourceType] = apply_filters('PoP_ServiceWorkers_Job_Fetch:exclude:partial', array(), $resourceType);
}
…
}
opts.locales.domain
будет рассчитано на время выполнения (подробнее об этом позже).
var config = {
…
excludedPaths: {
full: $excludedFullPaths,
partial: $excludedPartialPaths
},
…
};
self.addEventListener('fetch', event => {
function shouldHandleFetch(event, opts) {
var request = event.request;
var resourceType = getResourceType(request);
var url = new URL(request.url);
var fullExcluded = opts.excludedPaths.full[resourceType].some(path => request.url.startsWith(path)),
var partialExcluded = opts.excludedPaths.partial[resourceType].some(path => request.url.startsWith(opts.locales.domain+path));
if (fullExcluded || partialExcluded) return false;
if (resourceType == 'static') {
// Do not handla dynamic images, eg: the Captcha image, captcha.png.php
var isDynamic = request.url.endsWith('.php') && request.url.indexOf('.php?') === -1;
if (isDynamic) return false;
}
…
}
…
});
wp-content/
с, который работает только для типа ресурса «статический».
class PoP_ServiceWorkers_Hooks_WPExclude {
function __construct() {
add_filter('PoP_ServiceWorkers_Job_Fetch:exclude:full', array($this, 'get_excluded_fullpaths'), 10, 2);
}
function get_excluded_fullpaths($excluded, $resourceType) {
if ($resourceType == 'json' || $resourceType == 'html') {
// Do not intercept access to the WP Dashboard
$excluded[] = admin_url();
$excluded[] = content_url();
$excluded[] = includes_url();
}
elseif ($resourceType == 'static') {
// Do not cache the service-workers.js file!!!
$excluded[] = WP_CONTENT_DIR.'/sw/service-workers.js';
}
return $excluded;
}
}
new PoP_ServiceWorkers_Hooks_WPExclude();
Предедомовые ресурсы
Для того, чтобы сайт WordPress работать в автономном режиме, мы должны получить полный список необходимых ресурсов и precache их. Мы хотим иметь возможность кэшировать как локальные, так и внешние ресурсы (например, с CDN).
-
$origins
Определите, из каких доменов мы позволяем сервис-работнику перехватить запрос (например, из нашего собственного домена плюс наш CDN). -
$cacheItems
Список ресурсов для precache. Первоначально это пустой массив, обеспечивающий крючок для впрыскивания всех значений.
var config = {
…
cacheItems: $cacheItems,
origins: $origins,
…
};
function get_sw_configuration() {
…
$resourceTypes = get_sw_resourcetypes();
$configuration['$origins'] = get_sw_allowed_domains();
$configuration['$cacheItems'] = array();
foreach ($resourceTypes as $resourceType) {
$configuration['$cacheItems'][$resourceType] = return apply_filters('PoP_ServiceWorkers_Job_CacheResources:precache', array(), $resourceType);
}
…
}
function get_sw_allowed_domains() {
return array(
get_site_url(), // 'https://www.mydomain.com',
'https://cdn.mydomain.com'
);
}
cache.addAll
не будет работать. Вместо этого нам нужно использовать fetch
функцию, передавая параметр {mode: 'no-cors'}
для них.
self.addEventListener('install', event => {
function onInstall(event, opts) {
var resourceTypes = ['static', 'json', 'html'];
return Promise.all(resourceTypes.map(function(resourceType) {
return caches.open(cacheName(resourceType, opts)).then(function(cache) {
return Promise.all(opts.cacheItems[resourceType].map(function(url) {
return fetch(url, (new URL(url)).origin === self.location.origin ? {} : {mode: 'no-cors'}).then(function(response) {
return cache.put(url, response.clone());
});
}))
})
}))
}
event.waitUntil(
onInstall(event, config).then( () => self.skipWaiting() )
);
});
https://cdnjs.cloudflare.com
):
self.addEventListener('fetch', event => {
function shouldHandleFetch(event, opts) {
…
var fromMyOrigins = opts.origins.indexOf(url.origin) > -1;
var precached = opts.cacheItems[resourceType].indexOf(url) > -1;
if (!(fromMyOrigins || precached)) return false;
…
}
…
});
Активы загружаются wp_enqueue_script
через и могут быть script_loader_tag
извлечены легко. Поиск других активов включает в себя ручной процесс, в зависимости от того, они приходят из WordPress основных файлов, от темы или от установленных плагинов:
- изображения;
- CSS и JavaScript не загружены до конца
wp_enqueue_script
иscript_loader_tag
; - Файлы JavaScript условно загружены (например, добавленные между
html
тегами); - ресурсы, запрошенные во время выполнения (например, тема TinyMCE, файлы кожи и плагина);
- ссылки на файлы JavaScript, закодированные другим файлом JavaScript;
- Файлы шрифтов, упомянутые в файлах CSS (TTF, WOFF и т.д.);
- локаль файлы;
- файлы i18n.
Чтобы получить все файлы JavaScript загружены через wp_enqueue_script
функцию, мы бы подключить в script_loader_tag
, и для всех файлов CSS загружены через wp_enqueue_style
функцию, мы бы подключить в style_loader_tag
:
class PoP_ServiceWorkers_Hooks_WP {
private $scripts, $styles, $dom;
function __construct() {
$this->scripts = $this->styles = array();
$this->doc = new DOMDocument();
add_filter('script_loader_tag', array($this, 'script_loader_tag'));
add_filter('style_loader_tag', array($this, 'style_loader_tag'));
…
}
function script_loader_tag($tag) {
if (!empty($tag)) {
$this->doc->loadHTML($tag);
foreach($this->doc->getElementsByTagName('script') as $script) {
if($script->hasAttribute('src')) {
$this->scripts[] = $script->getAttribute('src');
}
}
}
return $tag;
}
function style_loader_tag($tag) {
if (!empty($tag)) {
$this->doc->loadHTML($tag);
foreach($this->doc->getElementsByTagName('link') as $link) {
if($link->hasAttribute('href')) {
$this->styles[] = $link->getAttribute('href');
}
}
}
return $tag;
}
…
}
new PoP_ServiceWorkers_Hooks_WP();
Затем мы просто добавляем все эти ресурсы в список precache:
class PoP_ServiceWorkers_Hooks_WP {
function __construct() {
…
add_filter('PoP_ServiceWorkers_Job_CacheResources:precache', array($this, 'get_precache_list'), 10, 2);
}
function get_precache_list($precache, $resourceType) {
if ($resourceType == 'static') {
$precache = array_merge(
$precache,
$this->scripts,
$this->styles
);
}
return $precache;
}
}
WordPress загрузит несколько файлов, которые должны быть добавлены вручную. Обратите внимание, что ссылка на файл должна быть добавлена точно так же, как это будет предложено, включая все параметры. Таким образом, этот процесс включает в себя много копирования и вставки из исходного кода:
class PoP_ServiceWorkers_Hooks_WPManual {
function __construct() {
add_filter('PoP_ServiceWorkers_Job_CacheResources:precache', array($this, 'get_precache_list'), 10, 2);
}
function get_precache_list($precache, $resourceType) {
if ($resourceType == 'static') {
// File json2.min.js is not added through the $scripts list because it's 'lt IE 8'
global $wp_scripts;
$suffix = SCRIPT_DEBUG ? '' : '.min';
$this->scripts[] = add_query_arg('ver', '2015-05-03', $wp_scripts->base_url."/wp-includes/js/json2$suffix.js");
// Needed for the thickboxL10n['loadingAnimation'] javascript code produced in the front-end, loaded in wp-includes/script-loader.php
$precache[] = includes_url('js/thickbox/loadingAnimation.gif');
}
return $precache;
}
}
new PoP_ServiceWorkers_Hooks_WPManual();
TinyMCE представляет собой трудную задачу для получения своего списка ресурсов, потому что файлы, которые он загружает (например, плагины, скины и тематические файлы) на самом деле создаются и запрашиваются во время выполнения. Кроме того, полный путь ресурса не печатается в HTML-коде, а собирается в функции JavaScript. Таким образом, чтобы получить список ресурсов, можно проверить исходный код TinyMCE и проверить, как он генерирует имена файлов, или угадать их, создав редактор TinyMCE при проверке Chrome’s Developer Tools «Сеть» вкладку и видя, какие файлы он запрашивает. Делая последнее, я смог вывести все имена файлов (например, для тематических файлов, путь представляет собой сочетание домена, имя темы и версии в качестве параметров).
Чтобы получить конфигурацию TinyMCE, которая будет использоваться во время выполнения, мы подключаем store_tinymce_resources
и teeny_mce_before_init
проверяем значения, установленные в $mceInit
переменной:
class PoP_ServiceWorkers_Hooks_TinyMCE {
private $content_css, $external_plugins, $plugins, $others;
function __construct() {
$this->content_css = $this->external_plugins = $this->plugins = $this->others = array();
// Execute last one
add_filter('teeny_mce_before_init', array($this, 'store_tinymce_resources'), PHP_INT_MAX, 1);
add_filter('tiny_mce_before_init', array($this, 'store_tinymce_resources'), PHP_INT_MAX, 1);
}
function store_tinymce_resources($mceInit) {
// Code copied from wp-includes/class-wp-editor.php function editor_js()
$suffix = SCRIPT_DEBUG ? '' : '.min';
$baseurl = includes_url( 'js/tinymce' );
$cache_suffix = $mceInit['cache_suffix'];
if ($content_css = $mceInit['content_css']) {
foreach (explode(',', $content_css) as $content_css_item) {
// The $cache_suffix is added in runtime, it can be safely added already. Eg: wp-includes/css/dashicons.min.css?ver=4.6.1&wp-mce-4401-20160726
$this->content_css[] = $content_css_item.'&'.$cache_suffix;
}
}
if ($external_plugins = $mceInit['external_plugins']) {
if ($external_plugins = json_decode($external_plugins, true)) {
foreach ($external_plugins as $plugin) {
$this->external_plugins[] = "{$plugin}?{$cache_suffix}";
}
}
}
if ($plugins = $mceInit['plugins']) {
if ($plugins = explode(',', $plugins)) {
// These URLs are generated on runtime in TinyMCE, without a $version
foreach ($plugins as $plugin) {
$this->plugins[] = "{$baseurl}/plugins/{$plugin}/plugin{$suffix}.js?{$cache_suffix}";
}
if (in_array('wpembed', $plugins)) {
// Reference to file wp-embed.js, without any parameter, is hardcoded inside file wp-includes/js/tinymce/plugins/wpembed/plugin.min.js!!!
$this->others[] = includes_url( 'js' )."/wp-embed.js";
}
}
}
if ($skin = $mceInit['skin']) {
// Must produce: wp-includes/js/tinymce/skins/lightgray/content.min.css?wp-mce-4401-20160726
$this->others[] = "{$baseurl}/skins/{$skin}/content{$suffix}.css?{$cache_suffix}";
$this->others[] = "{$baseurl}/skins/{$skin}/skin{$suffix}.css?{$cache_suffix}";
// Must produce: wp-includes/js/tinymce/skins/lightgray/fonts/tinymce.woff
$this->others[] = "{$baseurl}/skins/{$skin}/fonts/tinymce.woff";
}
if ($theme = $mceInit['theme']) {
// Must produce: wp-includes/js/tinymce/themes/modern/theme.min.js?wp-mce-4401-20160726
$this->others[] = "{$baseurl}/themes/{$theme}/theme{$suffix}.js?{$cache_suffix}";
}
// Files below are always requested. Code copied from wp-includes/class-wp-editor.php function editor_js()
global $wp_version, $tinymce_version;
$version = 'ver=' . $tinymce_version;
$mce_suffix = false !== strpos( $wp_version, '-src' ) ? '' : '.min';
$this->others[] = "{$baseurl}/tinymce{$mce_suffix}.js?$version";
$this->others[] = "{$baseurl}/plugins/compat3x/plugin{$suffix}.js?$version";
$this->others[] = "{$baseurl}/langs/wp-langs-en.js?$version";
return $mceInit;
}
}
new PoP_ServiceWorkers_Hooks_TinyMCE();
Наконец, мы добавляем извлеченные ресурсы в список precache:
class PoP_ServiceWorkers_Hooks_TinyMCE {
function __construct() {
…
add_filter('PoP_ServiceWorkers_Job_CacheResources:precache', array($this, 'get_precache_list'), 1000, 2);
}
…
function get_precache_list($precache, $resourceType) {
if ($resourceType == 'static') {
// In addition, add all the files in the tinymce plugins folder, since these will be needed during runtime when initializing the tinymce textarea
$precache = array_merge(
$precache,
$this->content_css,
$this->external_plugins,
$this->plugins,
$this->others
);
}
return $precache;
}
}
Мы должны также precache все изображения, требуемые темой и все плагины. В приведенном ниже коде мы предкэшировали все файлы темы в папке, img/
предполагая, что они запрашиваются без добавления параметров:
class PoPTheme_Wassup_ServiceWorkers_Hooks_ThemeImages {
function __construct() {
add_filter('PoP_ServiceWorkers_Job_CacheResources:precache', array($this, 'get_precache_list'), 10, 2);
}
function get_precache_list($precache, $resourceType) {
if ($resourceType == 'static') {
// Add all the images from the active theme
$theme_dir = get_stylesheet_directory();
$theme_uri = get_stylesheet_directory_uri();
foreach (glob($theme_dir."/img/*") as $file) {
$precache[] = str_replace($theme_dir, $theme_uri, $file);
}
}
return $precache;
}
}
new PoPTheme_Wassup_ServiceWorkers_Hooks_ThemeImages();
Если мы используем Twitter Bootstrap, загруженный из CDN (например, https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css
), то мы должны precache соответствующие файлы шрифта глифокона:
class PoPTheme_Wassup_ServiceWorkers_Hooks_Bootstrap {
function __construct() {
add_filter('PoP_ServiceWorkers_Job_CacheResources:precache', array($this, 'get_precache_list'), 10, 2);
}
function get_precache_list($precache, $resourceType) {
if ($resourceType == 'static') {
// Add all the fonts needed by Bootstrap inside the bootstrap.min.css file
$precache[] = 'https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/fonts/glyphicons-halflings-regular.eot';
$precache[] = 'https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/fonts/glyphicons-halflings-regular.svg';
$precache[] = 'https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/fonts/glyphicons-halflings-regular.ttf';
$precache[] = 'https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/fonts/glyphicons-halflings-regular.woff';
$precache[] = 'https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/fonts/glyphicons-halflings-regular.woff2';
}
return $precache;
}
}
new PoPTheme_Wassup_ServiceWorkers_Hooks_Bootstrap();
Все языковые ресурсы для всех языков также должны быть предварительно засекречены, так что веб-сайт может быть загружен на любом языке, когда в автономном режиме. В приведенном ниже коде мы предполагаем, что плагин имеет js/locales/
папку с файлами перевода locale-en.js
и locale-es.js
т.д.:
class PoP_UserAvatar_ServiceWorkers_Hooks_Locales {
function __construct() {
add_filter('PoP_ServiceWorkers_Job_CacheResources:precache', array($this, 'get_precache_list'), 10, 2);
}
function get_precache_list($precache, $resourceType) {
if ($resourceType == 'static') {
$dir = dirname(__FILE__));
$url = plugins_url('', __FILE__));
foreach (glob($dir."/js/locales/fileupload/*") as $file) {
$precache[] = str_replace($dir, $url, $file);
}
}
return $precache;
}
}
new PoP_UserAvatar_ServiceWorkers_Hooks_Locales();
Стратегии некэширования
В следующем, мы будем углубляться в кэширование и не-кэширования стратегий. Давайте сначала рассмотрим стратегию некэширования:
-
Только сеть
Всякий раз, когда запросы JSON имеют состояние пользователя.
Чтобы не кэшировать запрос, если мы знаем, какие URL-адреса не должны быть кэшированы заранее, то мы можем просто добавить их полные или частичные пути в список исключенных элементов. Например, ниже мы установили все страницы, на которых есть состояние пользователя (например, «Мои публикации» и «Изменить мой профиль»), чтобы не перехватываться работником службы, потому что мы не хотим кэшировать любую личную информацию пользователя:
function get_page_path($page_id) {
$page_path = substr(get_permalink($page_id), strlen(home_url()));
// Remove the first and last '/'
if ($page_path[0] == '/') $page_path = substr($page_path, 1);
if ($page_path[strlen($page_path)-1] == '/') $page_path = substr($page_path, 0, strlen($page_path)-1);
return $page_path;
}
class PoP_ServiceWorkers_Hooks_UserState {
function __construct() {
add_filter('PoP_ServiceWorkers_Job_Fetch:exclude:partial', array($this, 'get_excluded_partialpaths'), 10, 2);
}
function get_excluded_partialpaths($excluded, $resourceType) {
if ($resourceType == 'json') {
// Variable $USER_STATE_PAGES contains all IDs of pages that have a user state
foreach ($USER_STATE_PAGES as $page_id) {
$excluded[] = get_page_path($page_id);
}
}
return $excluded;
}
}
new PoP_ServiceWorkers_Hooks_UserState();
Если стратегия некэширования должна применяться во время выполнения, то мы можем добавить параметр sw-networkonly=true
или sw-strategy=networkonly
к запрошенному URL-адресу и уволить его с работником службы в функции: shouldHandleFetch
self.addEventListener('fetch', event => {
function shouldHandleFetch(event, opts) {
…
var params = getParams(url);
if (params['sw-strategy'] == 'networkonly') return false;
…
}
…
});
Приложение использует следующие стратегии кэширования, в зависимости от типа ресурса и функционального использования:
- Кэш, отсеивая обратно в сеть для апогной оболочки и статических ресурсов.
- Кэш, затем сеть для запросов JSON.
- Сеть, отскакивая к кэшу для запросов JSON, которые не затягивают ожидания пользователя, например, данные с ленивыми загрузками (например, комментарии к публикации) или которые должны быть актуальными (например, просмотр публикации после его обновления).
Как статические, так и типы ресурсов HTML всегда требуют одной и той же стратегии. Только тип ресурсов JSON может быть переключен между стратегиями. Мы устанавливаем стратегию «кэш, а затем сеть» как стратегию по умолчанию и определяем правила по запрашиваемому URL-адресу для переключения на «сеть, отскакивая к кэшу»:
-
startsWith
URL-адрес начинается с заданного полного или частичного пути. -
hasParams
URL содержит заранее определенный параметр. Параметрsw-networkfirst
уже определен, поэтому запросhttps://www.mydomain.com/en/?output=json
будет использовать стратегию «кэш первый», в то время какhttps://www.mydomain.com/en/?output=json&sw-networkfirst=true
переключится на «сеть в первую очередь».
// Cache falling back to network
const SW_CACHEFIRST = 1;
// Cache then network
const SW_CACHEFIRSTTHENREFRESH = 2;
// Network falling back to cache
const SW_NETWORKFIRST = 3;
var config = {
…
strategies: $strategies,
…
};
function get_sw_configuration() {
…
$resourceTypes = get_sw_resourcetypes();
$configuration['$strategies'] = array();
foreach ($resourceTypes as $resourceType) {
$strategies = array();
if ($resourceType == 'json') {
$strategies['networkFirst'] = array(
'startsWith' => array(
'full' => apply_filters('PoP_ServiceWorkers_Job_Fetch:strategies:json:networkFirst:startsWith:full', array()),
'partial' => apply_filters('PoP_ServiceWorkers_Job_Fetch:strategies:json:networkFirst:startsWith:partial', array()),
),
'hasParams' => apply_filters('PoP_ServiceWorkers_Job_Fetch:strategies:json:networkFirst:hasParams', array('sw-networkfirst')),
);
}
$configuration['$strategies'][$resourceType] = $strategies;
}
…
}
class PoP_ServiceWorkers_Hooks_LazyLoaded {
function __construct() {
add_filter('PoP_ServiceWorkers_Job_Fetch:strategies:json:networkFirst:startsWith:partial', array($this, 'get_networkfirst_json_partialpaths'));
}
function get_networkfirst_json_partialpaths($paths) {
foreach ($LAZY_LOADED_PAGES as $page_id) {
$paths[] = get_page_path($page_id);
}
return $paths;
}
}
new PoP_ServiceWorkers_Hooks_LazyLoaded();
После слияния sw-template.js
service-workers.js
в, это будет выглядеть следующим образом:
var config = {
…
strategies: {
json: {
networkFirst: {
startsWith: {
partial: […]
},
hasParams: […]
}
}
},
…
};
Наконец, мы перейдем к логике в service-workers.js
файле. Пожалуйста, обратите внимание, что для получения запросов JSON нам также необходимо добавить в параметр кэша sw-cachebust
URL-параметра с отметкой времени, чтобы избежать получения ответа от кэша HTTP браузера.
function getCacheBustRequest(request, opts) {
var url = new URL(request.url);
// Put in a cache-busting parameter to ensure we’re caching a fresh response.
if (url.search) {
url.search += '&';
}
url.search += 'sw-cachebust=' + Date.now();
return new Request(url.toString());
}
function addToCache(cacheKey, request, response) {
if (response.ok) {
var copy = response.clone();
caches.open(cacheKey).then( cache => {
cache.put(request, copy);
});
}
return response;
}
self.addEventListener('fetch', event => {
function getStrategy(request, opts) {
var strategy = '';
var resourceType = getResourceType(request);
// JSON requests have two strategies: cache first + update (the default) or network first
if (resourceType === 'json') {
var networkFirst = opts.strategies[resourceType].networkFirst;
var criteria = {
startsWith: networkFirst.startsWith.full.some(path => request.url.startsWith(path)),
// The pages do not included the locale domain, so add it before doing the comparison
pageStartsWith: networkFirst.startsWith.partial.some(path => request.url.startsWith(opts.locales.domain+path)),
// Code for function stripIgnoredUrlParameters is in https://github.com/leoloso/PoP/blob/master/wp-content/plugins/pop-serviceworkers/kernel/serviceworkers/assets/js/jobs/lib/utils.js
hasParams: stripIgnoredUrlParameters(request.url, networkFirst.hasParams) != request.url
}
var successCriteria = Object.keys(criteria).filter(criteriaKey => criteria[criteriaKey]);
if (successCriteria.length) {
strategy = SW_NETWORKFIRST;
}
else {
strategy = SW_CACHEFIRSTTHENREFRESH;
}
}
else if (resourceType === 'html' || resourceType === 'static') {
strategy = SW_CACHEFIRST;
}
return strategy;
}
function onFetch(event, opts) {
var request = event.request;
var resourceType = getResourceType(request);
var cacheKey = cacheName(resourceType, opts);
var strategy = getStrategy(request, opts);
var cacheBustRequest = getCacheBustRequest(request, opts);
if (strategy === SW_CACHEFIRST || strategy === SW_CACHEFIRSTTHENREFRESH) {
/* Load immediately from the Cache */
event.respondWith(
fetchFromCache(request)
.catch(() => fetch(request))
.then(response => addToCache(cacheKey, request, response))
);
/* Bring fresh content from the server, and show a message to the user if the cached content is stale */
if (strategy === SW_CACHEFIRSTTHENREFRESH) {
event.waitUntil(
fetch(cacheBustRequest)
.then(response => addToCache(cacheKey, request, response))
.then(response => refresh(request, response))
);
}
}
else if (strategy === SW_NETWORKFIRST) {
event.respondWith(
fetch(cacheBustRequest)
.then(response => addToCache(cacheKey, request, response))
.catch(() => fetchFromCache(request))
.catch(function(err) {/*console.log(err)*/})
);
}
}
if (shouldHandleFetch(event, config)) {
onFetch(event, config);
}
});
refresh
функцию для кэша наиболее обновленного содержимого, поступающего с сервера, и если она отличается от ранее кэшированного, а затем отправить сообщение в браузер клиента, чтобы уведомить пользователя. Это делает сравнение не фактического содержания, но их заголовки ETag (поколение заголовка ETag будет объяснено ниже). Кэшированное значение ETag хранится с помощью localForage, простой, но мощной упаковки API IndexedDB:
function refresh(request, response) {
var ETag = response.headers.get('ETag');
if (!ETag) return null;
var key = 'ETag-'+response.url;
return localforage.getItem(key).then(function(previousETag) {
// Compare the ETag of the response with the previous one saved in the IndexedDB
if (ETag == previousETag) return null;
// Save the new value
return localforage.setItem(key, ETag).then(function() {
// If there was no previous ETag, then send no notification to the user
if (!previousETag) return null;
// Send a message to the client
return self.clients.matchAll().then(function (clients) {
clients.forEach(function (client) {
var message = {
type: 'refresh',
url: response.url
};
client.postMessage(JSON.stringify(message));
});
return response;
});
});
});
}
function showRefreshMsgToUser() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.onmessage = function (event) {
var message = JSON.parse(event.data);
if (message.type === 'refresh') {
var msg = 'This page has been updated, <a href="'+message.url+'">click here to refresh it</a>.';
var alert = '<div class="alert alert-warning alert-dismissible" role="alert"><button type="button" class="close" aria-hidden="true" data-dismiss="alert">×</button>'+msg+'</div>';
jQuery('body').prepend(alert);
}
};
}
}
Создание заголовка ETag
Заголовок ETag — это хэш, представляющий обслуживаемый контент; поскольку это хэш, минимальное изменение источника приведет к созданию совершенно другого ETag. Мы должны убедиться, что ETag генерируется из фактического содержимого веб-сайта, и игнорировать информацию, не видимую для пользователя, например HTML-идентиматологов. В противном случае рассмотрим следующую последовательность, которая происходит для стратегии «кэш, то сеть»:
- Идентификатор генерируется, используя
now()
его, чтобы сделать его уникальным, и напечатанв в HTML страницы. - При первом доступе эта страница создается и создается eTag.
- При доступе во второй раз страница передается непосредственно из кэша работника службы, и срабатывает сетевой запрос для обновления содержимого.
- Этот запрос снова генерирует страницу. Даже если он не был обновлен, его содержание будет отличаться, потому что
now()
будет производить другое значение, и его заголовок ETag будет отличаться. - Браузер сравнит два ETags и, поскольку они отличаются, подскажет пользователю обновить содержимое, даже если страница не была обновлена.
Одним из решений является удаление всех динамически генерируемых значений, таких как current_time('timestamp')
now()
и, прежде чем генерировать ETag. Для этого мы можем установить все динамические значения в константах, а затем использовать эти константы на протяжении всего приложения. Наконец, мы удалим их из ввода функции генерации хэша:
define('TIMESTAMP', current_time('timestamp'));
define('NOW', now());
ob_start();
// All the application code in between (using constants TIMESTAMP, NOW if needed)
$content = ob_get_clean();
$dynamic = array(TIMESTAMP, NOW);
$etag_content = str_replace($dynamic, '', $content);
header('ETag: '.wp_hash($etag_content));
echo($content);
Аналогичная стратегия необходима для тех частей информации, которые могут стать устаревшими, такие как количество комментариев поста, упомянутых ранее. Поскольку это значение не важно, мы не хотим, чтобы пользователь получил уведомление об обновлении страницы только потому, что количество комментариев увеличилось с 5 до 6.
Appshell с поддержкой для многоязычных, несколько режимов презентации
Независимо от того, какой URL запрашивается пользователем, приложение загружает приложение вместо этого, которое будет немедленно загружать содержимое запрошенного URL (по-прежнему доступны в window.location.href
) через API, проходя по месту локали и все необходимые параметры.
Приложение имеет разные представления и языки, и мы хотим, чтобы эти различные appshells, чтобы быть precached, а затем загрузить соответствующий на время выполнения, извлечения информации из запрошенного URL: https://www.mydomain.com/language-code/path/to/page/?view=
….
Как упоминалось ранее, учитывая два языка (английский и испанский) и три представления (по умолчанию, встраиваемые и распечатать), нам нужно будет предварительно прекэшировать следующие аполины:
https://www.mydomain.com/en/appshell/?view=default
https://www.mydomain.com/en/appshell/?view=embed
https://www.mydomain.com/en/appshell/?view=print
https://www.mydomain.com/es/appshell/?view=default
https://www.mydomain.com/es/appshell/?view=embed
https://www.mydomain.com/es/appshell/?view=print
Помимо языка и представления, приложение может иметь другие параметры (скажем, «стиль» и «формат»). Тем не менее, добавление этих сделает комбинации URL-адресов для precache расти чрезвычайно. Таким образом, мы должны остановиться на компромисс, решая, какие параметры precache (наиболее часто используемых) и какие из них не делать. Для последних, их соответствующий URL можно получить в автономном режиме только начиная со второго доступа.
URL запросил | Appshell | Precached |
---|---|---|
https://www.mydomain.com/en/ |
https://www.mydomain.com/en/appshell/?view=default |
Да |
https://www.mydomain.com/en/?view=print |
https://www.mydomain.com/en/appshell/?view=print |
Да |
https://www.mydomain.com/en/?view=print&style=classic |
https://www.mydomain.com/en/appshell/?view=print&style=classic |
Нет |
Добавляя крючки в конфигурацию, мы позволяем многоязычным плагинам, таким как qTranslate X,изменять языки и языки соответственно.
var config = {
…
appshell: {
pages: $appshellPages,
params: $appshellParams
},
…
};
function get_sw_configuration() {
…
$configuration['$appshellPages'] = get_sw_appshell_pages();
$configuration['$appshellParams'] = apply_filters('PoP_ServiceWorkers_Job_Fetch:appshell_params', array("themestyle", "settingsformat", "mangled"));
…
}
function get_sw_appshell_pages() {
// Locales: can be hooked into by qTranslate to input the language codes
$locales = apply_filters('PoP_ServiceWorkers_Job_Fetch:locales', array(get_locale()));
$views = array("default", "embed", "print");
$appshellPages = array();
foreach ($locales as $locale) {
foreach ($views as $view) {
// By adding a hook to the URL, we can allow plugins to modify the URL
$appshellPages[$locale][$view] = apply_filters('PoP_ServiceWorkers_Job_Fetch:appshell_url', add_query_arg('view', $view, get_permalink($APPSHELL_PAGE_ID), $locale);
}
}
return apply_filters('PoP_ServiceWorkers_Job_Fetch:appshell_pages', $appshellPages);
}
class PoP_ServiceWorkers_QtransX_Job_Fetch_Hooks {
function __construct() {
add_filter('PoP_ServiceWorkers_Job_Fetch:locales', array($this, 'get_locales'));
add_filter('PoP_ServiceWorkers_Job_Fetch:appshell_url', array($this, 'get_appshell_url'), 10, 2);
…
}
function get_locales($locales) {
global $q_config;
if ($languages = $q_config['enabled_languages']) {
return $languages;
}
return $locales;
}
function get_appshell_url($url, $lang) {
return qtranxf_convertURL($url, $lang);
}
}
new PoP_ServiceWorkers_QtransX_Job_Fetch_Hooks();
После слияния sw-template.js
service-workers.js
в, это будет выглядеть следующим образом:
var config = {
appshell: {
pages: {
es: {
default: "https://www.mydomain.com/es/appshell/?view=default",
embed: "https://www.mydomain.com/es/appshell/?view=embed",
print: "https://www.mydomain.com/es/appshell/?view=print"
},
en :{
default: "https://www.mydomain.com/en/appshell/?view=default",
embed: "https://www.mydomain.com/en/appshell/?view=embed",
print: "https://www.mydomain.com/en/appshell/?view=print"
}
},
params: ["style", "format"]
},
};
Запрос перехватывается с помощью onFetch
метода, и если он имеет тип ресурса HTML, он будет заменен URL-адресом appshell, сразу после принятия решения о том, какая стратегия должна быть использована. (Ниже мы увидим, как получить текущий локал, установленный в opts.locales.current
.)
function onFetch(event, opts) {
var request = event.request;
…
var strategy = getStrategy(request, opts);
// Allow to modify the request, fetching content from a different URL
request = getRequest(request, opts);
…
}
function getRequest(request, opts) {
var resourceType = getResourceType(request);
if (resourceType === 'html') {
// The different appshells are a combination of locale and view.
var params = getParams(request.url);
var view = params.view || 'default';
// The initial appshell URL has the params that we have precached.
var url = opts.appshell.pages[opts.locales.current][view];
// In addition, there are other params that, if provided by the user, must be added to the URL. These params are not originally precached in any appshell URL, so such a page will have to be retrieved from the server.
opts.appshell.params.forEach(function(param) {
// If the param was passed in the URL, then add it along.
if (params[param]) {
url += '&'+param+'='+params[param];
}
});
request = new Request(url);
}
return request;
}
Наконец, мы приступим к precache appshells:
class PoP_ServiceWorkers_Hooks_AppShell {
function __construct() {
add_filter('PoP_ServiceWorkers_Job_CacheResources:precache', array($this, 'get_precache_list'), 10, 2);
}
function get_precache_list($precache, $resourceType) {
if ($resourceType == 'html') {
foreach (get_sw_appshell_pages() as $locale => $views) {
foreach ($views as $view => $url) {
$precache[] = $url;
}
}
}
return $precache;
}
}
new PoP_ServiceWorkers_Hooks_AppShell();
Получение локализующего
Приложение имеет многоязычную поддержку, поэтому нам необходимо извлечь языковую информацию из запрошенного URL-
var config = {
…
locales: {
all: $localesByURL,
default: $defaultLocale,
current: null,
domain: null
},
…
};
get_locale()
и мы позволяем плагинам подключать их значения в:
function get_sw_configuration() {
…
$configuration['$localesByURL'] = apply_filters('PoP_ServiceWorkers_Job_Fetch:locales_byurl', array(site_url() => get_locale()));
$configuration['$defaultLocale'] = apply_filters('PoP_ServiceWorkers_Job_Fetch:default_locale', get_locale());
…
}
class PoP_ServiceWorkers_QtransX_Job_Fetch_Hooks {
function __construct() {
…
add_filter('PoP_ServiceWorkers_Job_Fetch:locales_byurl', array($this, 'get_locales_byurl'));
add_filter('PoP_ServiceWorkers_Job_Fetch:default_locale', array($this, 'get_default_locale'));
}
function get_locales_byurl($locales) {
global $q_config;
if ($languages = $q_config['enabled_languages']) {
$locales = array();
$url = trailingslashit(home_url());
foreach ($languages as $lang) {
$locales[qtranxf_convertURL($url, $lang)] = $lang;
}
}
return $locales;
}
function get_default_locale($default) {
if ($lang = qtranxf_getLanguage()) {
return $lang;
}
return $default;
}
}
После слияния sw-template.js
service-workers.js
в, это будет выглядеть следующим образом:
var config = {
locales: {
all: {
"https://www.mydomain.com/es/":"en",
"https://www.mydomain.com/en/":"es"
},
default: "en",
current: null,
domain: null
},
};
Наконец, мы получаем языковой код из запрашиваемого URL-адреса и инициализируем значения локализации current
и domain
конфигурации в начале fetch
события:
self.addEventListener('fetch', event => {
config = initOpts(config, event);
if (shouldHandleFetch(event, config)) {
…
}
});
function initOpts(opts, event) {
// Find the current locale and set it on the configuration object
opts.locales.current = getLocale(event, opts);
opts.locales.domain = getLocaleDomain(event, opts);
return opts;
}
function getLocale(event, opts) {
var currentDomain = getCurrentDomain(event, opts);
if (currentDomain.length) {
return opts.locales.all[currentDomain];
}
return opts.locales.default;
}
function getLocaleDomain(event, opts) {
var currentDomain = getCurrentDomain(event, opts);
if (currentDomain.length) {
return currentDomain[0];
}
// Return the default domain
return Object.keys(opts.locales.all).filter(function(key) {return opts.locales.all[key] === opts.locales.default})[0];
}
function getCurrentDomain(event, opts) {
return Object.keys(opts.locales.all).filter(path => event.request.url.startsWith(path));
}
Работа с nonces
Nonce (или «номер, используемый один раз») представляет собой криптографический хэш, используемый для проверки подлинности человека или клиента. WordPress использует nonces в качестве маркеров безопасности для защиты URL-адресов и форм от вредоносных атак. Несмотря на свое название, WordPress использует nonce более одного раза, придав ему ограниченный срок службы, после чего он истекает. Несмотря на то, что они не являются конечной мерой безопасности, nonces являются хорошим первым фильтром для предотвращения хакерских атак.
HTML-код, напечатанный на любой странице WordPress, будет содержать nonces, такие как nonce для загрузки изображений в медиа-менеджер, сохраненные в объекте _wpPluploadSettings.defaults.multipart_params._wpnonce
JavaScript. Срок службы nonce по умолчанию установлен на 24 часа (настроен в nonce_life
крючке). Однако это значение короче ожидаемой продолжительности кэша работы в кэше работника службы. Это проблема: уже через 24 часа приложение будет содержать недействительные неплатежеспособные, что приведет к сбою приложения, например, к отправке сообщений об ошибках при попытке пользователя загрузить изображения.
Есть несколько решений для преодоления этой проблемы:
- Сразу же после загрузки приложения загрузите другую страницу в фоновом режиме, используя стратегию «только для сети», чтобы обновить значение nonce в исходном объекте JavaScript:
_wpPluploadSettings.defaults.multipart_params._wpnonce="";
- Внедрить более
nonce_life
длительный срок, например, три месяца, а затем не забудьте развернуть новую версию обслуживающего работника в течение этого срока службы:
add_filter('nonce_life', 'sw_nonce_life');
function sw_nonce_life($nonce_life) {
return 90*DAY_IN_SECONDS;
}
Because this solution weakens the security of nonces, tougher security measures must also be put in place throughout the application, such as making sure that the user can edit a post:
if (!current_user_can('edit_post', $post_id))
wp_die( __( 'Sorry, you are not allowed to edit this item.'));
Дополнительные соображения
Тот факт, что что-то может быть сделано, не означает, что это должно быть сделано. Разработчик должен оценить, нужно ли добавлять каждую функцию в соответствии с требованиями приложения, а не только потому, что технология позволяет это.
Ниже я покажу несколько примеров функциональности, которые могут быть прекрасно реализованы с помощью других решений или которые нуждаются в некотором обходном пути для того, чтобы заставить их работать с работниками службы.
Отображение простого сообщения «Ты в автономном режиме»
Работники службы могут использоваться для отображения автономной резервной страницы всякий раз, когда пользователь не имеет подключения к Интернету. В своей самой основной реализации просто показывая «Вы в автономном режиме» сообщение, я считаю, что мы не получаем максимальную ценность из этой резервной страницы. Скорее, он мог бы делать более интересные вещи:
- Предоставьте дополнительную информацию, например, покажите список всех уже кэшированных ресурсов, которые пользователь все еще может просматривать в автономном режиме (проверить, как в автономном режиме демо-версии Википедии Джейка Арчибальда перечислены все уже кэшированные ресурсы на своей главной странице).
- Пусть пользователь играет в игру в ожидании соединения, чтобы вернуться (например, сделано The Guardian).
С ПОМОЩЬю SPA мы можем предложить другой подход: мы можем перехватить автономное состояние, и просто отобразить небольшое сообщение «Ты в автономном режиме» в верхней части страницы, которую пользователь в настоящее время просматривает. Это позволяет избежать перенаправления пользователя на еще одну страницу, что может ухудшить поток приложения.
Html:
<div id="error-msg"></div>
Css:
#error-msg {
display: none;
position: fixed;
top: 0;
left: 50%;
width: 300px;
margin-left: -150px;
color: #8a6d3b;
background-color: #fcf8e3;
border-color: #faebcc;
text-align: center;
}
Javascript:
function intercept_click() {
$(document).on('click', 'a[href^="'+WEBSITE_DOMAIN+'"]', function(e) {
var anchor = $(this);
var url = anchor.attr('href');
$.ajax({
url: url,
error: function(jqXHR) {
var msg = 'Oops, there was an error';
if (jqXHR.status === 0) { // status = 0 => user is offline
msg = 'You are offline!';
}
else if (jqXHR.status === 404) {
msg = 'That page doesn't exist';
}
$('#error-msg').text(msg).css('display', 'block');
}
});
});
}
Использование локального хранилища для кэша данных
Работники службы не являются единственным решением, предлагаемым браузерами для кэширования данных ответа. Старая технология, с еще более широкой поддержкой (Internet Explorer и Safari поддерживают ее), является localStorage. Он обеспечивает хорошую производительность для кэширования малых и средних частей информации (обычно он может кэшировать до 5 МБ данных).
/* Using Modernizr library */
function intercept_click() {
$(document).on('click', 'a[href^="'+WEBSITE_DOMAIN+'"]', function(e) {
var anchor = $(this);
var url = anchor.attr('href');
var stored = '';
if (Modernizr.localstorage) {
stored = localStorage[url];
}
if (stored) {
// We already have the data!
process(stored);
}
else {
$.ajax({
url: url,
success: function(response){
// Save the data in the localStorage
if (Modernizr.localstorage) {
localStorage[url] = response;
}
process(response);
}
});
}
});
}
Делать вещи красивее
Чтобы заставить работника службы использовать стратегию «первая сеть», мы можем добавить дополнительный параметр sw-networkfirst=true
к запрашиваемому URL. Однако добавление этого параметра в фактическую ссылку будет выглядеть уродливо (детали технической реализации должны быть максимально скрыты от пользователя).
Вместо этого, атрибут данных, data-sw-networkfirst
может быть добавлен в якорь. Затем, во время выполнения, клик пользователя будет перехвачен для обработки вызова AJAX, проверяя, имеет ли ссылка нажатую этот атрибут данных; если это произойдет, только тогда параметр sw-networkfirst=true
будет добавлен в URL, чтобы получить:
function intercept_click() {
$(document).on('click', 'a[href^="'+WEBSITE_DOMAIN+'"]', function(e) {
var anchor = $(this);
var url = anchor.attr('href');
if (anchor.data('sw-networkfirst')) {
url = add_query_arg('sw-networkfirst', 'true', url);
}
$.ajax({
url: url,
…
});
});
}
Планирование вещей, которые не работают
Не все будет работать в автономном режиме. Например, CAPTCHA не будет работать, потому что ему необходимо синхронизировать свою ценность с сервером. Если форма имеет CAPTCHA, то попытка отправить форму в автономном режиме не должна сохранять значение локально, который будет отправлен после того, как подключение к Интернету возвращается. Вместо этого он может заполнить форму еще раз со всеми значениями, ранее заполненными пользователем, попросить пользователя ввести CAPTCHA, и только после этого отправить форму.
Заключение
Мы видели, как работники сферы услуг могут быть реализованы для веб-сайта WordPress с архитектурой SPA. SPA значительно улучшают работы служб, например, позволяя вам выбирать из различных апослов для загрузки во время выполнения. Интеграция с WordPress не все, что гладко, по крайней мере, чтобы сделать веб-сайт стал в автономном режиме во-первых, потому что мы должны найти все ресурсы из темы и все плагины, чтобы добавить в список precache. Однако длительная интеграция стоит: сайт будет загружаться быстрее и будет работать в автономном режиме.
Источник: smashingmagazine.com