Когда мы взялись за гибридный проект, в котором одновременно использовались Django и React, мы столкнулись с дилеммой: как интегрировать две эти части, в особенности, как разрешить шаблонам Django отображать ресурсы JavaScript, сгенерированные при клиентской сборке. Мы нашли изящный способ, позволяющий с этим справиться: использовать Webpack-загрузчик для Django с трекером бандлов Webpack, при помощи которых нам поддался этот этап работы сборочного конвейера. А в этой статье мы научим вас, как это делается.

Настройка клиентской части

Сначала мы должны разобраться с настройкой клиентского приложения. Для этого воспользуемся трекером бандлов Webpack. Он будет добавлен в ваш проект webpack.config.js как новый плагин и будет отвечать за собирание  всех ресурсов, которые будут выданы нам в ходе работы конвейера Webpack (например, файлы JavaScript и CSS, изображения, шрифты, т.д.), а также сохранять их пути в новый файл, который я далее буду называть «файл со статикой».

Для начала нам нужно добавить трекер бандлов Webpack в файл package.json и установить его. Оба этих шага можно завершить за один раз, выполнив в окне терминала  npm install --save webpack-bundle-tracker. Если выполнить тоже с --save, а не с --save-dev, это позволит нам скомпилировать ресурсы клиентской части внутри конвейера развертывания (подробнее об этом ниже).

Закончив с установкой, мы должны сконфигурировать сборку Webpack. Если вы пользуетесь Create React App для начальной загрузки проекта, то сначала нужно выполнить npm run eject. Это поможет вам далее настроить под себя этапы сборки вашего проекта, а также откроет вам доступ к полезным ресурсам, среди прочего – к конфигурационным файлам Webpack. Учтите, что такой выброс – это односторонняя операция, обратить ее нельзя. Подробнее об этом можете почитать здесь.

Ниже приведен пример файла webpack.config.js после того, как мы добавили трекер бандлов Webpack. Этот файл расположен в корне проекта:

const path = require('path');
const webpack = require('webpack');
const BundleTracker = require('webpack-bundle-tracker');

module.exports = {
  context: __dirname,
  entry: './assets/js/index',
  output: {
    path: path.resolve('./dist/'),
    filename: "[name]-[hash].js"
  },
  plugins: [
    new BundleTracker({filename: './webpack-stats.json'})
  ],
};

webpack.config.js

Значение, сохраненное в output.path, представляет, где у нас будут сохраняться скомпилированные ресурсы. В plugins мы предоставляем массив экземпляров тех плагинов, которые собираемся использовать. В данном случае мы прибегнем только к плагину BundleTracker. Инициализируясь, он принимает параметр filename, соответствующий пути к файлу со статикой.

Настоятельно рекомендуем не хранить никаких сгенерированных файлов (будь то файл со статикой или скомпилированные ресурсы) в системе контроля версий, поэтому обязательно убедитесь, что добавили webpack-stats.json и каталог /dist/ в .gitignore.

Настройка серверной части

После того, как у нас будет настроена клиентская часть, мы должны сконфигурировать Django, чтобы стали видны скомпилированные ресурсы.  Затем вы сможете установить загрузчик Webpack для Django, выполнив в вашем окружении pip install django-webpack-loader. Чтобы долговременно сохранить установленные пакеты, так, чтобы их можно было использовать в других окружениях, можете выполнить pip freeze > requirements.txt, сохранив их таким образом в файле. Отсюда и далее вы сможете устанавливать пакеты, перечисленные в файле с требованиями, при помощи pip install -r requirements.txt.

Установив пакет, приступаем к конфигурированию проекта. Как и в большинстве приложений Django, все начинается с файла settings.py. Начинаем с добавления приложения webpack_loader в список INSTALLED_APPS.

Также необходимо добавить переменную STATICFILES_DIRS в settings.py. Так мы укажем на каталог, где будут расположены статические файлы – чтобы Django знал, где их искать.

STATICFILES_DIRS = (
  os.path.join(BASE_DIR, 'dist'),
)

Ниже в этом же файле мы должны сконфигурировать поведение загрузчика Webpack, инстанцировав переменную WEBPACK_LOADER. Ниже приведен пример простейшего конфигурационного файла для этой цели.

