Это первая статья из серии, посвященной технологическому стеку Remix. Последние несколько месяцев мы работали над созданием идеального стека и гордимся тем, что создали. Пришло время поделиться им.

Мы начнем с наших интерфейсных инструментов сборки. Мы хотели использовать новейшие функции Javascript, не беспокоясь о старых браузерах (Babel), уверенность при изменении CSS путем определения классов (Модули CSS), немедленную обратную связь при изменении кода (React Hot Loader) и так далее. Все это легко настроить при использовании Webpack, поэтому мы перешли на него. В этой статье мы рассмотрим, как это настроить.

Конфигурация Webpack

Мы хотим, чтобы Webpack вел себя несколькими способами в зависимости от контекста. Первый - dev-server против статической сборки.

  • При разработке мы используем webpack-dev-server, который кеширует файлы в памяти и обслуживает их с веб-сервера, что, в свою очередь, экономит время. Он также поддерживает горячую загрузку модуля.
  • При построении на сервере непрерывной интеграции или CI (мы используем CircleCI) или при развертывании с использованием Heroku мы хотим сгенерировать статическую сборку, чтобы мы могли размещать их статически в производственной среде. Таким образом мы можем установить правильные заголовки кеширования.

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

  • В производстве нам нужна сжатая и минифицированная сборка, чтобы она загружалась и анализировалась быстрее. Мы также включили все оптимизации React, чтобы он работал быстрее. Иногда мы также хотим сделать это при локальной разработке, когда измеряем производительность.
  • Однако обычно мы хотим отключить минимизацию при разработке. Таким образом, разработка происходит быстрее, так как минимизация является дополнительным шагом. Мы также хотим получать все предупреждения React при разработке.

Чтобы установить этот контекст, мы используем две переменные среды, которые мы используем в нашем package.json следующим образом:

"scripts": {
  "build": "webpack",
  "build-min": "WEBPACK_MINIFY=true npm run build",
  "dev-server": "WEBPACK_DEV_SERVER=true webpack-dev-server",
  "dev-server-min": "WEBPACK_MINIFY=true npm run dev-server"
},

В нашем webpack.config.js мы сначала настроили некоторые общие вещи:

const config = {
  plugins: [ /* Some plugins here. */ ],
  module: {
    loaders: [ /* Some loaders here. */ ],
  },
  entry: {
    build: ['client/build'], // Our main entry point.
    tests: ['client/tests'], // Jasmine unit tests.
    happo: ['client/happo'], // Happo screenshot tests.
  },
  resolve: {
    alias: {
      client: path.resolve('./client'),
    },
  },
};

Здесь нет ничего особенного. Весь наш код находится в каталоге client, плюс node_modules для библиотек (которые Webpack находит по умолчанию). Мы используем псевдоним каталога client, чтобы мы могли легко ссылаться на него. У нас есть три точки входа: одна для нашего приложения (client / build.js) и две для тестов (о которых мы поговорим в следующей статье).

Затем мы смотрим, минимизируем мы или нет:

if (process.env.WEBPACK_MINIFY) {
  config.plugins.push(new webpack.DefinePlugin({
    'process.env': {
      // Disable React warnings and assertions.
      'NODE_ENV': JSON.stringify('production'),
    },
    '__DEV__': false, // For internal use.
  }));
  config.plugins.push(new webpack.optimize.UglifyJsPlugin({
    compress: { warnings: false },
  }));
} else {
  // Only use source maps when not minifying.
  config.devtool = 'eval-source-map'; 
  config.plugins.push(new webpack.DefinePlugin({
    '__DEV__': true,
  }));
}

Если мы минимизируем, мы хотим установить для NODE_ENV значение «production», так как React использует это, чтобы убрать все виды утверждений и предупреждений, делая его быстрее. Когда не минифицируем, мы включаем исходные карты.

Затем у нас есть настройка, специфичная для использования dev-сервера:

if (process.env.WEBPACK_DEV_SERVER) {
  // Development configuration, assumes this is loaded with 
  // webpack-dev-server, running on port 8080 (default).
  // Hot loading for build.js.
  config.devServer = { noInfo: true, host: '0.0.0.0', hot: true};
  config.entry.build.unshift(
    'webpack-dev-server/client?http://localhost:8080');
  config.entry.build.unshift('webpack/hot/dev-server');
  config.plugins.push(new webpack.HotModuleReplacementPlugin());
  config.plugins.push(new webpack.NoErrorsPlugin());
  // React components hot loading.
  config.module.loaders.unshift({
    test: /\.js$/,
    include: path.resolve(__dirname, 'client/components'),
    loader: 'react-hot',
  });
  // Expose Jasmine test page as index page on http://localhost:8080
  config.plugins.push(new JasmineWebpackPlugin({
    htmlOptions: {
      chunks: ['tests'],
      filename: 'index.html',
    },
  }));
  config.output = { 
    publicPath: 'http://localhost:8080/',
    filename: '[name].js',
    // In case this is run without webpack-dev-server.
    path: 'public/client',
  };
}
  • Мы инициализируем dev-сервер с помощью noInfo, что делает его менее подробным, и привязываем его к 0.0.0.0, чтобы вы могли получить к нему доступ с других компьютеров в сети (полезно для отладки ).
  • Мы включаем горячую замену модуля, согласно инструкции здесь. Мы также включаем React Hot Loader, но только для реальных компонентов React (client / components).
  • Затем мы размещаем индексную страницу Jasmine на том же порту, так что вы можете просто перейти туда для запуска тестов.
  • Наконец, мы устанавливаем config.output на что-то простое, чтобы вы могли легко просмотреть, что сгенерировано на http: // localhost: 8080 / build.js. В случае, если мы запустим это без dev-сервера (что обычно не должно происходить), мы пишем туда, где записываются другие статические файлы.

Если мы не запускаем dev-сервер, мы записываем на диск:

else {
  // Static configuration, outputs to public/client.
  // For use with Heroku/CircleCI.
  if (process.env.WEBPACK_MINIFY) {
    // Gzip.
    config.plugins.push(new CompressionPlugin());
    
    // Generate stats.html.
    config.plugins.push(new StatsPlugin('stats.json'));
    config.plugins.push(new Visualizer());
  }
  config.output = {
    path: 'public/client',
    publicPath: '/client/',
    // Unique filenames (for caching).
    filename: '[id].[name].[chunkhash].js',
  };
}
  • При минификации мы архивируем все файлы (которые автоматически использует хостинг статических ресурсов Rails) и генерируем визуализацию использования файлов, которую мы обслуживаем на наших внутренних страницах разработки.
  • Мы также генерируем уникальные имена файлов для каждой сборки, поэтому мы можем обслуживать их с бесконечным кешированием заголовков.

Наконец, мы немного ускорили развертывание, исключив тесты при развертывании на Heroku:

// Don't build tests when deploying on Heroku.
if (process.env.HEROKU_APP_ID) {
  delete config.entry.tests;
  delete config.entry.happo;
}

Обслуживание из Rails

Давайте теперь посмотрим на часть плагинов в нашей конфигурации Webpack. Выглядит это так:

plugins: [
  new CircularDependencyPlugin(),
  new ManifestPlugin(),
],

Первый - предотвратить циклические зависимости, отлаживать которые в Webpack может затруднительно. Второй генерирует файл manifest.json, который выглядит примерно так:

{
  "build.js": "0.build.fc79868b1fdedc95cd1f.js",
  "happo.js": "1.happo.d44f048f66291e0e73ca.js",
  "tests.js": "2.tests.305167cc0309faddc140.js"
}

Мы используем это в нашем приложении Rails для обслуживания нужных ресурсов. Для этого мы создали вспомогательный файл assets_helper.rb:

# Adds `webpack_include_tag`.
module AssetsHelper
  def webpack_include_tag(filename)
    if Rails.application.config.use_webpack_dev_server
      # Assumes that Webpack is configured with
      # config.output.filename = '[name].js'.
      return javascript_include_tag(root_url(port: 8080) + filename)
    end
    webpack_filename = webpack_manifest[filename]
    if webpack_filename
      javascript_include_tag("/client/#{webpack_filename}")
    else
      raise ArgumentError, "Webpack file not found: #{filename}"
    end
  end
  private def webpack_manifest
    @webpack_manifest ||= JSON.load(Rails.root.join(
      'public', 'client', 'manifest.json'))
  end
end

Это позволяет нам вызывать webpack_include_tag (‘build.js’) в шаблонах и использовать имя файла, включая хэш. Теперь мы можем указать браузеру кэшировать эти файлы навсегда, так как у них будет другое имя файла, если они когда-либо изменятся.

