diff --git a/core/includes/common.inc b/core/includes/common.inc index 19beefa..63372e3 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -630,7 +630,8 @@ function drupal_merge_attached(array $a, array $b) { */ function drupal_process_attached(array $elements) { // Asset attachments are handled by \Drupal\Core\Asset\AssetResolver. - foreach (array('library', 'drupalSettings') as $type) { + // @todo This needs to be configurable somehow. + foreach (array('library', 'drupalSettings', 'big_pipe_placeholders') as $type) { unset($elements['#attached'][$type]); } diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php index bb8f1ff..7870042 100644 --- a/core/lib/Drupal/Core/Render/Renderer.php +++ b/core/lib/Drupal/Core/Render/Renderer.php @@ -167,7 +167,7 @@ public function renderPlain(&$elements) { * * @todo Make public as part of https://www.drupal.org/node/2469431 */ - protected function renderPlaceholder($placeholder, array $elements) { + public function renderPlaceholder($placeholder, array $elements) { // Get the render array for the given placeholder $placeholder_elements = $elements['#attached']['placeholders'][$placeholder]; diff --git a/core/modules/big_pipe/big_pipe.info.yml b/core/modules/big_pipe/big_pipe.info.yml new file mode 100644 index 0000000..c10cc4f --- /dev/null +++ b/core/modules/big_pipe/big_pipe.info.yml @@ -0,0 +1,6 @@ +name: big_pipe +type: module +description: 'Enables BigPipe for authenticated users; first send+render the cheap parts of the page, then the expensive parts.' +package: Core +version: VERSION +core: 8.x diff --git a/core/modules/big_pipe/big_pipe.libraries.yml b/core/modules/big_pipe/big_pipe.libraries.yml new file mode 100644 index 0000000..88ecff6 --- /dev/null +++ b/core/modules/big_pipe/big_pipe.libraries.yml @@ -0,0 +1,11 @@ +big_pipe: + version: VERSION + js: + js/big_pipe.js: {} + drupalSettings: + bigPipePlaceholders: [] + dependencies: + - core/jquery + - core/drupal + - core/drupal.ajax + - core/drupalSettings diff --git a/core/modules/big_pipe/big_pipe.module b/core/modules/big_pipe/big_pipe.module new file mode 100644 index 0000000..a4abe2d --- /dev/null +++ b/core/modules/big_pipe/big_pipe.module @@ -0,0 +1,2 @@ +bigPipe = $big_pipe; + } + + /** + * Processes placeholders for HTML responses. + * + * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event + * The event to process. + */ + public function onRespond(FilterResponseEvent $event) { + if (!$event->isMasterRequest()) { + return; + } + + $response = $event->getResponse(); + if (!$response instanceof HtmlResponse) { + return; + } + + $attachments = $response->getAttachments(); + if (empty($attachments['big_pipe_placeholders'])) { + return; + } + + // Create a new Response. + $big_pipe_response = new BigPipeResponse(); + + // Clone the response. + $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()); + + // Inject the placeholders and service into the response. + $big_pipe_response->setBigPipePlaceholders($attachments['big_pipe_placeholders']); + $big_pipe_response->setBigPipeService($this->bigPipe); + unset($attachments['big_pipe_placeholders']); + + // Set the remaining attachments. + $big_pipe_response->setAttachments($attachments); + + // And set the new response. + $event->setResponse($big_pipe_response); + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + // Run as pretty much last subscriber. + $events[KernelEvents::RESPONSE][] = ['onRespond', -10000]; + return $events; + } + +} diff --git a/core/modules/big_pipe/src/Render/BigPipe.php b/core/modules/big_pipe/src/Render/BigPipe.php new file mode 100644 index 0000000..09508cf --- /dev/null +++ b/core/modules/big_pipe/src/Render/BigPipe.php @@ -0,0 +1,130 @@ +renderer = $renderer; + $this->ajaxResponseAttachmentsProcessor = $ajax_response_attachments_processor; + } + + /** + * {@inheritdoc} + */ + public function sendContent($content, array $placeholders) { + // Split it up in various chunks. + $split = ''; + if (strpos($content, $split) === FALSE) { + $split = ''; + } + $page_parts = explode($split, $content); + + if (count($page_parts) !== 2) { + throw new \LogicException("You need to have only one body or one tag in your html.html.twig template file."); + } + + // Support streaming on NGINX + php-fpm (nginx >= 1.5.6). + header('X-Accel-Buffering: no'); + + print $page_parts[0]; + + // Print a container and the start signal. + print "\n"; + print '
' . "\n"; + print ' ' . "\n"; + + ob_end_flush(); + flush(); + + ksort($placeholders); + + foreach ($placeholders as $placeholder => $placeholder_elements) { + // Check if the placeholder is present at all. + if (strpos($content, $placeholder) === FALSE) { + continue; + } + + // Create elements to process in right format. + $elements = [ + '#markup' => $placeholder, + '#attached' => [ + 'placeholders' => [ + $placeholder => $placeholder_elements, + ], + ], + ]; + + $elements = $this->renderer->renderPlaceholder($placeholder, $elements); + + // Create a new AjaxResponse. + $response = new AjaxResponse(); + $response->addCommand(new ReplaceCommand(sprintf('[data-big-pipe-selector="%s"]', $placeholder), $elements['#markup'])); + $response->setAttachments($elements['#attached']); + + $this->ajaxResponseAttachmentsProcessor->processAttachments($response); + + // @todo Filter response. + $json = $response->getContent(); + + $output = << + $json + + +EOF; + print $output; + + flush(); + } + + // Send the stop signal. + print ' ' . "\n"; + print '
' . "\n"; + + // Now that we have processed all the placeholders, attach the behaviors + // on the page again. + print $behaviors; + + print $split; + print $page_parts[1]; + + return $this; + } +} diff --git a/core/modules/big_pipe/src/Render/BigPipeInterface.php b/core/modules/big_pipe/src/Render/BigPipeInterface.php new file mode 100644 index 0000000..221791d --- /dev/null +++ b/core/modules/big_pipe/src/Render/BigPipeInterface.php @@ -0,0 +1,29 @@ + tag + * and afterwards processes placeholders to send when they have been rendered. + * + * The output buffers are flushed in between. + * + * @param array $placeholders + * The placeholders to process. + * @param string $content + * The content to send. + */ + public function sendContent($content, array $placeholders); + +} diff --git a/core/modules/big_pipe/src/Render/BigPipeResponse.php b/core/modules/big_pipe/src/Render/BigPipeResponse.php new file mode 100644 index 0000000..168b0d1 --- /dev/null +++ b/core/modules/big_pipe/src/Render/BigPipeResponse.php @@ -0,0 +1,60 @@ +bigPipePlaceholders = $placeholders; + } + + /** + * Sets the big pipe service to use. + * + * @param \Drupal\big_pipe\Render\BigPipeInterface $big_pipe + * The BigPipe service. + */ + public function setBigPipeService(BigPipeInterface $big_pipe) { + $this->bigPipe = $big_pipe; + } + + /** + * {@inheritdoc} + */ + public function sendContent() { + $this->bigPipe->sendContent($this->content, $this->bigPipePlaceholders); + + return $this; + } +} diff --git a/core/modules/big_pipe/src/Render/Placeholder/BigPipeStrategy.php b/core/modules/big_pipe/src/Render/Placeholder/BigPipeStrategy.php new file mode 100644 index 0000000..a956ff2 --- /dev/null +++ b/core/modules/big_pipe/src/Render/Placeholder/BigPipeStrategy.php @@ -0,0 +1,58 @@ + $placeholder_elements) { + $html_placeholder = Html::getId($placeholder . '-' . $token); + $return[$placeholder] = [ + '#markup' => '
', + // Big Pipe placeholders are not cacheable. + '#cache' => [ + 'max-age' => 0, + ], + '#attached' => [ + // Use the big_pipe library. + 'library' => [ + 'big_pipe/big_pipe', + ], + // Add the placeholder to a white list of JS processed placeholders. + 'drupalSettings' => [ + 'bigPipePlaceholders' => [ $html_placeholder => TRUE ], + ], + 'big_pipe_placeholders' => [ + $html_placeholder => $placeholder_elements, + ], + ], + ]; + } + + return $return; + } +}