WEBPACK_LOADER = {
  'DEFAULT': {
    'BUNDLE_DIR_NAME': '/',
    'CACHE': not DEBUG,
    'STATS_FILE': os.path.join(BASE_DIR, 'webpack-stats.json'),
    'POLL_INTERVAL': 0.1,
    'IGNORE': [r'.+\.hot-update.js', r'.+\.map'],
  }
}

Давайте кратко охарактеризуем, чтобы именно означает каждое из этих свойств.

  • DEFAULT – это имя, по которому мы будем ссылаться на конфигурацию в рамках всего проекта. Загрузчик Webpack для Django поддерживает множество вариантов установки, поэтому WEBPACK_LOADER это dict из dict-ов , содержащих эти конфигурации. Ключи внешнего словаря могут именоваться таким образом, как угодно разработчику, но существует соглашение, что хотя бы одна из конфигураций должна называться DEFAULT;

  • BUNDLE_DIR_NAME – это строка , которая должна добавляться в качестве префикса к путям, ведущим к файлам ресурсов, это понадобится при загрузке ресурсов из хранилища со статическими файлами. Поскольку мы проинструктировали  Django, чтобы при сборе статических файлов он смотрел в dist/, а Webpack настроили так, чтобы он хранил файлы в корне этого каталога, указываем в качестве значения корень каталога со статическими файлами. Если в словаре не будет этого ключа, то по умолчанию вместо него будет принято  webpack_bundles/;

  • CACHE – это свойство, от которого зависит, должны ли мы кэшировать пути доступа к файлам ресурсов (True) или же должны всегда считывать файл со статикой (False).  Наилучший вариант настроек на этот случай определяется в зависимости от переменной  DEBUG в Django,по которой понятно, работаем мы в продакшен-окружении или нет. Если работаем в продакшене, то результаты лучше кэшировать, чтобы улучшить производительность, ведь после первичной компиляции файлы уже не изменятся. Но при разработке пути лучше никогда не кэшировать, поскольку мы будем постоянно перекомпилировать ресурсы клиентской части, по мере того, как будем менять код;

  • В тандеме с CACHE работает POLL_INTERVAL. Она сообщает Django, как часто (в секундах) он должен выбирать содержимое файла со статикой, чтобы получать новейшие пути к файлам ресурсов. В продакшен-окружении (DEBUG == False), значение POLL_INTERVAL игнорируется, поскольку подразумевается, что после развертывания скомпилированные ресурсы не изменятся; поэтому и нет необходимости выбирать эти файлы заново;

  • STATS_FILE – это путь к файлу со статикой, сгенерированный трекером бандлов Webpack. Это значение должно совпадать с output.path из клиентского файла webpack.config.js;

  • IGNORE – это список регулярных выражений, с которыми будут сопоставляться файлы, сгенерированные в ходе компиляции Webpack. Если какой-либо файл совпадет с регулярными выражениями отсюда, то он будет игнорироваться.

Есть и еще некоторые, более продвинутые свойства, которые можно использовать для тонкой настройки конфигураций.

  • TIMEOUT – это время (в секундах), в течение которого загрузчик Webpack будет дожидаться окончания компиляции, а не дождавшись – выбросит исключение. Если указать здесь 0None или исключив этот ключ из словаря, вы деактивируете задержки;

  • LOADER_CLASS – это строка с именем класса на Python, реализующего ваш собственный загрузчик Webpack. Эта возможность пригодится, если нужно реализовать новое поведение для того, как именно можно загружать файл со статикой – например, из базы данных или по внешнему url. Ниже показан пример, как загружать файл со статикой из внешнего источника, расширяя класс webpack_loader.loader.WebpackLoader.

import requests
from webpack_loader.loader import WebpackLoader

