Часть 2: Как использовать клиентские библиотеки, такие как Leaflet, в Node.

Как обсуждалось в Часть первая: почему?, было бы действительно полезно иметь возможность взять интересный компонент пользовательского интерфейса, например карту, и предварительно отрендерить его на сервере как веб-компонент, с использованием Серверных компонентов.

Однако мы не хотим сами составлять жесткое картирование. На самом деле, мы хотели бы, чтобы это было так же просто, как создание компонента пользовательского интерфейса на стороне клиента. Мы хотели бы использовать блестящую библиотеку картографии, такую ​​как Leaflet, чтобы предоставить нам все основные функции прямо из коробки. К сожалению, Leaflet не работает на стороне сервера.

В этой статье мы сосредоточимся на том, как это исправить, чтобы вы могли использовать Leaflet с серверными компонентами, но вы столкнетесь с теми же проблемами (и вам потребуются очень похожие исправления), если вы выполняете любой другой серверный рендеринг на основе Node, в том числе с React. . Экосистема JS сейчас плохо справляется с изоморфизмом, но с помощью нескольких небольших настроек вы можете преобразовать любую библиотеку, которая вам нравится, для работы где угодно.

Давайте пока сосредоточимся на Leaflet. Он не работает на стороне сервера, потому что не так много людей всерьез смотрят на рендеринг красивых пользовательских интерфейсов вне браузера, поэтому библиотеки JS довольно легко справляются с большими предположениями на основе браузера. Leaflet ожидает нескольких вещей, которые не подходят вне браузера:

  • Глобальные объекты окно, документ и навигатор.
  • Активный элемент в HTML DOM, в который нужно вставить.
  • Тег Leaflet ‹script› на странице, чтобы он мог найти свой URL, чтобы он мог автоматически определять путь к значкам Leaflet.
  • Чтобы экспортировать себя, просто добавив свойство «L» к объекту window.

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

Однако Leaflet - относительно сложный случай. Большинство библиотек не так сильно вовлечены в сложные взаимодействия DOM, и им просто нужны базовые глобальные объекты, которые, как они ожидают, внедряются в них.

Итак, как нам это исправить?

Управление глобальными каталогами браузера

Если вы npm install sheet, а затем require («sheetlet»), вы сразу увидите нашу первую проблему:

›ReferenceError: окно не определено

Исправьте это, и мы коснемся еще нескольких во время require (), как для документа, так и для навигатора. Нам нужно запустить Leaflet в ожидаемом контексте.

Было бы неплохо сделать это, имея где-нибудь модуль DOM, который дает нам документ и окно, и используя их как наши глобальные объекты. Предположим, у нас есть такой модуль. Учитывая это, мы могли бы добавить перед модулем Leaflet что-то вроде:

var fakeDOM = require("my-fake-dom");
var window = fakeDOM.window;
var document = fakeDOM.document;
var navigator = window.navigator;
[...insert Leaflet code...]

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

Сделав что-то подобное, вы станете намного ближе. С любой разумной заглушкой DOM вы сможете успешно импортировать Leaflet сюда. К сожалению, это не удается из-за фундаментальной разницы между браузером и рендерингом Node. На сервере мы должны поддерживать несколько контекстов DOM в одном процессе, поэтому нам нужно иметь возможность изменять документ и окно.

Тем не менее, мы все еще можем это сделать, просто сделав еще один шаг, например:

module.exports = function (window, document) {
  var navigator = window.navigator;
  [...insert Leaflet code...]
}

Теперь это модуль Node, который экспортирует не отдельную Leaflet, а фабричную функцию для создания Leaflet для данного окна и документа, предоставляемого кодом, использующим библиотеку. На самом деле это ничего не возвращает, хотя при вызове, как и следовало ожидать, вместо создания window.L, как это обычно бывает для JS-библиотек браузера. В некоторых случаях это, вероятно, нормально, но в моем случае я бы предпочел оставить Window в покое и напрямую взять экземпляр Leaflet, добавив ниже в конец функции после кода Leaflet:

return window.L.noConflict();

Это говорит Leaflet удалить себя как глобальный и просто предоставить вам библиотеку в качестве ссылки напрямую.

Таким образом, require («leaflet») теперь возвращает функцию, а передача этого окна и документа дает вам рабочий, готовый к использованию Leaflet.

