Asset Pipeline

Это руководство раскрывает конвейер ресурсов (asset pipeline).

Обратившись к этому руководству, вы узнаете:

  • Что такое конвейер ресурсов, и зачем он нужен.
  • Как должным образом организовывать ассеты своего приложения.
  • Преимущества конвейера ресурсов.
  • Как добавить препроцессор к конвейеру ресурсов.
  • Как упаковывать ассеты в гем.

1. Что такое конвейер ресурсов (Asset Pipeline)?

Конвейер ресурсов представляет фреймворк для обработки доставки ассетов JavaScript и CSS. Это выполняется с использованием технологий, таких как HTTP/2, и техник, таких как конкатенация и минификация. Наконец, это позволяет приложению автоматически соединяться с ассетами других гемов.

Конвейер ресурсов реализован в гемах importmap-rails, sprockets и sprockets-rails и включен по умолчанию. Можно отключить конвейер ресурсов при создании нового приложения, передав опцию --skip-asset-pipeline.

$ rails new appname --skip-asset-pipeline

Это руководство фокусируется на конвейере ресурсов по умолчанию с использованием только sprockets для обработки CSS и importmap-rails для JavaScript. Главное ограничение у них в том, что нет поддержки для транспиляции, поэтому нельзя использовать такие вещи как Babel, Typescript, Sass, React JSX format или TailwindCSS. Мы рекомендуем прочитать раздел про альтернативные библиотеки, если вам необходима транспиляция для JavaScript/CSS.

2. Основные особенности

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

Второй особенностью конвейера ресурсов является использование карт импорта при раздаче файлов JavaScript. Это позволяет создавать современное приложение с помощью библиотек Javascript, сделанных для модулей ES (ESM), без необходимости транспиляции и сборки. В свою очередь, это устраняет необходимость Webpack, yarn, node или любой другой части инструментария JavaScript.

Третьей особенностью конвейера ресурсов является соединение всех CSS файлов в один главный файл .css, который затем минифицируется или сжимается. Как будет сказано далее в этом руководстве, можно настроить эту стратегию, сгруппировав файлы любым способом. В production, Rails вставляет метку SHA256 в каждое имя файла, таким образом файл кэшируется браузером. Кэш можно сделать недействительным, изменив эту метку, что происходит автоматически каждый раз, когда изменяется содержимое файла.

Четвертой особенностью конвейера ресурсов является то, что он позволяет писать эти ассеты на языке более высокого уровня для CSS.

2.1. Что за метки и зачем они нужны?

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

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

Техникой, используемой Sprockets для меток, является вставка хэша содержимого в имя, обычно в конце. Например, файл CSS global.css:

global-908e25f4bf641868d8683022a5b62f54.css

Это стратегия, принятая конвейером ресурсов Rails.

Метки включены по умолчанию для сред development и production. Их также можно включить или отключить в конфигурации с помощью опции config.assets.digest.

2.2. Что такое карты импорта (Import Maps), и зачем они мне?

Карты импорта позволяют импортировать модули JavaScript с помощью логичных имен, связанных с версионированными/мечеными файлами, – прямо из браузера. Таким образом, можно создавать современное приложение с помощью библиотек Javascript, сделанных для модулей ES (ESM), без необходимости транспиляции и сборки.

С таким подходом можно поставлять множество маленьких файлов JavaScript вместо одного большого файла JavaScript. Это благодаря HTTP/2, который больше не несет ухудшения материального быстродействия во время первоначальной доставки, а фактически даже предлагает значительные преимущества в долгосрочной перспективе за счет лучшей динамики кэширования.

3. Как использовать карты импорта в качестве конвейера ресурсов Javascript

Карты импорта являются обработчиком Javascript по умолчанию, логика генерации карт импорта обрабатывается гемом importmap-rails.

Карты импорта используются только для файлов Javascript, и не могут быть использованы для доставки CSS. Чтобы узнать о CSS, обратитесь к разделу по Sprockets.

Детальные инструкции по использованию находятся на домашней странице гема, но важно понимать основы importmap-rails.

3.1. Как они работают

Карты импорта по сути это строковая замена для того, что называется "bare module specifiers". Они позволяют стандартизировать имена модулей импорта JavaScript.

Возьмем, к примеру, такое определение импорта, оно не будет работать без карты импорта:

import React from "react"

Вы бы могли определить его так, чтобы оно заработало:

import React from "https://ga.jspm.io/npm:react@17.0.2/index.js"

Тут вступает карта импорта, мы определяем, что имя react прикреплено к адресу https://ga.jspm.io/npm:react@17.0.2/index.js. С такой информацией браузер принимает упрощенное определение import React from "react". Можно думать о карте импорта как о псевдониме для адреса исходника библиотеки.

3.2. Использование

С помощью importmap-rails создается файл конфигурации importmap, привязывая путь библиотеки к имени:

# config/importmap.rb
pin "application"
pin "react", to: "https://ga.jspm.io/npm:react@17.0.2/index.js"

