Что будет если соединить Drupal 8 и React.js узнали при переносе сайта

Главные вкладки

Аватар пользователя LLC VelvetDev LLC VelvetDev 6 июля 2017 в 22:22
1

Эта статья о том, как уже существующий проект переносили на React.

Библиотека React считается быстрым из-за VirtualDOM. В компоненте есть метод render, который вызывается при каждом обновлении компонента. Затем результат рендера (здесь и далее под рендером будет иметься в виду именно вызов функции render компонента, а не рендер в реальный DOM) обрабатывается Реактом, сравнивается результат текущего рендера с результатом предыдущего и в реальный DOM вносятся только необходимые изменения, а не целиком. Учитывая, что операции с реальным DOM медленные, это должно быть быстрее.

React хорош тем, что его можно использовать с любым backend. При разворачивании проекта, запуск
npm run build приведет к созданию оптимизированной сборки приложения в build-папке.

В качестве документации был взят сайт https://facebook.github.io/react/.

Начнем мы с создания среды для успешной разработки на React.

npm install -g create-react-app
create-react-app my-app
 
cd my-app
npm start

Создали файл package.json, который является инструкцией/описанием для нашего проекта. Выглядит он таким образом:

{
  "name": "VelvetDev",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "axios": "^0.16.2",
    "jquery": "^3.2.1",
    "prop-types": "^15.5.10",
    "react": "^15.5.4",
    "react-dom": "^15.5.4",
    "react-popup": "^0.8.0",
    "react-slick": "^0.14.11",
    "react-validation": "^2.10.9",
    "slick-carousel": "^1.6.0",
    "validator": "^7.0.0"
  },
  "devDependencies": {
    "react-scripts": "1.0.7"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  }
}

Заполним наш index.html файл

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="theme-color" content="#000000">
    <meta name="description" content="VelvetDev is a Drupal Development company that specialized in portals, social Network and E-commerce sites. Drupal is our passion. Drupal is our life. Drupal is our everything."/>
    <meta name="keywords" content=”drupal, drupal 7, drupal 8, php,
         development, site” />
 
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/latest/css/bootstrap.min.css">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/latest/css/bootstrap-theme.min...>
    <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/slick-carousel/1.6.0/slick.min.css" />
    <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/slick-carousel/1.6.0/slick-theme.... />
    <title>VelvetDev - Drupal Development company</title>
  </head>
  <body>
    <noscript>
      You need to enable JavaScript to run this app.
    </noscript>
    <div id="root"></div>
    <div id="popupContainer"></div>
  </body>
</html>

Далее, запустим наш сайт с помощью команды npm start, это возможно, потому что у нас в файле package.json есть секция scripts, в которой прописана команда start.

Главный компонент App

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

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

Файл app.js выглядит следующим образом:

import React, {Component} from 'react';
import './App.css';
import Header from './components/Header/Header';
import Welcome from './components/Welcome/Welcome';
import Services from './components/Services/Services';
import WhatWeDo from './components/WhatWeDo/WhatWeDo';
import Team from './components/Team/Team';
import Contacts from './components/Contacts/Contacts';
import Footer from './components/Footer/Footer';
import axios from 'axios';
 
const baseUrl = 'YUOR_SITE';
const suffixUrl = 'YOUR_SUFFIX';
 
class App extends Component {
 
  constructor(props) {
    super(props);
    this.state = {
      configData: {},
      xCSRFToken: ''
    };
  };
 
  componentDidMount() {
    axios.get(baseUrl + suffixUrl + '/jsonapi/config_pages/front_page')
      .then(res => {
        if (res.status === 200) {
          const configData = res.data.data[0].attributes;
          this.setState({configData: configData});
        }
      });
    axios.get(baseUrl + suffixUrl + '/session/token')
      .then(res => {
        if (res.status === 200) {
          this.setState({xCSRFToken: res.data});
        }
      });
  }
 
