performance(frontend): Preload FontAwesome, JS and CSS (#3057)

* Add preloads support to Document class

* Add frontend extender for asset preloading

* Provide default preloads for FontAwesome

* Add tests for preload extender and default preloads

* Apply fixes from StyleCI

[ci skip] [skip ci]

* Fix typo

* Fix two more typos 🙃

* Preload core JS and CSS

* Apply fixes from StyleCI

[ci skip] [skip ci]

* Reorder preloads

* Remove singular preloads method

* Use filesystem disk driver for getting FA font paths

* Update test to use full URL

* Apply fixes from StyleCI

[ci skip] [skip ci]

* Address review comment

* Apply fixes from StyleCI

[ci skip] [skip ci]

* Fix typo

* Apply fixes from StyleCI

[ci skip] [skip ci]

* Correct callback wrapping

* Update src/Extend/Frontend.php

Co-authored-by: Sami Mazouz <sychocouldy@gmail.com>

* Update src/Extend/Frontend.php

Co-authored-by: Sami Mazouz <sychocouldy@gmail.com>

* Update src/Extend/Frontend.php

* Fix preload extender logic

* Convert base FontAwesome preloads into a Singleton

* Apply fixes from StyleCI

[ci skip] [skip ci]

Co-authored-by: luceos <luceos@users.noreply.github.com>
Co-authored-by: Sami Mazouz <sychocouldy@gmail.com>
Co-authored-by: Alexander Skvortsov <38059171+askvortsov1@users.noreply.github.com>
Co-authored-by: Alexander Skvortsov <sasha.skvortsov109@gmail.com>
This commit is contained in:
David Wheatley 2021-09-20 23:12:09 +01:00 committed by GitHub
parent 1637b90531
commit 88724bb4cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 234 additions and 0 deletions

View File

@ -16,6 +16,7 @@ use Flarum\Foundation\ContainerUtil;
use Flarum\Foundation\Event\ClearingCache;
use Flarum\Frontend\Assets;
use Flarum\Frontend\Compiler\Source\SourceCollector;
use Flarum\Frontend\Document;
use Flarum\Frontend\Frontend as ActualFrontend;
use Flarum\Frontend\RecompileFrontendAssets;
use Flarum\Http\RouteCollection;
@ -33,6 +34,7 @@ class Frontend implements ExtenderInterface
private $routes = [];
private $removedRoutes = [];
private $content = [];
private $preloadArrs = [];
/**
* @param string $frontend: The name of the frontend.
@ -124,11 +126,45 @@ class Frontend implements ExtenderInterface
return $this;
}
/**
* Adds multiple asset preloads.
*
* The parameter should be an array of preload arrays, or a callable that returns this.
*
* A preload array must contain keys that pertain to the `<link rel="preload">` tag.
*
* For example, the following will add preload tags for a script and font file:
* ```
* $frontend->preloads([
* [
* 'href' => '/assets/my-script.js',
* 'as' => 'script',
* ],
* [
* 'href' => '/assets/fonts/my-font.woff2',
* 'as' => 'font',
* 'type' => 'font/woff2',
* 'crossorigin' => ''
* ]
* ]);
* ```
*
* @param callable|array $preloads
* @return self
*/
public function preloads($preloads): self
{
$this->preloadArrs[] = $preloads;
return $this;
}
public function extend(Container $container, Extension $extension = null)
{
$this->registerAssets($container, $this->getModuleName($extension));
$this->registerRoutes($container);
$this->registerContent($container);
$this->registerPreloads($container);
}
private function registerAssets(Container $container, string $moduleName): void
@ -236,6 +272,25 @@ class Frontend implements ExtenderInterface
);
}
private function registerPreloads(Container $container): void
{
if (empty($this->preloadArrs)) {
return;
}
$container->resolving(
"flarum.frontend.$this->frontend",
function (ActualFrontend $frontend, Container $container) {
$frontend->content(function (Document $document) use ($container) {
foreach ($this->preloadArrs as $preloadArr) {
$preloads = is_callable($preloadArr) ? ContainerUtil::wrapCallback($preloadArr, $container)($document) : $preloadArr;
$document->preloads = array_merge($document->preloads, $preloads);
}
});
}
);
}
private function getModuleName(?Extension $extension): string
{
return $extension ? $extension->getId() : 'site-custom';

View File

@ -122,6 +122,28 @@ class Document implements Renderable
*/
public $css = [];
/**
* An array of preloaded assets.
*
* Each array item should be an array containing keys that pertain to the
* `<link rel="preload">` tag.
*
* For example, the following will add a preload tag for a FontAwesome font file:
* ```
* $this->preloads[] = [
* 'href' => '/assets/fonts/fa-solid-900.woff2',
* 'as' => 'font',
* 'type' => 'font/woff2',
* 'crossorigin' => ''
* ];
* ```
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types/preload
*
* @var array
*/
public $preloads = [];
/**
* @var Factory
*/
@ -203,6 +225,19 @@ class Document implements Renderable
return $this->view->make($this->contentView)->with('content', $this->content);
}
protected function makePreloads(): array
{
return array_map(function ($preload) {
$attributes = '';
foreach ($preload as $key => $value) {
$attributes .= " $key=\"".e($value).'"';
}
return "<link rel=\"preload\"$attributes>";
}, $this->preloads);
}
/**
* @return string
*/
@ -216,6 +251,8 @@ class Document implements Renderable
$head[] = '<link rel="canonical" href="'.e($this->canonicalUrl).'">';
}
$head = array_merge($head, $this->makePreloads());
$head = array_merge($head, array_map(function ($content, $name) {
return '<meta name="'.e($name).'" content="'.e($content).'">';
}, $this->meta, array_keys($this->meta)));