Все сконфигурированные карты импорта должны быть прикреплены к элементу <head> приложения, добавив <%= javascript_importmap_tags %>. javascript_importmap_tags рендерит ряд скриптов в элемент head:

  • JSON со всеми сконфигурированными картами импорта:
<script type="importmap">
{
  "imports": {
    "application": "/assets/application-39f16dc3f3....js"
    "react": "https://ga.jspm.io/npm:react@17.0.2/index.js"
  }
}
</script>
  • Es-module-shims, действующий как полифил, обеспечивающий поддержку для import maps в старых браузерах:
<script src="/assets/es-module-shims.min" async="async" data-turbo-track="reload"></script>
  • Точку входа для загрузки JavaScript из app/javascript/application.js:
<script type="module">import "application"</script>

3.3. Использование пакетов npm из JavaScript CDN

Можно использовать команду ./bin/importmap, добавленную как часть importmap-rails, чтобы привязать, отвязать или обновить пакеты npm в карте импорта. Этот исполняемый файл использует JSPM.org.

Он работает так:

./bin/importmap pin react react-dom
Pinning "react" to https://ga.jspm.io/npm:react@17.0.2/index.js
Pinning "react-dom" to https://ga.jspm.io/npm:react-dom@17.0.2/index.js
Pinning "object-assign" to https://ga.jspm.io/npm:object-assign@4.1.1/index.js
Pinning "scheduler" to https://ga.jspm.io/npm:scheduler@0.20.2/index.js

./bin/importmap json

{
  "imports": {
    "application": "/assets/application-37f365cbecf1fa2810a8303f4b6571676fa1f9c56c248528bc14ddb857531b95.js",
    "react": "https://ga.jspm.io/npm:react@17.0.2/index.js",
    "react-dom": "https://ga.jspm.io/npm:react-dom@17.0.2/index.js",
    "object-assign": "https://ga.jspm.io/npm:object-assign@4.1.1/index.js",
    "scheduler": "https://ga.jspm.io/npm:scheduler@0.20.2/index.js"
  }
}

Как видите, два пакета react и react-dom разрешаются в четыре зависимости, при jspm по умолчанию.

Теперь их можно использовать в вашей точке входа application.js, как и любой другой модуль:

import React from "react"
import ReactDOM from "react-dom"

Также можно назначить указанную версию для привязки:

./bin/importmap pin react@17.0.1
Pinning "react" to https://ga.jspm.io/npm:react@17.0.1/index.js
Pinning "object-assign" to https://ga.jspm.io/npm:object-assign@4.1.1/index.js

Или даже убирать привязки:

./bin/importmap unpin react
Unpinning "react"
Unpinning "object-assign"

Можно контролировать среду пакета для пакетов с раздельными сборками для "production" (по умолчанию) и "development":

./bin/importmap pin react --env development
Pinning "react" to https://ga.jspm.io/npm:react@17.0.2/dev.index.js
Pinning "object-assign" to https://ga.jspm.io/npm:object-assign@4.1.1/index.js

Также можно подобрать альтернативу, поддерживаемую провайдером CDN, при привязке, например unpkg или jsdelivr (по умолчанию jspm):

./bin/importmap pin react --from jsdelivr
Pinning "react" to https://cdn.jsdelivr.net/npm/react@17.0.2/index.js

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

Запустите ./bin/importmap, чтобы увидеть все параметры.

Отметьте, что эта команда - просто удобная обертка для разрешения логических имен пакетов к CDN URL. Можно просто найти CDN URL самостоятельно и привязать их. Например, если нужен Skypack для React, можно просто добавить следующее в config/importmap.rb:

pin "react", to: "https://cdn.skypack.dev/react"

3.4. Предварительная загрузка привязанных модулей

Чтобы избежать эффекта водопада, когда браузеру нужно загружать файлы один за другим, до того, как он доберется до самого глубоко вложенного импорта, importmap-rails поддерживает ссылки modulepreload. Привязанные модули могут быть предварительно загружены, если добавить preload: true к привязке.

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

Например:

# config/importmap.rb
pin "@github/hotkey", to: "https://ga.jspm.io/npm:@github/hotkey@1.4.4/dist/index.js", preload: true
pin "md5", to: "https://cdn.jsdelivr.net/npm/md5@2.3.0/md5.js"

# app/views/layouts/application.html.erb
<%= javascript_importmap_tags %>

# включит следующую ссылку перед настройкой importmap:
<link rel="modulepreload" href="https://ga.jspm.io/npm:@github/hotkey@1.4.4/dist/index.js">
...

Обратитесь к репозиторию importmap-rails за актуальной документацией.

4. Как использовать Sprockets

Наивным подходом выложить ассеты вашего приложение в веб будет хранить их в поддиректориях папки public, таких как images и stylesheets. Делать это вручную может быть сложно, так как большинство современных приложений требует обработки ассетов определенным образом, например, сжатие и добавление меток к ассетам.

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

Ассеты все еще можно размещать в иерархии public. Любой ассет в public будет роздан как статичный файл приложением или веб-сервером, когда config.public_file_server.enabled установлена true. Можно определить директивы manifest.js для файлов, которые должны подвергаться некоторой предварительной обработке перед тем, как раздаваться.