  render() {
    return (
      <div className="App">
        <Header configData={ this.state.configData }
                baseUrl={ baseUrl }
                xCSRFToken={ this.state.xCSRFToken } />
        <div className="wrapper">
          <div className="container-fluid">
            <Welcome configData={ this.state.configData } />
            <Services configData={ this.state.configData }
                      baseUrl={ baseUrl }
                      source={suffixUrl + "/api/v1/services"} />
            <WhatWeDo configData={ this.state.configData }
                      baseUrl={ baseUrl }
                      source={suffixUrl + "/api/v1/projects"} />
            <Team configData={ this.state.configData }
                  baseUrl={ baseUrl }
                  source={suffixUrl + "/api/v1/teams"} />
            <Contacts configData={ this.state.configData }
                      baseUrl={ baseUrl + suffixUrl }
                      xCSRFToken={ this.state.xCSRFToken } />
            <Footer configData={ this.state.configData }
                    baseUrl={ baseUrl }
                    source={suffixUrl + "/api/v1/social-links"} />
          </div>
        </div>
      </div>
    );
  }

}
 
export default App;

Опишем используемые для создания Velvetdev методы и компоненты

Создание класса: class App extends Component.

       Состояние
Задавая свойство state для текущего класса, мы говорим реакту: “Это данные, за которыми стоит следить”. При изменении состояния React будет проводить свои магические манипуляции с виртуальным DOM и заново рендерить все изменившиеся элементы. При использовании свойства state необходимо придерживаться одного простого правила: состояние задается присваиванием всего один раз при инициализации компонента. Другими словами, не стоит присваивать значения напрямую, а вместо этого использовать функцию setState. Событие onChange меняет состояние.

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

Внутри этого класса мы создали конструктор, которому необходимо передать аргумент, содержащий все эти настройки (чаще всего называют просто props).

У каждого компонента могут быть свойства. Они хранятся в this.props, и передаются компоненту как атрибуты. В свойство можно передать любой javascript примитив, объект, переменную и даже выражение. Значение свойства должно быть взято в фигурные скобки.

Ключевое слово this будет относиться к экземпляру компонента внутри которого находится.

getInitialState() вызывается один раз перед монтированием компонента. Возвращаемое значение будет использоваться в качестве начального значения this.state.

       Рендеринг

Мы создали компонент. Но React пока не знает, что с ним делать. Чтобы увидеть результат нашей работы необходимо сообщить библиотеке react-dom, что надо это отрендерить и показать. Делается это с помощью функции render, которую мы предварительно изъяли из библиотеки: первым параметром функция принимает компонент, который нужно отрендерить, вторым — DOM элемент (или элементы) в которых нужно отрендерить данный компонент.

       Валидация

Валидация нужна для блока контакты. Код этого блока выглядит следующим образом:

import React, {Component} from 'react';
import Validation from 'react-validation';
import axios from 'axios';
import Popup from 'react-popup';
 
class ContactForm extends Component {
 
  constructor(props) {
    super(props);
    this.state = {
      email: '',
      name: '',
      message: '',
    };
    this.onChange = this.onChange.bind(this);
    this.onSubmit = this.onSubmit.bind(this);
  }
 
  onChange(event) {
    let fieldName = event.target.name;
    let stateObject = {};
    stateObject[fieldName] = event.target.value;
    this.setState(stateObject);
  }
 
  onSubmit(event) {
    event.preventDefault();
    const self = this;
    const url = this.props.baseUrl + '/entity/contact_message?_format=json';
    const data = {
      "name": [{"value": this.state.name}],
      "mail": [{"value": this.state.email}],
      "subject": [{"value": this.state.message.split(' ')[0]}],
      "contact_form": [{"target_id": "contact"}],
      "uid": [{"target_id": "0"}],
      "message": [{"value":  this.state.message}]
    };
    axios.get(this.props.baseUrl + '/session/token')
      .then(res => {
        if (res.status === 200) {
          const instance = axios.create({
            headers: {'X-CSRF-Token': res.data}
          });
          instance.post(url, data)
            .then(function (response) {
              if (response.status === 201) {
                Popup.alert('Your message has been successfully sent!');
                self.setState({
                  email: '',
                  name: '',
                  message: '',
                });
              }
            }).catch(function (error) {
            console.log(error);
            Popup.alert('Error!');
          });
        }
      });
  }
 
