Введение

Это набор экспериментов в попытке понять, как Marko достигает прогрессивного рендеринга на расстоянии 10 000 футов.

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

  • асинхронный рендеринг не по порядку + сброс по порядку
  • неупорядоченный асинхронный рендеринг + неупорядоченный сброс + неупорядоченная краска

Хотя внутренняя работа может быть сложной, основная идея, которая помогает достичь этого на сервере, заключается в использовании модуля async-writer и его вариаций.

Хотя внутренние API Marko, возможно, эволюционировали из V3 в V5, основная идея заключается в том, что мы пытаемся освоить.

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

Эта запись в значительной степени исследует это по шагам и расширяет каждый предыдущий шаг.

Содержание

Это начинается с примера приложения, которое иллюстрирует, как интегрировать шаблон Marko с простым сервером Node js HTTP и рендерить шаблон на сервере в выходной поток ответов в следующих случаях:

  • Change-set-1: рендер базового шаблона через template.render(templateData, writeableStream);
  • Change-set-2: Почему http ничего не пишет в провод после моего template.render(templateData, writeableStream);
  • Change-set-3: Что это за Async-writer и что происходит, когда мы обертываем им наш writeableStream?
  • Change-set-4: у меня есть асинхронная операция, как мне заставить http writeableStream ждать и отображать в правильном порядке?
  • Change-set-5: исправление № 2 с новыми знаниями об обертывании записываемого потока ответа с помощью Async-Writer.
  • Change-set-6: исправление времени, затраченного в # 4, путем сброса асинхронного записывающего устройства по мере получения байтов.
  • Change-set-7: Исправление фрагментов, отображающихся не по порядку.

Монтаж

> npm i
> node server.js

Change-set-1 (рендеринг базового шаблона Marko v3 на сервере)

Файл package.json, который мы будем использовать. В дальнейшем будет обновляться только файл server.js.

Пример файла server.js будет выглядеть так:

index.marko

нижний колонтитул.marko

Что делает приведенный выше код?

  • Здесь мы вызываем indexTemplate ~ index.marko.
  • Мы используем template.render(templateData, writeableStream).
  • наши данные шаблона {name: 'Frank', count: 30, colors: ['red', 'green', 'blue']}
  • res или объект ответа в http не обернут Async Writer, т.е. это не асинхронный поток, если уж на то пошло.
  • Это простой записываемый поток.
  • Обратите внимание, что templateData — это {}. Это поднимает нашу страницу