В production Rails по умолчанию прекомпилирует эти файлы в public/assets. Прекомпилированные копии затем раздаются как статичные ассеты веб сервером. Файлы в app/assets никогда не раздаются напрямую в production.

4.1. Файлы манифеста и директивы

При компиляции ассетов с помощью Sprockets, ему нужно решить, какие цели верхнего уровня компилировать, обычно application.css и изображения. Цели верхнего уровня определяются в файле Sprockets manifest.js, по умолчанию он выглядит так:

//= link_tree ../images
//= link_directory ../stylesheets .css
//= link_tree ../../javascript .js
//= link_tree ../../../vendor/javascript .js

Он содержит директивы - инструкции, сообщающие Sprockets, какие файлы требуются, чтобы создать отдельный файл CSS или JavaScript.

Он означает, что надо включить содержимое всех файлов, найденных в директории ./app/assets/images, а также любых поддиректориях, а также любой файл, распознанный как JS, непосредственно в./app/javascript или ./vendor/javascript.

Он загрузит любой CSS из директории ./app/assets/stylesheets (не включая поддиректории). Допустим, что у нас есть файлы application.css и marketing.css в папке ./app/assets/stylesheets, он позволит загрузить эти таблицы стилей с помощью <%= stylesheet_link_tag "application" %> или <%= stylesheet_link_tag "marketing" %> из вью.

Возможно, вы заметили, что наши файлы JavaScript не загружаются из директории assets по умолчанию, то потому, что ./app/javascript по умолчанию точка входа для гема importmap-rails, и папка vendor это место, где будут храниться скачанные пакеты JS.

В manifest.js можно также указать директиву link, чтобы загрузить определенный файл вместо целой директории. Директива link требует предоставления явного расширения файла.

Sprockets загружает указанные файлы, при необходимости обрабатывает их, соединяет в единый файл, а затем сжимает (на основании значения config.assets.css_compressor или config.assets.js_compressor). Сжатие уменьшает размер файла, позволяя браузеру скачивать файлы быстрее.

You can also opt to include controller-specific stylesheets files only in their respective controllers using the following:

<%= stylesheet_link_tag params[:controller] %>

4.2. Ассеты конкретного контроллера

При генерации скаффолда или контроллера, Rails также генерирует файл CSS для этого контроллера. Дополнительно при генерации скаффолда, Rails генерирует файл scaffolds.css.

Например, если генерируете ProjectsController, Rails также добавит новый файл app/assets/stylesheets/projects.css. По умолчанию эти файлы будут готовы к немедленному использованию вашим приложением, с помощью директивы link_directory в файле manifest.js.

Опционально можно включить таблицы стилей конкретного контроллера только для их соответствующих контроллеров, используя следующее:

<%= stylesheet_link_tag params[:controller] %>

When doing this, ensure you are not using the require_tree directive in your application.css, as that could result in your controller-specific assets being included more than once.

4.3. Asset Organization

Pipeline assets can be placed inside an application in one of three locations: app/assets, lib/assets or vendor/assets.

  • app/assets is for assets that are owned by the application, such as custom images or stylesheets.

При этом убедитесь, что не используете директиву require_tree в application.css, так как она приведет к тому, что ассеты контроллера будут включены более одного раза.

4.4. Организация ассетов

Ассеты конвейера ресурсов могут быть размещены в приложении в одном из этих трех мест расположений: app/assets, lib/assets или vendor/assets.

  • app/assets предназначено для ассетов, принадлежащих приложению, таких как изображения или таблицы стилей, изготовленные специально для приложения.

  • app/javascript для кода JavaScript

  • vendor/[assets|javascript] предназначено для ассетов, принадлежащих сторонним субъектам, таких как фреймворки CSS или библиотеки JavaScript. Имейте в виду, что код третьей стороны со ссылками на другие файлы, также обрабатывающиеся конвейером ресурсов (изображения, таблицы стилей и так далее), должен быть переписан с помощью хелперов, таких как asset_path.

В файле manifest.js можно настроить другие расположения, обратитесь к Файлы манифеста и директивы.

4.4.1. Пути поиска

Когда к файлу обращаются из манифеста или хелпера, Sprockets ищет во всех местах, указанных в manifest.js для него. Путь поиска можно увидеть, просмотрев Rails.application.config.assets.paths в консоли Rails.

4.4.2. Использование индексных файлов как прокси для папок

Sprockets использует файлы с именем index (с соответствующим расширением) для специальных целей.

Например, если имеется библиотека CSS с множеством модулей, хранящаяся в lib/assets/stylesheets/library_namee, файл lib/assets/stylesheets/library_name/index.css служит манифестом для всех файлов в этой библиотеке. Этот файл может включать список всех требуемых файлов в нужном порядке, или просто директиву require_tree.

Это чем-то похоже на способ, которым файл public/library_name/index.html можно достичь запросом к/library_name. Это означает, что нельзя напрямую использовать индексный файл.

