Создайте собственную точку входа для быстрой доставки данных

tl; dr: Узнайте, как создать собственную точку входа в серверную часть WordPress, чтобы как можно быстрее доставлять данные в ваше веб-приложение.

Раскрытие информации: я люблю wordpress ... правда :) Я считаю, что это самый простой и быстрый способ запустить веб-сайт. А экосистема и сообщество wordpress упрощают создание практически любых функциональных возможностей.

(здесь идет "но")

Но… Веб-разработка стремительно меняется, это эпоха SPA и PWA, javascript-фреймворки, такие как Angular, React и Vue, предоставляют нам потрясающие инструменты для создания потрясающих реактивных приложений внешнего интерфейса.

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

Как медленно? Что ж ... давайте попробуем старый метод wordpress для доставки данных во внешний интерфейс (jQuery.ajax):

//Frontend script
jQuery.ajax({
    url: 'https://fastshopping.online/wp-admin/admin-ajax.php',
    type: 'post',
    data: {
      action: 'getsomeproducts'
    },
    dataType: 'json',
    success: function(response) {
     console.log(response);
    },
    error: function(jqXHR, textStatus, errorThrown) {
     console.log('error', jqXHR, textStatus, errorThrown);
    }
  });
//Backend
add_action( 'wp_ajax_getsomeproducts', 'getsomeproducts' );
function getsomeproducts(){
  $query = new WC_Product_Query( array(
    'limit' => 12,
  ));
  $products = $query->get_products();
  wp_send_json($products);
}

В приведенном выше примере я беру 12 продуктов из базы данных и отправляю их в свое веб-приложение. Я использую WC_Product_Query, который является оболочкой woocommerce для WP_Query, на новой установке WordPress с 5000 продуктами в базе данных, полдюжине плагинов на общем сервере NGINX. Это чрезвычайно простой и распространенный запрос данных, без каких-либо специальных эффектов ... Но для получения данных потребовалось около 1 секунды.

