Представьте, что вы создаете длинную веб-страницу с помощью React. Вы разделили всю свою страницу на компоненты и визуализируете их все один за другим, чтобы создать удобную посадочную страницу.

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

Чтобы справиться с этой проблемой, мы собираемся разделить код нашего приложения на части, а затем асинхронно загрузить его по запросу. Этот подход определенно не нов: вы, вероятно, столкнулись с разделением кода при использовании response-router. Но у нас есть несколько иная задача: нам может потребоваться загрузить десятки компонентов за один маршрут, но при этом сохранить красивый DRY-код.

Вот результат, которого мы хотим добиться:

Проект

Вот пример структуры проекта, которую мы получим:

├── dist                  - Public folder (for bundled assets)
│ └── index.html
├── src                   - Project source folder
│ ├── components          - React components
│ │ ├── blocks            - Components to load asynchronously
│ │ │ ├── Block1.jsx
│ │ │ ├── Block2.jsx
│ │ │ └── Block3.jsx
│ │ ├── App.jsx
│ │ └── Trigger.jsx 
│ ├── index.jsx
│ ├── loaders.js
│ └── style.css
├── .babelrc
├── package.json
└── webpack.config.js

Начнем с конфигурации Webpack.

$ npm init -y
# npm install -g webpack
$ npm install path css-loader style-loader babel-loader --save-dev

webpack.config.js:

const path = require('path');

const PATHS = {
   app: path.join(__dirname, 'src'),
   dist: path.join(__dirname, 'dist')
};

module.exports = {
   entry: {
      app: PATHS.app,
   },
   resolve: {
      extensions: ['', '.js', '.jsx']
   },
   output: {
      path: PATHS.dist,
      filename: '[name].js'
   },

   devtool: 'eval',
   
   module: {
      loaders: [
         {
            test: /\.jsx?$/,
            loader: 'babel?cacheDirectory',
            include: PATHS.app
         },
         {
            test: /\.css$/,
            loader: 'style!css'
         }
      ]
   }
};

Обратите внимание: поскольку я старался сделать конфигурацию как можно более тонкой для ясности и сосредоточиться на основной идее, она не подходит для производства.
Как видно из конфигурации, у нас есть src для всех исходных текстов и dist для скомпилированных ресурсов. Мы используем загрузчики для обработки исходных файлов js и css.

Настройте babel для переноса нашего кода в ES5.

$ npm install babel-loader babel-preset-es2015 babel-preset-react babel-preset-stage-0 --save-dev

.babelrc

{
  "presets": [
    "es2015",
    "react",
    "stage-0"
  ]
}

dist / index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <title>Consecutive components loading</title>
</head>

<body>
    <div id="app"></div>
    <script src="app.js"></script>
</body>

</html>

Теперь у нас есть базовая структура нашего простого проекта React. Нам нужно написать минимальный код для рендеринга еще не реализованного компонента контейнера App в макет.

$ npm install react react-dom --save

src / index.jsx

import React from 'react';
import ReactDOM from 'react-dom';

import App from './components/App';
require('./style.css');
ReactDOM.render(<App />, document.getElementById('app'));

На данный момент мы просто хотим загрузить блоки веб-сайта при первоначальном рендеринге, чтобы подтвердить конфигурацию.

src / components / App.jsx

import React from 'react';
import Block1 from './blocks/Block1';
import Block2 from './blocks/Block2';
import Block3 from './blocks/Block3';
export default class App extends React.Component {
    render() {       
        return <div>
            <Block1 />
            <Block2 />
            <Block3 />
        </div>
    }
}

Давайте создадим компоненты-заполнители для имитации блоков с реальной информацией.

src / components / blocks / Block1.jsx

import React from 'react';
const Block1 = () =>
    <div className="block" style={{background: "#448AFF"}}>
    </div>;

export default Block1;

src / components / blocks / Block2.jsx

import React from 'react';
const Block2 = () =>
    <div className="block" style={{background: "#FF5722"}}>
    </div>;
export default Block2;

src / components / blocks / Block3.jsx

import React from 'react';
const Block3 = () =>
    <div className="block" style={{background: "#727272"}}>
    </div>;
export default Block3;

И вот фрагмент CSS, чтобы придать этим блокам некоторые пропорции:

src / style.css

.block {
    height: 1200px;
    position: relative;
}

Мы готовы к запуску (но мы еще не подошли к идее статьи)!

$ webpack
Hash: 0bbcc19513dd74441a5f
Version: webpack 1.13.1
Time: 1746ms
 Asset    Size  Chunks             Chunk Names
app.js  754 kB       0  [emitted]  app
    + 176 hidden modules

Создание чанков

Дело в том, что эти два нижних блока (красный и серый), когда они находятся за пределами экрана, могут испортить пользовательский опыт быстрой загрузки страницы (представьте, что блоки - это не просто пустые блоки div с цветным фоном). Возможно, пользователям даже не нужно прокручивать первый блок!
Итак, основная идея состоит в том, что мы изначально загружаем только компонент Block1 (т.е. первый фрагмент), а остальные загружаем как пользователь. продолжает прокрутку.

Есть два способа создания чанков в Webpack:

  1. используя require.ensure ()
  2. с использованием Bundle-Loader

Оба метода приводят к созданию отдельных файлов .js для требуемых модулей. Мы собираемся использовать второй, потому что было бы сложнее и многословнее использовать require.ensure () в нашем случае (нам нужно было бы поиграть с require.context).

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

требуется (пакет! ./ file.js) (функция (fileJsExports) {
console.log (fileJsExports);
});

Этот функционал как раз то, что нам нужно. Мы вызываем эту возвращаемую функцию, когда пользователь прокручивает страницу.