Change-set-2 (Проблема: все, что написано после template.render(), не отображается

Далее давайте напишем несколько вещей на нашу страницу:

Скажем, мы хотим просто вывести

  • "<h1>------Before body--------</h1>"

перед нашей страницей, &

  • "<h1>------After body--------</h1>"

после страницы.

Давайте обновим файл server.js, чтобы отразить это.

  • Здесь мы вызываем indexTemplate ~ index.marko.
  • Мы используем template.render(templateData, writeableStream).
  • res или объект ответа в http не обернут Async writer, т.е. это не асинхронный поток, если уж на то пошло.
  • Это простой записываемый поток.
  • наши данные шаблона {name: 'Frank', count: 30, colors: ['red', 'green', 'blue']}
  • Здесь мы пишем что-то до и после templateData. Результат выглядит следующим образом:

Обратите внимание: все, что мы написали после templateRender(), так и не появилось!. Это связано с тем, что выходной записываемый поток res http не обернут Async writer.

Итак, Марко создает внутри него Async writer, сопоставляет его с потоком ответов http и, наконец, завершает его, если:

  • Входящий ответ не был потоком/не асинхронным потоком
  • если текущий рендер создал асинхронный поток.
  • Если этот вызов был частью отрисовываемого дочернего компонента, новый асинхронный поток не создается, поскольку сам входящий поток нормализуется Марко как асинхронный поток, и этот асинхронный поток не будет завершен.
  • Асинхронный поток будет завершен Marko только тогда, когда он достигнет стека вызовов, который его создал.

Таким образом, мы получим следующую ошибку при выполнении этого:

Error: write after end
    at ServerResponse.OutgoingMessage.write (_http_outgoing.js:439:15)
    at Server.server.on (/Users/homedesktop/workspaces/browse/marko-http/server.js:18:7)
    at emitTwo (events.js:106:13)
    at Server.emit (events.js:191:7)
    at HTTPParser.parserOnIncoming [as onIncoming] (_http_server.js:546:12)
    at HTTPParser.parserOnHeadersComplete (_http_common.js:99:23)

Обратите внимание, что res.send() является внутренним для выражения JS, который здесь не используется. res.end() используется для завершения потока, что и сделал Марко.

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

Change-set-3 (Понимание асинхронной записи и базового варианта использования обертывания нашего потока ответов с помощью асинхронной записи)

Давайте обновим файл server.js.

Ответ будет правильно содержать A B C в правильном порядке.

Что происходит, когда шаблон .marko получает такой поток ответов, упакованный асинхронным модулем записи?

index.marko — это начальный шаблон, который отображается на странице. Метод render его рендерера, который принимает параметр out (который отмечает выходной поток), не будет асинхронным потоком, если не указано иное.

Однако, если в этом index.marko есть дополнительные компоненты, как в случае

<include("./footer.marko") name=data.name/>

Затем footer.marko получит асинхронный поток (и все последующие компоненты после него будут записывать в выходной асинхронный поток)

Приведенный выше случай — это простейшее использование Async-writer, которое предоставляет нам асинхронный выходной поток. Он оборачивает поток response/out файла http.

Это делается внутри самого Marko.

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

Но посмотрим, что мы можем сделать с ним дальше.

Change-set-4 (асинхронный модуль записи с рендерингом не по порядку, сбросом по порядку)

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

Итак, если это так, скажем, левая навигация на вашей странице занимает некоторое время:

  1. вы бы продолжали ждать его? Или,
  2. не могли бы вы просто продолжить и отобразить другие зелья шаблона, у которых есть данные?

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

Давайте обновим server.js.

Вывод этого примера по-прежнему будет «ABCD». Как?

Это классический случай использования Async-Writer для записи в асинхронный поток. Как и прежде, входящий ответный записываемый поток http обертывается Async-Writer, чтобы стать выходным записываемым потоком Async.

Затем мы сначала пишем в него букву «А». Далее мы хотим написать «В», но чувствуем некоторую задержку в ответе на «В». Итак, мы заходим внутрь асинхронной операции через

var asyncOut = out.beginAsync()

Конец этой асинхронной операции будет отмечен значком

asyncOut.end();

Конец записываемого потока будет отмечен значком

out.end()

Итак, на данный момент, чтобы имитировать асинхронную операцию, мы используем setTimeout, а затем записываем в нее «B». Затем мы пишем «C», затем еще одну асинхронную операцию, где пишем «D».

После завершения написания «D» мы просто завершаем асинхронную операцию через asyncOut.end().

Но обратите внимание, что мы по-прежнему вызывали out.end гораздо раньше, чем asyncOut.end.

Из этого следует,

  • когда out.end выполнится => Больше писать нечего, можно завершать стрим, если нет асинхронных опов.
  • Но мы создали асинхронную операцию, так что ждем.
  • При вызове asyncOut.end мы смотрим, осталось ли что-то еще написать. Ничего нет, так как поток ответа уже помечен out.end(). Итак, конец стрима.

Это дает результат «ACBD».

Вы могли заметить, что мы записывали в поток ответов в разном порядке. Тем не менее, наш вывод по-прежнему отображается в правильном порядке, как указано выше: «ABCD». Как ?

  • Async-Writer позволяет сбрасывать байты в выходной поток не по порядку, в то же время сбрасывая байты в правильном порядке.
  • Таким образом, вы можете записывать части потока не по порядку. Но байты будут сброшены в правильном порядке
  • Контент, который записывается после асинхронной операции, будет храниться в буфере, и после выполнения асинхронной операции выходные данные буферизируются в правильном порядке.

Change-set-5 (Исправление нашего change-set-2 с помощью наших новых знаний об Async-Writer)

Теперь, когда мы знаем, что может сделать async-writer, давайте посмотрим, сможем ли мы исправить наш набор изменений-2.

Вы заметили, что мы только что сделали? Мы обернули записываемый поток входящего выходного ответа с помощью асинхронного записывающего устройства. Теперь функция render() нашего исходного шаблона index.marko получает асинхронный модуль записи:

  • Пишем сначала '<h1>------Before body--------</h1>'
  • Затем мы визуализируем шаблон.
  • Затем пишем '<h1>------After body--------</h1>'

Поскольку мы передали доступный для записи поток ответа асинхронного вывода в render() marko-runtime, поток не будет помечен как завершенный. Наша обязанность — прекратить поток ответов. Итак, мы заканчиваем его

out.end()

Но так как это происходит после вызова template.render(), поток ответа заканчивается только после завершения этой функции.

Это приносит нам результат, которого мы так долго желали!

Change-set-6 (Улучшение скорости рендеринга change-set-4. Неправильный рендеринг, неправильная промывка и неупорядоченная краска)

Вернемся к change-set-4, о чем он был:

  • асинхронный рендеринг наших данных.
  • неупорядоченный рендеринг (мы хорошо обернули асинхронные операции, чтобы можно было рендерить другие разделы. Фактически, «C» рендерится раньше, чем «B»)
  • правильный порядок буферизации ответа. (Несмотря на то, что сначала рендерится буква C, мы буферизуем ее в правильном порядке благодаря Async-Writer)

Итак, что же в нем было не так?

  • Это заняло слишком много времени.

Если вы заметили, как мы делаем asyncOut.write('D'). Это занимает жалкие 4 секунды. Это может быть любой сервисный вызов на странице. Что бы тогда произошло? Время рендеринга нашей страницы также заняло бы 4 секунды. Как нам этого избежать?

Лучшим способом было бы очищать буфер по мере получения данных.

Давайте обновим server.js

Здесь мы просто используем EventEmitter для отправки данных порциями (чтобы ответ можно было записывать порциями в выходной поток по мере его поступления).

В приведенном выше примере мы используем Async-Writer для устранения проблем в наборе изменений-4.

Async-Writer используется для записи в асинхронный выходной записываемый поток.

Сначала он записывает <h1>Hello world!</h1>, а затем <h1>Bye world</h1> в выходной буфер.

После этого он получает первое событие данных от генератора событий.
Итак, теперь у него есть <h1>Hello world!</h1>, <h1>Bye world</h1>, за которыми следует <h1>1</h1>. А теперь важная строчка:

asyncOut.flush();

Эта строка немедленно очищает буфер и записывает HTML в выходной поток. Итак, мы сразу видим их на странице. Загрузка страницы больше не ждет 4 секунды, чтобы завершить загрузку всех данных. В тот момент, когда приходят первые данные, они передаются.

Вслед за этим, когда происходит событие данных, буфер очищается и записывается в выходной записываемый асинхронный поток.

Но есть одна оговорка: ответ выходит не по порядку. Посмотрим, как это можно исправить. Результат, который мы получаем сейчас:

Change-set-7 (исправление Change-set-6, у которого был неправильный рендеринг, неупорядоченный сброс, с помощью отрисовки по порядку)

Мы видели в Change-set-6, что, хотя мы добились неупорядоченного рендеринга и неупорядоченного сброса, проблема заключалась в том, что ответ приходит не по порядку. Таким образом, вы можете увидеть, как изменился порядок на странице.

Как это можно исправить? Хорошо. для начала это та же проблема, которую Марко решает через client-reorder=true. Здесь Марко сказал бы что-то из следующего:

  • настроить заполнители для асинхронного контента
  • очистить асинхронный контент через теги <noscript/>
  • также сбросьте фрагмент JS, который заботится о перемещении данных, сопоставленных с определенным заполнителем (который поддерживается в определенной позиции в DOM), в заполнитель.

Но предостережение в том, что для этого абсолютно необходим JS, и это может вызвать перекомпоновки (есть несколько способов смягчить это).

Марко называет эту технику неупорядоченной промывкой и рендерингом, но отрисовкой по порядку. Несмотря на то, что фрагменты отображаются и сбрасываются не по порядку, они «фиксированы» и отображаются в правильном порядке в DOM.

Мы попробуем добиться того же с помощью очень элементарного кода. Давайте еще раз обновим файл server.js.

Обратите внимание на строку:

asyncOut.write('<div id="placeholder"></div>');

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

Затем мы делаем

asyncOut.write('<noscript><h3>' + data + '</h3></noscript>');
asyncOut.flush();

Это делается в рамках события .on('data'). Данные записываются в теги <noscript> и сбрасываются из буфера.

Наконец, мы вставляем фрагмент JS в конце, чтобы исправить порядок:

asyncOut.write(`
      <script>
          console.log('executing script...');
          (function(){
              const noscripts = document.getElementsByTagName('noscript');
              const placeholder = document.querySelector('#placeholder');
              let data = '';
              console.log(placeholder);
              Array.from(noscripts).forEach(function (noscript) {
                  data = data + noscript.innerHTML;
              });
              if (placeholder) {
                  placeholder.innerHTML = data;
              }
          })();
      </script>
  `);
  asyncOut.flush();

Это, после сброса, будет запрашивать теги <noscript>, собирать html и помещать их в элемент-заполнитель, тем самым позволяя «появляться» в нужном месте и поддерживать порядок.

Обратите внимание, что в последних 2 примерах мы вообще не использовали/рендерили шаблоны Marko.

Это связано с тем, что Marko помогает вам достичь этого декларативно, с помощью своих основных тегов, называемых ‹await›, которые поддерживают удаление вне очереди через атрибут client-reorder=true

Надеюсь, это было полезное упражнение для раскрытия различных аспектов прогрессивного рендеринга Marko JS.

Между прочим, Marko JS применил все эти подходы задолго до того, как появились сегодняшние основные JS-фреймворки!

Для дальнейшего чтения: