Статьи

Унификация рендеринга на стороне клиента и на стороне сервера с помощью встраивания JSON

Не многие приложения, над которыми я работаю в эти дни, используют исключительно традиционную модель рендеринга на стороне сервера. Они также не используют 100% клиентскую визуализацию и шаблоны. Обычно это смесь, в которой «старый» мир встречает новый мир, порождая некоторые интересные дизайнерские решения. В этой статье я хочу исследовать решение, которое объединяет оба мира и минимизирует дублирование логики рендеринга путем встраивания JSON в представление.

Давайте представим простой пример, иллюстрирующий трюк встраивания JSON. Это на GitHub , с несколькими ветками, показывающими разные подходы. Скажем, у нас есть экран, содержащий список продуктов с подкачкой:Пример экрана

После первоначального запроса к серверу отображается первая страница товаров. После того, как страница показана, мы хотим, чтобы ссылки на пейджинговые страницы были включены с поддержкой AJAX: полная перезагрузка страницы не выполняется. Сервер просто возвращает JSON, представляющий продукты, а клиент представляет их в виде таблицы.

Какое дублирование?

У нас две основные цели:

  • Код для рендеринга таблицы должен быть написан один раз
  • Пользовательский опыт должен быть как можно более плавным (т. Е. Никаких видимых мерцаний для обновлений AJAX на начальной странице)

Подумайте, как бы вы реализовали этот пример. Строго говоря, вы не можете предотвратить дублирование логики. С одной стороны, вам нужно визуализировать таблицу с продуктами со стороны сервера на первом запросе. Как в этом примере на основе JSP / JQuery:

<table>
    <thead>
        <tr>
            <th>Product</th>
            <th>Description</th>
            <th>Price</th>
        </tr>
    </thead>
    <c:forEach var="product" items="${products}">
        <tr>
            <td><c:out value="${product.name}" /></td>
            <td><em><c:out value="${product.description}" /></em></td>
            <td><c:out value="${product.price}" /> Euro</td>
        </tr>
    </c:forEach>
</table>

Затем вам также необходимо снова построить таблицу после успешного вызова AJAX подкачки:

 
var products = // JSON result of AJAX call
$.each(products, function(index, product) {
    productTable.append($('<tr><td>'+product.name+'</td><td><em>'
          +product.description+'</em></td><td>'+product.price+'</td></tr>'));
});

(делайте это только тогда, когда вы можете гарантировать, что конкатенированные строки правильно экранированы!)

Это не просто тяжелая работа по построению строк таблицы снова. Обратите внимание, как нам нужно добавить тег em вокруг описания в обоих шаблонах. Мы уже можем видеть, как происходит конкретное дублирование. А примеры из реальной жизни на порядок сложнее! Кстати, момент, который я хочу подчеркнуть в этом посте, не зависит от используемой платформы Javascript. Вы можете использовать свою любимую библиотеку шаблонов Javascript / MVC (слишком много, чтобы выбрать из!), Я просто выбрал простой JQuery, так как все знакомы с ним. Конечно, то же самое относится и к серверной библиотеке: эта проблема возникает с Rails, Django и любой «традиционной» веб-структурой.

Очевидное решение

Как предотвратить этот беспорядок? Одним из вариантов является создание пустой таблицы заполнителей на стороне сервера и запуск вызова AJAX со стороны клиента после загрузки страницы, чтобы заполнить ее продуктами первой страницы. Твиттер популяризировал этот метод некоторое время с помощью веб-шкалы времени. Несмотря на то, что это перемещает все рендеринг разбитых на страницы вызовов к клиенту, решение, к сожалению, нарушает нашу вторую цель: удобство работы с пользователем. Пользователь сначала видит пустую таблицу, которая позже заполняется:

Диаграмма только на стороне клиента

В зависимости от задержки в сети эффекты варьируются от почти незаметных до очень раздражающих. В моем примере я добавил задержку в 400 мс, чтобы проиллюстрировать эффект дополнительной поездки туда и обратно. Откладывание рендеринга полной страницы до завершения вызова Ajax еще хуже. И подумайте, как это будет масштабироваться со многими компонентами на странице с использованием этого подхода (подсказка: существует ограничение на количество одновременных подключений, которые устанавливают браузеры).

