Smashing Magazine дал нам небольшой сюрприз в последнее время: его веб-сайт был полностью отремонтирован, переход от WordPress к Netlify. Одной из нескольких причин переезда является стоимость: Netlify позволяет статичную версию веб-сайта, которая может быть размещена непосредственно в сети доставки контента (CDN), уменьшая количество веб-серверов, которые необходимы. Кроме того, поскольку CDN расположены рядом с пользователями, доступ к веб-сайту становится более быстрым. Это мудрый шаг, действительно.
Тем не менее, это решение приходит с следствием выхода из экосистемы WordPress. Если ваш сайт нуждается в поддержке AMP или Dropbox или AWS S3 или почти все остальное, скорее всего, есть плагин WordPress для этого. Экосистема WordPress, которая опирается на огромное сообщество разработчиков, позволяет нам постоянно включать новые функции в наши веб-сайты без каких-либо серьезных усилий, или, по крайней мере, с гораздо меньшими усилиями, чем требуется для разработки функциональности с нуля. Переход от WordPress к Netlify имеет компромиссы: скорость по сравнению с функциональностью, стоимость по сравнению с поддержкой сообщества.
Что делать, если можно было объединить лучшее из обоих миров? То есть, что, если бы мы могли иметь WordPress веб-сайт динамического содержания которых (т.е. содержание, такое как блог, а не статические активы, такие как файлы CSS) могут быть экспортированы в качестве статических файлов? В настоящее время есть плагины, которые экспортируют веб-сайт WordPress как статические HTML-файлы; однако, они экспортируют весь вебсайт заранее, и они имеют предел до вокруг 1000 страниц, поэтому они не масштабируют к вебсайту размер Smashing кассеты. Вместо этого нам нужно, чтобы иметь возможность создавать статичную версию веб-сайта на лету,страница за страницей.
В этой статье мы решим эту проблему. Мы рассмотрим, как настроить архитектуру нашего сайта, чтобы направить его динамическое содержание через CDN, что делает его статическим. Таким образом, мы сможем получить преимущества, которые Smashing Magazine получил от перехода на статический генератор сайта, но без необходимости отказываться от WordPress.
Как работает cdN контента?
Направление запроса через CDN приводит к тому, что динамическое содержимое становится статичным, так как CDN вернет кэшированную версию этого содержимого (запрашиваемую под определенным URL). Всякий раз, когда веб-сервер имеет более свежую версию этого контента, он должен сказать CDN, чтобы служить самой современной версии. Существует два способа справиться с этой ситуацией:
- Очистка объекта из кэша CDN, чтобы он снова принес содержимое для этого URL;
- Добавление параметра версий к URL для обслуживания другой версии объекта под другим URL-адресом при изменении содержимого.
При создании динамичного веб-сайта, в котором контент постоянно меняется и должен немедленно отражать эти изменения, то первое решение не является адекватным. Почему? Ну, по следующим причинам:
- Мы не можем гарантировать, что объект будет эффективно удален от всех различных слоев между веб-сервером и браузером клиента. Например, у пользователя может быть кэшированная версия локально или за корпоративным прокси-сервером кэширования;
- Поскольку края CDN расположены по всему миру, обычно требуется несколько секунд для очистки своих объектов.
Кроме того, программное очищение объектов от CDN зависит от AA, предоставляемых поставщиками CDN, такими как Cloudflare или AWS CloudFront. Если мы хотим иметь абсолютный контроль, что приложение будет работать независимо от инфраструктуры, где он работает, то очистка объекта не рекомендуется.
По всем этим причинам я решил реализовать механизм признания недействительных Content CDN через второе решение, то есть с приложением добавить параметр версии к каждому запрошенного URL. Этот параметр версий, который отныне будет называться «отпечаток пальца», в основном является меткой времени с момента последнего изменения запрашиваемого ресурса.
Процесс, то довольно простой:
- Пользователь делает запрос (например, нажимает на ссылку на веб-сайте).
- Приложение в клиенте изменяет URL-адрес, который будет запрашиваться, изменяя домен на этот из CDN и добавляя «thumbprint» (эти изменения скрыты для пользователя).
- CDN, если он не имеет этой записи, будет тянуть его из веб-сервера.
- Сервер возвращает ответ.
- CDN кэши ответ и возвращает его клиенту.
- Клиент отображает содержимое и обновляет URL-адрес браузера до первоначально запрошенного URL (без изменения домена, никаких дополнительных параметров не добавляется).
- В следующий раз, когда пользователь (любой пользователь) делает тот же запрос, если значение отпечатка пальца не изменилось, то это содержимое будет непосредственно извлечено из CDN.
Этот подход пока не обеспечивает 100% статический веб-сайт: начальная загрузка веб-сайта извлекается из веб-сервера, а не из CDN. Возможным решением этой проблемы является перехват первого запроса через сервисных работников и применение вышеуказанной процедуры, позволяющей повторять посетителям (просмотр сайта на Chrome и Firefox, а вскоре и Safari и Edge), чтобы всегда загружать веб-сайт с CDN (мне все еще нужно реализовать это решение, поэтому я не буду описывать его в этой статье.)
Реализация контента CDN
В качестве примера, я реализовал содержание CDN для PoP, обертка для WordPress сайтов, который оптимизирует их производительность и делает их более динамичными.
После загрузки первоначальной оболочки приложения, PoP загружает все содержимое исключительно через звонки AJAX. Для этого он перехватывает события пользователя (например, когда пользователь нажимает на ссылку) и изменяет запрашиваемый URL- URL, заставляя веб-сервер предоставлять ответ как JSON вместо HTML. Мы расширяем эту процедуру, чтобы сделать запрос также пройти через CDN.
Изменение URL-адреса, который необходимо запрашивать
Давайте приступим к изменению запрошенного URL-адреса. Предполагая, что URL https://getpop.org/en/
является, шаги, предпринятые следующим образом:
- Все запросы перехватываются в клиенте браузера, и дополнительный параметр
output=json
добавляется к URL, на который сервер будет возвращать ответ JSON.https://getpop.org/en/
?output=json
- Мы изменяем домен запрашиваемого ресурса: В то время как
https://getpop.org
указывает на веб-сервер,https://content.getpop.org
указывает на CDN, что, когда он не имеет запрошенного актива, происхождение будет тянуть егоhttps://getpop.org
из, а затем кэшировать его.https://
content.getpop.org
/en/?output-json - Мы добавляем параметр приложения
version
в URL, так что развертывание новой версии приложения приведет к аннулированию всех ресурсов, кэшированных в CDN:https://content.getpop.org/en/?output=json
&version=${APP_VERSION}
- Чтобы получить последнюю версию первоначально изменяемого актива, нам нужно ввести в свой URL другой параметр: «отпечаток пальца» — т.е. номер, указывающий последний раз, когда он был изменен:
https://content.getpop.org/en/?output=json&version=${APP_VERSION}
&thumbprint=${LATEST_THUMBPRINT}
И это то, что наш окончательный запрос URL будет выглядеть.
Отпечаток пальца делает кэшированный актив снова динамическим
Значение отпечатка пальца постоянно меняется, отражая активность пользователей на сайте. По этой причине приложение в клиенте должно иметь доступ к последнему значению отпечатков пальцев, которое будет добавлено в качестве параметра для всех запросов. Для того, чтобы всегда получить последнюю отпечаток пальца, процесс в фоновом режиме всегда должны получить это значение от сервера, либо каждый х количество времени (который также может быть направлен через CDN, кэширование последнего значения отпечатков пальцев для всех клиентов после первого) или через WebSockets. (PoP в настоящее время реализует прежний подход. Я опустил код реализации из этой статьи, но он доступен в репозитории GitHub.)
Чтобы сделать реализацию менее сложной, отпечаток пальца, связанный со страницей, не обязательно должен представлять время, в которое эта конкретная страница была обновлена в последний раз, а время, в течение которого любая страница, похожая на эту, была изменена. Мы можем определить несколько отпечатков пальцев, каждый из которых связан с другим типом объекта:
- Поместить
Когда была обновлена публикация или создана новая? - Пользователя
Когда пользователь обновил свою информацию или зарегистрировался новый пользователь? -
Комментарий
Когда был добавлен новый комментарий? -
Др.
Страница может потребовать одного или нескольких значений отпечатков пальцев выше. Страница, показывающая содержание публикации и имя и описание автора публикации, должна опираться как на публикацию, так и на отпечатки пальцев пользователей. Если сообщение также содержит комментарии, он также должен будет полагаться на комментарий отпечаток пальца. Комбинация отпечатков пальцев должна определяться по странице; таким образом, мы можем определить страницу публикации, чтобы использовать комбинацию значений отпечаток публикации, пользователя и комментария, но определить страницу автора, чтобы использовать только значение отпечатков пальцев пользователя, максимально ежефизируя время, которое страница автора будет оставаться свежей в кэше CDN.
Реализация кода
Весь приведенный ниже код был адаптирован для этой статьи из оригинальной версии, которая доступна здесь.
Цель состоит в том, чтобы преобразовать URL, который будет извлечен, перенагой запрос через CDN:
function fetchURL(url, type) {
…
// Route the request through the content CDN. Only for GET requests, POST go straight to the server
if (type == 'GET') {
url = contentCDN.convertURL(url);
}
// Fetch it
$.ajax({
dataType: "json",
url: url,
type: type,
…
}, …);
}
Мы должны function convertURL
реализовать, который был помещен под Javascript объект contentCDN
:
window.contentCDN = {
convertURL : function(url) {
// Apply some logic here to transform the URL, to route it through the Content CDN
...
}
};
Логике необходимо будет получить доступ к некоторым значениям конфигурации:
- домен веб-сайта,
-
содержание домена CDN,
-
версия приложения,
-
список всех доступных отпечатков пальцев и значение каждого отпечатка пальца,
-
критерии использования URL-адреса CDN,
-
критерии, для которых отпечатки пальцев необходимы для URL.
Два набора критериев необходимы для определения того, следует ли направлять определенный URL через содержимое CDN и, если это разрешено, то какие отпечатки пальцев должны быть приложены к URL.
Когда URL должен быть отклонен? Некоторые страницы не должны кэшированы не только на CDN, но и на веб-сервере, например, на веб-сервере, например, содержащие состояние пользователя. Например, страница «Мои предпочтения», доступная /my-preferences/
для всех пользователей, загружает содержимое, специфичное для зарегистрированного пользователя, поэтому оно не должно быть кэшировано. Кроме того, всякий раз, когда POST
операция выполняется, она должна идти прямо на сервер.
Я определил четыре критерия для оценки URL, из которых мы сможем решить, должен ли URL быть отклонен или какие отпечатки пальцев применять к нему:
startsWith
Начинается ли URL-адрес с данной строки? (Например, мы будем использовать отпечаток пальца почты для всех URL-адресов, начиная с/posts/
)-
hasParamValues
Содержит ли URL данный параметр и значение? (Например, мы будем использовать отпечаток пальца пользователя для всех URL-адресов, начиная сtab=authors
параметра,/posts/this-is-a-post/?tab=authors
например, который будет генерировать список авторов поста) -
noParamValues
Не содержит ли URL-адрес данный параметр и значение? (Например, мы могли бы использовать отпечаток пальца поста, если URL не имеетtab=followers
параметра) -
isHome
Это домашний URL???? Это исключительный случай, который должен быть оценен сам по себе, потому что мы не можем использоватьstartsWith
элемент для него
Определив все необходимые свойства конфигурации, мы помещаем их под contentCDNConfig
объект Javascript:
window.contentCDNConfig = {
cdnDomain: "...",
homeDomain: "...",
appVersion: ...,
thumbprints: [%THUMBPRINT_NAME_1%, %THUMBPRINT_NAME_2%, ...],
thumbprintValues : {
%THUMBPRINT_NAME_1%: %THUMBPRINT_VALUE_1%,
%THUMBPRINT_NAME_2%: %THUMBPRINT_VALUE_2%,
...
},
criteria: {
// Criteria to know if the URL must not use the Content CDN
rejected: {
startsWith: [...],
hasParamValues: [...],
noParamValues: [...],
isHome: true/false
},
// Criteria for obtaining the thumbprints which are needed for a given URL
thumbprints: {
%THUMBPRINT_NAME_1%: {
startsWith: [...],
hasParamValues: [...],
noParamValues: [...],
isHome: true/false
},
%THUMBPRINT_NAME_2%: {
startsWith: [...],
hasParamValues: [...],
noParamValues: [...],
isHome: true/false
},
...
}
}
};
Теперь мы можем приступить к кодированию логики. Оценка различных критериев осуществляется в function evalCriteria
. Обратите внимание, что var criterias
это массив из двух критериев, которые должны быть успешными; чтобы быть успешным, каждый критерий должен иметь по крайней мере один успешный подпункт. В то время как первый критерий имеет три подпункта startsWith
hasParamValues
(и isHome
), второй имеет только один: noParamValues
. Логика была разработана таким образом, чтобы noParamValues
можно было отклонить отпечаток пальца, установленный в первом критерии. Например, используйте отпечаток пальца поста для всех URL-адресов, начиная /author/
с, если у них нет tab=followers
параметра. (В PoP, запрос /author/leo/
покажет список всех сообщений от пользователя Лео, так что он опирается на отпечаток пальца поста. Тем не менее, вместо этого запрос /author/leo/?tab=followers
покажет пользователям, которые следуют Лео, так что он не полагается на отпечаток пальца поста.)
(function($){
window.contentCDN = {
...
evalCriteria : function(url, entries) {
var evalParam = function(elem) {
// Function getParam gets the value of the query string "key" from the URL. Code can be found here: https://github.com/leoloso/PoP/blob/master/wp-content/plugins/pop-frontendengine/js/utils.js#L91
var key = elem[0], value = elem[1];
var paramValue = getParam(key, url);
return paramValue == value;
};
var criterias = [
{
// isHome: special case, we can't ask for path pattern, or otherwise its thumbprints will always be true for everything else (since everything has the path of the home)
isHome: entries.isHome && this.isHome(url),
startsWith: entries.startsWith.some(function(path) {
return url.indexOf(contentCDNConfig.homeDomain + '/' + path) == 0;
}),
// Check if the combination of key=>value is present as a param in the URL
hasParamValues: entries.hasParamValues.some(evalParam)
},
{
// Check that the combination of key=>value is NOT present as a param in the URL
noParamValues: !entries.noParamValues.some(evalParam)
}
];
// Check that all criterias were successful
var successCounter = 0;
$.each(criterias, function(index, criteria) {
var successCriteria = Object.keys(criteria).filter(function(criteriaKey) {
return criteria[criteriaKey];
});
if (successCriteria.length) {
successCounter++;
}
});
return (successCounter == criterias.length);
},
isHome : function(url) {
var path = (url.indexOf('?') > -1) ? url.substr(0, url.indexOf('?')) : url;
return path == contentCDNConfig.homeDomain || path == contentCDNConfig.homeDomain+'/';
},
};
})(jQuery);
Проверка того, следует ли направлять URL через CDN, реализуется function shouldUseCDN
в, и получение списка отпечатков пальцев для использования для URL реализовано function getThumbprints
в:
(function($){
window.contentCDN = {
...
shouldUseCDN : function(url) {
return url.indexOf(contentCDNConfig.homeDomain) == 0 && !this.evalCriteria(url, contentCDNConfig.criteria.rejected);
},
getThumbprints : function(url) {
var that = this;
var thumbprints = [];
$.each(contentCDNConfig.thumbprints, function(index, thumbprint) {
if (that.evalCriteria(url, contentCDNConfig.criteria.thumbprints[thumbprint])) {
thumbprints.push(thumbprint);
}
});
return thumbprints;
}
};
})(jQuery);
Мы в значительной степени там. Мы можем, наконец, реализовать function convertURL
:
window.contentCDN = {
convertURL : function(url) {
if (this.shouldUseCDN(url)) {
// Important: get the thumbprint now, before replacing the domain, after which the thumbprint values won't be found anymore
var thumbprint = this.getThumbprintValue(url);
// Modify the URL, replacing the home domain with the Content CDN domain
url = contentCDNConfig.cdnDomain + url.substr(contentCDNConfig.homeDomain.length);
// Add the version parameter to the URL. Code for function add_query_arg can be found here: https://github.com/leoloso/PoP/blob/master/wp-content/plugins/pop-frontendengine/js/utils.js#L31
url = add_query_arg("version", contentCDNConfig.appVersion, url);
// Add the thumbprints as params
if (thumbprint) {
url = add_query_arg("thumbprint", thumbprint, url);
}
}
return url;
},
getThumbprintValue : function(url) {
var thumbprints = this.getThumbprints(url);
var value = thumbprints.map(function(thumbprint) {
return this.thumbprintValues[thumbprint];
});
// Join all the thumbprints together using a dot, to make it easier to identify the different thumbprint values
return value.join('.');
}
};
Далее мы приступим к созданию файла конфигурации. Файл конфигурации должен быть расширяемым, чтобы плагины WordPress могли подключиться к нему и добавить свои собственные отпечатки пальцев и критерии. Таким образом, я буду следовать тому же процессу я писал о в моей статье о обслуживающих работников, генерации его на время выполнения и выполнения логики при развертывании новой версии веб-сайта. (Весь процесс объясняется в этой статье, так что я не буду повторять его здесь.)
Объединяя все воедино, файл заполнителя, из которого можно создать файл конфигурации Javascript во время выполнения, выглядит следующим образом:
window.contentCDNConfig = {
cdnDomain: $cdnDomain,
homeDomain: $homeDomain,
appVersion: $appVersion,
thumbprints: $thumbprints,
thumbprintValues: $thumbprintValues,
criteria: {
rejected: $rejectedCriteria,
thumbprints: $thumbprintsCriteria,
}
};
И мы определяем крючки WordPress, чтобы заполнить эти значения, позволяя плагинам добавлять свою собственную конфигурацию.
class ContentCDN_ThumbprintsConfig {
...
public function get_configuration() {
$configuration = parent::get_configuration();
$configuration['$cdnDomain'] = "https://content.getpop.org";
$configuration['$homeDomain'] = get_site_url();
$configuration['$appVersion'] = "4.3";
// Thumbprints are obtained from the Manager instance
global $contentcdn_thumbprint_manager;
$thumbprints = $contentcdn_thumbprint_manager->get_thumbprints();
$configuration['$thumbprints'] = $thumbprints;
$configuration['$thumbprintValues'] = $thumbprint_values;
$configuration['$rejectedCriteria'] = $this->get_rejected_criteriaitems();
$configuration['$thumbprintsCriteria'] = array();
foreach ($thumbprints as $thumbprint) {
$configuration['$thumbprintsCriteria'][$thumbprint] = $this->get_thumbprints_criteriaitems($thumbprint);
}
return $configuration;
}
protected function get_rejected_criteriaitems() {
return array(
'startsWith' => apply_filters(
'ContentCDN_ThumbprintsConfig:criteriaitems:rejected:startsWith',
array()
),
// Array of Arrays: elem[0] = URL param, elem[1] = value
'hasParamValues' => apply_filters(
'ContentCDN_ThumbprintsConfig:criteriaitems:rejected:hasParamValues',
array()
),
// Array of Arrays: elem[0] = URL param, elem[1] = value
'noParamValues' => apply_filters(
'ContentCDN_ThumbprintsConfig:criteriaitems:rejected:noParamValues',
array()
),
'isHome' => apply_filters(
'ContentCDN_ThumbprintsConfig:criteriaitems:rejected:isHome',
false
)
);
}
protected function get_thumbprints_criteriaitems($thumbprint) {
return array(
'startsWith' => apply_filters(
'ContentCDN_ThumbprintsConfig:criteriaitems:thumbprint:startsWith',
array(),
$thumbprint
),
'hasParamValues' => apply_filters(
'ContentCDN_ThumbprintsConfig:criteriaitems:thumbprint:hasParamValues',
array(),
$thumbprint
),
'noParamValues' => apply_filters(
'ContentCDN_ThumbprintsConfig:criteriaitems:thumbprint:noParamValues',
array(),
$thumbprint
),
'isHome' => apply_filters(
'ContentCDN_ThumbprintsConfig:criteriaitems:thumbprint:isHome',
false,
$thumbprint
)
);
}
}
new ContentCDN_ThumbprintsConfig();
Для того чтобы иметь дело с отпечатками пальцев, мы ссылаемся на $contentcdn_thumbprint_manager
объект, который является экземпляром, class ContentCDN_Thumbprint_Manager
который позволяет плагинам зарегистрировать свои собственные отпечатки пальцев, просто расширяя от абстрактного: class ContentCDN_ThumbprintBase
class ContentCDN_ThumbprintBase {
function __construct() {
global $contentcdn_thumbprint_manager;
$contentcdn_thumbprint_manager->add($this);
}
public function get_name() {
return '';
}
public function get_query() {
return array();
}
public function execute_query($query) {
return '';
}
public function get_timestamp($object_id) {
return (int) 0;
}
}
class ContentCDN_Thumbprint_Manager {
var $thumbprints;
function __construct() {
$this->thumbprints = array();
}
function add($thumbprint) {
$this->thumbprints[$thumbprint->get_name()] = $thumbprint;
}
function get_thumbprints() {
return array_keys($this->thumbprints);
}
function get_thumbprint_value($name) {
$thumbprint = $this->thumbprints[$name];
$query = $thumbprint->get_query();
// Get the ID for the last modified object
if ($results = $thumbprint->execute_query($query)) {
$object_id = $results[0];
// The thumbprint is the modification date timestamp
return (int) $thumbprint->get_timestamp($object_id);
}
return '';
}
}
global $contentcdn_thumbprint_manager;
$contentcdn_thumbprint_manager = new ContentCDN_Thumbprint_Manager();
Мы приступаем к регистрации отпечатков пальцев по умолчанию. Пожалуйста, обратите внимание, что как для публикации, так и для отпечатков пальцев комментариев, последняя метка времени может быть непосредственно запрошена из базы данных. Тем не менее, для отпечатка пальца пользователя, у нас нет этой информации, поэтому мы должны явно сохранить последнюю метку времени с момента, когда пользователь обновил свой профиль, под meta_key
именем last_edited
.
/* Post Thumbprint */
class ContentCDN_Thumbprint_Post extends ContentCDN_ThumbprintBase {
public function get_name() {
return 'post';
}
public function get_query() {
return array(
'fields' => 'ids',
'limit' => 1,
'orderby' => 'modified',
'order' => 'DESC',
'post_status' => 'publish',
);
}
public function execute_query($query) {
return get_posts($query);
}
public function get_timestamp($post_id) {
$post = get_post($post_id);
return mysql2date('U', $post->post_modified);
}
}
new ContentCDN_Thumbprint_Post();
/* Comment Thumbprint */
class ContentCDN_Thumbprint_Comment extends ContentCDN_ThumbprintBase {
public function get_name() {
return 'comment';
}
public function get_query() {
return array(
'fields' => 'ids',
'number' => 1,
'status' => 'approve',
'type' => 'comment', // Only comments, no trackbacks or pingbacks
'order' => 'DESC',
'orderby' => 'comment_date_gmt',
);
}
public function execute_query($query) {
return get_comments($query);
}
public function get_timestamp($comment_id) {
return get_comment_date('U', $comment_id);
}
}
new ContentCDN_Thumbprint_Comment();
/* User Thumbprint */
class ContentCDN_Thumbprint_User extends ContentCDN_ThumbprintBase {
public function get_name() {
return 'user';
}
public function get_query() {
return array(
'fields' => 'ID',
'limit' => 1,
'orderby' => 'meta_value',
'meta_key' => 'last_edited',
'order' => 'DESC',
);
}
public function execute_query($query) {
return get_users($query);
}
public function get_timestamp($user_id) {
return get_user_meta($user_id, 'last_edited', true);
}
}
new ContentCDN_Thumbprint_User();
add_action('edit_user_created_user', 'save_extra_user_info');
add_action('personal_options_update', 'save_extra_user_info');
add_action('edit_user_profile_update', 'save_extra_user_info');
function save_extra_user_info($user_id) {
update_user_meta($user_id, 'last_edited', current_time('timestamp'));
}
Плагины могут добавлять свои собственные отпечатки пальцев. В этом случае интеграция с плагином Events Manager добавляет экземпляр отпечатка пальца местоположения:
class EM_ContentCDN_Thumbprint_Location extends ContentCDN_Thumbprint_Post {
public function get_name() {
return 'location';
}
public function get_query() {
return array_merge(
parent::get_query(),
array(
'post_type' => array(EM_POST_TYPE_LOCATION),
)
);
}
}
new EM_ContentCDN_Thumbprint_Location();
Далее мы приступаем к подключению значений к конфигурации. Во-первых, какие шаблоны URL будут отклонены от использования содержимого CDN? (Например, мы должны подключиться к списку идентиверера со всех страниц, требующих состояния пользователя, таких как /my-preferences/
и /edit-post/
которые, как таковые, не могут быть кэшированы.)
function get_page_path($page_id) {
$page_path = substr(get_permalink($page_id), strlen(home_url()));
// Remove the first and last '/'
return trim($page_path, '/');
}
class ContentCDN_RejectedPageHooks {
function __construct() {
add_filter(
'ContentCDN_ThumbprintsConfig:criteriaitems:rejected:startsWith',
array($this, 'get_rejected_paths')
);
}
function get_rejected_paths($rejected) {
// Variable $user_state_pages is a list of ids from all pages which require user state, and as such cannot be cached
// Eg: /my-preferences/, /edit-profile/, '/edit-post/', etc
$user_state_pages = apply_filters('user_state_pages', array());
// Exclude all pages with user state
foreach ($user_state_pages as $page) {
$rejected[] = trailingslashit(get_page_path($page));
}
return $rejected;
}
}
new ContentCDN_RejectedPageHooks();
Плагины также могут подключиться, чтобы отклонить URL-адреса. В этом случае наша интеграция с плагином Public Post Preview определяет, что всякий раз, когда URL имеет параметр preview=1
(который необходим для предварительного просмотра неопубликованного поста), мы перейдем прямо к серверу:
class ContentCDN_PPP_RejectedURLHooks {
function __construct() {
add_filter(
'ContentCDN_ThumbprintsConfig:criteriaitems:rejected:hasParamValues',
array($this, 'get_rejected_paramvalues')
);
}
function get_rejected_paramvalues($paramvalues) {
// Reject the CDN if viewing a preview post
$paramvalues[] = array(
'preview',
1
);
return $paramvalues;
}
}
new ContentCDN_PPP_RejectedURLHooks();
Давайте определим, какие отпечатки пальцев использовать, на основе шаблона URL. Обратите внимание, что, как уже упоминалось, авторы будут использовать пользователя и размещать отпечатки пальцев, если URL имеет tab=followers
параметр, и в этом случае он будет использовать только пользователь отпечаток пальца. Кроме того, отпечаток пальца будет использоваться, когда URL начинается /posts/
с; однако, если tab=comments
присутствует в URL, то комментарий отпечаток пальца также будет использоваться. (В PoP запрос /posts/this-is-the-post-slug/
покажет содержание поста, но вместо этого запрос /posts/this-is-the-post-slug/?tab=comments
покажет комментарии поста.)
class ContentCDN_ThumbprintHooks {
function __construct() {
add_filter(
'ContentCDN_ThumbprintsConfig:criteriaitems:thumbprint:isHome',
array($this, 'get_thumbprint_ishome'),
10,
2
);
add_filter(
'ContentCDN_ThumbprintsConfig:criteriaitems:thumbprint:startsWith',
array($this, 'get_thumbprint_authorpaths'),
10,
2
);
add_filter(
'ContentCDN_ThumbprintsConfig:criteriaitems:thumbprint:noParamValues',
array($this, 'get_thumbprint_postnoparamvalues'),
10,
2
);
add_filter(
'ContentCDN_ThumbprintsConfig:criteriaitems:thumbprint:startsWith',
array($this, 'get_thumbprint_postpaths'),
10,
2
);
add_filter(
'ContentCDN_ThumbprintsConfig:criteriaitems:thumbprint:hasParamValues',
array($this, 'get_thumbprint_commentparamvalues'),
10,
2
);
}
function get_thumbprint_ishome($ishome, $thumbprint) {
// Home needs POST and USER thumbprints
$home_thumbprints = array(
'user',
'post',
);
if (in_array($thumbprint, $home_thumbprints)) {
return true;
}
return $ishome;
}
function get_thumbprint_authorpaths($paths, $thumbprint) {
// The author page displays the user information + user posts. So simply add the partial path for the author URL slug prefix, eg: 'author/', to catch all URLs for the authors, such as getpop.org/en/author/leo/
$author_thumbprints = array(
'user',
'post',
);
if (in_array($thumbprint, $author_thumbprints)) {
global $wp_rewrite;
$paths[] = $wp_rewrite->author_base.'/'; // This will produce "author/"
}
return $paths;
}
function get_thumbprint_postnoparamvalues($noparamvalues, $thumbprint) {
if ($thumbprint == 'post') {
// Array of: elem[0] = URL param, elem[1] = value
$noparamvalues[] = array(
'tab',
'followers'
);
$noparamvalues[] = array(
'tab',
'following'
);
}
return $noparamvalues;
}
function get_thumbprint_postpaths($paths, $thumbprint) {
// Posts displays the post content and the post authors’ information. Because they are placed under category "Posts", whose slug is "posts", then they will all have posts/ initially in their URL.
$posts_thumbprints = array(
'user',
'post',
);
if (in_array($thumbprint, $posts_thumbprints)) {
$paths[] = 'posts/';
}
return $paths;
}
function get_thumbprint_commentparamvalues($paramvalues, $thumbprint) {
$pages = array();
if ($thumbprint == 'comment') {
$paramvalues[] = array(
'tab',
'comments'
);
}
return $paramvalues;
}
}
new ContentCDN_ThumbprintHooks();
Как уже упоминалось, плагин Event Manager создал отпечаток пальца, который должен использоваться при просмотре /locations/
страницы, который отображает список всех местоположений событий:
class EM_ContentCDN_LocationThumbprintHooks {
function __construct() {
add_filter(
'ContentCDN_ThumbprintsConfig:criteriaitems:thumbprint:startsWith',
array($this, 'get_thumbprint_paths'),
10,
2
);
}
function get_thumbprint_paths($paths, $thumbprint) {
if ($thumbprint == 'location') {
$paths[] = 'locations/';
}
return $paths;
}
}
new EM_ContentCDN_LocationThumbprintHooks();
Теперь мы можем создать файл конфигурации. Как поясняется в моей предыдущей статье о обслуживающих работников,этот файл будет создан при развертывании новой версии веб-сайта, ссылаясь на внутреннюю страницу, /generate-contentcdn-configfile/
которая выполняет логику для создания файла конфигурации. (Вы можете просмотреть пример генерируемого файла конфигурации из PoP.) Он будет выглядеть следующим образом:
window.contentCDNConfig = {
cdnDomain: "https://content.getpop.org",
homeDomain: "https://getpop.org",
appVersion: 4.3,
thumbprints: ["post", "user", "comment", "location"],
thumbprintValues : {
post: 1492681259,
user: 1491829696,
comment: 1487996106,
location: 1490356354
},
criteria: {
rejected: {
"startsWith":["my-preferences/","my-posts/","edit-profile/","edit-post/","follow/","unfollow/","recommend/","unrecommend/"],
"hasParamValues":[["preview",1]],
"noParamValues":[],
"isHome":false
},
thumbprints: {
"post":{
"startsWith":["author/","posts/"],
"hasParamValues":[],
"noParamValues":[["tab","followers"], ["tab","following"]],
"isHome":true
},
"user":{
"startsWith":["author/","posts/"],
"hasParamValues":[],
"noParamValues":[],
"isHome":true
},
"comment":{
"startsWith":[],
"hasParamValues":[["tab","comments"]],
"noParamValues":[],
"isHome":false
},
"location":{
"startsWith":["locations/"],
"hasParamValues":[],
"noParamValues":[],
"isHome":false
}
}
}
};
Осталось добавить только одну вещь: конфигурацию CORS. Потому что веб-сайт под https://getpop.org
будет доступ к содержимому из , доступ https://content.getpop.org
к этому домену должны быть явно предоставлены в .htaccess
:
<IfModule mod_headers.c>
SetEnvIf Origin "http(s)?://(.+.)?(getpop.org|content.getpop.org)$" AccessControlAllowOrigin=
Header add Access-Control-Allow-Origin %{AccessControlAllowOrigin}e env=AccessControlAllowOrigin
Header add Access-Control-Allow-Methods GET
</IfModule>
Интеграция с работниками сферы услуг
Если на веб-сайте есть работники службы, чтобы предоставить возможности просмотра в автономном режиме, то содержимое CDN также должно быть интегрировано в соответствующий service-worker.js
файл. Интеграция ниже следует реализации для PoP, описанные в моей предыдущей статье о обслуживающих работников.
Интеграция необходима из-за постоянно меняющегося характера значения параметра отпечатков пальцев, добавленного в URL. Представим себе следующий сценарий:
- Текущее значение отпечатков пальцев последней должности
%THUMBPRINT_VALUE_1%
. -
Пользователь загружает «Первый пост», под URL
/posts/first-post/
. Запрошенный URL будет/posts/first-post/?thumbnail=%THUMBPRINT_VALUE_1%
. -
Работник службы проверяет, есть ли у него это содержимое в кэше. Предполагая, что это не так, он отправляет запрос в сеть.
-
После того, как содержимое извлекается с сервера, сотрудник службы кэширует ответ под URL
/posts/first-post/?thumbnail=%THUMBPRINT_VALUE_1%
. -
Пользователь создает еще один пост, «Второй пост». Это позволит обновить значение отпечатков пальцев последнего поста до
%THUMBPRINT_VALUE_2%
. -
Пользователь загружает «Первый пост» снова, под URL
/posts/first-post/
. Запрошенный URL будет/posts/first-post/?thumbnail=%THUMBPRINT_VALUE_2%
. -
Работник службы проверяет, есть ли у него это содержимое в кэше. Даже если он имеет его, потому что он кэшировал его в первый раз сообщение было загружено, он не найдет его, потому что URL изменился.
Таким образом, что необходимо для работников службы является ассоциировать /posts/first-post/?thumbnail=%THUMBPRINT_VALUE_1%
URL-адреса и /posts/first-post/?thumbnail=%THUMBPRINT_VALUE_2%
, так что если запись кэшируется под прежним URL, то запрос на последний URL также попал бы в кэш.
Решение, в нее выполнено, заключается в том, чтобы сохранить индекс в IndexedDB (сделано с использованием мощного локального API Forage), при этом каждая запись представляет собой пару исходного URL,отображенного на URL-адресе CDN. Эти записи отображать содержимое CDN URL, под которым содержимое было кэшировано с его исходным URL. Это позволяет рассматривать различные URL-адреса CDN контента как псевдонимы, но создавая один и тот же исходный URL. (Например, оба https://content.getpop.org/posts/first-post/?thumbnail=%THUMBPRINT_VALUE_1%
и производят тот же https://content.getpop.org/posts/first-post/?thumbnail=%THUMBPRINT_VALUE_2%
исходный URL: https://getpop.org/posts/first-post/
.) Таким образом, если есть промах при проверке кэша для данного URL, мы все еще можем проверить на хит на его псевдоним, если он существует.
Для этого мы модифицируем исходный sw-template.js
файл, из которого мы будем генерировать service-workers.js
файл. Мы function getOriginalURL
добавляем, что будет делать ровно противоположное function convertURL
тому, что было реализовано раньше: Учитывая URL, который направляется через содержимое CDN, получить URL он возник из.
Во-первых, мы добавляем дополнительную конфигурацию, необходимую в config
объекте: CDN и домашние домены, а также какие параметры были добавлены при первом преобразовании URL:
var config = {
...
contentCDN: {
params: $contentCDNParams,
domains: {
cdn: $contentCDNDomain,
original: $contentCDNOriginalDomain
}
}
};
Мы подключаем значения конфигурации:
function get_sw_configuration($configuration) {
...
$configuration['$contentCDNOriginalDomain'] = get_site_url();
$configuration['$contentCDNDomain'] = "https://content.getpop.org";
$configuration['$contentCDNParams'] = array(
"version",
"thumbprint"
);
return $configuration;
}
И мы реализуем логику:
function getOriginalURL(url, opts) {
// If the current URL is pointing to the Content CDN
if (opts.contentCDN.domains.cdn && url.substr(0, opts.contentCDN.domains.cdn.length) == opts.contentCDN.domains.cdn) {
// Replace the domain, from the CDN one to the original one
url = opts.contentCDN.domains.original + url.substr(opts.contentCDN.domains.cdn.length);
// Remove the unneeded parameters, eg: version, thumbprint
url = stripIgnoredUrlParameters(url, opts.contentCDN.params);
}
return url;
}
Мы function addToCache
изменим, чтобы установить индекс псевдонима URL в IndexedDB:
function addToCache(cacheKey, request, response, opts) {
if (response.ok) {
// Add to the cache
var copy = response.clone();
caches.open(cacheKey).then( cache => {
cache.put(request, copy);
});
// Save an entry on IndexedDB for the alias URL to point to this request
var original = getOriginalURL(request.url, opts);
if (original != request.url) {
// Set the new request on that position
localforage.setItem('Alias-'+original, request.url);
}
}
return response;
}
В function onFetch
, теперь мы можем изменить «кэш, падающий обратно в сеть» и «кэш, то сеть» стратегии, так что если есть промах для URL, мы проверяем на хит под псевдонимом URL:
function onFetch(event, opts) {
...
if (strategy === SW_STRATEGIES_CACHEFIRST || strategy === SW_STRATEGIES_CACHEFIRSTTHENREFRESH) {
/* Load immediately from the Cache */
event.respondWith(
fetchFromCache(request)
// Modification here
.catch(() => localforage.getItem('Alias-'+getOriginalURL(request.url, opts)).then(alternateRequestURL => fetchFromCache(new Request(alternateRequestURL))))
.catch(() => fetch(request, fetchOpts))
.then(response => addToCache(cacheKey, request, response, opts))
);
...
}
...
}
Мы также должны изменить, function refresh
чтобы сохранить запись ETag под исходным URL, позволяя различным URL-адресам CDN контента, принадлежащим к одной и той же странице, делиться своими значениями ETag. Таким образом, если страница была сначала кэширована под URL и обновленная версия была загружена под другим URL, мы все еще можем уведомить пользователя об обновлении.
function refresh(request, response, opts) {
var ETag = response.headers.get('ETag');
if (!ETag) {
return null;
}
// Modification here
var key = 'ETag-'+getOriginalURL(request.url, opts);
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
...
});
});
}
Наконец, нам нужно обновить конфигурацию CORS в .htaccess
файле. Потому что service-workers.js
живет под https://getpop.org
доменом, но содержание извлекается из https://content.getpop.org
, мы должны позволить для кросс-домен доступ к ETag
заголовку:
<IfModule mod_headers.c>
# Integration between content CDN and service workers: Allow service-worker.js running in getpop.org to read the ETag and Access-Control-Allow-Origin headers coming from content.getpop.org
Header add Access-Control-Allow-Headers ETag
Header add Access-Control-Expose-Headers ETag
</IfModule>
Интеграция с оффлайн первый
В моей предыдущей статье о обслуживающих работников, я объяснил, как реализовать в автономном режиме в первую очередь, с тем чтобы сделать веб-сайт browsable даже тогда, когда пользователь не имеет подключения к Интернету. Реализация, которую я предложила, опирается на использование стратегии кэширования«кэша, то это сеть»,в которой содержимое подается непосредственно из кэша сотрудника службы, когда это возможно, и сетевой запрос одновременно отправляется, чтобы проверить, является ли содержимое был обновлен, и в этом случае пользователю отображается уведомление для обновления страницы. К несчастью, сетевой запрос может обрабатываться http-кэшом браузера, таким образом, так и не достигая сервера. Чтобы избежать этого, мы должны добавить параметр sw-cachebust
со значением метки времени к URL.
Однако, если мы добавим параметр sw-cachebust
к URL с постоянно меняющимся значением, то этот запрос никогда не будет кэшироваться в CDN. Таким образом, мы должны удалить этот параметр из URL на CDN, прежде чем смотреть вверх, чтобы увидеть, является ли он кэширован.
Поскольку веб-сайт PoP размещается на Amazon Web Services (AWS), я внедрил решение этой проблемы для своего сервиса CDN CloudFront, использующего Lambda-Edge:
Вот функция Node.js, созданная в Lambda-Edge и инициированная CloudFront в качестве события запроса зрителя, чтобы удалить параметр sw-cachebust
из URI:
var stripIgnoredUrlParameters = function(queryString, stripParams) {
// Copied from https://developers.google.com/web/showcase/2015/service-workers-iowa
return queryString
.split('&')
.map(function(kv) {
return kv.split('=');
})
.filter(function(kv) {
return stripParams.every(function(param) {
return param != kv[0];
});
})
.map(function(kv) {
return kv.join('=');
})
.join('&');
}
exports.handler = function(event, context, callback) {
var request = event.Records[0].cf.request;
// Remove parameter 'sw-cachebust' from the request, which was generated using Date.now(),
// only to avoid the Browser Cache and make sure to hit the network in the service workers
request.querystring = stripIgnoredUrlParameters(request.querystring, ["sw-cachebust"]);
callback(null , event.Records[0].cf.request);
}
Функция Node.js, настроенная в Lambda-Edge и вызванная CloudFront в качестве события запроса зрителя, удаляет параметр «sw-cachebust» из URI.
Тестирования
У нас все готово! Пришло время протестировать приложение.
Чтобы проверить, работает ли процесс по назначению, просто откройте инструменты для разработчиков Chrome, перейдите на вкладку «Сеть» и проверьте фактический запрошенный URL от нажатия на любую ссылку:
В приведенном выше примере, при нажатии на https://getpop.org/en/implement/
ссылку, запрашиваемый URL:
URL был преобразован должным образом, чтобы быть направлены через CDN (параметр v
«версия» и tp
«thumbprint»). Мы можем убедиться, что он действительно исходит от CDN, проинспектируя его заголовки:
curl -I "https://content.getpop.org/en/implement/?target=main&module=settingsdata&output=json&v=0.237&theme=wassup&thememode=simple&themestyle=swift&tp=1491907905"
Из него мы получаем:
X-Cache: Hit from cloudfront
Это показывает, что запрос был кэширован CDN (в данном случае AWS CloudFront). Успех!
Заключение
Техника, объясняемая в этой статье, позволяет направлять большую часть контента с веб-сайта WordPress через CDN, тем самым снижая расходы на хостинг и делая веб-сайт быстрее. Мы получаем лучшее из статического веб-сайта без необходимости отказываться от WordPress. Миссия выполнена!
Источник: smashingmagazine.com