diff --git a/core/core.libraries.yml b/core/core.libraries.yml index 3a6906e..6d71ff0 100644 --- a/core/core.libraries.yml +++ b/core/core.libraries.yml @@ -121,6 +121,15 @@ drupal.batch: - core/drupal.progress - core/jquery.once +drupal.big_pipe: + version: VERSION + js: + misc/big_pipe.js: {} + drupalSettings: + bigPipePlaceholders: [] + dependencies: + - core/drupal.ajax + drupal.collapse: version: VERSION js: diff --git a/core/core.services.yml b/core/core.services.yml index 0fe0a7f..e9d4742 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -869,7 +869,7 @@ services: - { name: event_subscriber } main_content_renderer.html: class: Drupal\Core\Render\MainContent\HtmlRenderer - arguments: ['@title_resolver', '@plugin.manager.display_variant', '@event_dispatcher', '@module_handler', '@renderer', '@render_cache'] + arguments: ['@title_resolver', '@plugin.manager.display_variant', '@event_dispatcher', '@module_handler', '@renderer', '@render_cache', '@render_strategy_manager'] tags: - { name: render.main_content_renderer, format: html } main_content_renderer.ajax: @@ -1392,6 +1392,26 @@ services: renderer: class: Drupal\Core\Render\Renderer arguments: ['@controller_resolver', '@theme.manager', '@plugin.manager.element_info', '@render_cache', '%renderer.config%'] + # Render strategies for rendering placeholders. + render_strategy_manager: + class: Drupal\Core\Render\Strategy\RenderStrategyManager + arguments: ['@renderer'] + tags: + - { name: service_collector, tag: render_strategy, call: addRenderStrategy } + lazy: true + render_strategy.single_flush: + class: Drupal\Core\Render\Strategy\SingleFlushRenderStrategy + tags: + - { name: render_strategy, priority: -1000 } + render_strategy.big_pipe: + class: Drupal\Core\Render\Strategy\BigPipeRenderStrategy + arguments: ['@big_pipe'] + tags: + - { name: render_strategy, priority: 0 } + big_pipe: + class: Drupal\Core\Render\BigPipe + arguments: ['@renderer', '@ajax_response.attachments_processor'] + lazy: true email.validator: class: Egulias\EmailValidator\EmailValidator diff --git a/core/lib/Drupal/Core/Render/BigPipe.php b/core/lib/Drupal/Core/Render/BigPipe.php new file mode 100644 index 0000000..9119e18 --- /dev/null +++ b/core/lib/Drupal/Core/Render/BigPipe.php @@ -0,0 +1,128 @@ +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/lib/Drupal/Core/Render/BigPipeInterface.php b/core/lib/Drupal/Core/Render/BigPipeInterface.php new file mode 100644 index 0000000..884b294 --- /dev/null +++ b/core/lib/Drupal/Core/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/lib/Drupal/Core/Render/BigPipeResponse.php b/core/lib/Drupal/Core/Render/BigPipeResponse.php new file mode 100644 index 0000000..bf82bef --- /dev/null +++ b/core/lib/Drupal/Core/Render/BigPipeResponse.php @@ -0,0 +1,58 @@ +bigPipePlaceholders = $placeholders; + } + + /** + * Sets the big pipe service to use. + * + * @param \Drupal\Core\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/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php index cb987f4..12d5a2a 100644 --- a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php +++ b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php @@ -11,11 +11,13 @@ use Drupal\Core\Controller\TitleResolverInterface; use Drupal\Core\Display\PageVariantInterface; use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Render\BigPipeResponse; use Drupal\Core\Render\HtmlResponse; use Drupal\Core\Render\PageDisplayVariantSelectionEvent; use Drupal\Core\Render\RenderCacheInterface; use Drupal\Core\Render\RendererInterface; use Drupal\Core\Render\RenderEvents; +use Drupal\Core\Render\Strategy\RenderStrategyManagerInterface; use Drupal\Core\Routing\RouteMatchInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\Request; @@ -74,6 +76,13 @@ class HtmlRenderer implements MainContentRendererInterface { protected $renderCache; /** + * The render strategy manager service. + * + * @var \Drupal\Core\Render\Strategy\RenderStrategyManager + */ + protected $renderStrategyManager; + + /** * Constructs a new HtmlRenderer. * * @param \Drupal\Core\Controller\TitleResolverInterface $title_resolver @@ -89,13 +98,14 @@ class HtmlRenderer implements MainContentRendererInterface { * @param \Drupal\Core\Render\RenderCacheInterface $render_cache * The render cache service. */ - public function __construct(TitleResolverInterface $title_resolver, PluginManagerInterface $display_variant_manager, EventDispatcherInterface $event_dispatcher, ModuleHandlerInterface $module_handler, RendererInterface $renderer, RenderCacheInterface $render_cache) { + public function __construct(TitleResolverInterface $title_resolver, PluginManagerInterface $display_variant_manager, EventDispatcherInterface $event_dispatcher, ModuleHandlerInterface $module_handler, RendererInterface $renderer, RenderCacheInterface $render_cache, RenderStrategyManagerInterface $render_strategy_manager) { $this->titleResolver = $title_resolver; $this->displayVariantManager = $display_variant_manager; $this->eventDispatcher = $event_dispatcher; $this->moduleHandler = $module_handler; $this->renderer = $renderer; $this->renderCache = $render_cache; + $this->renderStrategyManager = $render_strategy_manager; } /** @@ -124,18 +134,38 @@ public function renderResponse(array $main_content, Request $request, RouteMatch // page.html.twig, hence add them here, just before rendering html.html.twig. $this->buildPageTopAndBottom($html); - // @todo https://www.drupal.org/node/2495001 Make renderRoot return a - // cacheable render array directly. - $this->renderer->renderRoot($html); + // This uses render() instead of renderRoot() to ensure that placeholders + // are not replaced. + $this->renderer->render($html); $content = $this->renderCache->getCacheableRenderArray($html); + $this->renderStrategyManager->renderPlaceholders($content); + // Also associate the "rendered" cache tag. This allows us to invalidate the // entire render cache, regardless of the cache bin. $content['#cache']['tags'][] = 'rendered'; - $response = new HtmlResponse($content, 200, [ - 'Content-Type' => 'text/html; charset=UTF-8', - ]); + + if (!empty($content['#attached']['big_pipe_placeholders'])) { + $response = new BigPipeResponse('', 200, [ + 'Content-Type' => 'text/html; charset=UTF-8', + ]); + + // Inject the placeholders and service into the response. + $response->setBigPipePlaceholders($content['#attached']['big_pipe_placeholders']); + $response->setBigPipeService(reset($content['#attached']['big_pipe_service'])); + + unset($content['#attached']['big_pipe_placeholders']); + unset($content['#attached']['big_pipe_service']); + + // Now after all pre-processing finally set the content. + $response->setContent($content); + } + else { + $response = new HtmlResponse($content, 200, [ + 'Content-Type' => 'text/html; charset=UTF-8', + ]); + } return $response; } diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php index d24b7d7..5499919 100644 --- a/core/lib/Drupal/Core/Render/Renderer.php +++ b/core/lib/Drupal/Core/Render/Renderer.php @@ -118,10 +118,8 @@ public function renderPlain(&$elements) { * The updated $elements. * * @see ::replacePlaceholders() - * - * @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]; @@ -211,12 +209,6 @@ protected function doRender(&$elements, $is_root_call = FALSE) { $cached_element = $this->renderCache->get($elements); if ($cached_element !== FALSE) { $elements = $cached_element; - // Only when we're in a root (non-recursive) Renderer::render() call, - // placeholders must be processed, to prevent breaking the render cache - // in case of nested elements with #cache set. - if ($is_root_call) { - $this->replacePlaceholders($elements); - } // Mark the element markup as safe. If we have cached children, we need // to mark them as safe too. The parent markup contains the child // markup, so if the parent markup is safe, then the markup of the @@ -233,6 +225,14 @@ protected function doRender(&$elements, $is_root_call = FALSE) { // Render cache hit, so rendering is finished, all necessary info // collected! $this->bubbleStack(); + + // Only when we're in a root (non-recursive) Renderer::render() call, + // placeholders must be processed, to prevent breaking the render cache + // in case of nested elements with #cache set. + if ($is_root_call) { + $this->replacePlaceholders($elements); + } + return $elements['#markup']; } } @@ -470,6 +470,15 @@ protected function doRender(&$elements, $is_root_call = FALSE) { $this->renderCache->set($elements, $pre_bubbling_elements); } + if ($is_root_call) { + if (static::$stack->count() !== 1) { + throw new \LogicException('A stray drupal_render() invocation with $is_root_call = TRUE is causing bubbling of attached assets to break.'); + } + } + + // Rendering is finished, all necessary info collected! + $this->bubbleStack(); + // Only when we're in a root (non-recursive) Renderer::render() call, // placeholders must be processed, to prevent breaking the render cache in // case of nested elements with #cache set. @@ -481,14 +490,8 @@ protected function doRender(&$elements, $is_root_call = FALSE) { // that is handled earlier in Renderer::render(). if ($is_root_call) { $this->replacePlaceholders($elements); - if (static::$stack->count() !== 1) { - throw new \LogicException('A stray drupal_render() invocation with $is_root_call = TRUE is causing bubbling of attached assets to break.'); - } } - // Rendering is finished, all necessary info collected! - $this->bubbleStack(); - $elements['#printed'] = TRUE; $elements['#markup'] = SafeMarkup::set($elements['#markup']); return $elements['#markup']; diff --git a/core/lib/Drupal/Core/Render/Strategy/BigPipeRenderStrategy.php b/core/lib/Drupal/Core/Render/Strategy/BigPipeRenderStrategy.php new file mode 100644 index 0000000..67608c2 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Strategy/BigPipeRenderStrategy.php @@ -0,0 +1,76 @@ +bigPipe = $big_pipe; + } + + /** + * {@inheritdoc} + */ + public function processPlaceholders(array $placeholders) { + $return = []; + + // @todo Only do this for authorized users. + + // Ensure placeholders are unique per page. + $token = Crypt::randomBytesBase64(55); + + foreach ($placeholders as $placeholder => $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' => [ + 'core/drupal.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, + ], + 'big_pipe_service' => [ $this->bigPipe ], + ], + ]; + } + + return $return; + } +} diff --git a/core/lib/Drupal/Core/Render/Strategy/RenderStrategyInterface.php b/core/lib/Drupal/Core/Render/Strategy/RenderStrategyInterface.php new file mode 100644 index 0000000..e6abbd5 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Strategy/RenderStrategyInterface.php @@ -0,0 +1,27 @@ + $render_array. + */ + public function processPlaceholders(array $placeholders); + +} diff --git a/core/lib/Drupal/Core/Render/Strategy/RenderStrategyManager.php b/core/lib/Drupal/Core/Render/Strategy/RenderStrategyManager.php new file mode 100644 index 0000000..4bc05e0 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Strategy/RenderStrategyManager.php @@ -0,0 +1,84 @@ +renderer = $renderer; + } + + /** + * Adds a render strategy to process. + * + * @param RenderStrategyInterface $strategy + * The strategy to add to the render strategies. + */ + public function addRenderStrategy(RenderStrategyInterface $strategy) { + $this->renderStrategies[] = $strategy; + } + + /** + * {@inheritdoc} + */ + public function renderPlaceholders(&$elements) { + if (!isset($elements['#attached']['placeholders']) || empty($elements['#attached']['placeholders'])) { + return FALSE; + } + + $new_placeholders = []; + $placeholders = $elements['#attached']['placeholders']; + + // Give each render strategy a chance to replace all not-yet replaced + // placeholders. + foreach ($this->renderStrategies as $strategy) { + $processed_placeholders = $strategy->processPlaceholders($placeholders); + $placeholders = array_diff_key($placeholders, $processed_placeholders); + $new_placeholders += $processed_placeholders; + + if (empty($placeholders)) { + break; + } + } + + // Now render the new placeholders. + $elements['#attached']['placeholders'] = $new_placeholders; + foreach (array_keys($elements['#attached']['placeholders']) as $placeholder) { + $elements = $this->renderer->renderPlaceholder($placeholder, $elements); + } + unset($elements['#attached']['placeholders']); + + return TRUE; + } + +} diff --git a/core/lib/Drupal/Core/Render/Strategy/RenderStrategyManagerInterface.php b/core/lib/Drupal/Core/Render/Strategy/RenderStrategyManagerInterface.php new file mode 100644 index 0000000..e7c0385 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Strategy/RenderStrategyManagerInterface.php @@ -0,0 +1,26 @@ +