  render() {
    return <Validation.components.Form ref={c => {
      this.form = c
    }} className="contact-form" onSubmit={this.onSubmit}>
      <div className="col-md-8">
        <div className="contacts-form form-group">
          <div className="form-group border row">
            <div className="col-md-5 half-block top row ">
              <label className="col-sm-4 half-left text-left">
                Name*
              </label>
              <div className="col-sm-8 half-right">
                <Validation.components.Input className="form-control border"
                                             onChange={this.onChange}
                                             value={this.state.name}
                                             errorClassName='error'
                                             placeholder="Name" name='name'
                                             validations={['required']}/>
              </div>
            </div>
            <div className="col-md-2">
            </div>
            <div className="col-md-5 half-block top row">
              <label className="col-sm-4 half-left text-left">
                Email*
              </label>
              <div className="col-sm-8 half-right">
                <Validation.components.Input className="form-control border"
                                             onChange={this.onChange}
                                             errorClassName='error'
                                             value={this.state.email}
                                             placeholder="Email" name='email'
                                             validations={['required', 'email']}/>
              </div>
            </div>
            <div className="col-md-12 whole-block row">
              <label className="col-sm-4 col-form-label whole-left text-left">
                Message*
              </label>
              <div className="col-sm-8 half-center">
                <Validation.components.Textarea className="form-control border"
                                                onChange={this.onChange}
                                                value={this.state.message}
                                                placeholder="Hi"
                                                errorClassName="error"
                                                name='message'
                                                validations={['required']}/>
              </div>
            </div>
            <div className="col-md-12 col-null">
              <Validation.components.Button
                className="btn btn-lg btn-primary btn-block text-uppercase send left">Submit</Validation.components.Button>
            </div>
          </div>
        </div>
      </div>
    </Validation.components.Form>;
  }
 
}
 
export default ContactForm;

Непросто проверять формы с помощью React. Причина - односторонний стиль потока данных. В этом случае мы не можем легко влиять на формы с входов. React-validation предоставляет несколько компонентов, которые «подключены» к форме через метод ввода, прикрепленный компонентом Form.
ПРИМЕЧАНИЕ. Всегда нужно передавать name и validations реквизиты. Они необходимы.
Для использования валидации мы установили react-validation:

npm install react-validation

Компоненты и реквизит

React-validation обеспечивает components объект, который содержит Form, Input, Select, Textarea и Button компоненты. Все они - только пользовательские обертки вокруг собственных компонентов. Они могут принимать любые действительные атрибуты и несколько дополнительных:

  1. containerClassName - Input, Select и Textarea: react-validation обертывает нативные компоненты дополнительным блоком. Эта опора добавляет className к обертке.
  2. errorContainerClassName: Модификатор ошибки обертки className.
  3. validations- Input, Select и Textarea: принимает массив строк проверки, который ссылается на ключи объекта правил.
  4. errorClassName - Input, Select, Button и Textarea: добавляет переданное значение className на появлениях ошибок.

Компонент формы

       Validation.components.Form

Самый важный компонент, который обеспечивает сердцевину проверки реакции. Он в основном смешивает привязку между самой формой и дочерними компонентами проверки реакции через context. Любые действительные реквизиты могут быть легко перенесены на Form такие onSubmit и method.
Form Предоставляет четыре общедоступных метода:

  1. validate(name)- проверяет ввод с переданным именем. Разница между этим методом и проверкой по умолчанию заключается в том, что validate маркирует ввод как isUsed и isChanged. name -- имя соответствующего компонента.
  2. showError(name [,hint])- помогает обрабатывать ошибки асинхронного API. hint - необязательный подсказку для показа. Может быть строкой (ключ ошибки, ex 'required') или функция, которая возвращает подсказку (jsx).
  3. hideError(name) - скрывает ошибку соответствующего компонента.
  4. validateAll()- проверяет все компоненты проверки реакции. Возвращает карту (ключ: имя поля prop, значение: Array не прошел правила проверки) недопустимых полей.

       Mounting: componentDidMount

componentDidMount()

Вызывается один раз, только на клиенте (не на сервере), сразу же после того, как происходит инициализация рендеринга. На данном этапе в жизненном цикле компонент имеет представление DOM, к которому мы можем получить доступ с помощью this.getDOMNode().

       Array.prototype.map()