Библиотека в целом может быть доступна из файлов .css следующим образом:

/* ...
*= require library_name
*/

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

4.5. Кодирование ссылок на ассеты

Sprockets не добавляет какие-либо новые методы для доступа к вашим ассетам - используйте знакомый метод stylesheet_link_tag.

<%= stylesheet_link_tag "application", media: "all" %>

При использовании гема turbo-rails, который включен по умолчанию в Rails, включите опцию 'data-turbo-track', которая вызывает проверку Turbo, что ассет был обновлен, таким образом загружая его на страницу:

<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>

В обычных вью можно получить доступ к изображениям в директории app/assets/images следующим образом:

<%= image_tag "rails.png" %>

При условии, что конвейер ресурсов включен в вашем приложении (и не отключен в контексте текущей среды), этот файл будет отдан с помощью Sprockets. Если файл существует в public/assets/rails.png, он будет отдан веб-сервером.

Кроме того, запрос файла с хэшем SHA256, такого как public/assets/rails-f90d8a84c707a8dc923fca1ca1895ae8ed0a09237f6992015fef1e11be77c023.png будет обработан тем же образом. Как генерируются эти хэши будет раскрыто позже в этом руководстве в разделе В production.

Изображения также могут быть организованы в поддиректории и могут быть доступны с помощью указания имени директории в теге:

<%= image_tag "icons/rails.png" %>

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

4.5.1. CSS и ERB

Конвейер ресурсов автоматически вычисляет ERB. Это означает, что, если добавить расширение erb к ассету CSS (например, application.css.erb), будут доступны хелперы, такие как asset_path, в правилах вашего CSS:

.class { background-image: url(<%= asset_path 'image.png' %>) }

Этот фрагмент кода записывает путь к определенному указанному ассету. Этот пример имеет смысл, если имеется изображение в одном из путей загрузки ассетов, такое как app/assets/images/image.png, на которое тут будет ссылка. Если это изображение уже имеется в public/assets как файл с меткой, то будет ссылка на него.

Если хотите использовать data URI - метод встраивания данных изображения непосредственно в файл CSS - используйте хелпер asset_data_uri.

#logo { background: url(<%= asset_data_uri 'logo.png' %>) }

Этот фрагмент кода вставит правильно отформатированный URI в код CSS.

Отметьте, что закрывающий тег не может быть в стиле -%>.

4.6. Вызов ошибки, если ассет не найден

Если используется sprockets-rails >= 3.2.0, можно настроить, что произойдет, когда выполнен поиск ассета, и ничего не было найдено. Если выключить "asset fallback", тогда будет вызвана ошибка, когда ассет не может быть найден.

config.assets.unknown_asset_fallback = false

Если "asset fallback" включен, тогда, когда ассет не может быть найден, вместо этого будет выведен путь, а не вызвана ошибка. Поведение "asset fallback" выключено по умолчанию.

4.7. Включение дайджестов

Можно отключить дайджесты, добавив в config/environments/development.rb:

config.assets.digest = false

Когда эта опция true, для URL ассета будет генерироваться дайджест.

4.8. Включение карт исходников

Можно включить карты исходников, добавив в config/environments/development.rb:

config.assets.debug = true

Когда включена отладка, Sprockets сгенерирует карту исходников для каждого ассета. Это позволит вам отлаживать каждый файл по отдельности в средствах разработчика вашего браузера.

Ассеты компилируются и кэшируются при первом запросе после запуска сервера. Sprockets устанавливает HTTP-заголовок контроля кэша must-revalidate для уменьшения нагрузки на последующие запросы - на них браузер получает отклик 304 (Not Modified).

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

5. В production

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

В течение фазы прекомпиляции из содержимого компилированных файлов генерируется SHA256 и вставляется в имена файлов, когда они записываются на диск. Эти имена меток используются хелперами Rails вместо имени манифеста.

Например, это:

<%= stylesheet_link_tag "application" %>

сгенерирует что-то наподобие этого:

<link href="/assets/application-4dd5b109ee3439da54f5bdfd78a80473.css" rel="stylesheet" />

Режим меток контролируется с помощью инициализационной опции config.assets.digest (которая по умолчанию true).

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

5.1. Прекомпиляция ассетов

В Rails имеется встроенная команда для компиляции на диск манифестов ассетов и других файлов в конвейере ресурсов.

Скомпилированные ассеты записываются в место расположения, указанное в config.assets.prefix. По умолчанию это директория /assets.

Эту команду можно вызвать на сервере во время деплоя, чтобы создать скомпилированные версии ассетов непосредственно на сервере. Смотрите следующий раздел, чтобы узнать о том, как скомпилировать локально.

Команда следующая:

$ RAILS_ENV=production rails assets:precompile

Это свяжет папку, указанную в config.assets.prefix с shared/assets. Если вы уже используете эту общую папку, вам следует написать собственную команду для деплоя.

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

Всегда определяйте ожидаемое имя скомпилированного файла, оканчивающееся на .js или .css.

