diff --git a/Model/Config.php b/Model/Config.php index 68d0ae7..1e446a9 100644 --- a/Model/Config.php +++ b/Model/Config.php @@ -29,6 +29,7 @@ class Config public const XML_PATH_DEFERRED_ENABLED = 'mfrocketjavascript/deferred_javascript/enabled'; public const XML_PATH_DEFERRED_DISALLOWED_PAGES = 'mfrocketjavascript/deferred_javascript/disallowed_pages_for_deferred_js'; public const XML_PATH_DEFERRED_IGNORE_JAVASCRIPT = 'mfrocketjavascript/deferred_javascript/ignore_deferred_javascript_with'; + public const XML_PATH_MOVE_SCRIPTS_TO_EXTERNAL_FILE = 'mfrocketjavascript/deferred_javascript/movebody_scripts_to_file'; /** * JavaScript Bundling config @@ -131,6 +132,17 @@ public function getIncludedInBundling(): string return (string)$this->getConfig(self::XML_PATH_JAVASCRIPT_BUNDLING_INCLUDED_IN_BUNDLING); } + /** + * Retrieve true if move scripts to external file is enabled + * + * @param string|null $storeId + * @return bool + */ + public function isMoveToFileEnabled(?string $storeId = null): bool + { + return (bool)$this->getConfig(self::XML_PATH_MOVE_SCRIPTS_TO_EXTERNAL_FILE, $storeId); + } + /** * Retrieve true if amp enabled * diff --git a/Model/Controller/ResultPlugin.php b/Model/Controller/ResultPlugin.php index 39fc12c..1aec143 100644 --- a/Model/Controller/ResultPlugin.php +++ b/Model/Controller/ResultPlugin.php @@ -8,7 +8,11 @@ namespace Magefan\RocketJavaScript\Model\Controller; +use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\Response\Http as ResponseHttp; +use Magento\Framework\Filesystem; +use Magento\Framework\View\LayoutInterface; +use Magento\PageCache\Model\Config; /** * Plugin for processing relocation of javascript @@ -38,24 +42,57 @@ class ResultPlugin */ protected $storeManager; + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var Config + */ + private $pageCacheConfig; + + /** + * @var LayoutInterface + */ + private $layout; + + /** + * @var \Magento\Framework\View\Asset\Repository + */ + private $assetRepository; + /** * ResultPlugin constructor. * @param \Magento\Framework\App\RequestInterface $request * @param \Magefan\RocketJavaScript\Model\Config $config + * @param Filesystem $filesystem + * @param Config $pageCacheConfig + * @param LayoutInterface $layout * @param \Magento\Store\Model\StoreManagerInterface|null $storeManager + * @param \Magento\Framework\View\Asset\Repository|null $assetRepository */ public function __construct( \Magento\Framework\App\RequestInterface $request, \Magefan\RocketJavaScript\Model\Config $config, - ?\Magento\Store\Model\StoreManagerInterface $storeManager = null + Filesystem $filesystem, + Config $pageCacheConfig, + LayoutInterface $layout, + ?\Magento\Store\Model\StoreManagerInterface $storeManager = null, + ?\Magento\Framework\View\Asset\Repository $assetRepository = null ) { $this->request = $request; $this->config = $config; - + $this->filesystem = $filesystem; + $this->pageCacheConfig = $pageCacheConfig; + $this->layout = $layout; $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); $this->storeManager = $storeManager ?: $objectManager->get( \Magento\Store\Model\StoreManagerInterface::class ); + $this->assetRepository = $assetRepository ?: $objectManager->get( + \Magento\Framework\View\Asset\Repository::class + ); } /** @@ -88,12 +125,14 @@ public function aroundRenderResult( $html = $response->getBody(); $scripts = []; $positions = []; + $priorityScripts = []; $startTag = 'config->isMoveToFileEnabled() && $this->canWriteToFile(); // First pass: find all script tags and their positions while (false !== ($start = stripos($html, $startTag, $start))) { @@ -106,12 +145,19 @@ public function aroundRenderResult( $script = substr($html, $start, $scriptEnd - $start); // Check for exclusion flags or ignored content - if (false !== stripos($script, self::EXCLUDE_FLAG_PATTERN) || - false !== stripos($script, 'application/ld+json')) { + if (false !== stripos($script, 'application/ld+json')) { $start = $scriptEnd; // Move pointer past this script continue; } + if (false !== stripos($script, self::EXCLUDE_FLAG_PATTERN)) { + if (!$moveToFile) { + $start = $scriptEnd; + continue; + } + $priorityScripts[] = $script; + } + $isIgnored = false; foreach ($ignoredStrings as $ignoredString) { if (false !== stripos($script, $ignoredString)) { @@ -148,6 +194,49 @@ public function aroundRenderResult( // Append the remaining HTML after the last script tag $newHtml .= substr($html, $lastPos); + if ($moveToFile) { + $combinedJs = ''; + $externalScriptTags = []; + $allScripts = array_merge($priorityScripts, $scripts); + foreach ($allScripts as $script) { + $openTagEnd = strpos($script, '>'); + $openTag = false !== $openTagEnd ? substr($script, 0, $openTagEnd + 1) : $script; + + if (false !== stripos($openTag, ' src=') || + false !== stripos($openTag, 'x-magento-init') || + false !== stripos($openTag, 'x-magento-template') || + false !== strpos($script, 'require.config(') || + false !== strpos($script, 'var require =') + ) { + $externalScriptTags[] = $script; + continue; + } + + if (false === $openTagEnd) { + continue; + } + $jsContent = trim(substr($script, $openTagEnd + 1, strrpos($script, '') - $openTagEnd - 1)); + + if (!empty($jsContent)) { + if (!str_ends_with($jsContent, ';')) { + $jsContent .= ';'; + } + $combinedJs .= $jsContent . "\n"; + } + } + + if (!empty($combinedJs)) { + $key = sha1($combinedJs); + $asset = $this->assetRepository->createAsset('mfrocketjs/' . $key . '.js'); + $relativePath = $asset->getPath(); + $staticDir = $this->filesystem->getDirectoryWrite(DirectoryList::STATIC_VIEW); + if (!$staticDir->isExist($relativePath)) { + $staticDir->writeFile($relativePath, $combinedJs); + } + $scripts = array_merge($externalScriptTags, ['']); + } + } + // Append the scripts before the closing tag or at the end $allScripts = implode(PHP_EOL, $scripts); $bodyEndPos = stripos($newHtml, ''); @@ -162,6 +251,14 @@ public function aroundRenderResult( return $result; } + /** + * @return bool + */ + private function canWriteToFile(): bool + { + return $this->pageCacheConfig->isEnabled() && $this->layout->isCacheable(); + } + private function isEnabled() { $enabled = $this->config->isEnabled() && $this->config->isDeferredEnabled(); diff --git a/etc/adminhtml/system.xml b/etc/adminhtml/system.xml index a462f1c..0d6ca3c 100644 --- a/etc/adminhtml/system.xml +++ b/etc/adminhtml/system.xml @@ -60,6 +60,14 @@ data-rocketjavascript="false" will automatically be ignored. Example <script data-rocketjavascript="false">/* some script *</script>]]> + + + + 1 + + Extract inline scripts from the HTML body and combine them into a separate JavaScript file. Helps reduce DOM size and improves page parsing performance. + Magento\Config\Model\Config\Source\Yesno + diff --git a/etc/config.xml b/etc/config.xml index 6ccc965..aa871dd 100644 --- a/etc/config.xml +++ b/etc/config.xml @@ -20,6 +20,7 @@ onestepcheckout/* www.googletagmanager.com + 0 1