Для нашего сайта использовались массивы, для хранения объектов. Поэтому нам пригодился такой метод как map().
Метод map() создаёт новый массив с результатом вызова указанной функции для каждого элемента массива. Метод map вызывает переданную функцию callback один раз для каждого элемента, в порядке их появления и конструирует новый массив из результатов её вызова. Функция callback вызывается только для индексов массива, имеющих присвоенные значения, включая href="https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/undefined">undefined. Она не вызывается для пропущенных элементов массива (то есть для индексов, которые никогда не были заданы, которые были удалены или которым никогда не было присвоено значение.

Функция callback вызывается с тремя аргументами: значением элемента, индексом элемента и массивом, по которому осуществляется проход.
Если в метод map был передан параметр thisArg, при вызове callback он будет использоваться в качестве значения this. В противном случае в качестве значения this будет использоваться значение
undefined . В конечном итоге, значение this, наблюдаемое из функции callback, определяется согласно обычным правилам определения this, видимого из функции.

Метод map не изменяет массив, для которого он был вызван (хотя функция callback может это делать).

Диапазон элементов, обрабатываемых методом map, устанавливается до первого вызова функции callback. Элементы, добавленные в массив после начала выполнения метода map, не будут посещены функцией callback. Если существующие элементы массива изменяются функцией callback, их значения, переданные в функцию, будут значениями на тот момент времени, когда метод map посетит их; удалённые элементы посещены не будут.
Код из блока Team, где использовался данный метод:

<Slider {...settings}>
            {
              this.state.team.map(function (el, key) {
                return (
                  <div key={key}>
                    <TeamItem
                      image={ self.props.baseUrl + el.user_picture }
                      name={el.first_name}
                      position={el.position}
                    />
                  </div>
                );
              })
            }
</Slider>

Мы использовали у родительского элемента атрибут key. Если объяснить предельно просто: реакту нужна уникальность, чтобы все его механизмы работали корректно. По "ключу" он будет понимать с каким именно дочерним узлом мы работаем и какому родителю он принадлежит.

       React-axios

Данный метод используется для получения данных.
Axios.get является асинхронной функцией, которая означает, что остальная часть кода будет выполнена. И когда ответ сервера будет получен, функция then будет выполнена.

Код из блока WhatWeDo, где использовался данный метод:

  componentDidMount() {
    axios.get(this.props.baseUrl + this.props.source)
      .then(res => {
        if (res.status === 200) {
          this.setState({ projects:res.data });
        }
      });
  }
ВложениеРазмер
Иконка изображения d8.png448.3 КБ

Комментарии

Аватар пользователя fairrandir fairrandir 14 июля 2017 в 14:12

Честно попытался прочитать, тема-то интересная. Но оформление не алё. И при чём здесь перенос сайта? И Друпал вообще? Выглядит как спам на самом деле, криво переведённой статьёй с распиханными ссылками.

UPD: нет претензий

Аватар пользователя bumble bumble 6 июля 2017 в 23:05

Поддержу мнение выше.
И, в качестве компромисса, дам сутки на оформление топа. Если доведете до ума - выведем на главную, если нет - блокируем акк за спам.

Аватар пользователя gun_dose gun_dose 7 июля 2017 в 8:08

А зачем вам джейквери в реакте?))

ЗЫ: похоже, что вчера мой первый проект на реакт + д8 подошёл к своему логическому завершению, планирую написать об этом много букв.

Аватар пользователя Ruslan_03492 Ruslan_03492 14 июля 2017 в 13:19

Админка, оставлена друпаловская. Config_pages ставилось для выборки статических данных вот пример http://prntscr.com/fvl5cz. в нем хранятся заголовки и подзаголовки блоков.
Юзера - это обычные пользователи друпал
Services - taxonomy_term с иконками
WHAT WE DO - это ноды типа project, которые вытаскиваются через вьюс rest api
Формы сделаны через модуль из ядра contact forms

Аватар пользователя gun_dose gun_dose 14 июля 2017 в 14:41

Спасибо, интересный модуль этот config_pages. Но с выпиливанием админки кейс был бы намного интереснее)) Сейчас как раз такой проект завершаем.