Статьи

Как работает прекомпиляция активов, часть II

Абстрактные зубчатые колеса

Это вторая часть работы As Precompile в Rails. В первой части мы начали изучать встроенную в Rails поддержку упаковочных активов, как она компилирует статические активы (изображения) и как генерируется дайджест на основе их содержимого и т. Д. Эта статья содержит чуть более подробное описание Rails трубопровод активов.

Типы активов

Если вы помните, есть три типа активов в соответствии со звездочками

  • Связанные активы
  • Обработанные активы
  • Статические активы

Мы рассмотрели статические активы в части 1. Связанные активы — это активы, которые требуют некоторой обработки для создания. Активы, такие как CSS-файлы и файлы Javascript, являются примером Bundled Assets. Обработанные активы являются частью связанных активов. Проще говоря, комплектный актив состоит из различных обработанных активов и используется для хранения файла после обработки из комплектованного актива.

Например, application.js сначала является Связанным Активом и состоит из различных Обработанных Активов (включая себя), которые определены в нем в соответствии со структурой Sprockets . Связанные активы сохраняются на диске и содержат содержимое обработанных активов. Связанные активы служат контейнером для различных обработанных активов.

Теперь давайте углубимся и посмотрим, как Rails Asset Precompile выполняет эту работу. Для этой статьи в качестве справочного ресурса используется файл application.js умолчанию, а статья посвящена тому, как прекомпилируется файл application.js .

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

рейк активы: прекомпиляция

Для прекомпиляции активов мы 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 .