Команда также генерирует .sprockets-manifest-randomhex.json (где randomhex - это 16-байтовая случайная шестнадцатеричная строка), который содержит список всех ваших ассетов и соответствующие им метки. Это используется методами хелпера Rails, чтобы избежать направления запроса в Sprockets. Обычный файл манифеста выглядит так:

{"files":{"application-<fingerprint>.js":{"logical_path":"application.js","mtime":"2016-12-23T20:12:03-05:00","size":412383,
"digest":"<fingerprint>","integrity":"sha256-<random-string>"}},
"assets":{"application.js":"application-<fingerprint>.js"}}

В реальном приложении будет больше файлов и ассетов, перечисленных в манифесте, также будут сгенерированы <fingerprint> и <random-string>.

Место расположения манифеста по умолчанию - корень папки, определенной в config.assets.prefix (по умолчанию '/assets').

Если в production отсутствуют прекомпилированные файлы, вы получите исключение Sprockets::Helpers::RailsHelper::AssetPaths::AssetNotPrecompiledError, указывающее имя отсутствующего файла(-ов).

5.1.1. Вечный заголовок Expires

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

Для Apache:

# Директивы Expires* требуют, чтобы модуль Apache `mod_expires` был включен.
<Location /assets/>
  # Не рекомендуется использование ETag, когда присутствует Last-Modified
  Header unset ETag
  FileETag None
  # RFC предписывает кэшировать только на 1 год
  ExpiresActive On
  ExpiresDefault "access plus 1 year"
</Location>

Для NGINX:

location ~ ^/assets/ {
  expires 1y;
  add_header Cache-Control public;

  add_header ETag "";
}

5.2. Локальная прекомпиляция

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

В таких случаях можно прекомпилировать ассеты локально — то есть добавить окончательный набор скомпилированных, готовых к production ассетов в репозиторий исходного кода перед отправлением на production. Таким образом, их не нужно прекомпилировать отдельно на сервере production при каждом деплое.

Как указано выше, это можно сделать с помощью

$ RAILS_ENV=production rails assets:precompile

Есть следующие оговорки:

  • Если доступны прекомпилированные ассеты, они будут отданы, даже если они больше не соответствуют оригинальным (не компилированным) ассетам, даже на сервере development.

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

    Это можно сделать, добавив следующую строчку в config/environments/development.rb:

    config.assets.prefix = "/dev-assets"
    
  • Задача прекомпиляции ассетов в вашей системе развертывания (например, Capistrano) должна быть отключена.

  • Все необходимые компрессоры или минификаторы должны быть доступны что в вашей системе development.

Также можно установить ENV["SECRET_KEY_BASE_DUMMY"], чтобы запустить использование случайно сгенерированного secret_key_base, хранящегося во временном файле. Это полезно при прекомпиляции ассетов для production как части шага сборки, которому тогда не нужен доступ к секретам production.

$ SECRET_KEY_BASE_DUMMY=1 bundle exec rails assets:precompile

5.3. Компиляция в реальном времени

В некоторых обстоятельствах, возможно, необходимо использовать компиляцию в реальном времени. В этом режиме все запросы для ассетов в конвейере ресурсов обрабатываются непосредственно Sprockets.

Чтобы включить эту опцию, установите:

config.assets.compile = true

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

Sprockets также устанавливает HTTP-заголовок Cache-Control как max-age=31536000. Это сигнализирует всем кэшам между вашим сервером и браузером клиента, что это содержимое (отданный файл) может быть закэшировано на 1 год. В результате уменьшается количество запросов для этого ассета на ваш сервер; есть хороший шанс, что ассет будет в локальном кэше браузера или в каком-либо промежуточном кэше.

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

5.4. CDN

CDN расшифровывается как Content Delivery Network, она в основном предназначена для кэширования ассетов по всему миру, поэтому когда браузер запрашивает ассет, кэшированная копия будет географически ближайшая к этому браузеру. Если отдавать ассеты непосредственно от сервера Rails в production, лучшей практикой будет использовать CDN перед приложением.

Обычным образцом использования CDN является установка вашего приложения в production как "origin" сервер. Это означает, что когда браузер запрашивает ассет из CDN, и кэш отсутствует, он возьмет файл с вашего сервера на лету и кэширует его. Например, если вы запустили приложение Rails на example.com, и у вас настроен CDN на mycdnsubdomain.fictional-cdn.com, то, когда делается запрос к mycdnsubdomain.fictional-cdn.com/assets/smile.png, CDN единожды запросит ваш сервер на example.com/assets/smile.png и кэширует запрос. Следующий запрос к CDN, пришедший по тому же самому URL, получит кэшированную копию. Когда CDN может отдать ассет напрямую, запрос никогда не затронет сервер Rails. Так как ассеты из CDN географически ближе к браузеру, запрос быстрее, и, так как серверу не нужно тратить время на раздачу ассетов, он может сфокусироваться на как можно быстром обслуживании кода приложения.

5.4.1. Настройка CDN на раздачу статических ассетов