Другой вариант — не передавать JSON клиенту, а возвращать (частично) HTML на стороне сервера только для этого компонента. Хитрость заключается в модульности вашего серверного кода, чтобы вы могли иметь один шаблон как для первоначального запроса, так и для последующих вызовов AJAX. Основанные на компонентах структуры как JSF одобряют этот подход. Тем не менее, консенсус, похоже, движется в сторону использования JSON на клиенте, что дает дополнительное преимущество реального API, а не конкретного рендеринга.

Ленивый вариант — назвать это днем ​​и вернуться к старой серверной модели рендеринга skool , где все вызывает полное обновление страницы. Но это было бы отказом, в то время как есть еще один вариант …

Встроенный JSON

Хотя общая идея рендеринга только на стороне клиента хороша, выполнение может быть улучшено. Что, если сервер сделает данные продукта доступными в формате, удобном для восприятия клиентом при первом запросе, но без необходимости отображать фактический HTML? Оказывается, мы можем сделать это, внедрив JSON в страницу, отображаемую на сервере:

<html>
<head>
    <!-- other header tags omitted -->
    <script>
        // Where the magic happens:
        var initialProducts = ${productJSON};
        // Fetch products using Ajax call and update html.
        function getProducts(page, pageSize) {
               // snipped
        }
        // Render products from JSON
        function updateProducts(products) {
              // similar to previous JS snippet, render table rows
        }
        // On document.ready, render first products.
        $(function () {
            updateProducts(initialProducts);
        });
    </script>
</head>
<body>
   <!-- body containing an empty table and paging links etc. -->
</body>
</html>

(Смотрите полный источник этой страницы здесь.)

Переменная initialProducts JS инициализируется литералом JSON. Этот литерал JSON создается на сервере в контроллере и предоставляется с помощью переменной productJSON в шаблон на стороне сервера. Когда страница находится на клиенте, она может использовать логику рендеринга на стороне клиента без предварительного выполнения запроса AJAX . Другие страницы продукта извлекаются с использованием вызовов AJAX, а затем с использованием той же логики рендеринга на стороне клиента, инкапсулированной в функцию updateProducts. Обратите внимание на диаграмму последовательности с одним обходным циклом на сервере:

Схема встраивается JSON

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

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

Недостатки

Конечно, после того, как я «открыл» этот трюк, оказалось, что я не первый . Есть некоторые недостатки, которые вы должны иметь в виду. Самым большим недостатком является то, что всякий раз, когда ваш JSON содержит строку </ script>, этот трюк не работает . Браузер закрывает блок скрипта в тот момент, когда он встречает этот тег, независимо от того, является ли он частью строки JS или литералом объекта, независимо от контекста. Экранирование на сервере в порядке, так как в противном случае межсайтовый скриптинг возможен .

Поскольку большинство веб-фреймворков отображают JSON или HTML как ответ, а не комбинацию, вам, возможно, придется поискать решение. В моем примере я использую Jackson ObjectMapper непосредственно из контроллера , с небольшой хитростью, чтобы преобразовать результат в строку. Другие фреймворки и языки предоставляют аналогичные возможности, например simplejson.dumps в Python.

Одним из привлекательных свойств подхода только на стороне клиента является то, что вы можете полностью отказаться от визуализированных шаблонов на стороне сервера. Вместо этого HTML может подаваться, например, из краевого узла в CDN. Очевидно, что если вы хотите встроить динамический JSON, это не сработает. Может быть, Edge Side Includes могут помочь, но это не было принято W3C.

И наконец, когда вы пытаетесь объединить клиентский и серверный рендеринг для проблем доступности (то есть сайт должен работать без JS), это не уловка для вас. Даже если вся информация предоставляется в одном запросе, вам все равно нужен JS для рендеринга. Это не прогрессивное улучшение по любому определению. Если вы этого хотите, вам может понравиться публикация в блоге инженера LinkedIn о том, как они поступили наоборот: в этих случаях обращайтесь к «клиентским» шаблонам на сервере.

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