Мы хотим удобства клиентского одностраничного приложения, использующего нашу MV * framework по выбору, но не хотим жертвовать SEO и UX. преимущества отрисовки начальной разметки на сервере. И хотя мы здесь не для того, чтобы выбросить мусор на Node.js, мы также не готовы переходить на технический стек, полностью состоящий из JavaScript. Вот наша попытка получить лучшее из обоих миров, при этом сводя к минимуму дублирование логики или кода между интерфейсным и внутренним мирами.

В OddBird нам посчастливилось в основном работать над проектами с нуля - что означает, что мы выбираем свой собственный стек технологий. Один из первых вопросов - как визуализировать шаблоны для начальной загрузки страницы. Есть много причин предпочесть рендеринг на стороне сервера чистому одностраничному приложению, которое всегда отображает контент в браузере - это лучше для SEO, пользователям не нужно ждать инициализации JavaScript, прежде чем увидеть контент на странице. и т. д. Но также нужно потрудиться, чтобы убедить клиентскую платформу MV * работать хорошо - и эффективно - с разметкой, отрендеренной сервером.

Kit изложил некоторые варианты совместного использования шаблонов между клиентом и сервером и обрисовал в общих чертах один способ, которым мы пытались уменьшить дублирование кода / логики на уровне API. Это отличное начало, но нам все еще нужно превратить эту разметку, отображаемую на сервере, в интерактивное одностраничное приложение.

Получение данных

Есть несколько способов передачи данных с сервера в наше клиентское приложение:

  1. Запросить JSON из API через XHR
  2. Вставить JSON в тег <script> (type="text/javascript" или type="application/json")
  3. Встраивать JSON в атрибуты данных в элементы DOM, соответствующие моделям или коллекциям

Первый вариант является «самым чистым» (позволяет JS последовательно получать данные через API), но добавляет ненужный XHR и время ожидания, прежде чем страница будет готова для взаимодействия с пользователем.

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

Сохранение JSON в атрибутах данных на отдельных элементах DOM имеет то преимущество, что объединяет данные и разметку вместе для каждого компонента, но требует согласованных шаблонов разметки, если JS должен быть повторно использован для различных частей приложения. Это требует взаимодействия с DOM для получения данных, что может легко вызвать проблемы с производительностью для больших коллекций. В нашем случае - с относительно небольшим набором данных для каждой страницы - этот вариант обеспечивает как разумную производительность, так и четкую связь между данными и соответствующей разметкой.

Например, предположим, что мы хотим прикрепить модели и представления к списку комментариев, отображаемому на сервере. При использовании Jinja2 / Nunjucks наша разметка может выглядеть так:

<div class="comment-list">
  <article class="comment" data-js-model="{{ comment|json }}">
    <p>{{ comment.body }}</p>
    <p>{{ comment.author }}</p>
  </article>
</div>

Краткое <aside>

Обратите внимание, что мы используем специальный фильтр json для преобразования объекта в строку JSON. Одним из недостатков совместного использования шаблонов между интерфейсом и сервером (Nunjucks, написанные на JS на интерфейсе, и Jinja2, написанные на Python на сервере), является то, что любые пользовательские фильтры, используемые в общих шаблонах, должны быть написаны на обоих языках. . Итак, чтобы это работало, мы добавили json фильтр в нашу среду Nunjucks:

import nunjucks from 'nunjucks';
const env = new nunjucks.Environment();
env.addFilter('json', (val) => JSON.stringify(val));

И соответствующий фильтр добавлен в нашу среду Jinja2:

from json import dumps
from jinja2 import Environment

def environment(**options):
    env = Environment(**options)
    env.filters.update({
        'json': json,
    })
    return env

def json(val):
    """Return given value as a JSON string."""
    return dumps(val)

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

Ok, </aside>.

Использование данных

Итак, мы сделали данные модели / коллекции доступными в DOM, не требуя дополнительного XHR. Теперь нам нужно добавить наш слой JS, превратив данные в реальные модели или коллекции, которые управляются представлениями.

Детали здесь различаются от одного фреймворка к другому. Поскольку мы используем Backbone.js и Marionette (^ 3.0.0), давайте рассмотрим один подход с этими фреймворками.

import BB from 'backbone';
import Mnt from 'backbone.marionette';

const ViewWithModel = Mnt.View.extend({
  initialize () {
    // Only run this code if an ``el`` option is passed in, signifying
    // that the view is being attached to existing markup in the DOM.
    if (this.options.el) {
      this.attachModel();
    }
  },
  // Find the existing [data-js-model] element, adding a model to the view.
  attachModel () {
    const child = this.$('[data-js-model]');
    const modelData = child.data('js-model');
    this.model = new BB.Model(modelData);
    // Trigger any onRender handlers attached to the view.
    this.triggerMethod('render', this);
  }
});

const myView = new ViewWithModel({ el: $('.comment') });

Или для представления с коллекцией моделей:

import BB from 'backbone';
import Mnt from 'backbone.marionette';

// Create a child view (used for each individual model).
const MyChildView = Mnt.View.extend({
  // ...
});

const ViewWithCollection = Mnt.CollectionView.extend({
  collection: new BB.Collection(),
  childView: MyChildView,
  initialize () {
    // Only run this code if an ``el`` option is passed in, signifying
    // that the view is being attached to existing markup in the DOM.
    if (this.options.el) {
      this.attachChildren();
    }
  },
  // Look through existing child [data-js-model] elements, adding models
  // to the collection, and attaching views to the models.
  attachChildren () {
    const view = this;
    const collection = view.collection;
    const children = this.$('[data-js-model]');
    children.each((idx, el) => {
      const $el = $(el);
      const modelData = $el.data('js-model');
      // Check to see if this model already exists in the collection.
      let model = collection.get(modelData.id);
      if (!model) {
        // Create the new model, and add it to the collection.
        model = collection.add(modelData, { silent: true });
      }
      const childView = new view.childView({ model, el });
      view.addChildView(childView, idx);
    });
    // Prevent the collectionView from rendering children initially.
    view._isRendered = true;
    // Trigger any onRender handlers attached to the view.
    view.triggerMethod('render', view);
  }
});

const myView = new ViewWithCollection({ el: $('.comment-list') });

Теперь у нас есть модель (или коллекция моделей), созданная с использованием данных из нашей разметки, отображаемой на сервере, и все это управляется представлениями Marionette! 🎉

Куда мы идем отсюда?

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

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

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

Как вы решили проблему подключения одностраничного приложения к рендерингу на стороне сервера? Чего нам не хватает или где мы могли бы улучшить наши методы? Напишите нам в Twitter или свяжитесь с нами на нашем общедоступном канале Slack!

Первоначально опубликовано на сайте oddbird.net 6 февраля 2017 г.