Для настройки CDN вам нужно, чтобы ваше приложение было запущено в production в интернете на публично доступном URL, например example.com. Далее необходимо зарегистрироваться на сервисе CDN облачного провайдера. После этого необходимо настроить "origin" для CDN, указав ваш сайт example.com. Обратитесь к документации провайдера по настройке origin-сервера.

Подготовленный CDN даст определенный поддомен для вашего приложения, такой как mycdnsubdomain.fictional-cdn.com (отметьте, что fictional-cdn.com это не существующий провайдер CDN в настоящее время). Теперь, когда есть настроенный сервер CDN, необходимо сообщить браузерам использовать ваш CDN для того, чтобы брать ассеты оттуда, а не от сервера Rails. Это можно осуществить, настроив Rails, установив ваш CDN в качестве хоста ассетов, вместо использования относительного пути. Для настройки хоста ассетов в Rails, необходимо установить config.asset_host в config/environments/production.rb:

config.asset_host = 'mycdnsubdomain.fictional-cdn.com'

Необходимо предоставить только "host", это поддомен и корневой домен, не нужно указывать протокол или "scheme", такие как http:// или https://. Когда запрашивается страница, протокол в сгенерированной ссылке на ассет будет соответствовать тому, какой доступ к странице.

Это значение также можно настроить с помощью переменной среды, чтобы упростить запуск staging-копий вашего сайта:

config.asset_host = ENV['CDN_HOST']

Чтобы это работало, вам необходимо установить на сервере CDN_HOST значение mycdnsubdomain.fictional-cdn.com.

После того, как вы настроили свой сервер и ваш CDN, пути ассета из хелперов такие как:

<%= asset_path('smile.png') %>

Будут отрендерены полные пути к CDN, наподобие http://mycdnsubdomain.fictional-cdn.com/assets/smile.png (дайджест опущен для читаемости).

Если на CDN имеется копия smile.png, она будет отдана браузеру, и ваш сервер даже не узнает, что она была запрошена. Если на CDN нет копии, он попытается найти ее на "origin" example.com/assets/smile.png, а затем сохранить ее для дальнейшего использования.

Если хотите отдавать только некоторые ассеты из CDN, можно использовать опцию :host в хелпере ассета, переопределяющую значение, установленное в config.action_controller.asset_host.

<%= asset_path 'image.png', host: 'mycdnsubdomain.fictional-cdn.com' %>
5.4.2. Настройка поведения кэширования CDN

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

5.4.2.1. Кэширование запросов CDN

Хотя CDN описывается как кэширующий файлы ассетов, фактически он кэширует целые запросы. Они включают тело ассета, а также его заголовки. Наиболее важным является Cache-Control, который сообщает CDN (и браузерам), как кэшировать содержимое. Это означает, что если кто-то запрашивает несуществующий ассет, наподобие /assets/i-dont-exist.png, и ваше приложение Rails возвращает 404, тогда ваш CDN скорее всего закэширует страницу 404, если присутствует валидный заголовок Cache-Control.

5.4.2.2. Отладка заголовков CDN

Одним из способов проверить, что заголовки кэшируются правильно на CDN, является использование curl. Вы можете запросить заголовки от сервера и от CDN, чтобы сверить, что они одинаковые:

$ curl -I http://www.example/assets/application-
d0e099e021c95eb0de3615fd1d8c4d83.css
HTTP/1.1 200 OK
Server: Cowboy
Date: Sun, 24 Aug 2014 20:27:50 GMT
Connection: keep-alive
Last-Modified: Thu, 08 May 2014 01:24:14 GMT
Content-Type: text/css
Cache-Control: public, max-age=2592000
Content-Length: 126560
Via: 1.1 vegur

Против копии на CDN:

$ curl -I http://mycdnsubdomain.fictional-cdn.com/application-
d0e099e021c95eb0de3615fd1d8c4d83.css
HTTP/1.1 200 OK Server: Cowboy Last-
Modified: Thu, 08 May 2014 01:24:14 GMT Content-Type: text/css
Cache-Control:
public, max-age=2592000
Via: 1.1 vegur
Content-Length: 126560
Accept-Ranges:
bytes
Date: Sun, 24 Aug 2014 20:28:45 GMT
Via: 1.1 varnish
Age: 885814
Connection: keep-alive
X-Served-By: cache-dfw1828-DFW
X-Cache: HIT
X-Cache-Hits:
68
X-Timer: S1408912125.211638212,VS0,VE0

Проверьте документацию вашего CDN, чтобы найти подробности о том, что такое X-Cache или любые другие добавленные ими заголовки.

5.4.2.3. CDN и заголовок Cache-Control

Заголовок Cache-Control описывает, как может быть закэширован запрос. Когда не используется CDN, браузер использует эту информацию для кэширования содержимого. Это очень полезно для ассетов, которые не модифицированы, так как браузеру не нужно повторно скачивать CSS или JavaScript сайта при каждом запросе. Как правило, мы хотим, чтобы наш сервер Rails сообщил нашему CDN (и браузеру), что ассет "public". Это означает, что любой кэш может сохранять запрос. Также мы в основном хотим установить max-age, который означает, как долго кэш будет хранить объект до недействительности кэша. Значение max-age устанавливается в секундах с максимально возможным значением 31536000, равным одному году. Это можно сделать в вашем приложении Rails, установив

config.public_file_server.headers = {
  'Cache-Control' => 'public, max-age=31536000'
}

Теперь, когда ваше приложение отдает ассет в production, CDN сохранит ассет на один год. Так как большинство CDN также кэшируют заголовки запроса, этот Cache-Control будет передан всем браузерам, обращающимся к этому ассету. Браузер тогда будет знать, что он может хранить этот ассет очень долго без необходимости повторного запроса.

5.4.2.4. CDN и недействительность кэша, основанного на URL

Большинство CDN кэшируют содержимое ассета, основываясь на полном URL. Это означает, что запрос к

http://mycdnsubdomain.fictional-cdn.com/assets/smile-123.png

Будет полностью по-другому закэширован, чем

http://mycdnsubdomain.fictional-cdn.com/assets/smile.png

Если хотите установить длительный max-age в вашем Cache-Control (и делаете так), то убедитесь, что, когда вы изменяете ассеты, ваш кэш прекращается. Например, при изменении рожицы смайлика в изображении с желтого на синий, вы хотите, чтобы все посетители вашего сайта получили новую синюю рожицу. При использовании CDN с настройкой конвейера ресурсов Rails config.assets.digest, установленной true по умолчанию, каждый ассет будет иметь другое имя, если он изменится. Таким образом, вам даже не нужно вручную прекращать любые элементы в вашем кэше. Используя иную технику для уникального имени ассета, ваши пользователи также получат самый свежий ассет.

6. Настройка конвейера ресурсов

6.1. Сжатие CSS

Одним из вариантов для сжатия CSS является YUI. YUI CSS compressor предоставляет минификацию.

Следующая строчка включает сжатие YUI и требует гем yui-compressor.

config.assets.css_compressor = :yui

6.2. Сжатие JavaScript

Возможные варианты для сжатия JavaScript это :terser, :closure и :yui. Они требуют использование гемов terser, closure-compiler или yui-compressor соответственно.

Возьмем, к примеру, гем terser. Этот гем оборачивает Terser (написанный для Node.js) в Ruby. Он сжимает ваш код, убирая пробелы и комментарии, сокращая имена локальных переменных и выполняя иные микро-оптимизации, наподобие замены ваших выражений if и else на тернарные операторы там, где возможно.

Следующая строчка вызывает terser для сжатия JavaScript.

config.assets.js_compressor = :terser

Необходим runtime, поддерживаемый ExecJS, чтобы использовать terser. Если используете macOS или Windows, у вас уже имеется JavaScript runtime, установленный в операционной системе.

Компрессия JavaScript также будет работать для ваших файлов JavaScript, когда вы загружаете свои ассеты через гемы importmap-rails или jsbundling-rails.

6.3. Сжатие ассетов

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

config.assets.gzip = false # отключает генерацию сжатых ассетов

Обратитесь к документации своего веб-сервера за инструкцией, как раздавать сжатые ассеты.

6.4. Использование собственного компрессора

Настройки конфигурации компрессора для CSS и JavaScript также могут принимать любой объект. Этот объект должен иметь метод compress, принимающий строку как единственный аргумент, и он должен возвращать строку.

class Transformer
  def compress(string)
    do_something_returning_a_string(string)
  end
end

Чтобы включить это, передайте новый объект в конфигурационную опцию в application.rb:

config.assets.css_compressor = Transformer.new

6.5. Изменение пути assets

Публичный путь, используемый Sprockets по умолчанию, это /assets.

Он может быть заменен на что-то другое:

config.assets.prefix = "/some_other_path"

Это удобная опция, если вы обновляете старый проект, не использующий конвейер ресурсов и уже использующий этот путь, или вы хотите использовать этот путь для нового ресурса.

6.6. Заголовки X-Sendfile

Заголовок X-Sendfile — это указание веб-серверу игнорировать отклик от приложения, и вместо этого отдать определенный файл с диска. Эта опция отключена по умолчанию, но может быть включена, если ее поддерживает сервер. Когда опция включена, обязанность по отдаче файла передается веб-серверу, который справляется с ней быстрее. Обратитесь к send_file, чтобы узнать, как использовать эту особенность.

Apache и NGINX поддерживают эту опцию. Она включается в config/environments/production.rb.

# config.action_dispatch.x_sendfile_header = "X-Sendfile" # для Apache
# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # для NGINX

Если вы производите апгрейд существующего приложения и намереваетесь использовать эту опцию, убедитесь, что скопировали ее только в production.rb и в любую другую среду, которую вы определили, как имеющую поведение production (не в application.rb).

За дальнейшими подробностями обращайтесь к документации своих веб-серверов:

7. Хранилище кэша ассетов

По умолчанию Sprockets кэширует ассеты в tmp/cache/assets в development и production. Это может быть изменено следующим образом:

config.assets.configure do |env|
  env.cache = ActiveSupport::Cache.lookup_store(:memory_store,
                                                { size: 32.megabytes })