$ npm install bundle-loader --save-dev

src / loaders.js

const blocks = ["Block2", "Block3"];
const loaders = {};
for (const block of blocks) {
    loaders[block] = require('bundle?lazy&name=[name]!./components/blocks/' + block + ".jsx");
}

export default loaders;

Этот файл экспортирует объект с именами блоков в качестве ключей и функциями «загрузчика» в качестве соответствующих значений. Когда наступит подходящий момент, мы вызовем эти функции для загрузки необходимых компонентов (параметр lazy указывает Webpack не загружать эти скрипты сразу).
Вы можете заметить, что мы не создаем загрузчик для первого блока - он будет изначально отрисован.

Вернемся к контейнеру приложения:

src / components / App.jsx

import React from 'react';
import $ from 'jquery';
import update from 'react-addons-update';
import loaders from '../loaders';

import Block1 from '../components/blocks/Block1';

export default class App extends React.Component {
    constructor() {
        super();

        // initial state
        this.state = {
            components: [],
            loading: false
        }
    }

    /**
     * Calls a "loader" function to load a dynamic component and updates the state
     * @param blockName the name of the block to load
     */
    loadBlock = blockName => {
        loaders[blockName](component => {
            const newState = update(this.state, {
                components: {$push: [component.default]},
                loading: {$set: false}
            });
            this.setState(newState);
        });
    };


    /**
     * Finds the last trigger element and updates its position
     */
    updateTrigger = () => {
        this.triggerPosition = $('.trigger').last().offset().top;
    };

    componentDidUpdate() {
        this.updateTrigger(); // fetch a new trigger position
    }

    componentDidMount() {
        const windowHeight = $(window).height();
        const blocks = Object.keys(loaders);

        const checkScroll = () => {
            const currentOffset = $(window).scrollTop() + windowHeight; //  tracking the position of the bottom edge of the window
            if (!this.state.loading && currentOffset >= this.triggerPosition) { //  load new component!
                this.setState({loading: true});
                const blockToLoad = blocks.shift();
                this.loadBlock(blockToLoad);
            }

            if (blocks.length === 0) { //  all blocks have been loaded - remove the listener
                $(document).unbind('scroll', checkScroll);
            }
        };

        this.updateTrigger();
        checkScroll();

        $(document).scroll(checkScroll);
    }

    render() {
        const blocks = this.state.components.map((Component, index) => {
            return <Component key={index}/>
        });

        return <div>
            <Block1 />
            {blocks}
            {this.state.loading ? <div>Loading...</div> : null}
        </div>
    }
}

Давайте пройдемся по всем функциональным блокам в этом файле.

  • Мы избавились от явного импорта Block2 и Block3, потому что они нам не нужны во время монтирования, поэтому они не будут отображаться в основном пакете . Вместо этого они будут загружаться при необходимости нашими «загрузчиками»!
  • constructor ()
    Состояние приложения будет содержать текущие загруженные динамические компоненты и флаг загрузки.
  • loadBlock ()
    Как вы помните, массив loaders содержит функции, загружающие динамические компоненты. После успешной загрузки мы обновляем состояние с помощью React Immutability Helpers.

Механизм загрузки динамических компонентов следующий:
Блок может содержать специальный «триггерный» элемент в своей DOM. Каждый раз, когда нижний край экрана достигает этого невидимого элемента, должен загружаться следующий блок.

  • updateTrigger ()
    Мы получаем позицию последнего элемента триггера на странице с помощью функций jQuery (хотя мы, конечно, можем обойтись и без них), а затем сохраняем число в состоянии ( triggerPosition).
  • componentDidUpdate ()
    Обновленный макет контейнера может содержать новый элемент триггера (положение которого мы можем получить только после завершения процесса обновления), о котором мы должны знать.
  • componentDidMount ()
    Начальный обработчик пост-рендеринга. Определенная реализация логики отслеживания позиции на самом деле довольно проста.
  • render ()
    Здесь мы возвращаем Block1 как статический начальный блок и массив элементов JSX, представляющих динамические блоки. Когда браузер занят (верно загрузка), мы показываем простое сообщение.

Установите новые зависимости:

npm install jquery react-addons-update -S

Теперь мы должны создать компонент триггера и поместить его в нужное место.

src / components / Trigger.jsx

import React from 'react';

const Trigger = () => {
    return <div className="trigger"></div>;
};

export default Trigger;

Давайте также добавим немного стиля:

src / style.css

...
.trigger {

    position: absolute;
    top: 1000px; /* suppose 1000px is more than browser height
    /* For visualization purposes */
    width: 100%;
    height: 5px;
    background: #FFCCBC;

}

И наконец:

SRC / компоненты / блоки / Block1.jsx

import React from 'react';
import Trigger from '../Trigger';

const Block1 = ({props}) =>
    <div className="block" style={{background: "#448AFF"}}>
        <Trigger />
    </div>;

export default Block1;

SRC / компоненты / блоки / Block2.jsx

import React from 'react';
import Trigger from '../Trigger';

const Block2 = ({props}) =>
    <div className="block"  style={{background: "#FF5722"}}>
        <Trigger />

    </div>;

export default Block2;

Пришло время финального запуска!

$ webpack
Hash: 547fc6fed01532224132
Version: webpack 1.13.1
Time: 1923ms
      Asset       Size  Chunks             Chunk Names
     app.js    1.07 MB       0  [emitted]  app
1.Block2.js    1.04 kB       1  [emitted]  Block2
2.Block3.js  861 bytes       2  [emitted]  Block3

Чанки сгенерированы, давайте посмотрим на результат.

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

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