Нередко веб-сайты перерастают свою первоначальную архитектуру по мере того, как они развиваются и адаптируются к потребностям своих пользователей. Так было с одним из моих клиентов, который изначально использовал комбинацию плагинов, предназначенных для создания галерей изображений, для создания собственной системы продуктов. С тысячами продуктов установка начала показывать свой возраст, вызывая проблемы с производительностью и надежностью, но, что наиболее важно, ни один из их продуктов не имел выделенных страниц с собственными URL-адресами, вместо этого он был больше похож на веб-приложение. Это было ужасно для пользовательского опыта и SEO — и поэтому самым большим преимуществом было то, что у каждого продукта woocommerce были свои собственные страницы и хорошие канонические URL-адреса.

Я сообщил им, что уверен, что можно будет программно перенести их продукты на Woocommerce, сэкономив сотни часов ручного повторного входа.

Первоначальный подход: собственный серверный плагин

Логичным первым шагом было разработать подход на чистом PHP, разработав специальный серверный плагин для обработки миграции. Этот плагин был разработан для анализа и объединения разрозненных разрозненных данных для каждого продукта с целью беспрепятственного импорта всех данных в Woocommerce.

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

Разворот: очистка внешнего интерфейса

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

let finalResult = {
    "categories": []
};

let anchors = Array.from($(".et_pb_module_header > a"));

function processIframe(index) {
    if (index >= anchors.length) {
        // All iframes processed, show the download modal...
        let dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(finalResult));
        let downloadAnchorNode = document.createElement('a');
        downloadAnchorNode.setAttribute("href", dataStr);
        downloadAnchorNode.setAttribute("download", "gallery.json");
        downloadAnchorNode.innerHTML = "Download JSON";
        downloadAnchorNode.setAttribute("href", dataStr);
        downloadAnchorNode.setAttribute("download", "gallery.json");
        downloadAnchorNode.innerHTML = "Download JSON";
        downloadAnchorNode.style.color = "blue";
        downloadAnchorNode.style.padding = "10px";
        downloadAnchorNode.style.fontFamily = "Arial, sans-serif";
        downloadAnchorNode.style.fontSize = "16px";
        downloadAnchorNode.style.marginRight = "20px";

        let closeAnchorNode = document.createElement('a');
        closeAnchorNode.innerHTML = "Close";
        closeAnchorNode.onclick = function() {
            modal.style.display = "none";
        };
        closeAnchorNode.style.color = "red";
        closeAnchorNode.style.padding = "10px";
        closeAnchorNode.style.fontFamily = "Arial, sans-serif";
        closeAnchorNode.style.fontSize = "16px";

        let modalContent = document.createElement('div');
        modalContent.appendChild(downloadAnchorNode);
        modalContent.appendChild(closeAnchorNode);
        modalContent.style.padding = "20px";
        modalContent.style.textAlign = "center";

        let modal = document.createElement('div');
        modal.appendChild(modalContent);
        modal.style.display = "block";
        modal.style.width = "300px";
        modal.style.height = "150px";
        modal.style.margin = "15% auto";
        modal.style.border = "1px solid #888";
        modal.style.boxShadow = "0 4px 8px 0 rgba(0,0,0,0.2), 0 6px 20px 0 rgba(0,0,0,0.19)";
        modal.style.background = "#fefefe";
        modal.style.zIndex = "9999";
        modal.style.position = "absolute";
        modal.style.left = "0";
        modal.style.right = "0";
        document.body.prepend(modal);
    } else {
        let iframe = document.createElement('iframe');
        iframe.src = anchors[index].href;
        iframe.style.display = "none";
        document.body.appendChild(iframe);

        iframe.onload = function() {
            let iframeWindow = iframe.contentWindow;
            let iframeDocument = iframeWindow.document;

            let jsonGallery = {
                "category_title": iframeWindow.$("h1").text(),
                "woocommerce_products": []
            };

            // Run the original function within the iframe
            iframeWindow.$('.modula-item').each(function() {
                let titleElement = iframeWindow.$(this).find(".jtg-title");
                let siblingElements = iframeWindow.$(this).find("a[data-caption]");

                let woocommerceProduct = {
                    "title": titleElement.text() || 'N/A',
                    "description": 'N/A',
                    "featured_image": 'N/A',
                    "filter_classes": []
                };

                if (siblingElements.length > 0) {
                    let description = siblingElements.attr('data-caption');
                    let imageId = siblingElements.attr('data-image-id');

                    woocommerceProduct.description = description || 'N/A';
                    woocommerceProduct.featured_image = imageId || 'N/A';
                }

                let classList = iframeWindow.$(this).attr('class').split(/\s+/);
                iframeWindow.$.each(classList, function(index, item) {
                    if (item.startsWith('jtg-filter-')) {
                        woocommerceProduct.filter_classes.push(item);
                    }
                });

                jsonGallery.woocommerce_products.push(woocommerceProduct);
            });

            finalResult.categories.push(jsonGallery);
            // Remove the iframe when we're done
            document.body.removeChild(iframe);

            // Move to next iframe
            processIframe(index + 1);
        };
    }
}

// Start processing
processIframe(0);

При запуске скрипта на странице верхнего уровня магазина он начинает с инициализации пустого объекта (finalResult) для хранения очищенных данных. Затем он собирает все ссылки категорий в массив (anchors).

Функция processIframe принимает в качестве параметра индекс, представляющий текущую категорию для обработки. Если все категории были обработаны, он компилирует JSON и представляет ссылку для скачивания во всплывающем модальном окне.

Если еще есть категории для обработки, функция создает невидимый iframe для категории, соскребая данные о продукте при загрузке iframe. Название, описание, избранное изображение и теги каждого продукта (называемые здесь «фильтрами») извлекаются, затем сохраняются в объекте woocommerceProduct, который помещается в массив woocommerce_products категории. Это продолжается до тех пор, пока не будут обработаны все продукты из всех категорий.

Наконец, скрипт упаковывает все очищенные данные в один файл JSON, готовый для загрузки и импорта в Woocommerce.

После успешной миграции мы смогли еще больше повысить производительность, используя переходные процессы и реализовав несколько уровней кэширования на стороне сервера, о счастливом конце и мечтать нельзя!