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);