class ExternalWebpackLoader(WebpackLoader):
  def load_assets(self):
    url = self.config['STATS_URL']
    return requests.get(url).json(

Рендеринг в шаблон

Настроив приложение таким образом, можем и далее опираться на утилиты загрузчика Webpack для  Django, чтобы выполнить рендеринг всего того, что было разработано в клиентском приложении. В данном случае мы отобразим файлы JavaScript в HTML-шаблон для Django при помощи библиотечных инструментов.

В загрузчике Webpack для Django есть встроенный тег для работы с шаблонами render_bundle, отвечающий за включение всех файлов из заданной входной точки, перечисленных в файле со статикой. Он используется с шаблоном Django так, как показано ниже.

{% load render_bundle from webpack_loader %}
{% render_bundle 'main' %}

Рекомендуется, чтобы на каждый шаблон Django у вас была всего одна входная точка, но можно вызывать render_bundle столько раз, сколько хотите, чтобы отобразить все необходимые файлы, поскольку движок рендеринга дедуплицирует повторяющиеся фрагменты.

{% load render_bundle from webpack_loader %}
{% render_bundle 'main' %}
{% render_bundle 'other_entrypoint' %}

Также можно включать файлы из заданного расширения в рамках входной точки. Например, вы, возможно, захотите отдельно включить файлы CSS и JavaScript из входной точки main, чтобы первые попали в раздел <head> вашего шаблона, а вторые – до </body>. Этого можно добиться, предоставив и второй аргумент (его также можно передать при помощи ключевого слова extension) для render_bundle, представляющего желаемое расширение файла.

{% load render_bundle from webpack_loader %}
<html>
  <head>
    {% render_bundle 'main' 'css' %}
  </head>
  <body>
    {% render_bundle 'main' extension='js' %}
  </body>
</head>

Как работать в среде для разработки

Если вы начали проект при помощи «Create React App» (Создать приложение React), то можете скомпилировать и подать ресурсы, выполнив команду npm run start. Так сгенерируется файл со статикой. Если бы приложение было настроено иначе, то файлы могли бы компилироваться и подаваться при помощи команды npx webpack --config webpack.config.js --watch.

В случае с приложением для серверной части мы будем иметь дело с обычным приложением Django, воспользовавшись командой python manage.py runserver.

Использование в продакшене

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

Способ настройки  может варьироваться в зависимости от того, на какой платформе вы работаете (Heroku, AWS, т.д.), но при конфигурации должны быть выполнены как минимум две нижеприведенные команды.

npm run build
python manage.py collectstatic --noinput

Первая команда запустит конвейер сборки клиентской части, что заставит трекер бандлов Webpack создать и заполнить файл со статикой. Вторая команда последует за первой и соберет все файлы со статикой.

Важно отметить, что на некоторых платформах, в частности, на Heroku, шаг collectstatic по умолчанию автоматически выполняется на этапе развертывания. Рекомендуем вам отключить эту опцию, установив переменную окружения  is DISABLE_COLLECTSATIC=1. После этого нужно создать хук post_compile, чтобы он выполнял этот шаг за вас. Вот пример, показывающий, как реализовать такой поток задач.

Разделение кода

Трекер бандлов Webpack после релиза 1.0.0 поддерживает разделение кода для ресурсов с клиентской части. Это делается, чтобы можно было создавать множество бандлов, а эти бандлы – загружать по отдельности. Подробнее об этом можно почитать в отличной документации по Webpack . Ниже показано, как вы можете использовать разделение кода в вашем проекте.

Создайте ваш файл конечной точки, чтобы добавлять элементы в DOM. Чтобы воспользоваться в этом примере разделением кода, мы реализуем внутри нашего компонента ленивую загрузку, чтобы динамически импортировать lodash.

function getComponent() {
  return import('lodash')
    .then(({ default: _ }) => {
      const element = document.createElement('div');
      element.innerHTML = _.join(['Hello', 'webpack'], ' ');
      return element;
    }).catch(error => 'An error occurred while loading the component');
}

getComponent().then((component) => {
  document.body.appendChild(component);
});

assets/js/principal.js

Затем сделайте так, чтобы ваш конфигурационный файл Webpack принял следующий вид:

module.exports = {
  context: __dirname,
  entry: {
    principal: './assets/js/principal',
  },
  output: {
    path: path.resolve('./dist/'),
    // publicPath должен совпадать с указателем STATIC_URL на вашу клнфигурацию.
    // Это необходимо, иначе webpack попытается выбрать 
    // наш фрагмент, сгенерированный автоматическим импортом, из "/" а не из "/dist/".
    publicPath: '/dist/', 
    chunkFilename: '[name].bundle.js',
    filename: "[name]-[fullhash].js"
  },
  plugins: [
    new BundleTracker({ filename: './webpack-stats.json' })
  ]
};

 webpack.config.js

Если вы работаете с Webpack 4, замените пожалуйста filename: "[name]-[fullhash].js" на filename: "[name]-[hash].js", а также замените return import('lodash') на return import(/* webpackChunkName: "lodash" */ 'lodash').

Справившись с этим, можете отобразить бандл в вашем шаблоне, как обычно

{% load render_bundle from webpack_loader %}
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>My test page</title>
  </head>
  <body>
    <p>This is my page</p>
    {% render_bundle 'principal' 'js' %}
  </body>
</html>

index.html

Обработка путей S3

Чтобы пользоваться в проекте путями S3, значения для STATIC_URL (в настройках Django) и publicPath (в конфигурационном файле Webpack дл продакшена) должны совпадать.

Чтобы добиться согласованности значений во всем проекте, мы будем хранить их в переменных окружения. В настройках Django для продакшена добавьте следующий код.

from decouple import config

AWS_STATIC_BUCKET_NAME = config("AWS_STATIC_BUCKET_NAME")
AWS_STATIC_DIRECTORY = config("AWS_STATIC_DIRECTORY")

STATIC_URL = f"https://{AWS_STATIC_BUCKET_NAME}.s3.amazonaws.com/{AWS_STATIC_DIRECTORY}/"

Мы используем python-decouple, чтобы управлять переменными окружения на стороне Django.

Здесь мы собираем значения STATIC_URL, используя значения из переменных окружения, например, имя корзины AWS и имя каталога, где хранятся файлы.

В конфигурационном файле Webpack для продакшена делаем то же самое, но уже с использованием нотации и синтаксиса JavaScript.

const AWS_STATIC_BUCKET_NAME = process.env.AWS_STATIC_BUCKET_NAME;
const AWS_STATIC_DIRECTORY = process.env.AWS_STATIC_DIRECTORY;

module.exports = {
  // ...
  output: {
    // ...
    publicPath: `https://${AWS_STATIC_BUCKET_NAME}.s3.amazonaws.com/${AWS_STATIC_DIRECTORY}/`,
    // ...
  },
  // ...
};

webpack.config.js

Использование множественных конфигураций

Как упоминалось выше в разделе о настройке серверной части, в одном проекте может быть несколько конфигураций Webpack. В файле настроек можете добавить новую запись в словарь WEBPACK_LOADER, как показано ниже:

WEBPACK_LOADER = {
    'DEFAULT': {
        'BUNDLE_DIR_NAME': 'bundles/',
        'STATS_FILE': os.path.join(BASE_DIR, 'webpack-stats.json'),
    },
    'OTHER': {
        'BUNDLE_DIR_NAME': 'other_bundles/',
        'STATS_FILE': os.path.join(BASE_DIR, 'webpack-stats-other.json'),
    }
}

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

{% load render_bundle from webpack_loader %}
<html>
  <body>
    {% render_bundle 'main' config='DASHBOARD' %}

    <!-- можете даже сочетать его с другими аргументами, например, с расширениями файлов -->
    {% render_bundle 'main' 'css' 'DASHBOARD' %}
    {% render_bundle 'main' config='DASHBOARD' extension='css' %}
  </body>
</head>

Автор благодарит Лучиано Ратамеро за рецензирование этого поста. Можете почитать Лучиано в Твиттере: @lucianoratamero.

Комментарии (3)


  1. kawena54
    14.02.2022 16:44
    +2

    а вебпак актуален в 2022 когда появилисись нативные ? тот же что использует vite


  1. funca
    15.02.2022 00:36

    В режиме разработки на стороне фронтенда будет работать HMR / hot reload?


    1. alexshipin
      15.02.2022 10:16

      Скорее всего, что не будет, из-за использования Django как бэкенда, плюс использование Django шаблонов