Это вторая часть работы As Precompile в Rails. В первой части мы начали изучать встроенную в Rails поддержку упаковочных активов, как она компилирует статические активы (изображения) и как генерируется дайджест на основе их содержимого и т. Д. Эта статья содержит чуть более подробное описание Rails трубопровод активов.
Типы активов
Если вы помните, есть три типа активов в соответствии со звездочками
- Связанные активы
- Обработанные активы
- Статические активы
Мы рассмотрели статические активы в части 1. Связанные активы — это активы, которые требуют некоторой обработки для создания. Активы, такие как CSS-файлы и файлы Javascript, являются примером Bundled Assets. Обработанные активы являются частью связанных активов. Проще говоря, комплектный актив состоит из различных обработанных активов и используется для хранения файла после обработки из комплектованного актива.
Например, application.js
сначала является Связанным Активом и состоит из различных Обработанных Активов (включая себя), которые определены в нем в соответствии со структурой Sprockets
. Связанные активы сохраняются на диске и содержат содержимое обработанных активов. Связанные активы служат контейнером для различных обработанных активов.
Теперь давайте углубимся и посмотрим, как Rails Asset Precompile выполняет эту работу. Для этой статьи в качестве справочного ресурса используется файл application.js
умолчанию, а статья посвящена тому, как прекомпилируется файл application.js
.
Прежде чем продолжить, пожалуйста, откройте следующие файлы в вашем любимом редакторе. Если вам нравится, вы можете нажать ниже, чтобы просмотреть их в браузере.
- actionpack-3.2.15 / Библиотека / Звездочки / railtie.rb
- actionpack-3.2.15 / Библиотека / Звездочки / assets.rake
- actionpack-3.2.15 / Библиотека / Звездочки / static_compiler.rb
- Звездочки-2.2.2 / Библиотека / Звездочки / base.rb
- Звездочки-2.2.2 / Библиотека / Звездочки / index.rb
- Звездочки-2.2.2 / Библиотека / Звездочки / bundled_asset.rb
- Звездочки-2.2.2 / Библиотека / Звездочки / processed_asset.rb
- Звездочки-2.2.2 / Библиотека / Звездочки / context.rb
- Звездочки-2.2.2 / Библиотека / Звездочки / directive_processor.rb
рейк активы: прекомпиляция
Для прекомпиляции активов мы rake assets:precompile
. Это задача rake, и она добавлена в actionpack-3.2.15/lib/sprockets/railtie.rb
в строке 13, где sprockets/asset.rake
файл sprockets/asset.rake
. Этот код находится в Sprockets::Railtie
который является подклассом Rails::Railtie
. Как обсуждалось в части 1, этот Railtie является ядром платформы Rails и предоставляет несколько хуков для расширения Rails и / или изменения процесса инициализации. Он также используется для добавления граблей.
В actionpack-3.2.15/lib/sprockets/assets.rake
в строках 59-67 есть задача rake. Это задача rake, которая вызывается при запуске rake assets:precompile
. Эта задача rake, в свою очередь, вызывает другую задачу rake, которая вызывает метод internal_precompile
.
В internal_precompile
в строке 50 экземпляр Sprockets::StaticCompiler
создается путем передачи различных параметров и присваивается compiler
. env
который передается в Sprockets::StaticCompiler
является экземпляром Sprockets::Environment
который был назначен Rails.application.assets
в actionpack-3.2.15/lib/sprockets/railtie.rb
в строке 23 (о чем мы поговорим в Части 1.)
internal_precompile
вызывает compile
в Sprockets::StaticCompiler
. Это метод, который выполняет все необходимые операции для предварительной компиляции наших ресурсов и записи их на диск.
Давайте actionpack-3.2.15/lib/sprockets/static_compiler.rb
к методу compile
в actionpack-3.2.15/lib/sprockets/static_compiler.rb
. В этом методе вы увидите следующую строку:
env.each_logical_path(paths) do |logical_path|
env
— это Rails.application.assets
который является экземпляром Sprockets::Environment
. В приведенных выше Rails.application.config.assets.precompile
paths
находится в основном Rails.application.config.assets.precompile
который был передан в Sprockets::StaticCompiler
в actionpack-3.2.15/lib/sprockets/assets.rake
.
each_logical_path
определен в each_logical_path
sprockets-2.2.2/lib/sprockets/base.rb
в строке 332. Этот метод, в свою очередь, вызывает each_file
который определен чуть выше each_logical_path
.
each_file
выполняет each
на paths
. paths
— это список каталогов, которые содержат наши активы. Пути для недавно созданного приложения Rails 3.2.15, в которое вы не добавили никаких дополнительных гемов, могут выглядеть следующим образом.
/home/ubuntu/Desktop/work/asset_pipeline_article/app/assets/images /home/ubuntu/Desktop/work/asset_pipeline_article/app/assets/javascripts /home/ubuntu/Desktop/work/asset_pipeline_article/app/assets/stylesheets /home/ubuntu/Desktop/work/asset_pipeline_article/vendor/assets/javascripts /home/ubuntu/Desktop/work/asset_pipeline_article/vendor/assets/stylesheets /home/ubuntu/.rvm/gems/ruby-1.9.3-p194@exporter-imranlatif/gems/jquery-rails-3.0.1/vendor/assets/javascripts /home/ubuntu/.rvm/gems/ruby-1.9.3-p194@exporter-imranlatif/gems/coffee-rails-3.2.2/lib/assets/javascripts
Ваши абсолютные пути будут отличаться в зависимости от пути вашего приложения на Rails. Первые 5 путей относятся к app/assets
и vendor/assets
. Последние два пути — из драгоценных камней jquery-rails
и coffee-rails
, соответственно.
Если вы являетесь автором Rails.application.config.assets.paths
и хотите включить пути Rails.application.config.assets.paths
вашего Rails.application.config.assets.paths
в Rails.application.config.assets.paths
, то вам нужно создать класс engine, который наследуется от Rails::Engine
. Это описано здесь . paths
сначала назначаются Rails.application.config.assets.paths
а затем назначаются Rails.application.assets.paths
. Вы можете проверить код, который назначает Rails.application.config.assets.paths
для Rails.application.assets.paths
здесь .
Вернуться к each_file
в each_file
sprockets-2.2.2/lib/sprockets/base.rb
each_file
выполняет each
paths
и в этом блоке each_entry
.
each_entry
определяется чуть выше each_file
. each_entry
рекурсивно перебирает каждый каталог и собирает все файлы по этому пути. После сбора всех файлов с заданного пути он выполняет переданный ему блок. Вы можете увидеть следующую строку в методе each_entry
:
paths.sort_by(&:to_s).each(&block)
Вышеприведенная строка вызывает блок, который был передан методу each_logical_path
из each_logical_path
Этот блок вызывает logical_path_for_filename
, который определен в том же файле.
matches_filter
вызывает matches_filter
который также определен в том же файле. matches_filter
сопоставляет filename
соответствии с переданными ему filters
. filters
— это, в основном, Rails.application.config.assets.precompile
, массив, содержащий различные фильтры, в соответствии с которыми должен обрабатываться актив.
По умолчанию массив состоит из двух элементов. Во-первых, Proc
который соответствует названию для Static Assets. Второй элемент содержит объект RegExp
который соответствует файлам CSS и Javascript.
Если имя файла не соответствует, тогда не будет никакого актива для него. Для статических активов, таких как изображения, он должен соответствовать фильтру, определенному процедурой в массиве filters
. Для связанных активов имя файла должно соответствовать RegExp
определенному в filters
.
Если мы хотим связать файлы CSS или Javascript, которые не соответствуют регулярному выражению, мы должны добавить их в Rails.application.config.assets.precompile
в наших файлах конфигурации. Мы делаем это в config/environment/production
, используя следующую строку:
config.assets.precompile += %w( sitepoint.js )
Процедура в массиве filters
только статическим активам или активам, которые не имеют расширения .css
или .js
. RegExp
в filters
только файлам, которые являются application.css
или application.js
. Поскольку sitepoint.js не соответствует ни одному из этих критериев, он не будет объединен. При добавлении sitepoint.js
к Rails.application.config.assets.precompile
выполняется сопоставление и связывание актива.
После успешного совпадения имени filename
переданного методу filters_matches
, logical_path_for_filename
возвращает этот результат в блок, переданный each_file
от each_logical_path
. Этот блок actionpack-3.2.15/lib/sprockets/static_compiler.rb
имя файла другому блоку, переданному ему из compile
в actionpack-3.2.15/lib/sprockets/static_compiler.rb
. Этот блок вызывает find_asset
определенный в find_asset
sprockets-2.2.2/lib/sprockets/index.rb
. Этот метод устанавливает для options[:bundle]
значение true и вызывает find_asset
из своего Base
класса в sprockets-2.2.2/lib/sprockets/base.rb
find_asset
разрешает путь на основе имени файла и присваивает абсолютный путь к файлу.
logical_path
— это просто имя файла, т.е. application.js
. find_asset
вызывает build_asset
, который решает, как обрабатывать актив. Если нет запущенных процессоров, он обрабатывает его как StaticAsset
.
Если нужно запустить несколько процессоров, он рассматривает актив как BundledAsset
. При первом build_asset
options[:bundle]
имеет значение true
, поэтому создается BundledAsset
. find_asset
вызывается снова, для options[:bundle]
установлено значение false
, поэтому создается ProcessedAsset
.
Помните наше обсуждение, что BundledAsset
фактически сохраняется на диск, содержащий различные экземпляры ProcessedAsset
. Каждый BundledAsset
имеет по крайней мере один ProcessedAsset
. В случае одного файла CSS или Javascript, такого как sitepoint.js
, и BundledAsset
и ProcessedAsset
указывают на один и тот же файл.
Инициализатор для ProcessedAsset
выполняет context.evaluate(pathname)
чтобы получить содержимое файла в pathname
. evaluate
в sprockets-2.2.2/lib/sprockets/context.rb
выполняет различные операции с файлами, указанными в path
. Он собирает processors
для запуска на файл.
По умолчанию для файла Javascript используются два processors
: Sprockets::DirectiveProcessor
и Sprockets::SafetyColons
. Sprockets::DirectiveProcessor
используется для запуска процессора директив в каждом файле CSS и Javascript. Он в основном сканирует файл CSS или Javascript и захватывает файлы, которые требуются в этом файле в соответствии с синтаксисом Sprockets
. Популярные директивы являются require
, require_tree
, requite_self
и т. Д.
evaluate
запускается в массиве processors
и создается новый экземпляр processor
. Затем метод render
вызывается для каждого шаблона процессора. Sprockets::DirectiveProcessor
определен в sprockets-2.2.2/lib/sprockets/directive_processor.rb
и является подклассом Tilt::Template
. Когда render
вызывается в Sprockets::DirectiveProcessor
он также вызывает Sprockets::DirectiveProcessor
evaluate
для Sprockets::DirectiveProcessor
. В evaulate
process_directives
, который сканирует файлы и собирает директивы, которые необходимо запустить для текущего файла.
Для нашего ссылочного актива application.js
результат может выглядеть так:
[[13, "require", "jquery"], [14, "require", "jquery_ujs"], [15, "require_tree", "."]]
После получения директив и присвоения их @directives
process_directives
. Как видите, эта строка кода отправляет сообщение process_<name>_directive
каждой директиве:
send("process_#{name}_directive", *args)
Все эти методы принадлежат Sprockets::DirectiveProcessor
. Давайте посмотрим, что происходит, когда вызывается process_require_tree_directive
.
process_require_tree_directive
вызывает each_entry
, который, как мы теперь знаем, возвращает пути всех файлов из пути, переданного ему. require_tree
означает, что все файлы по указанному пути должны быть required
. Вы, возможно, заметили в блоке each_entry
, что если имя файла соответствует текущему имени файла, то оно пропускается со next
оператором. Какова цель этого заявления? Мы вернемся к этому позже.
context.require_asset
используется для назначения ресурсов массиву @_required_paths
. @_required_paths
— пути активов, от которых зависит текущий ProcessedAsset
. После завершения process_source
вызывается process_source
, который фиксирует источник текущего ProcessedAsset
, удаляя директивные процессоры.
После завершения evaluate
всех процессоров источник текущего ProcessedAsset
возвращается и сохраняется в @source
в ProcessedAsset
.
Затем build_required_assets
из build_required_assets
sprockets-2.2.2/lib/sprockets/processed_asset.rb
build_required_assets
. В build_required_assets
вы увидите следующий код:
@required_assets = resolve_dependencies(environment, context._required_paths + [pathname.to_s]) - resolve_dependencies(environment, context._stubbed_assets.to_a)
resolve_dependencies
используется для разрешения зависимостей текущего ProcessedAsset
. До этого момента у нас есть только пути к файлам, которые требуются для нашего текущего ProcessedAsset
. Для context._required_paths
application.js
context._required_paths
может выглядеть следующим образом.
{{GEMPATH}}/jquery-rails-3.0.1/vendor/assets/javascripts/jquery.js {{GEMPATH}}/jquery-rails-3.0.1/vendor/assets/javascripts/jquery_ujs.js
{{GEMPATH}}
представляет абсолютный путь к каталогу драгоценных камней, который содержит различные драгоценные камни. Вы заметили, что в указанных выше путях нет application.js
который должен быть там из-за директивы require_tree
. Посмотрите на параметры, передаваемые в resolve_dependencies
. Мы явно добавляем текущий pathname
к списку context._required_paths
. Помните, из нашего последнего обсуждения, что в process_require_tree_directive
мы пропускаем текущий файл.
Как мы уже говорили, у BundledAsset
будет хотя бы один ProcessedAsset
. По умолчанию наши файлы не содержат никаких процессоров для запуска, поэтому, если мы хотим, чтобы его путь был включен в context._required_paths
мы должны добавить директивный процессор к каждому файлу, который явно не идеален. Мы выбрали другой путь. Вместо того, чтобы включать путь к файлу во время обработки директивы, мы явно включили его в context._required_paths
при вызове resolve_dependencies
из build_required_assets
. Если для файла есть несколько директивных процессоров, то мы пропускаем включение этого имени файла, потому что знаем, что добавляем его вручную во время вызова resolve_dependencies
.
resolve_dependencies
зацикливается на всех путях, которые ему переданы, и, если он совпадает с путем текущего ProcessedAsset
, он добавляется в массив assets
. Если имя файла не совпадает, выполняется оператор elsif
вызывающий метод find_asset
.
Чтобы собрать исходный код и запустить процессоры для любого файла, нам нужно создать экземпляр ProcessedAsset
. Файл jquery.js
является одним из этих путей. Давайте посмотрим, как он рекурсивно обрабатывается и добавляется в массив assets
для экземпляра ProcessedAsset
application.js
. Начиная с elsif
в resolve_dependencies
вы увидите следующую строку
asset = environment.find_asset(path, :bundle => false)
Как упоминалось ранее, когда мы вызываем find_asset
, устанавливая значение bundle
в false
, это означает, что мы создаем экземпляр ProcessedAsset
. ProcessedAsset
создается для jquery.js
в elsif
of resolve_dependencies
.
Помните, что инициализатор ProcessedAsset
вызывает context.evaluate
который выполняет различные операции с активом. jquery.js
является файлом JavaScript и не содержит никаких зависимостей, поэтому никакие зависимости не будут добавлены к нему при вызове context.require_asset
. После того, как процессоры были запущены в jquery.js
, resolve_dependencies
вызывается так же, как и для application.js
несколькими шагами ранее.
Поскольку jquery.js
не зависит от какого-либо актива, context._required_paths
пуст, и мы явно добавляем текущий путь и resolve_depedencies
массив в resolve_depedencies
. Существует только одна зависимость jquery.js
, которая является самой jquery.js
. Оператор if
имеет значение true, и текущий экземпляр ProcessedAsset
который мы можем обозначить как self
, добавляется в массив assets
, который назначается для @required_assets
в build_required_assets
. Для тех активов, которые не имеют каких-либо зависимостей и являются частью некоторого BundledAsset
, @required_assets
указывает на один элемент, который является самим активом.
После создания экземпляра ProcessedAsset
для jquery.js
и создания его необходимых активов, элемент управления возвращается обратно в elsif
в resolve_depedencies
который resolve_depedencies
необходимые активы для application.js
. Экземпляр ProcessedAsset
jquery.js
назначается asset
в resolve_depedencies
. Затем мы перебираем required_assets
из jquery.js
и добавляем экземпляр ProcessedAsset
в массив assets
.
Эти шаги выполняются для jquery_ujs.js
и других путей тоже. После добавления экземпляров ProcessedAsset
для всех путей application.js
в массив assets
для application.js
, мы возвращаем этот массив из метода resolve_dependencies
который назначен переменной экземпляра @required_assets
объекта ProcessedAsset
application.js
.
ProcessedAsset
завершит выполнение и будет возвращен инициализатору BundledAsset
. Экземпляр ProcessedAsset
для application.js
назначается @processed_asset
а необходимые ресурсы назначаются @required_assets
. Мы знаем, что у каждого ProcessedAsset
есть свой источник, поэтому мы начинаем присваивать источник всех экземпляров ProcessedAsset
@source
. to_a
возвращает required_assets
и мы получаем строковое представление экземпляра ProcessedAsset
, то есть источника. Это поведение определяется здесь . Когда мы собрали источники всех экземпляров ProcessedAsset
, мы снова запустили некоторые processors
на основе content_type
текущего файла. Для файлов Javascript мы запускаем Sprockets::Processor (js_compressor)
, передавая ему источник. После обработки @source
и получения нового @source
дайджест рассчитывается на основе источника / содержимого.
Когда конструктор BundledAsset
завершен, управление в конечном итоге возвращается к compile
в actionpack-3.2.15/lib/sprockets/static_compiler.rb
. Это где магия началась, и мы вернулись сюда с нашим завершенным BundledAsset
. Следующие шаги просты, так как BundledAsset
сохраняется на диске таким же образом, как StaticAsset
сохраняется в формате file_name-digest.file_ext
.
Весь процесс, который я описал выше, продолжается для каждого файла в массиве paths
.
Неотвеченные вопросы отвеченные
Вам может быть интересно, почему мы создали дайджест для StaticAsset
заранее, а дайджест для BundledAsset
только после выполнения всей этой обработки. Ответ на этот вопрос очень прост. Как обсуждалось в части 1, в StaticAssets
обработка не StaticAssets
, поэтому мы просто копируем и вставляем их в каталог public/assets
с дайджестом на основе его содержимого. Содержимое StaticAsset
такое как изображение и т. Д., Не будет изменяться при копировании / вставке, поэтому мы можем заранее рассчитать его дайджест. Принимая во внимание, что вычисление дайджеста для BundledAsset
требует некоторых шагов, прежде чем дайджест может быть вычислен.
В первой части этой статьи мы обсуждали, что метод write_to
используется для создания одного ресурса, так как же Sprockets
создает две версии одного и того же файла? Когда мы делаем rake assets:precompile
задачу :all
вызывается из actionpack-3.2.15/lib/sprockets/assets.rake
в строках 59-67. В этой задаче есть две задачи rake: одна вызывается для создания перевариваемой версии ресурсов, а другая — для создания непереваренной версии ресурсов. Вы можете проверить эти две задачи в строках 69-71 и в строках 73-75. Обе задачи вызывают метод internal_precompile
с соответствующим параметром digest
. Непереваренный вызов internal_precompile
не занимает много времени, потому что все уже кэшировано.
Последние мысли
Я приложил все усилия, чтобы объяснить внутреннее кодирование конвейеров Rails Asset. Чтение кода — это искусство и удовольствие одновременно. Читая код другого разработчика, weocan расширяет наши технические навыки. Было очень приятно читать потрясающий код Sprockets
.