Эмуляция ожидаемого DOM

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

var LeafletFactory = require("leaflet");
var components = require("server-components");
var MapElement = components.newElement();   MapElement.createdCallback = function (document) {
  var L = LeafletFactory(new components.dom.Window(), document);
  var map = L.map(this).setView([41.3851, 2.1734], 12);
  L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
    maxZoom: 19,
  }).addTo(map);
});
components.registerElement("leaflet-map", {prototype: MapElement});

Это должно определять компонент, который генерирует HTML для полной рабочей карты при визуализации. Это не так. Проблема в том, что Leaflet здесь предоставляется узел DOM для рендеринга («это» внутри компонента), и он пытается автоматически рендерить с подходящим размером. Однако это не настоящий браузер, у нас нет размера экрана и мы не занимаемся макетом (поэтому он дешевый), и все на самом деле имеет нулевую высоту или ширину.

Это не такое элегантное исправление, но оно неизбежно при любом подходе к серверному рендерингу: вам нужно выбрать фиксированный размер для исходного рендеринга и подтолкнуть Leaflet к его использованию. Здесь это просто, просто убедитесь, что перед созданием карты вы добавляете:

this.clientHeight = 500;
this.clientWidth = 500;

И с этим все работает.

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

После добавления этого компонента вы можете взять этот компонент, визуализировать его с помощью дерзкого components.renderFragment («‹leaflet-map›‹ / leaflet-map ») и получить рабочий HTML-код для создания прекрасной статической карты. можете отправлять прямо вашим пользователям. Восхитительно.

Остается сделать еще один последний шаг, если вы хотите пойти дальше. Листовка по умолчанию включает набор значков и использует «href» в теге скрипта на странице для автоматического определения URL-адресов этих значков. Это немного хрупко во многих отношениях, в том числе в этой среде, и если вы расширите этот пример, чтобы использовать какие-либо значки (например, добавив маркеры), вы обнаружите, что ваши значки не загружаются.

Однако этот шаг очень простой, вам просто нужно правильно установить L.Icon.Default.imagePath. Если вы хотите сделать это в красивом портативном серверном компоненте, это означает:

var componentsStatic = require("server-components-static");
var leafletContent = componentsStatic.forComponent("leaflet");
L.Icon.Default.imagePath = leafletContent.getUrl("images");

Это вычисляет обращенный к клиенту URL-адрес, который вам понадобится, который сопоставляется с папкой изображений Leaflet на диске (см. Server-Components-Static для более подробной информации).

Делаем это (более) ремонтопригодным

Но есть еще один шаг. В некоторых отношениях это немного беспорядочно, но особенно в том, что мы должны вручную форкнуть и изменить код Leaflet, и поддерживать это самостоятельно в будущем. Было бы здорово автоматизировать это вместо этого, чтобы динамически обернуть нормальный код Leaflet, не дублируя его. С Sandboxed-Module мы можем это сделать.

Sandboxed-Module позволяет вам динамически подключаться к процессу require для Node, чтобы преобразовать код модуля, как вам нравится. Есть много сумасшедших применений этого (например, компиляция языков, отличных от JS), но есть и некоторые очень практичные, такие как наши изменения здесь.

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

Итак, как это выглядит?

var SandboxedModule = require('sandboxed-module');
module.exports = SandboxedModule.require('leaflet', {
  sourceTransformers: {
      wrapToInjectGlobals: function (source) {
        return `
        module.exports = function (window, document) {
          var navigator = window.navigator;
          ${source}
          return window.L.noConflict();
        }`;
      }
  }
});

Вот и все! Теперь ваш проект может зависеть от любой версии Leaflet и требовать, чтобы этот обернутый модуль автоматически получил версию, совместимую с Node, без необходимости поддерживать свою собственную вилку.

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

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

На этом остановимся. В следующем посте мы подробно рассмотрим полноценный рабочий компонент карты с возможностью настройки, статическим контентом и поддержкой маркеров, а также посмотрим, что вы можете сделать, чтобы начать применять это на практике. Не могу дождаться? Ознакомьтесь с https://github.com/pimterry/leaflet-map-server-component, чтобы узнать о кодовой базе компонентов карты.

Как это? Порекомендуйте его ниже и нажмите "Подписаться", чтобы увидеть следующий выпуск.