Commit 5d3a761e authored by alexpott's avatar alexpott
Browse files

Issue #2506369 by catch, dawehner, Wim Leers: Cache CSS/JS asset resolving

parent d305a8ef
......@@ -1318,7 +1318,7 @@ services:
arguments: ['@library.discovery']
asset.resolver:
class: Drupal\Core\Asset\AssetResolver
arguments: ['@library.discovery', '@library.dependency_resolver', '@module_handler', '@theme.manager']
arguments: ['@library.discovery', '@library.dependency_resolver', '@module_handler', '@theme.manager', '@language_manager', '@cache.data']
info_parser:
class: Drupal\Core\Extension\InfoParser
twig:
......
......@@ -175,7 +175,7 @@ protected function ajaxRender(Request $request) {
// Resolve the attached libraries into asset collections.
$assets = new AttachedAssets();
$assets->setLibraries(isset($this->attachments['library']) ? $this->attachments['library'] : [])
->setAlreadyLoadedLibraries(isset($ajax_page_state) ? explode(',', $ajax_page_state['libraries']) : [])
->setAlreadyLoadedLibraries(isset($ajax_page_state['libraries']) ? explode(',', $ajax_page_state['libraries']) : [])
->setSettings(isset($this->attachments['drupalSettings']) ? $this->attachments['drupalSettings'] : []);
$asset_resolver = \Drupal::service('asset.resolver');
$css_assets = $asset_resolver->getCssAssets($assets, $optimize_css);
......
......@@ -6,8 +6,11 @@
namespace Drupal\Core\Asset;
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Theme\ThemeManagerInterface;
/**
......@@ -43,6 +46,20 @@ class AssetResolver implements AssetResolverInterface {
*/
protected $themeManager;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface $language_manager
*/
protected $languageManager;
/**
* The cache backend.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cache;
/**
* Constructs a new AssetResolver instance.
*
......@@ -54,12 +71,18 @@ class AssetResolver implements AssetResolverInterface {
* The module handler.
* @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
* The theme manager.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The cache backend.
*/
public function __construct(LibraryDiscoveryInterface $library_discovery, LibraryDependencyResolverInterface $library_dependency_resolver, ModuleHandlerInterface $module_handler, ThemeManagerInterface $theme_manager) {
public function __construct(LibraryDiscoveryInterface $library_discovery, LibraryDependencyResolverInterface $library_dependency_resolver, ModuleHandlerInterface $module_handler, ThemeManagerInterface $theme_manager, LanguageManagerInterface $language_manager, CacheBackendInterface $cache) {
$this->libraryDiscovery = $library_discovery;
$this->libraryDependencyResolver = $library_dependency_resolver;
$this->moduleHandler = $module_handler;
$this->themeManager = $theme_manager;
$this->languageManager = $language_manager;
$this->cache = $cache;
}
/**
......@@ -92,6 +115,12 @@ protected function getLibrariesToLoad(AttachedAssetsInterface $assets) {
*/
public function getCssAssets(AttachedAssetsInterface $assets, $optimize) {
$theme_info = $this->themeManager->getActiveTheme();
// Add the theme name to the cache key since themes may implement
// hook_css_alter().
$cid = 'css:' . $theme_info->getName() . ':' . Crypt::hashBase64(serialize($assets)) . (int) $optimize;
if ($cached = $this->cache->get($cid)) {
return $cached->data;
}
$css = [];
$default_options = [
......@@ -149,6 +178,7 @@ public function getCssAssets(AttachedAssetsInterface $assets, $optimize) {
if ($optimize) {
$css = \Drupal::service('asset.css.collection_optimizer')->optimize($css);
}
$this->cache->set($cid, $css, CacheBackendInterface::CACHE_PERMANENT, ['library_info']);
return $css;
}
......@@ -180,10 +210,6 @@ protected function getJsSettingsAssets(AttachedAssetsInterface $assets) {
// Attached settings win over settings in libraries.
$settings = NestedArray::mergeDeepArray([$settings, $assets->getSettings()], TRUE);
// Allow modules and themes to alter the JavaScript settings.
$this->moduleHandler->alter('js_settings', $settings, $assets);
$this->themeManager->alter('js_settings', $settings, $assets);
return $settings;
}
......@@ -191,115 +217,142 @@ protected function getJsSettingsAssets(AttachedAssetsInterface $assets) {
* {@inheritdoc}
*/
public function getJsAssets(AttachedAssetsInterface $assets, $optimize) {
$javascript = [];
$default_options = [
'type' => 'file',
'group' => JS_DEFAULT,
'every_page' => FALSE,
'weight' => 0,
'cache' => TRUE,
'preprocess' => TRUE,
'attributes' => [],
'version' => NULL,
'browsers' => [],
];
$libraries_to_load = $this->getLibrariesToLoad($assets);
$theme_info = $this->themeManager->getActiveTheme();
// Add the theme name to the cache key since themes may implement
// hook_js_alter(). Additionally add the current language to support
// translation of JavaScript files.
$cid = 'js:' . $theme_info->getName() . ':' . $this->languageManager->getCurrentLanguage()->getId() . ':' . Crypt::hashBase64(serialize($assets));
// Collect all libraries that contain JS assets and are in the header.
$header_js_libraries = [];
foreach ($libraries_to_load as $library) {
list($extension, $name) = explode('/', $library, 2);
$definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
if (isset($definition['js']) && !empty($definition['header'])) {
$header_js_libraries[] = $library;
}
if ($cached = $this->cache->get($cid)) {
list($js_assets_header, $js_assets_footer, $settings, $settings_in_header) = $cached->data;
}
// The current list of header JS libraries are only those libraries that are
// in the header, but their dependencies must also be loaded for them to
// function correctly, so update the list with those.
$header_js_libraries = $this->libraryDependencyResolver->getLibrariesWithDependencies($header_js_libraries);
foreach ($libraries_to_load as $library) {
list($extension, $name) = explode('/', $library, 2);
$definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
if (isset($definition['js'])) {
foreach ($definition['js'] as $options) {
$options += $default_options;
// 'scope' is a calculated option, based on which libraries are marked
// to be loaded from the header (see above).
$options['scope'] = in_array($library, $header_js_libraries) ? 'header' : 'footer';
else {
$javascript = [];
$default_options = [
'type' => 'file',
'group' => JS_DEFAULT,
'every_page' => FALSE,
'weight' => 0,
'cache' => TRUE,
'preprocess' => TRUE,
'attributes' => [],
'version' => NULL,
'browsers' => [],
];
$libraries_to_load = $this->getLibrariesToLoad($assets);
// Collect all libraries that contain JS assets and are in the header.
$header_js_libraries = [];
foreach ($libraries_to_load as $library) {
list($extension, $name) = explode('/', $library, 2);
$definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
if (isset($definition['js']) && !empty($definition['header'])) {
$header_js_libraries[] = $library;
}
}
// The current list of header JS libraries are only those libraries that
// are in the header, but their dependencies must also be loaded for them
// to function correctly, so update the list with those.
$header_js_libraries = $this->libraryDependencyResolver->getLibrariesWithDependencies($header_js_libraries);
foreach ($libraries_to_load as $library) {
list($extension, $name) = explode('/', $library, 2);
$definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
if (isset($definition['js'])) {
foreach ($definition['js'] as $options) {
$options += $default_options;
// 'scope' is a calculated option, based on which libraries are
// marked to be loaded from the header (see above).
$options['scope'] = in_array($library, $header_js_libraries) ? 'header' : 'footer';
// Preprocess can only be set if caching is enabled and no
// attributes are set.
$options['preprocess'] = $options['cache'] && empty($options['attributes']) ? $options['preprocess'] : FALSE;
// Always add a tiny value to the weight, to conserve the insertion
// order.
$options['weight'] += count($javascript) / 1000;
// Local and external files must keep their name as the associative
// key so the same JavaScript file is not added twice.
$javascript[$options['data']] = $options;
}
}
}
// Preprocess can only be set if caching is enabled and no attributes
// are set.
$options['preprocess'] = $options['cache'] && empty($options['attributes']) ? $options['preprocess'] : FALSE;
// Allow modules and themes to alter the JavaScript assets.
$this->moduleHandler->alter('js', $javascript, $assets);
$this->themeManager->alter('js', $javascript, $assets);
// Always add a tiny value to the weight, to conserve the insertion
// order.
$options['weight'] += count($javascript) / 1000;
// Sort JavaScript assets, so that they appear in the correct order.
uasort($javascript, 'static::sort');
// Local and external files must keep their name as the associative
// key so the same JavaScript file is not added twice.
$javascript[$options['data']] = $options;
// Prepare the return value: filter JavaScript assets per scope.
$js_assets_header = [];
$js_assets_footer = [];
foreach ($javascript as $key => $item) {
if ($item['scope'] == 'header') {
$js_assets_header[$key] = $item;
}
elseif ($item['scope'] == 'footer') {
$js_assets_footer[$key] = $item;
}
}
}
// Allow modules and themes to alter the JavaScript assets.
$this->moduleHandler->alter('js', $javascript, $assets);
$this->themeManager->alter('js', $javascript, $assets);
// Sort JavaScript assets, so that they appear in the correct order.
uasort($javascript, 'static::sort');
// Prepare the return value: filter JavaScript assets per scope.
$js_assets_header = [];
$js_assets_footer = [];
foreach ($javascript as $key => $item) {
if ($item['scope'] == 'header') {
$js_assets_header[$key] = $item;
if ($optimize) {
$collection_optimizer = \Drupal::service('asset.js.collection_optimizer');
$js_assets_header = $collection_optimizer->optimize($js_assets_header);
$js_assets_footer = $collection_optimizer->optimize($js_assets_footer);
}
elseif ($item['scope'] == 'footer') {
$js_assets_footer[$key] = $item;
// If the core/drupalSettings library is being loaded or is already
// loaded, get the JavaScript settings assets, and convert them into a
// single "regular" JavaScript asset.
$libraries_to_load = $this->getLibrariesToLoad($assets);
$settings_required = in_array('core/drupalSettings', $libraries_to_load) || in_array('core/drupalSettings', $this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getAlreadyLoadedLibraries()));
$settings_have_changed = count($libraries_to_load) > 0 || count($assets->getSettings()) > 0;
// Initialize settings to FALSE since they are not needed by default. This
// distinguishes between an empty array which must still allow
// hook_js_settings_alter() to be run.
$settings = FALSE;
if ($settings_required && $settings_have_changed) {
$settings = $this->getJsSettingsAssets($assets);
// Allow modules to add cached JavaScript settings.
foreach ($this->moduleHandler->getImplementations('js_settings_build') as $module) {
$function = $module . '_' . 'js_settings_build';
$function($settings, $assets);
}
}
$settings_in_header = in_array('core/drupalSettings', $header_js_libraries);
$this->cache->set($cid, [$js_assets_header, $js_assets_footer, $settings, $settings_in_header], CacheBackendInterface::CACHE_PERMANENT, ['library_info']);
}
if ($optimize) {
$collection_optimizer = \Drupal::service('asset.js.collection_optimizer');
$js_assets_header = $collection_optimizer->optimize($js_assets_header);
$js_assets_footer = $collection_optimizer->optimize($js_assets_footer);
}
// If the core/drupalSettings library is being loaded or is already loaded,
// get the JavaScript settings assets, and convert them into a single
// "regular" JavaScript asset.
$libraries_to_load = $this->getLibrariesToLoad($assets);
$settings_needed = in_array('core/drupalSettings', $libraries_to_load) || in_array('core/drupalSettings', $this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getAlreadyLoadedLibraries()));
$settings_have_changed = count($libraries_to_load) > 0 || count($assets->getSettings()) > 0;
if ($settings_needed && $settings_have_changed) {
$settings = $this->getJsSettingsAssets($assets);
if (!empty($settings)) {
$settings_as_inline_javascript = [
'type' => 'setting',
'group' => JS_SETTING,
'every_page' => TRUE,
'weight' => 0,
'browsers' => [],
'data' => $settings,
];
$settings_js_asset = ['drupalSettings' => $settings_as_inline_javascript];
// Prepend to the list of JS assets, to render it first. Preferably in
// the footer, but in the header if necessary.
if (in_array('core/drupalSettings', $header_js_libraries)) {
$js_assets_header = $settings_js_asset + $js_assets_header;
}
else {
$js_assets_footer = $settings_js_asset + $js_assets_footer;
}
if ($settings !== FALSE) {
// Allow modules and themes to alter the JavaScript settings.
$this->moduleHandler->alter('js_settings', $settings, $assets);
$this->themeManager->alter('js_settings', $settings, $assets);
$settings_as_inline_javascript = [
'type' => 'setting',
'group' => JS_SETTING,
'every_page' => TRUE,
'weight' => 0,
'browsers' => [],
'data' => $settings,
];
$settings_js_asset = ['drupalSettings' => $settings_as_inline_javascript];
// Prepend to the list of JS assets, to render it first. Preferably in
// the footer, but in the header if necessary.
if ($settings_in_header) {
$js_assets_header = $settings_js_asset + $js_assets_header;
}
else {
$js_assets_footer = $settings_js_asset + $js_assets_footer;
}
}
return [
$js_assets_header,
$js_assets_footer,
......
......@@ -825,6 +825,27 @@ function hook_library_info_build() {
return $libraries;
}
/**
* Modify the JavaScript settings (drupalSettings).
*
* @param array &$settings
* An array of all JavaScript settings (drupalSettings) being presented on the
* page.
* @param \Drupal\Core\Asset\AttachedAssetsInterface $assets
* The assets attached to the current response.
*
* @see \Drupal\Core\Asset\AssetResolver
*
* The results of this hook are cached, however modules may use
* hook_js_settings_alter() to dynamically alter settings.
*/
function hook_js_settings_build(array &$settings, \Drupal\Core\Asset\AttachedAssetsInterface $assets) {
// Manipulate settings.
if (isset($settings['dialog'])) {
$settings['dialog']['autoResize'] = FALSE;
}
}
/**
* Perform necessary alterations to the JavaScript settings (drupalSettings).
*
......
......@@ -158,7 +158,7 @@ protected function getThingsToCheck() {
// Editor.module's JS present. Note: ckeditor/drupal.ckeditor depends on
// editor/drupal.editor, hence presence of the former implies presence of
// the latter.
isset($settings['ajaxPageState']) && in_array('ckeditor/drupal.ckeditor', explode(',', $settings['ajaxPageState']['libraries'])),
isset($settings['ajaxPageState']['libraries']) && in_array('ckeditor/drupal.ckeditor', explode(',', $settings['ajaxPageState']['libraries'])),
// Body field.
$this->xpath('//textarea[@id="edit-body-0-value"]'),
// Format selector.
......
......@@ -130,7 +130,7 @@ function testCommentClasses() {
if ($case['comment_status'] == CommentInterface::PUBLISHED || $case['user'] == 'admin') {
$this->assertIdentical(1, count($this->xpath('//*[contains(@class, "comment")]/*[@data-comment-timestamp="' . $comment->getChangedTime() . '"]')), 'data-comment-timestamp attribute is set on comment');
$expectedJS = ($case['user'] !== 'anonymous');
$this->assertIdentical($expectedJS, isset($settings['ajaxPageState']) && in_array('comment/drupal.comment-new-indicator', explode(',', $settings['ajaxPageState']['libraries'])), 'drupal.comment-new-indicator library is present.');
$this->assertIdentical($expectedJS, isset($settings['ajaxPageState']['libraries']) && in_array('comment/drupal.comment-new-indicator', explode(',', $settings['ajaxPageState']['libraries'])), 'drupal.comment-new-indicator library is present.');
}
}
}
......
......@@ -2053,9 +2053,13 @@ protected function drupalPostWithFormat($path, $format, array $post, $options =
protected function getAjaxPageStatePostData() {
$post = array();
$drupal_settings = $this->drupalSettings;
if (isset($drupal_settings['ajaxPageState'])) {
if (isset($drupal_settings['ajaxPageState']['theme'])) {
$post['ajax_page_state[theme]'] = $drupal_settings['ajaxPageState']['theme'];
}
if (isset($drupal_settings['ajaxPageState']['theme_token'])) {
$post['ajax_page_state[theme_token]'] = $drupal_settings['ajaxPageState']['theme_token'];
}
if (isset($drupal_settings['ajaxPageState']['libraries'])) {
$post['ajax_page_state[libraries]'] = $drupal_settings['ajaxPageState']['libraries'];
}
return $post;
......
......@@ -606,10 +606,46 @@ function system_page_attachments(array &$page) {
}
}
/**
* Implements hook_js_settings_build().
*
* Sets values for the core/drupal.ajax library, which just depends on the
* active theme but no other request-dependent values.
*/
function system_js_settings_build(&$settings, AttachedAssetsInterface $assets) {
// Generate the values for the core/drupal.ajax library.
// We need to send ajaxPageState settings for core/drupal.ajax if:
// - ajaxPageState is being loaded in this Response, in which case it will
// already exist at $settings['ajaxPageState'] (because the core/drupal.ajax
// library definition specifies a placeholder 'ajaxPageState' setting).
// - core/drupal.ajax already has been loaded and hence this is an AJAX
// Response in which we must send the list of extra asset libraries that are
// being added in this AJAX Response.
/** @var \Drupal\Core\Asset\LibraryDependencyResolver $library_dependency_resolver */
$library_dependency_resolver = \Drupal::service('library.dependency_resolver');
if (isset($settings['ajaxPageState']) || in_array('core/drupal.ajax', $library_dependency_resolver->getLibrariesWithDependencies($assets->getAlreadyLoadedLibraries()))) {
// Provide the page with information about the theme that's used, so that
// a later AJAX request can be rendered using the same theme.
// @see \Drupal\Core\Theme\AjaxBasePageNegotiator
$theme_key = \Drupal::theme()->getActiveTheme()->getName();
$settings['ajaxPageState']['theme'] = $theme_key;
// Provide the page with information about the individual asset libraries
// used, information not otherwise available when aggregation is enabled.
$minimal_libraries = $library_dependency_resolver->getMinimalRepresentativeSubset(array_merge(
$assets->getLibraries(),
$assets->getAlreadyLoadedLibraries()
));
sort($minimal_libraries);
$settings['ajaxPageState']['libraries'] = implode(',', $minimal_libraries);
}
}
/**
* Implements hook_js_settings_alter().
*
* Sets values for the core/drupalSettings and core/drupal.ajax libraries.
* Sets values which depend on the current request, like core/drupalSettings
* as well as theme_token ajax state.
*/
function system_js_settings_alter(&$settings, AttachedAssetsInterface $assets) {
// url() generates the script and prefix using hook_url_outbound_alter().
......@@ -647,36 +683,15 @@ function system_js_settings_alter(&$settings, AttachedAssetsInterface $assets) {
if (!isset($settings['pluralDelimiter'])) {
$settings['pluralDelimiter'] = LOCALE_PLURAL_DELIMITER;
}
// Now generate the values for the core/drupal.ajax library.
// We need to send ajaxPageState settings for core/drupal.ajax if:
// - ajaxPageState is being loaded in this Response, in which case it will
// already exist at $settings['ajaxPageState'] (because the core/drupal.ajax
// library definition specifies a placeholder 'ajaxPageState' setting).
// - core/drupal.ajax already has been loaded and hence this is an AJAX
// Response in which we must send the list of extra asset libraries that are
// being added in this AJAX Response.
// Add the theme token to ajaxPageState, ensuring the database is available
// before doing so.
/** @var \Drupal\Core\Asset\LibraryDependencyResolver $library_dependency_resolver */
$library_dependency_resolver = \Drupal::service('library.dependency_resolver');
if (isset($settings['ajaxPageState']) || in_array('core/drupal.ajax', $library_dependency_resolver->getLibrariesWithDependencies($assets->getAlreadyLoadedLibraries()))) {
// Provide the page with information about the theme that's used, so that
// a later AJAX request can be rendered using the same theme.
// @see \Drupal\Core\Theme\AjaxBasePageNegotiator
$theme_key = \Drupal::theme()->getActiveTheme()->getName();
$settings['ajaxPageState']['theme'] = $theme_key;
// Checks that the DB is available before filling theme_token.
if (!defined('MAINTENANCE_MODE')) {
$settings['ajaxPageState']['theme_token'] = \Drupal::csrfToken()->get($theme_key);
$settings['ajaxPageState']['theme_token'] = \Drupal::csrfToken()
->get(\Drupal::theme()->getActiveTheme()->getName());
}
// Provide the page with information about the individual asset libraries
// used, information not otherwise available when aggregation is enabled.
$minimal_libraries = $library_dependency_resolver->getMinimalRepresentativeSubset(array_merge(
$assets->getLibraries(),
$assets->getAlreadyLoadedLibraries()
));
sort($minimal_libraries);
$settings['ajaxPageState']['libraries'] = implode(',', $minimal_libraries);
}
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment