Перевод статьи «Building robust web apps with React: Part 4, server-side rendering», Matt Hinchliffe
От переводчика: это перевод четвертой части цикла статей «Building robust web apps with React»
Переводы:
- Построение надежных веб-приложений на React: Часть 1, браузерные прототипы
- Построение надежных веб-приложений на React: Часть 2, оптимизация с Browserify
- Построение надежных веб-приложений на React: Часть 3, тестирование с Jasmine
- Построение надежных веб-приложений на React: Часть 4, серверная генерация
Несколько месяцев назад я написал первую часть этой серии, я очень заинтересовался возможностями использования React'а для создания разумных приложений, которые могут избежать слабостей, существующих во многих современных JavaScript приложениях. Наконец, я собираюсь сделать так, чтобы мое приложение запускалось сначала на сервере, а потом запускало тот же код в браузере, завершая цикл изоморфного или адаптивно-гибридного приложения.
В предыдущей части этой серии я блуждал в поисках стратегии тестирования, и эта часть не сильно отличается. И хотя здесь есть примеры и базовые демо изоморфного JavaScript, здесь не так много опенсорс реализаций для изучения. В сторону детали реализации, для меня, самое большое отличие, от написания клиентского JavaScript, это то, что e меня изначально есть данные.
Реорганизация потока данных
Браузерная версия моего приложения Tube Tracker имеет очень простую модель потока данных, но данная модель не совсем подходит для серверной стороны. В браузере данные запрашиваются AJAX запросом, как только загрузится приложение, но изоморфное приложение должно предоставлять в браузер готовые HTML страницы.
React приложение имеет одну точку доступа к стеку компонентов – коренной компонент отрисовывается методом renderComponent
или renderComponentToString
, таким образом данные должны поступать в вершину стека и передаваться вниз. Это идет вразрез с обычным мышлением при работе с React, так как данные должны обрабатываться только тем компонентом, которому они нужны.
Так как данные поступают вниз по стеку, компоненты, которые изначально должны были запрашивать данные, теперь могут использовать метод getInitialState
, как предзагрузочные данные. В остальном компоненты не изменяются, кроме того, что пропускается стадия начальной загрузки состояния.
// ~/app/component/predictions.jsx
var Predictions = React.createClass({
getInitialState: function() {
return {
status: this.props.initialData ? "success" : "welcome",
predictionData: this.props.initialData
};
},
...
});
Данные предоставленные в корень приложения на серверной стороне, также должны быть получены на клиентской стороне, если приложение должно загружаться без тех же данных предоставленных на клиентской стороне, тогда оно будет перерендерено в свое начальное, пустое состояние. Самый простой способ предоставить начальные данные, это сгенерировать их в script элемент, а затем подобрать их при инициализации приложения:
// ~/app/browser/bootstrap.jsx
var React = require("react");
var networkData = require("../common/data");
var TubeTracker = require("../component/tube-tracker.jsx");
window.app = (function() {
...
var initialData = JSON.parse(document.getElementById("initial-data").innerHTML);
return React.renderComponent(<TubeTracker networkData={networkData} initialData={initialData} />, document.body);
})();
React на сервере
Компоненты, использующие обычный JSX синтаксис, перед использованием должны быть трансформированы в обычный JavaScript, но на сервере это не обязательно означает шаг прекомпиляции. Библиотека React Tools может производить трансформацию «на лету». И она упакована в пакет Node-JSX, который прозрачно интерпретирует модули, как только они потребуются. Node-JSX нуждается только в одноразовом подключении к приложению, так как метод require
работает глобально, но пользуйтесь этим осторожно: парсер используемый библиотекой React Tools может упираться на некоторых особенно креативных моментах использования JavaScript, так что, в целях безопасности и производительности, лучше выносить компоненты в файлы с расширением .jsx
.
// ~/server.jsx
require("node-jsx").install({ extension: ".jsx" });
var config = require("./config");
var express = require("express");
var API = require("./app/server/api");
var Bootstrap = require("./app/server/bootstrap.jsx");
Стек компонентов генерируется так же как в браузере, но вместо создания динамического древа, нужна только HTML строка. Для этих целей React предоставляет метод renderComponentToString
, он просто запускает методы getInitialState
и componentWillMount
для каждого компонента. Компоненты должны транслироваться на сервер, не вызывая проблем, если только нигде не используется браузерный (browser-specific) код, так что убедитесь, чтобы весь клиентский код был перемещен в метод componentDidMount
.
Остальное на сервере
Последние несколько шагов для предоставления начального HTML кода в браузер, заключаются в конкретной реализации, но для справки я быстро пройдусь по закулисной реализации приложения Tube Tracker.
Приложение уже использовало Express для предоставления статических ассетов, я добавил дополнительный маршрут для обработки запросов и ответа на них статичным HTML. Данный маршрут, если нужно, делает запрос к API и вбивает данные в шаблон, загруженный из файловой системы:
// ~/server.js
app.get("/", function(req, res) {
new API(config).for(req.query.line, req.query.station).get(function(err, data) {
if (err) {
return res.send(500, "API error");
}
new Bootstrap(data).load(function(err, responseHTML) {
if (err) {
return res.send(500, "Template error");
}
res.send(responseHTML);
});
});
});
Модуль шаблонов крайне прост, он загружает запрошенный файл с диска и заменяет в нем плейсхолдеры на переданные данные. Нет смысла использовать более сложную библиотеку для шаблонизации, так как все, с чем будет работать этот шаблонизатор, это два небольших куска данных:
// ~/app/server/template.js
var fs = require("fs");
var path = require("path");
function Template(target) {
this.target = target;
}
Template.prototype.render = function(data, callback) {
var fullPath = path.resolve(__dirname, this.target);
fs.readFile(fullPath, { encoding: "utf8" }, function(err, template) {
if (err) {
return callback(err);
}
var rendered = template.replace(/{{yield:([a-z0-9_]+)}}/g, function(match, property) {
return data[property];
});
callback(null, rendered);
});
};
module.exports = Template;
Из-за несовместимости браузеров, больше не возможно создавать полный HTML документ используя React компоненты. Это может не касаться React на серверной стороне, но было бы странно, создавать компоненты, которые бы отличались в этом плане на серверной и клиентской стороне.
Я воспользовался возможностью заменить старый и недружественный TrackerNet API на новый TfL REST API. Эта замена немного уменьшает сложность работы, так как избегает работы с XML, но более удобные для использования JSON данные из нового API возвращаются несортированными и с недостатком некоторой полезной информации. По этой причине я построил небольшой прокси для сортировки и добавления дополнительных полезных данных. В будущем здесь можно добавить прослойку для кеширования и предотвращения лишних вызовов к API.
Заключение
Построение приложений, которые должны запускаться на сервере и в браузере, было интересным приключением. React, это замечательный инструмент для построения клиентских приложений и он дает огромную продуктивность, поскольку, с ним действительно просто строить динамические приложения. При тщательном планировании, он поможет превратить хрупкое клиентское JavaScript приложение в надежный, рабочий продукт. Тем не менее, пока-что, я неохотно проповедую изоморфные JavaScript приложения; возросшая сложность разработки превратила построение приложения Tube Tracker в кропотливую работу. Браузерная версия приложения заняла несколько часов разработки, а серверная даже меньше, но структурирование и абстрагирование кода для работы приложений, как единое целое, заняло во много раз больше времени, чем ожидалось.
Относительно небольшое количество проектного кода разделено между двумя окружениями, только хорошо-абстрагированные утилиты и React компоненты были перенесены.
Несомненно, время затрачиваемое на построение изоморфных приложений уменьшится, как только этот процесс станет более часто встречающимся и появятся новые шаблоны, но сейчас меня беспокоит то, что дополнительные затраченные усилия, это просто прихоть разработчика. Если приложение генерируемое на сервере, является достаточно функциональным, тогда нет действительно убеждающего аргумента, что юзер экспириенс будет улучшен загрузкой и выполнением большого объема JavaScript'а, который только повторяет приложение, которое пользователь и так имеет.
Усилия веб-разработчиков, для повторения поведения приложений для смартфонов, привели к бешеной революции используемых нами технологий и мы многому научились, в использовании веба, как платформы для доставки всех видов контента. Некоторые из появившихся фич и техник, как изоморфный JavaScript, очень интересны, но построение новых веб продуктов продолжает становится все сложнее.
Мне правда нравится React, это замечательный инструмент, но мне он нравится, когда он прост.
Вы можете попробовать готовое приложение прямо сейчас (внимание: пример запущен на бесплатном аккаунте, так что эта ссылка может быть неустойчивой) или пройти на GitHub, чтобы посмотреть исходный код. Пожалуйста, комментируйте или твитайте мне, я буду рад получить отзывы.
Автор: jojo97