core/modules/big_pipe/big_pipe.info.yml | 2 +-
.../HtmlResponseBigPipeSubscriber.php | 14 +-
core/modules/big_pipe/src/Render/BigPipe.php | 179 ++++++++++-----------
.../big_pipe/src/Render/BigPipeInterface.php | 7 +-
.../src/Render/Placeholder/BigPipeStrategy.php | 16 +-
5 files changed, 106 insertions(+), 112 deletions(-)
diff --git a/core/modules/big_pipe/big_pipe.info.yml b/core/modules/big_pipe/big_pipe.info.yml
index b0f6e91..13a739b 100644
--- a/core/modules/big_pipe/big_pipe.info.yml
+++ b/core/modules/big_pipe/big_pipe.info.yml
@@ -1,6 +1,6 @@
name: BigPipe
type: module
-description: 'Enables BigPipe for authenticated users; first send+render the cheap parts of the page, then the expensive parts.'
+description: 'Sends pages in a way that allows browsers to show them much faster. Uses the BigPipe technique.'
package: Core
version: VERSION
core: 8.x
diff --git a/core/modules/big_pipe/src/EventSubscriber/HtmlResponseBigPipeSubscriber.php b/core/modules/big_pipe/src/EventSubscriber/HtmlResponseBigPipeSubscriber.php
index 8f2aaec..9170fe2 100644
--- a/core/modules/big_pipe/src/EventSubscriber/HtmlResponseBigPipeSubscriber.php
+++ b/core/modules/big_pipe/src/EventSubscriber/HtmlResponseBigPipeSubscriber.php
@@ -114,11 +114,13 @@ public function onRespond(FilterResponseEvent $event) {
$big_pipe_response = new BigPipeResponse();
$big_pipe_response->setBigPipeService($this->bigPipe);
- // Clone the response.
+ // Clone the HtmlResponse's data into the new BigPipeResponse.
$big_pipe_response->headers = clone $response->headers;
- $big_pipe_response->setStatusCode($response->getStatusCode());
- $big_pipe_response->setContent($response->getContent());
- $big_pipe_response->addCacheableDependency($response->getCacheableMetadata());
+ $big_pipe_response
+ ->setStatusCode($response->getStatusCode())
+ ->setContent($response->getContent())
+ ->setAttachments($attachments)
+ ->addCacheableDependency($response->getCacheableMetadata());
// A BigPipe response can never be cached, because it is intended for a
// single user.
@@ -138,10 +140,6 @@ public function onRespond(FilterResponseEvent $event) {
// Add header to support streaming on NGINX + php-fpm (nginx >= 1.5.6).
$big_pipe_response->headers->set('X-Accel-Buffering', 'no');
- // Set the remaining attachments.
- $big_pipe_response->setAttachments($attachments);
-
- // And set the new response.
$event->setResponse($big_pipe_response);
}
diff --git a/core/modules/big_pipe/src/Render/BigPipe.php b/core/modules/big_pipe/src/Render/BigPipe.php
index 8befe24..19112f0 100644
--- a/core/modules/big_pipe/src/Render/BigPipe.php
+++ b/core/modules/big_pipe/src/Render/BigPipe.php
@@ -13,19 +13,14 @@
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Asset\AttachedAssets;
use Drupal\Core\Asset\AttachedAssetsInterface;
-use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\Render\HtmlResponse;
use Drupal\Core\Render\Markup;
use Drupal\Core\Render\RendererInterface;
-use Drupal\Core\Render\AttachmentsResponseProcessorInterface;
-use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
-use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
-use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\HttpKernel\KernelEvents;
/**
@@ -188,6 +183,89 @@ protected function sendPreBody($pre_body, array $no_js_placeholders, AttachedAss
}
/**
+ * Sends no-JS BigPipe placeholders' replacements as embedded HTML responses.
+ *
+ * @param string $html
+ * HTML markup.
+ * @param array $no_js_placeholders
+ * Associative array; the no-JS BigPipe placeholders. Keys are the BigPipe
+ * selectors.
+ * @param \Drupal\Core\Asset\AttachedAssetsInterface $cumulative_assets
+ * The cumulative assets sent so far; to be updated while rendering no-JS
+ * BigPipe placeholders.
+ */
+ protected function sendNoJsPlaceholders($html, $no_js_placeholders, AttachedAssetsInterface $cumulative_assets) {
+ $fragments = explode('
', $fragment, 2);
+ $placeholder = $t[0];
+ if (!isset($no_js_placeholders[$placeholder])) {
+ continue;
+ }
+
+ $token = Crypt::randomBytesBase64(55);
+
+ // Render the placeholder, but include the cumulative settings assets, so
+ // we can calculate the overall settings for the entire page.
+ $placeholder_plus_cumulative_settings = [
+ 'placeholder' => $no_js_placeholders[$placeholder],
+ 'cumulative_settings_' . $token => [
+ '#attached' => [
+ 'drupalSettings' => $cumulative_assets->getSettings(),
+ ],
+ ],
+ ];
+ $elements = $this->renderPlaceholder($placeholder, $placeholder_plus_cumulative_settings);
+
+ // Create a new HtmlResponse. Ensure the CSS and (non-bottom) JS is sent
+ // before the HTML they're associated with. In other words: ensure the
+ // critical assets for this placeholder's markup are loaded first.
+ // @see \Drupal\Core\Render\HtmlResponseSubscriber
+ // @see template_preprocess_html()
+ $css_placeholder = '';
+ $js_placeholder = '';
+ $elements['#markup'] = Markup::create($css_placeholder . $js_placeholder . (string) $elements['#markup']);
+ $elements['#attached']['html_response_attachment_placeholders']['styles'] = $css_placeholder;
+ $elements['#attached']['html_response_attachment_placeholders']['scripts'] = $js_placeholder;
+
+ $html_response = new HtmlResponse();
+ $html_response->setContent($elements);
+ $html_response->getCacheableMetadata()->setCacheMaxAge(0);
+
+ // Push a fake request with the asset libraries loaded so far and dispatch
+ // KernelEvents::RESPONSE event. This results in the attachments for the
+ // HTML response being processed by HtmlResponseAttachmentsProcessor and
+ // hence:
+ // - the HTML to load the CSS can be rendered.
+ // - the HTML to load the JS (at the top) can be rendered.
+ $fake_request = $this->requestStack->getMasterRequest()->duplicate();
+ $fake_request->request->set('ajax_page_state', ['libraries' => implode(',', $cumulative_assets->getAlreadyLoadedLibraries())] + $cumulative_assets->getSettings()['ajaxPageState']);
+ $this->requestStack->push($fake_request);
+ $event = new FilterResponseEvent($this->httpKernel, $fake_request, HttpKernelInterface::SUB_REQUEST, $html_response);
+ $this->eventDispatcher->dispatch(KernelEvents::RESPONSE, $event);
+ $html_response = $event->getResponse();
+ $this->requestStack->pop();
+
+ // Send this embedded HTML response.
+ print $html_response->getContent();
+ print $t[1];
+ flush();
+
+ // Another placeholder was rendered and sent, track the set of asset
+ // libraries sent so far. Any new settings also need to be tracked, so
+ // they can be sent in ::sendPreBody().
+ // @todo What if drupalSettings already was printed in the HTML ? That case is not yet handled. In that case, no-JS BigPipe would cause broken (incomplete) drupalSettingsā¦ This would not matter if it were only used if JS is not enabled, but that's not the only use case. However, this
+ $final_settings = $html_response->getAttachments()['drupalSettings'];
+ $cumulative_assets->setAlreadyLoadedLibraries(explode(',', $final_settings['ajaxPageState']['libraries']));
+ $cumulative_assets->setSettings($final_settings);
+ }
+ }
+
+ /**
* Sends BigPipe placeholders' replacements as embedded AJAX responses.
*
* @param array $placeholders
@@ -298,19 +376,20 @@ protected function sendPostBody($post_body) {
*
* @param string $placeholder
* The placeholder to render.
- * @param array $placeholder_elements
+ * @param array $placeholder_render_array
* The render array associated with that placeholder.
+ *
* @return array
* The render array representing the rendered placeholder.
*
* @see \Drupal\Core\Render\RendererInterface::renderPlaceholder()
*/
- protected function renderPlaceholder($placeholder, $placeholder_elements) {
+ protected function renderPlaceholder($placeholder, array $placeholder_render_array) {
$elements = [
'#markup' => $placeholder,
'#attached' => [
'placeholders' => [
- $placeholder => $placeholder_elements,
+ $placeholder => $placeholder_render_array,
],
],
];
@@ -324,6 +403,7 @@ protected function renderPlaceholder($placeholder, $placeholder_elements) {
*
* @param string $html
* HTML markup.
+ *
* @return array
* Indexed array; the order in which the BigPipe placeholders must be sent.
* Values are the BigPipe selectors.
@@ -342,87 +422,4 @@ protected function getPlaceholderOrder($html) {
return $order;
}
- /**
- * Sends no-JS BigPipe placeholders' replacements as embedded HTML responses.
- *
- * @param string $html
- * HTML markup.
- * @param array $no_js_placeholders
- * Associative array; the no-JS BigPipe placeholders. Keys are the BigPipe
- * selectors.
- * @param \Drupal\Core\Asset\AttachedAssetsInterface $cumulative_assets
- * The cumulative assets sent so far; to be updated while rendering no-JS
- * BigPipe placeholders.
- */
- protected function sendNoJsPlaceholders($html, $no_js_placeholders, AttachedAssetsInterface $cumulative_assets) {
- $fragments = explode('', $fragment, 2);
- $placeholder = $t[0];
- if (!isset($no_js_placeholders[$placeholder])) {
- continue;
- }
-
- $token = Crypt::randomBytesBase64(55);
-
- // Render the placeholder, but include the cumulative settings assets, so
- // we can calculate the overall settings for the entire page.
- $placeholder_plus_cumulative_settings = [
- 'placeholder' => $no_js_placeholders[$placeholder],
- 'cumulative_settings_' . $token => [
- '#attached' => [
- 'drupalSettings' => $cumulative_assets->getSettings(),
- ],
- ],
- ];
- $elements = $this->renderPlaceholder($placeholder, $placeholder_plus_cumulative_settings);
-
- // Create a new HtmlResponse. Ensure the CSS and (non-bottom) JS is sent
- // before the HTML they're associated with. In other words: ensure the
- // critical assets for this placeholder's markup are loaded first.
- // @see \Drupal\Core\Render\HtmlResponseSubscriber
- // @see template_preprocess_html()
- $css_placeholder = '';
- $js_placeholder = '';
- $elements['#markup'] = Markup::create($css_placeholder . $js_placeholder . (string) $elements['#markup']);
- $elements['#attached']['html_response_attachment_placeholders']['styles'] = $css_placeholder;
- $elements['#attached']['html_response_attachment_placeholders']['scripts'] = $js_placeholder;
-
- $html_response = new HtmlResponse();
- $html_response->setContent($elements);
- $html_response->getCacheableMetadata()->setCacheMaxAge(0);
-
- // Push a fake request with the asset libraries loaded so far and dispatch
- // KernelEvents::RESPONSE event. This results in the attachments for the
- // HTML response being processed by HtmlResponseAttachmentsProcessor and
- // hence:
- // - the HTML to load the CSS can be rendered.
- // - the HTML to load the JS (at the top) can be rendered.
- $fake_request = $this->requestStack->getMasterRequest()->duplicate();
- $fake_request->request->set('ajax_page_state', ['libraries' => implode(',', $cumulative_assets->getAlreadyLoadedLibraries())] + $cumulative_assets->getSettings()['ajaxPageState']);
- $this->requestStack->push($fake_request);
- $event = new FilterResponseEvent($this->httpKernel, $fake_request, HttpKernelInterface::SUB_REQUEST, $html_response);
- $this->eventDispatcher->dispatch(KernelEvents::RESPONSE, $event);
- $html_response = $event->getResponse();
- $this->requestStack->pop();
-
- // Send this embedded HTML response.
- print $html_response->getContent();
- print $t[1];
- flush();
-
- // Another placeholder was rendered and sent, track the set of asset
- // libraries sent so far. Any new settings also need to be tracked, so
- // they can be sent in ::sendPreBody().
- // @todo What if drupalSettings already was printed in the HTML ? That case is not yet handled. In that case, no-JS BigPipe would cause broken (incomplete) drupalSettingsā¦ This would not matter if it were only used if JS is not enabled, but that's not the only use case. However, this
- $final_settings = $html_response->getAttachments()['drupalSettings'];
- $cumulative_assets->setAlreadyLoadedLibraries(explode(',', $final_settings['ajaxPageState']['libraries']));
- $cumulative_assets->setSettings($final_settings);
- }
- }
-
}
diff --git a/core/modules/big_pipe/src/Render/BigPipeInterface.php b/core/modules/big_pipe/src/Render/BigPipeInterface.php
index 0950d9c..f4066a3 100644
--- a/core/modules/big_pipe/src/Render/BigPipeInterface.php
+++ b/core/modules/big_pipe/src/Render/BigPipeInterface.php
@@ -119,15 +119,12 @@
interface BigPipeInterface {
/**
- * Sends the content to the browser, splitting before the closing tag
- * and afterwards processes placeholders to send when they have been rendered.
- *
- * The output buffers are flushed in between.
+ * Sends an HTML response in chunks using the BigPipe technique.
*
* @param string $content
* The HTML response content to send.
* @param array $attachments
- * The HTML response's attachments
+ * The HTML response's attachments.
*/
public function sendContent($content, array $attachments);
diff --git a/core/modules/big_pipe/src/Render/Placeholder/BigPipeStrategy.php b/core/modules/big_pipe/src/Render/Placeholder/BigPipeStrategy.php
index 80bd378..58eb50b 100644
--- a/core/modules/big_pipe/src/Render/Placeholder/BigPipeStrategy.php
+++ b/core/modules/big_pipe/src/Render/Placeholder/BigPipeStrategy.php
@@ -72,14 +72,13 @@ public function __construct(AccountInterface $current_user) {
* {@inheritdoc}
*/
public function processPlaceholders(array $placeholders) {
- $return = [];
-
// @todo Move to a ResponsePolicy instead.
// @todo Add user.roles:authenticated cache context.
if (!$this->currentUser->isAuthenticated()) {
- return $return;
+ return [];
}
+ $overridden_placeholders = [];
foreach ($placeholders as $placeholder => $placeholder_elements) {
// BigPipe uses JavaScript and the DOM to find the placeholder to replace.
// This means finding the placeholder to replace must be efficient. Most
@@ -95,21 +94,21 @@ public function processPlaceholders(array $placeholders) {
// @see \Drupal\Core\Form\FormBuilder::renderFormTokenPlaceholder()
// @see \Drupal\Core\Form\FormBuilder::renderPlaceholderFormAction()
if ($placeholder[0] !== '<' || $placeholder !== Html::normalize($placeholder)) {
- $return[$placeholder] = static::createBigPipeNoJsPlaceholder($placeholder, $placeholder_elements);
+ $overridden_placeholders[$placeholder] = static::createBigPipeNoJsPlaceholder($placeholder, $placeholder_elements);
}
else {
// If the current session doesn't have JavaScript, fall back to no-JS
// BigPipe.
if (empty($_SESSION['big_pipe_has_js'])) {
- $return[$placeholder] = static::createBigPipeNoJsPlaceholder($placeholder, $placeholder_elements);
+ $overridden_placeholders[$placeholder] = static::createBigPipeNoJsPlaceholder($placeholder, $placeholder_elements);
}
else {
- $return[$placeholder] = static::createBigPipeJsPlaceholder($placeholder, $placeholder_elements);
+ $overridden_placeholders[$placeholder] = static::createBigPipeJsPlaceholder($placeholder, $placeholder_elements);
}
}
}
- return $return;
+ return $overridden_placeholders;
}
/**
@@ -171,6 +170,9 @@ protected static function createBigPipeJsPlaceholder($original_placeholder, arra
*
* @return array
* The resulting BigPipe no-JS placeholder render array.
+ *
+ * @todo Figure out how to simplify this. Perhaps no new placeholder is in fact necessary?
+ * @todo Related, perhaps distinguish between "HTML" and "non-HTML (attr value)" use cases? Because right now, this *breaks* HTML and therefore breaks response filters: this indiscriminately uses a as a placeholder, which is invalid inside a HTML attribute, and thus breaks DOM parsing.
*/
protected static function createBigPipeNoJsPlaceholder($original_placeholder, array $placeholder_render_array) {
$html_placeholder = Html::getId($original_placeholder);