Анализ запроса показывает TTFB (время до первого байта) 952 мс… Это практически навсегда :(

Хорошо ... старой школы недостаточно, давайте попробуем «нового» ребенка на блоке, WP REST API (Woocommerce реализовал свой собственный WC REST API поверх него)

fetch('https://fastshopping.online/wp-json/wc/v3/products?consumer_key=ck_XXXXX&consumer_secret=cs_XXXXX&per_page=12', {
   method: 'get'
  }).then(function(response) {
   console.log(response);
  }).catch(function(err) {
   console.log(err)
  });

Хороший Хью? Бэкэнд-код не нужен, и я переключился на fetch вместо jQuery.ajax. Обратите внимание на параметры ключа потребителя и секретного запроса. Вы должны создать их для аутентификации запросов (лично я не вижу причин для аутентификации запросов на получение, но это для другой статьи).

Опять же ... простой запрос, ничего сложного, но время отклика практически не изменилось.

TTFB теперь составляет 853 мс… Недостаточно. Нарушитель сделки?

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

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

tl;dr:

  1. Поддерживайте статический файл со всеми данными о товарах. (подключите wp_update_product для выполнения задания wp_cron, которое запрашивает базу данных и сохраняет результаты)
  2. Создайте свою собственную точку входа (простой скрипт php), которая читает содержимое файла, извлекает требуемые данные и отправляет их во внешний интерфейс.

Первое, что следует заметить, это то, что запросы требуют времени и потребляют ресурсы вашего сервера. Поскольку данные о продукте относительно статичны, мы можем сохранить их в файле json для более быстрого поиска. Этот файл необходимо обновлять при каждом обновлении продукта, поэтому мы будем подключаться к «woocommerce_update_product». Мы также будем использовать wp cron, потому что мы не хотим, чтобы этот процесс мешал или блокировал другие действия сервера.

add_action( 'woocommerce_update_product', 'run_my_cron_job');
function run_my_cron_job(){
 wp_schedule_single_event( time(), 'my_cron' );
 spawn_cron();
}
add_action( 'my_cron','update_products_cache_file' );

Итак, теперь у нас есть запланированное событие, на котором функция update_products_cache_file будет запускаться всякий раз, когда продукт обновляется.

Давайте посмотрим на функцию:

Первым делом мы запрашиваем в базе данных все продукты. Обратите внимание, что я использую $wpdb query. Это просто потому, что WP_Query и WC_Query могут не работать с большими наборами данных (помните, что у меня 5000 продуктов в базе данных).

Для небольших наборов данных я бы рекомендовал использовать WC_Query, он может сохранить весь этот блок кода.

function update_products_cache_file() {
  global $wpdb;
  $query = "
    SELECT p.*, 
    GROUP_CONCAT(pm.meta_key ORDER BY pm.meta_key DESC SEPARATOR 
    '||')
    as meta_keys, 
    GROUP_CONCAT(pm.meta_value ORDER BY pm.meta_key DESC SEPARATOR 
    '||')
    as meta_values 
    FROM $wpdb->posts p 
    LEFT JOIN $wpdb->postmeta pm on pm.post_id = p.ID 
    WHERE p.post_type = 'product' and p.post_status = 'publish' 
    GROUP BY p.ID
  ";
  $products = $wpdb->get_results($query);
 
  function map_meta($a){
    $a->meta = array_combine(
      explode('||',$a->meta_keys),
      array_map('maybe_unserialize',explode('||',$a->meta_values))
    );
    unset($a->meta_keys);
    unset($a->meta_values);
    return $a;
 }
 $products = array_map('map_meta',$products);

Теперь у нас есть массив продуктов, метаданные которых хранятся в $ products. Я пройдусь по массиву продуктов, чтобы извлечь данные, которые мне нужно сохранить, и расположить их в нужной мне структуре. (опять же, вы можете изменить эту часть на любую желаемую структуру данных). Обратите внимание, что мы можем сэкономить много места (а позже и пропускную способность), потому что мы сохраняем только те данные, которые нам нужны. (Если у вас нет артикулов, или вы не управляете запасами, или перекрестные продажи, вам не нужно включать это)

$_wfs_cache = [];
foreach($products as $product){
  $_wfs_cache[$product->ID]= array(
    'name' => $product->post_title,
    'desc' =>  $product->post_content,
    'date' => strtotime($product->post_modified),
    'sku' => $product->meta['_sku'],
    'price' => $product->meta['_price'],
    'sale_price' => $product->meta['_sale_price'],
    'stock' => $product->meta['_stock'],
    'rating' => $product->meta['_wc_average_rating'],
    'upsell' => $product->meta['_upsell_ids'],
    'crosssell' => $product->meta['_crosssell_ids'],
    'sales' => $product->meta['total_sales'],
  );
 }
 file_put_contents(plugin_dir_path( __FILE__ ) .
   'data/_wfs_cache.json'  , json_encode($_wfs_cache) );
}

Выполнено! С этого момента при каждом обновлении продукта будет обновляться наш файл _wfs_cache.json.

Теперь давайте создадим точку входа для извлечения этих данных ... Это простой файл php, ему не нужно загружать wordpress, просто войдите в наш файл _wfs_cache.json, обработайте данные и верните их.

//Backend ep_1.php
<?php
  // Read file content and decode it to PHP Object
  $products = json_decode(file_get_contents( __DIR__ . 
  "/data/_wfs_cache.json"));
  //Use query parameters
  $per_page = $_GET["per_page"];
  
  $result = [];
  custom_sort($products);
//Loop through the data
  foreach ($products as $key=>$value){
    if (custom_filter($value)){
      $result[]=$value;
    }
    if (count($result)>=$per_page){
     break;
    }
 }
 echo json_encode($result);
 exit;
?>
//Frontend
fetch('https://fastshopping.online/wp-content/plugins/woo-fast-
shop/ep1.php?per_page=12', {
   method: 'get'
  })
  .then(function(response) {
   return response.json();
  })
  .then(function(myJson) {
   console.log(myJson);
  })
  .catch(function(err) {
   console.log(err)
  });

Примечания к вышеизложенному:

В демонстрационных целях я не включил всю логику, встроенную в сценарий PHP (базовая аутентификация, сортировка и фильтрация результатов). Это действительно зависит от ваших потребностей и требований.

Хорошо ... Мы сделали это. Стоит ли прилагать усилия? Давай проверим результаты

TTFB теперь составляет всего 208 мсек! Это на 80% меньше, чем наш первоначальный jQuery.ajax запрос, и это то, что вы действительно можете почувствовать при просмотре веб-сайта.

Заключение

Wordpress великолепен как CMS, и его также можно использовать как серверную часть для ваших веб-приложений. Заметным недостатком этой настройки является смехотворно долгое время отклика при извлечении данных с использованием традиционных методов.

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