Часткове промальовування компонентів React на сервері. Частина 2

Table of Content

Це друга стаття із серії Не стандартна робота з React
Частина 1

В попередній частині) ми поговорили про базову версію реалізації часткового промальовування React компонетів на сервері.

Ми маємо значні напрацювання у відображені табличних даних (ZfcDataGrid) на PHP, було б не зовсім справедливо викинути роки роботи, тільки через те, що почали використовувати React. В данній спробуємо реалізувати новий jqGrid React компонет, який працює на базі jQuery і буде повністю взаємодіяти з сервером.

Не має значення яка бібліотека векористовується на сервері, ми будемо лише отримувати HTML і намагатимемось подружити його з React, тому кроки для інших бібліотек будуть схожі.

Налаштування jQuery та jQuery UI

Так як jQuery за роки стала стандартом де-факто у веб-розробці на стороні клієнта, не будемо поки обтяжувати себе вивченням нових біблотек і їх API.

Налаштування і підключення jQuery UI зайняло в мене доволі багато часу. Хоча jQuery UI одна з найвідоміших jQuery бібліотек але в неї зараз не найкращі часи, на даний момент вона не має належної підтримки ES6 синтаксису. Це можна легко можна вирішити додавши незначну конфігурацію в webpack. Ви дуже здивуєтесь але React не дозволяє цього зробити, і додавши webpack.config.js в корінь проекту React просто його проігнорує. Потрібно використовувати додатковий npm пакет react-app-rewired, який допоможе внести потрібні зміни в webpack. Цей пакет буде корисний і в майбутньому, тому варто ознайомитись з його мінімальними можливостями. Мене дуже допоміг коментар на stackoverflow

A lot of people come to this page with the goal of finding the webpack config and files in order to add their own configuration to them. Another way to achieve this without running npm run eject is to use react-app-rewired. This allows you to overwrite your webpack config file without ejecting.

Встановлюємо пакет react-app-rewired:

npm i react-app-rewired --save-dev

Створємо файл config-overrides.js в корні проекту з наступним змістом

module.exports = function override(config, env) {
  config.resolve = {
    alias: {
      'jquery-ui': 'jquery-ui-dist/jquery-ui.js'
    },
  };

  return config;
};

В цьому файлі просто додаються або перевизначаються властивості з оригінального файлу webpack.config.js.
Якщо не зробити описаного вище, то при спробі імпортувати jQuery UI буде помилка в консолі браузера Module not found: Can't resolve 'jquery'...

Перевизначаємо команди в package.json для запуску нашого додатку

/* package.json */

  "scripts": {
-   "start": "react-scripts start",
+   "start": "react-app-rewired start",
-   "build": "react-scripts build",
+   "build": "react-app-rewired build",
-   "test": "react-scripts test --env=jsdom",
+   "test": "react-app-rewired test --env=jsdom"
}

Встановлюємо jquery та jquery-ui:

npm i jquery --save
npm i jquery-ui-dist --save 

Тепер у файлах можна імпортувати jQuery та jQuery UI

import $ from 'jquery';
import 'jquery-ui-dist/jquery-ui.css';
import 'jquery-ui';

Обов’язково після всіх import ... викликів додати наступний код, це допоможе уникнути проблем при роботі з jQuery UI.

window.jQuery = $;
window.$ = $;

Інтеграція jqGrid в React

Основна суть заключається в отриманні згенерованого HTML та JS з серверу через AJAX запит і додавання цього всього на сторінку. Забороняємо React проводити будь які маніпуляції з компонентом після того як дані отримані і відображені на сторінці. Подальшу роботу перебирає на себе jqGrid і React до цього всього немає діла.

Спершу реалізовуємо render метод. Якщо дані ще не підвантажились з сервера показуємо текст Loading...

render() {
  if (this.state.loading) {
    return (<div>Loading...</div>);
  }

  return (
    <div className="animated fadeIn">
      <div ref={el => {
        this.grid = el;
        this.setDangerousHtml(el, this.state.grid);
      }} />
    </div>
  );
}

Коли метод render відпрацював перший раз, виконується componentDidMount, це найкраще місце щоб виконати AJAX запит на севрер і отримати згенерований HTML та JS.

componentDidMount() {
  // here we can get HTML from the server
  if (!this.state.grid) {
    $.ajax({
      url: 'http://localhost/product/marketplace',
      dataType: 'html',
      headers: {'X-Requested-With': 'XMLHttpRequest'},
    }).done(data => this.setState({
      grid: data,
      loading: false,
    })).fail(function (data) {
      console.log('Something went wrong:', data);
    });
  }
}