Обратите внимание, что когда use_webpack_dev_server включен, мы указываем на dev-сервер. Мы устанавливаем для этой переменной значение true в разработке, в false в промежуточной и рабочей среде, а в test.rb мы устанавливаем:

config.use_webpack_dev_server = !ENV['CI']

Предварительная обработка с использованием загрузчиков

Наконец, у нас есть несколько загрузчиков Webpack. Вот как это выглядит в конфигурации Webpack:

module: {
  loaders: [
    {
      test: /\.js$/,
      include: path.resolve('./client'),
      loader: 'babel',
      query: {
        cacheDirectory: '.babel-cache',
        // For code coverage.
        plugins: (!process.env.WEBPACK_DEV_SERVER &&
          !process.env.WEBPACK_MINIFY) ? ['istanbul'] : [],
      },
    },
    {
      test: /\.less$/,
      include: path.resolve('./client'),
      loaders: [
         // Inject into HTML (bundles it in JS).
        'style', 
        // Resolves url() and :local().
        'css?localIdentName=[path][name]--[local]--[hash:base64:10]',
        // Autoprefixer (see below at `postcss()`).
        'postcss-loader',
        // LESS preprocessor.
        'less', 
      ],
    },
    {
      test: /\.(jpe?g|png|gif)$/i,
      include: path.resolve('./client'),
      loaders: [
        // Inline small images, otherwise create file.
        'url?limit=10000',
        // Minify images.
        'img?progressive=true',
      ],
    },
    {
      test: /\.(geo)?json$/,
      include: [
        path.resolve('./client'),
        path.resolve('./spec'), // Shared fixtures.
      ],
      loader: 'json',
    },
    {
      test: /\.svg$/,
      loader: 'raw',
    },
  ],
},
postcss() {
  return [autoprefixer];
},
  • Во-первых, загрузчик Babel. Это позволяет нам использовать новейшие функции Javascript, не беспокоясь о поддержке браузером (в сочетании с babel-polyfill). Мы используем Istanbul для отслеживания покрытия кода для тестов, и мы устанавливаем явный путь кеширования, чтобы CI мог хранить этот кеш между сборками. Вот как это выглядит для CircleCI в circle.yml:
dependencies:
  cache_directories:
    - ".babel-cache"
  • Далее идет CSS. Мы используем LESS для предварительной обработки, хотя думаем о переходе на PostCSS, который использует будущие стандарты CSS. Мы уже используем PostCSS для автоприставки. Мы также используем модули CSS, задав css? LocalIdentName, который позволяет вам ограничивать классы CSS только тем файлом, который их использует.
  • Изображения встраиваются, если они представляют собой небольшой файл, в противном случае они загружаются отдельно. Они также минифицированы.
  • JSON и SVG просты. Мы импортируем SVG как текст, который мы используем с компонентом ‹Svg›, который выглядит следующим образом:
// Use a tool like https://jakearchibald.github.io/svgomg/
// to slim down the SVG, and then manually
// remove width/height/fill/stroke.
const Svg = React.createClass({
  propTypes: {
    height: React.PropTypes.number,
    offset: React.PropTypes.number,
    svg: React.PropTypes.string.isRequired,
    width: React.PropTypes.number.isRequired,
  },
  render() {
    return (
      <div
        className={styles.root}
        dangerouslySetInnerHTML={{ __html: this.props.svg }}
        style={{
          height: this.props.height || this.props.width,
          width: this.props.width,
          top: this.props.offset,
        }}
      />
    );
  },
});
export default Svg;

Который затем используется так:

<Svg svg={require('./pencil.svg')} width={14} offset={2} />

Вывод

Это лишь небольшая часть нашего стека, но потребовалось время, чтобы все исправить. В конце концов, важны мелочи, такие как возможность запускать минифицированную версию со всеми оптимизациями React в процессе разработки, или сохранение кеша Babel между запусками CI, или отказ от создания тестов при развертывании.

Надеюсь, это будет полезно для начала работы с Webpack или для настройки существующей настройки. Следите за следующими выпусками этой серии, в которых мы поговорим о нашей библиотеке компонентов, модульном тестировании, тестировании снимков экрана, развертывании, поддержании скорости CI и многом другом!