View File

@ -54,9 +54,58 @@ class FrontendServiceProvider extends AbstractServiceProvider
$frontend->content($container->make(Content\CorePayload::class));
$frontend->content($container->make(Content\Meta::class));
$frontend->content(function (Document $document) use ($container) {
$default_preloads = $container->make('flarum.frontend.default_preloads');
// Add preloads for base CSS and JS assets. Extensions should add their own via the extender.
$js_preloads = [];
$css_preloads = [];
foreach ($document->css as $url) {
$css_preloads[] = [
'href' => $url,
'as' => 'style'
];
}
foreach ($document->js as $url) {
$css_preloads[] = [
'href' => $url,
'as' => 'script'
];
}
$document->preloads = array_merge(
$css_preloads,
$js_preloads,
$default_preloads,
$document->preloads,
);
});
return $frontend;
};
});
$this->container->singleton(
'flarum.frontend.default_preloads',
function (Container $container) {
$filesystem = $container->make('filesystem')->disk('flarum-assets');
return [
[
'href' => $filesystem->url('fonts/fa-solid-900.woff2'),
'as' => 'font',
'type' => 'font/woff2',
'crossorigin' => ''
], [
'href' => $filesystem->url('fonts/fa-regular-400.woff2'),
'as' => 'font',
'type' => 'font/woff2',
'crossorigin' => ''
]
];
}
);
}
/**

View File

@ -0,0 +1,93 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Tests\integration\extenders;
use Flarum\Extend;
use Flarum\Testing\integration\TestCase;
class FrontendPreloadTest extends TestCase
{
private $customPreloadUrls = ['/my-preload', '/my-preload2'];
/**
* @test
*/
public function default_preloads_are_present()
{
$response = $this->send(
$this->request('GET', '/')
);
$filesystem = $this->app()->getContainer()->make('filesystem')->disk('flarum-assets');
$urls = [
$filesystem->url('fonts/fa-solid-900.woff2'),
$filesystem->url('fonts/fa-regular-400.woff2'),
];
$body = $response->getBody()->getContents();
foreach ($urls as $url) {
$this->assertStringContainsString("<link rel=\"preload\" href=\"$url\" as=\"font\" type=\"font/woff2\" crossorigin=\"\">", $body);
}
}
/**
* @test
*/
public function preloads_can_be_added()
{
$urls = $this->customPreloadUrls;
$this->extend(
(new Extend\Frontend('forum'))
->preloads(
array_map(function ($url) {
return ['href' => $url];
}, $urls)
)
);
$response = $this->send(
$this->request('GET', '/')
);
$body = $response->getBody()->getContents();
foreach ($urls as $url) {
$this->assertStringContainsString("<link rel=\"preload\" href=\"$url\">", $body);
}
}
/**
* @test
*/
public function preloads_can_be_added_via_callable()
{
$urls = $this->customPreloadUrls;
$this->extend(
(new Extend\Frontend('forum'))
->preloads(function () use ($urls) {
return array_map(function ($url) {
return ['href' => $url];
}, $urls);
})
);
$response = $this->send(
$this->request('GET', '/')
);
$body = $response->getBody()->getContents();
foreach ($urls as $url) {
$this->assertStringContainsString("<link rel=\"preload\" href=\"$url\">", $body);
}
}
}