end

Чтобы отключить хранилище кэша ассетов:

config.assets.configure do |env|
  env.cache = ActiveSupport::Cache.lookup_store(:null_store)
end

8. Добавление ассетов в ваши гемы

Ассеты также могут идти от внешних источников в виде гемов.

Хорошим примером этого является гем jquery-rails. Этот гем содержит класс engine, унаследованный от Rails::Engine. Сделав так, Rails становится проинформированным, что директории для этого гема могут содержать ассеты, и директории app/assets, lib/assets и vendor/assets этого engine добавляются в путь поиска Sprockets.

9. Создание препроцессора в вашей библиотеке или геме

Sprockets использует Процессоры, Трансформеры, Компрессоры и Экспортеры для расширения функциональности Sprockets. Обратитесь к Расширение Sprockets, чтобы узнать больше об этом. Здесь мы зарегистрировали препроцессор, чтобы добавить комментарий в конец text/css (.css) файлов.

module AddComment
  def self.call(input)
    { data: input[:data] + "/* Hello From my sprockets extension */" }
  end
end

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

Sprockets.register_preprocessor 'text/css', AddComment

10. Альтернативные библиотеки

С годами существовало несколько подходов по умолчанию для обработки ассетов. Веб развивался и мы видели все больше и больше тяжеловесных Javascript приложений. В доктрине Rails мы полагаем, что надо выбирать Омакасэ, поэтому мы сфокусировались на настройке по умолчанию: Sprockets с картами импорта.

Мы в курсе, что нет универсального решения, подходящего каждому доступному фреймворку/расширению JavaScript и CSS. В экосистеме Rails есть другие библиотеки сборки, которые должны помочь вам в ситуациях, когда настройки по умолчанию не достаточно.

10.1. jsbundling-rails

jsbundling-rails это зависимая от Node.js альтернатива способу importmap-rails сборки JavaScript с помощью esbuildrollup.js или Webpack.

Гем предоставляет процесс yarn build --watch для автоматической генерации вывода в development. Для production, он автоматически прицепляет задачу javascript:build к задаче assets:precompile, чтобы обеспечить, что все ваши зависимости от пакетов были установлены, и JavaScript был создан для всех точек входа.

Когда использовать вместо importmap-rails? Если ваш код JavaScript зависит от транспиляции, то есть если вы используете Babel, TypeScript или формат React JSX, то jsbundling-rails правильный способ.

10.2. Webpacker/Shakapacker

Webpacker был препроцессором JavaScript по умолчанию и сборщиком для Rails 5 и 6. Теперь он не разрабатывается. Существует преемник, называющийся shakapacker, но он не поддерживается командой или проектом Rails.

В отличие от других библиотек в этом списке, webpacker/shakapacker полностью не зависят от Sprockets и могут обрабатывать файлы как JavaScript, так и CSS.

Прочитайте документ по сравнению с Webpacker, чтобы понять разницу между jsbundling-rails и webpacker/shakapacker.

10.3. cssbundling-rails

cssbundling-rails позволяет сборку и обработку ваших CSS с помощью Tailwind CSSBootstrapBulmaPostCSS или Dart Sass, а затем доставляет CSS посредством конвейера ресурсов.

Он работает схожим образом с jsbundling-rails, поэтому добавьте зависимость от Node.js в ваше приложение с помощью процесса yarn build:css --watch, чтобы заново сгенерировать ваши таблицы стилей в development и прицепляется к задаче assets:precompile в production.

Какое отличие от Sprockets? Sprockets сам по себе не способен транспилировать Sass в CSS, требуется Node.js, чтобы сгенерировать файлы .css из файлов .sass. Как только сгенерированы файлы .css, тогда Sprockets сможет доставить их вашим клиентам.

cssbundling-rails полагается на Node для обработки CSS. Гемы dartsass-rails и tailwindcss-rails это самостоятельные версии Tailwind CSS и Dart Sass, что означает отсутствие зависимости от Node. Если вы используете importmap-rails для обработки Javascript и dartsass-rails или tailwindcss-rails для CSS, можно полностью избежать зависимости от Node, что приведет к менее сложному решению.

10.4. dartsass-rails

Если хотите использовать Sass в приложении, dartsass-rails пришел на замену для устаревшего гема sassc-rails. dartsass-rails использует реализацию Dart Sass вместо устаревшего в 2020 году LibSass, используемого в sassc-rails.

В отличие от sassc-rails, новый гем напрямую не интегрируется со Sprockets. Обратитесь к домашней странице гема за инструкциями по установке/миграции.

Популярный гем sassc-rails не поддерживается с 2019 года.

10.5. tailwindcss-rails

tailwindcss-rails это гем-обертка для самостоятельной выполняемой версии фреймворка Tailwind CSS v3. Используется для новых приложений, когда предоставлена --css tailwind для команды rails new. Предоставляет процесс watch, чтобы автоматически сгенерировать вывод Tailwind в development. Для production он прицепляется к задаче assets:precompile.