[Перевод] ReactJS 15.0.2 Tutorial
Мы создадим простой, но реалистичный модуль комментариев для блога, упрощенный аналог модуля комментариев реального времени, предлагаемый такими ресурсами как Disqus, LiveFyre и Facebook.
Мы обеспечим:
- Представление для отображения всех комментариев
- Форму для ввода и отправки комментариев
- Задел на будущее, для подключения настоящего бэк-енда
Также будут реализованы:
- Optimistic commenting: комментарии появляются на странице раньше чем они сохраняются на сервере, что визуально ускорит наш модуль
- Live updates: комментарии других пользователей появляются на странице в реальном времени
- Markdown formatting: пользователи могут использовать Markdown-разметку для форматирования текста
Финальная версия
Ссылка на GitHub
Запуск сервера
Перед тем как приступить к руководству, нам необходимо запустить сервер. Он представляет из себя простой API, который мы будем использовать для получения и хранения данных. Мы уже написали его для вас на нескольких интерпретируемых языках, он обладает минимально необходимым функционалом. Вы можете ознакомиться с исходным кодом или скачать zip архив содержащий все необходимое.
Приступая
В этом руководстве мы постараемся реализовать все на столько просто, на сколько это возможно. В архиве, что мы упоминали выше, вы найдете HTML файл, в котором мы продолжим работать. Откройте файл public/index.html в вашем редакторе кода. Он должен выглядеть так:
React Tutorial
Весь JavaScript код из руководства мы будем писать в теге script. Поскольку у нас не реализован live-reloading, вам придется обновлять страницу проекта в браузере каждый раз после сохранения изменений. Отслеживать свои результаты вы можете открыв ссылку http://localhost:3000 в своем браузере (после запуска сервера). Когда вы откроете ссылку в первый раз, без каких либо изменений кода, вы увидите финальную версию нашего модуля комментариев. Для того чтоб приступить к работе, необходимо удалить первый тег script, загружающий код финальной версии проекта «scripts/example.js».
Заметка: Мы используем jQuery в нашем проекте для упрощения кода наших будущих ajax запросов, но это НЕ обязательная для работы React библиотека.
Ваш первый компонент
React — модульный, компонуемый фреймворк. Наш проект состоит из нескольких компонентов находящихся в следующей структуре:
- CommentBox
- CommentList
- CommentForm
- CommentList
Создадим CommentBox компонент, который на выходе будет обычным тегом Обратите внимание, названия HTML элементов начинаются с маленькой буквы, когда как названия React классов с большой.
Первое, что бросается в глаза — это XML-подобный синтаксис в представленном JavaScript коде. Мы используем простой прекомпилятор, который на выходе дает чистый JavaScript: Использовать прекомпиляторы не обязательно, можно писать на чистом JavaScript, но в нашем руководстве мы будем использовать JSX синтаксис, на наш взгляд он проще и понятней. Подробнее ознакомиться с ним можно на странице JSX Syntax article.
Мы передаем JavaScript объект с несколькими методами в React.createClass () для создания нового React компонента. Самый важный из переданных методов называется render, он возвращает дерево React компонентов, которое в итоге будет преобразовано в HTML. Вам не требуется возвращать HTML код. Вы можете вернуть дерево компонентов, что вы (или кто-то другой) создали. Такой подход делает React компонуемым: ключевой признак сопровождаемой и хорошо спроектированной архитектуры фронтенда. ReactDOM.render () создает экземпляр корневого компонента, запускает фреймворк и вставляет разметку в DOM элемент, переданный вторым аргументом. Объект ReactDom содержит методы для работы с DOM, в то время как объект React содержит корневые методы, используемые в других библиотеках, например React Native. Вызов ReactDOM.render должен осуществляться после объявления всех компонентов. Это важно.
Создадим скелет для CommentList и CommentForm, которые будут обычными Далее, внесите изменения в компонент CommentBox для использования наших новых компонентов (строки с пометкой »// new»): Обратите внимание на то, как мы смешиваем HTML теги и компоненты, что мы создали. HTML компоненты являются стандартными React компонентами, как и те, что мы объявили, но лишь с одним отличием. JSX препроцессор автоматически перепишет HTML теги в React.createElement (tagName) выражения и оставит все остальное в покое. Это необходимо для предотвращения засорения глобального пространства имен.
Создадим Comment компонент, который будет зависеть от переданных родительским компонентом данных. Данные переданные от родителя доступны как свойство в дочернем компоненте. Доступ к свойствам осуществляется через this.props. Используя реквизиты, мы сможем прочитать данные переданные в Comment из CommentList, и отобразить разметку: Заключив JavaScript выражение в фигурные скобки внутри JSX, вы можете добавить текст или React компоненты в древо. Мы получаем доступ к именованным атрибутам переданным компоненту как ключи в this.props и к любым вложенным элементам, например this.props.children.
Теперь, когда у нас есть объявленный компонент Comments, мы передадим в него имя автора и текст комментария. Это позволит нам многократно использовать один и тот же код для каждого комментария. Теперь добавим несколько комментариев в компонент CommentList: Обратите внимание как мы передали данные из родительского компонента CommentList в дочерние компоненты Comment. Для примера, мы передали Pete Hunt (через атрибут) и This is one connent (через XML-подобный дочерний узел) в первый Comment. Как упоминалось ранее, Comment компонент получает доступ к этим свойствам через this.props.author и this.props.children.
Markdown — это удобный способ форматирования текста. Для примера, текст обернутый в звездочки будет подчеркнутым на выходе. В этом руководстве мы используем стороннюю библиотеку marked, которая конвертирует Markdown разметку в чистый HTML. Мы уже подключили эту библиотеку ранее в нашем HTML файле, так что мы можем начать её использовать. Давайте конвертируем текст комментария с учетом Markdown разметки и выведем его: Все что мы здесь сделали, это вызвали marked библиотеку. Теперь необходимо конвертировать this.props.children из React-подобного текста в обычную строку, которую поймет marked, так что мы специально для этого вызываем функцию toString (). Но здесь есть проблема! Наши обработанные компоненты выглядят в браузере так:» Так React защищает вас от XSS атак. Ниже представлен способ обойти это: Это специальный API, который намеренно усложняет работу с чистым HTML, но мы сделаем исключение для marked. Внимание!: используя подобное исключения, вы целиком полагаетесь на безопасность библиотеки marked. Для этого мы передаем второй аргумент senitize: true, который включает очистку от любых HTML тегов.
До этого момента мы вставляли комментарии напрямую из кода. Теперь попробуем преобразовать JSON объект в лист комментариев. Далее мы будем их брать с сервера, а пока добавим эти строки в наш код: Теперь нам необходимо передать этот объект в CommentList, соблюдая модульность. Изменим CommentBox и RenderDOM.render (), для передачи данных в компонент CommentList, используя метод props: Теперь данные доступны в компоненте CommentList, попробуем динамически отобразить комментарии: Готово!
Заменим комментарии зашитые в коде на данные с сервера. Для этого замените атрибут data на url, как показано далее: Внимание. На этом этапе код не работает.
До сих пор, основываясь на своих параметрах, каждый компонент отрисовывал себя единожды, props неизменны — это значит, что они передаются от родителя и он остается их владельцем. Для организации взаимодействия мы добавим изменяемое свойство в компонент. this.state является приватным для компонента и может быть изменен через вызов this.setState (). После обновления свойства, компонент заново отрисует себя. render () методы написаны декларативно, как функции this.props и this.state. Когда сервер отправляет данные, нам необходимо изменить комментарии в интерфейсе. Добавим в CommentBox компонент отдельным параметром массив с комментариями: getInitialState () выполняется один раз, в процессе жизненного цикла компонента и устанавливает первоначальное состояние компонента.
После создания компонента, мы хотим получить JSON от сервера и обновить данные в компоненте, для отображения их в интерфейсе. Для асинхронных запросов к серверу мы будем использовать jQuery. Данные уже есть на сервере (хранятся в comments.json), что вы запустили в самом начале. Когда данные будут получены с сервера, this.state.data будет содержать: Метод componentDidMount автоматически вызывается React’ом после первоначальной отрисовки компонента. Метод this.setState () несет ответственность за динамическое обновление. Мы заменим старый массив комментариев новым с сервера и наш интерфейс автоматически обновит сам себя. Благодаря этому, нам потребуется сделать незначительные правки для добавления обновления в реальном времени. Для простоты мы будем использовать технологию polling(Частые запросы), но в дальнейшем вы сможете без проблем воспользоваться WebSockets или любой другой технологией. Здесь мы переместили AJAX запрос в отдельный метод и инициируем его вызов после первой загрузки компонента и каждые 2 секунды после. Теперь попробуйте открыть нашу страницу комментариев в браузере и внесите изменения в comments.json файл (в корневой директории вашего сервера); в течении 2-х секунд вы увидите изменения на странице.
Теперь пришло время создать форму комментариев. Наш компонент CommentForm должен запрашивать у пользователя имя и текст комментария, далее отправлять запрос на сервер для дальнейшего сохранения комментария.
В традиционном DOM, input элемент отрисовывается и уже затем бразуер устанавливает его значение. Как результат, значение DOM будет отлично от значения компонента. Плохо, когда значение в представлении отличается от значения в компоненте. В React’е компонент всегда должен соответствовать представлению и не только в момент его инициализации. Следовательно, мы будем использовать this.state для хранения введенных пользователем данных. Объявим первоначальное состояние state с двумя свойствами author и text и присвоим им значение пустой строки. В нашем
React обработчики событий используют camelCase соглашение об именовании. Мы повесили onChange обработчики на два элемента
Сделаем форму интерактивной. После того как пользователь отправит форму, мы должны её очистить, отправить запрос на сервер и обновить список комментариев. Для начала получим данные формы и очистим её. Мы вешаем обработчик onSubmit на форму, который очистит её когда форма будет заполнена правильными данными и отправлена. Вызываем preventDefault () для предотвращения действий браузера по отправке формы по умолчанию.
Когда пользователь отправляет комментарий, нам необходимо обновить лист комментариев, для добавления нового. Имеет смысл реализовать всю эту логику в CommentBox, с тех пор как CommentBox управляет списком комментариев. Мы должны передать от дочернего компонента, родительскому. Мы будем делать это через наш родительский метод render, передав новый обратный вызов (hendleCommentSubmit) дочернему, связав его с событием onCommentSubmit дочернего компонента. Каждый раз, когда происходит событие, вызывается функция обратного вызова: Теперь когда компонент CommentBox предоставил доступ к функции обратного вызова компоненту CommentForm через параметр onCommentSubmit, компонент CommentForm может вызвать функцию обратного вызова когда пользовател отправит форму: Теперь, когда у нас есть функция обратного вызова, нам остается только отправить данные на сервер и обновить лист комментариев:
Наше приложение готово, но ожидание завершения запроса к серверу и появления вашего комментария на странице делает его визуально медленным. Мы можем сразу добавить наш комментарий в лист, не дожидаяс завершения запроса к серверу, и это будет происходить почти мгновенно.
Вы создали модуль комментариев за несколько простых шагов. Узнайте почему необходимо использовать React, или перейдите сразу к изучению API и начинайте писать код! Удачи!
// tutorial1.js
var CommentBox = React.createClass({
render: function() {
return (
JSX Синтаксис
// tutorial1-raw.js
var CommentBox = React.createClass({displayName: 'CommentBox',
render: function() {
return (
React.createElement('div', {className: "commentBox"},
"Hello, world! I am a CommentBox."
)
);
}
});
ReactDOM.render(
React.createElement(CommentBox, null),
document.getElementById('content')
);
Что происходит в коде
Комбинированные компоненты
// tutorial2.js
var CommentList = React.createClass({
render: function() {
return (
// tutorial3.js
var CommentBox = React.createClass({
render: function() {
return (
Comments
Использование реквизитов
// tutorial4.js
var Comment = React.createClass({
render: function() {
return (
{this.props.author}
{this.props.children}
Свойства компонентов
// tutorial5.js
var CommentList = React.createClass({
render: function() {
return (
Добавляем Markdown-разметку
// tutorial6.js
var Comment = React.createClass({
render: function() {
return (
{this.props.author}
{marked(this.props.children.toString())} // new
This is
another
comment
// tutorial7.js
//new start
var Comment = React.createClass({
rawMarkup: function() {
var rawMarkup = marked(this.props.children.toString(), {sanitize: true});
return { __html: rawMarkup };
},
//new end
render: function() {
return (
{this.props.author}
//new
Подключаем модель данных
// tutorial8.js
var data = [
{id: 1, author: "Pete Hunt", text: "This is one comment"},
{id: 2, author: "Jordan Walke", text: "This is *another* comment"}
];
// tutorial9.js
var CommentBox = React.createClass({
render: function() {
return (
Comments
// tutorial10.js
var CommentList = React.createClass({
render: function() {
//new start
var commentNodes = this.props.data.map(function(comment) {
return (
Получение комментариев с сервера
// tutorial11.js
ReactDOM.render(
Реактивное состояние
React гарантирует соответствие данных на сервере и в интерфейсе пользователя.
// tutorial12.js
var CommentBox = React.createClass({
//new start
getInitialState: function() {
return {data: []};
},
//new end
render: function() {
return (
Comments
Состояние обновления
[
{"id": "1", "author": "Pete Hunt", "text": "This is one comment"},
{"id": "2", "author": "Jordan Walke", "text": "This is *another* comment"}
]
// tutorial13.js
var CommentBox = React.createClass({
getInitialState: function() {
return {data: []};
},
//new start
componentDidMount: function() {
$.ajax({
url: this.props.url,
dataType: 'json',
cache: false,
success: function(data) {
this.setState({data: data});
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
//new end
render: function() {
return (
Comments
// tutorial14.js
var CommentBox = React.createClass({
loadCommentsFromServer: function() { // new
$.ajax({
url: this.props.url,
dataType: 'json',
cache: false,
success: function(data) {
this.setState({data: data});
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
}, // new
getInitialState: function() {
return {data: []};
},
componentDidMount: function() {
//new start
this.loadCommentsFromServer();
setInterval(this.loadCommentsFromServer, this.props.pollInterval);
//new end
},
render: function() {
return (
Comments
Добавление новых комментариев
// tutorial15.js
var CommentForm = React.createClass({
render: function() {
return (
//new start
//new end
);
}
});
Контролируемые компоненты
элементе мы присвоим параметру value значение state, и повесим на него обработчик onChange. Этот элемент
с установленным значением атрибута value называется контролируемым компонентом. Подробнее прочитать про контролируемые компоненты вы можете в статье Forms article.
// tutorial16.js
var CommentForm = React.createClass({
//new start
getInitialState: function() {
return {author: '', text: ''};
},
handleAuthorChange: function(e) {
this.setState({author: e.target.value});
},
handleTextChange: function(e) {
this.setState({text: e.target.value});
},
//new end
render: function() {
return (
);
}
});
События
. Теперь, когда пользователь ввел данные в поле
, обработчик событий совершает обратный вызов и модифицирует значение компонента. В последствии значение input будет обновлено для того чтоб отразить текущее значение компонента.
Отправка формы
// tutorial17.js
var CommentForm = React.createClass({
getInitialState: function() {
return {author: '', text: ''};
},
handleAuthorChange: function(e) {
this.setState({author: e.target.value});
},
handleTextChange: function(e) {
this.setState({text: e.target.value});
},
//new start
handleSubmit: function(e) {
e.preventDefault();
var author = this.state.author.trim();
var text = this.state.text.trim();
if (!text || !author) {
return;
}
// TODO: send request to the server
this.setState({author: '', text: ''});
},
//new end
render: function() {
return (
);
}
});
Обратные вызовы как параметры
// tutorial18.js
var CommentBox = React.createClass({
loadCommentsFromServer: function() {
$.ajax({
url: this.props.url,
dataType: 'json',
cache: false,
success: function(data) {
this.setState({data: data});
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
//new start
handleCommentSubmit: function(comment) {
// TODO: submit to the server and refresh the list
},
//new end
getInitialState: function() {
return {data: []};
},
componentDidMount: function() {
this.loadCommentsFromServer();
setInterval(this.loadCommentsFromServer, this.props.pollInterval);
},
render: function() {
return (
Comments
// tutorial19.js
var CommentForm = React.createClass({
getInitialState: function() {
return {author: '', text: ''};
},
handleAuthorChange: function(e) {
this.setState({author: e.target.value});
},
handleTextChange: function(e) {
this.setState({text: e.target.value});
},
handleSubmit: function(e) {
e.preventDefault();
var author = this.state.author.trim();
var text = this.state.text.trim();
if (!text || !author) {
return;
}
this.props.onCommentSubmit({author: author, text: text}); // new
this.setState({author: '', text: ''});
},
render: function() {
return (
);
}
});
// tutorial20.js
var CommentBox = React.createClass({
loadCommentsFromServer: function() {
$.ajax({
url: this.props.url,
dataType: 'json',
cache: false,
success: function(data) {
this.setState({data: data});
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
handleCommentSubmit: function(comment) {
//new start
$.ajax({
url: this.props.url,
dataType: 'json',
type: 'POST',
data: comment,
success: function(data) {
this.setState({data: data});
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
//new end
},
getInitialState: function() {
return {data: []};
},
componentDidMount: function() {
this.loadCommentsFromServer();
setInterval(this.loadCommentsFromServer, this.props.pollInterval);
},
render: function() {
return (
Comments
Оптимизация: оптимистические обновления
// tutorial21.js
var CommentBox = React.createClass({
loadCommentsFromServer: function() {
$.ajax({
url: this.props.url,
dataType: 'json',
cache: false,
success: function(data) {
this.setState({data: data});
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
handleCommentSubmit: function(comment) {
//new start
var comments = this.state.data;
// Optimistically set an id on the new comment. It will be replaced by an
// id generated by the server. In a production application you would likely
// not use Date.now() for this and would have a more robust system in place.
comment.id = Date.now();
var newComments = comments.concat([comment]);
this.setState({data: newComments});
//new end
$.ajax({
url: this.props.url,
dataType: 'json',
type: 'POST',
data: comment,
success: function(data) {
this.setState({data: data});
}.bind(this),
error: function(xhr, status, err) {
this.setState({data: comments}); // new
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
getInitialState: function() {
return {data: []};
},
componentDidMount: function() {
this.loadCommentsFromServer();
setInterval(this.loadCommentsFromServer, this.props.pollInterval);
},
render: function() {
return (
Comments
Поздравляем!