Якщо запит виконався успішно, ми змінємо стан компоненту, цим самим ініціалізуючи його перемальовування:

this.setState({
  grid: data,
  loading: false,
})

В grid: data присвоюємо сирий HTML та JS код з усіма його тегами <script /> і т.д. Через loading: false сигналізуємо, що дані вже отримано і потрібно забрати Loading... зі сторінки.

На даному етапі метод render запускається вдруге, і промальовує компонент з отриманого вмісту. Тут варто відзначити виклик this.setDangerousHtml(el, this.state.grid), те що ми намагаємось зробити не вітається в світі React, тому метод названий саме таким чином:

/**
 * Like React's dangerouslySetInnerHTML, but also with JS evaluation.
 *
 * @param ref
 * @param html
 */
setDangerousHtml = (ref, html) => {
  if (!html) {
    return null;
  }
  const range = document.createRange();
  range.selectNodeContents(ref);
  range.deleteContents();
  ref.appendChild(range.createContextualFragment(html));
};

Власне це основні моменти. Далі метод shouldComponentUpdate вказує, що після того як дані підвантажено, заборонити перемальовувати компонент

shouldComponentUpdate(props) {
  return this.state.loading;
}

Метод componentWillUnmount допомагає уникнути проблем протікання пам’яті і призначений для видалення попередньо прикріплений подій і т.д. В даному випадку наш компонент не перемальовується, але для чистоти реалізації ми його реалізували

componentWillUnmount() {
  this.getGrid().jqGrid('GridUnload');
}

Повна реалізація компоненту Grid:

import React, { Component } from 'react';
import $ from 'jquery';

import 'jquery-ui-dist/jquery-ui.css';
import 'jquery-ui';

import '../../../../node_modules/jqGrid/css/ui.jqgrid.css';
import '../../../../node_modules/jqGrid/js/jquery.jqGrid.js';
import '../../../libs/jqGrid/i18n/grid.locale-en.js';

window.jQuery = $;
window.$ = $;

let iconsGroup = {
  common: {
    icon_base: "fa",
  },
  // ...
};

$.each(iconsGroup,function(name, icons) {
  $.extend($.jgrid.styleUI.Bootstrap[name], icons);
});

class Grid extends Component {

  $grid = {};

  state = {
    loading: true,
    grid: null
  };

  componentDidMount() {
    // here we can get HTML from the server
    if (!this.state.grid) {
      $.ajax({
        url: 'http://localhost/product/marketplace',
        dataType: 'html',
        headers: {'X-Requested-With': 'XMLHttpRequest'},
      }).done(data => this.setState({
        grid: data,
        loading: false,
      })).fail(function (data) {
        console.log('Something went wrong:', data);
      });
    }
  }

  shouldComponentUpdate(props) {
    return this.state.loading;
  }

  componentWillUnmount() {
    this.getGrid().jqGrid('GridUnload');
  }

  getGrid = () => {
    if (!this.$grid) {
      this.$grid = $(this.grid).find('table:first');
    }
    return this.$grid;
  };

  /**
   * Like React's dangerouslySetInnerHTML, but also with JS evaluation.
   *
   * @param ref
   * @param html
   */
  setDangerousHtml = (ref, html) => {
    if (!html) {
      return null;
    }
    const range = document.createRange();
    range.selectNodeContents(ref);
    range.deleteContents();
    ref.appendChild(range.createContextualFragment(html));
  };

  render() {
    if (this.state.loading) {
      return (<div>Loading...</div>);
    }

    return (
      <div className="animated fadeIn">
        <div ref={el => {
          this.grid = el;
          this.setDangerousHtml(el, this.state.grid);
        }} />
      </div>
    );
  }
}

export default Grid;

Корисні посилання:
* Integrating with Other Libraries
* Using jQuery in React component (The ref’s way)
* [Webpack] Import jquery-ui in ES6 syntax
* Like React’s dangerouslySetInnerHTML
* Render HTML string as real HTML in a React component
* How to append a raw JavaScript tag to document.body without jQuery
* React.js and rich datagrid components OR at least hack [2015]
* Declaratively loading JavaScript in React
* Cross-Domain AJAX doesn’t send X-Requested-With header

Leave a Reply

Your email address will not be published. Required fields are marked *