提高性能感知:按需调整图像大小
本文是关于构建示例应用程序(多图像库博客)以进行性能基准测试和优化的系列文章的一部分。 (在此处查看回购协议。)
我们一直在构建一个示例应用程序——一个多图片库博客——用于性能基准测试和优化。 此时,我们的应用程序提供相同的图像,而不管它所提供的分辨率和屏幕尺寸如何。在这个图像大小调整教程中,我们将修改它以根据显示尺寸提供调整后的版本。
客观的
这种改进有两个阶段。
- 我们需要让所有图像都具有响应性,只要这可能有用。 一个地方是主页和图库页面上的缩略图,另一个是在图库中单击单个图像时的全尺寸图像。 我们需要向我们的应用程序添加调整大小逻辑。 关键是根据需要动态生成调整大小的图像。 这将防止不受欢迎的图像污染我们的硬盘驱动器,并确保流行的图像在后续请求中以最佳尺寸提供。
响应图像?
正如这篇文章所解释的,现代网络中的图像非常复杂。 而不仅仅是 <img src="https://www.sitepoint.com/improving-performance-perception-on-demand-image-resizing/mypic.jpg">
从过去开始,我们现在有这样疯狂的事情:
<picture>
<source media="(max-width: 700px)"
>
<source media="(max-width: 1400px)"
>
<img src="stick-original.png" alt="Human">
</picture>
的组合 srcset
, picture
和 sizes
如果您怀疑如果将相同的图像用于较小的屏幕尺寸,图像的主要主题可能会变得太小,则在这种情况下是必要的。 您想要在不同的屏幕尺寸中显示不同的图像(更专注于主要主题),但仍希望根据设备像素比显示同一图像的不同资源,并希望基于自定义图像的高度和宽度在视口上。
由于我们的图像是照片,我们总是希望它们位于默认的 DOM 指定位置,以填充其父容器的最大值,因此我们不需要 picture
(这让我们可以为不同的分辨率或浏览器支持定义一个替代源——比如尝试渲染 SVG,如果不支持 SVG,则渲染 PNG)或 sizes
(这让我们定义图像应该占据哪个视口部分)。 我们可以逃脱只是使用 srcset
它会根据屏幕尺寸加载同一图像的不同尺寸版本。
添加 srcset
我们遇到图像的第一个位置是 home-galleries-lazy-load.html.twig
,呈现主屏幕画廊列表的部分模板。
<a class="gallery__link" href="{{ url('gallery.single-gallery', {id: gallery.id}) }}">
<img src="{{ gallery.images.first|getImageUrl }}" alt="{{ gallery.name }}"
class="gallery__leading-image card-img-top">
</a>
我们可以在这里看到图像的链接是从 Twig 过滤器中获取的,该过滤器可以在 src/Twig/ImageRendererExtension.php
文件。 它采用图像的 ID 和路线的名称(在注释中定义 ImageController
的 serveImageAction
route) 并根据该公式生成一个 URL: /image/{id}/raw
– >更换 {id}
给出的ID:
public function getImageUrl(Image $image)
{
return $this->router->generate('image.serve', [
'id' => $image->getId(),
], RouterInterface::ABSOLUTE_URL);
}
让我们将其更改为以下内容:
public function getImageUrl(Image $image, $size = null)
{
return $this->router->generate('image.serve', [
'id' => $image->getId() . (($size) ? '--' . $size : ''),
], RouterInterface::ABSOLUTE_URL);
}
现在,我们所有的图片 URL 都会有 --x
作为后缀,其中 x
是他们的大小。 这是我们将应用于我们的更改 img
标签也是,形式为 srcset
. 让我们将其更改为:
<a class="gallery__link" href="{{ url('gallery.single-gallery', {id: gallery.id}) }}">
<img src="{{ gallery.images.first|getImageUrl }}"
alt="{{ gallery.name }}"
srcset="
{{ gallery.images.first|getImageUrl('1120') }} 1120w,
{{ gallery.images.first|getImageUrl('720') }} 720w,
{{ gallery.images.first|getImageUrl('400') }} 400w"
class="gallery__leading-image card-img-top">
</a>
如果我们现在刷新主页,我们会注意到列出了 srcset 的新大小:
不过,这对我们没有太大帮助。 如果我们的视口很宽,这将请求全尺寸图像,尽管它们是缩略图。 所以而不是 srcset
,这里最好使用固定的小缩略图尺寸:
<a class="gallery__link" href="{{ url('gallery.single-gallery', {id: gallery.id}) }}">
<img src="{{ gallery.images.first|getImageUrl('250') }}"
alt="{{ gallery.name }}"
class="gallery__leading-image card-img-top">
</a>
我们现在有按需缩略图,但是当它们已经生成时会被缓存和获取。
让我们追捕其他人 srcset
现在的位置。
在 templates/gallery/single-gallery.html.twig
,我们应用与以前相同的修复。 我们正在处理缩略图,所以让我们通过将 size 参数添加到我们的文件中来缩小文件 getImageUrl
筛选:
<img src="{{ image|getImageUrl(250) }}" alt="{{ image.originalFilename }}"
class="single-gallery__item-image card-img-top">
现在对于 srcset
实施,终于!
各个图像视图在同一单画廊视图的底部使用 JavaScript 模态窗口呈现:
{% block javascripts %}
{{ parent() }}
<script>
$(function () {
$('.single-gallery__item-image').on('click', function () {
var src = $(this).attr('src');
var $modal = $('.single-gallery__modal');
var $modalBody = $modal.find('.modal-body');
$modalBody.html('');
$modalBody.append($('<img src="' + src + '" class="single-gallery__modal-image">'));
$modal.modal({});
});
})
</script>
{% endblock %}
有一个 append
添加的调用 img
元素进入模态的主体,所以这就是我们的地方 srcset
属性必须去。 但是由于我们的图像 URL 是动态生成的,我们不能真正从内部调用 Twig 过滤器 script
. 一种替代方法是添加 srcset
进入缩略图,然后通过从缩略图元素复制它来在 JS 中使用它,但这不仅会使全尺寸图像加载到缩略图的背景中(因为我们的视口很宽),而且还会调用每个缩略图过滤 4 次,减慢速度。 相反,让我们创建一个新的 Twig 过滤器 src/Twig/ImageRendererExtension.php
这将生成完整的 srcset
每个图像的属性。
public function getImageSrcset(Image $image)
{
$id = $image->getId();
$sizes = [1120, 720, 400];
$string = '';
foreach ($sizes as $size) {
$string .= $this->router->generate('image.serve', [
'id' => $image->getId() . '--' . $size,
], RouterInterface::ABSOLUTE_URL).' '.$size.'w, ';
}
$string = trim($string, ', ');
return html_entity_decode($string);
}
我们不能忘记注册这个过滤器:
public function getFilters()
{
return [
new Twig_SimpleFilter('getImageUrl', [$this, 'getImageUrl']),
new Twig_SimpleFilter('getImageSrcset', [$this, 'getImageSrcset']),
];
}
我们必须将这些值添加到一个自定义属性中,我们称之为 data-srcset
在每个单独的缩略图上:
<img src="{{ image|getImageUrl(250) }}"
alt="{{ image.originalFilename }}"
data-
class="single-gallery__item-image card-img-top">
现在每个单独的缩略图都有一个 data-srcset
具有所需的属性 srcset
值,但这不会触发,因为它在自定义属性中,稍后将使用数据。
最后一步是更新 JS 以利用它:
{% block javascripts %}
{{ parent() }}
<script>
$(function () {
$('.single-gallery__item-image').on('click', function () {
var src = $(this).attr('src');
var srcset = $(this).attr('data-srcset');
var $modal = $('.single-gallery__modal');
var $modalBody = $modal.find('.modal-body');
$modalBody.html('');
$modalBody.append($('<img src="' + src + '" class="single-gallery__modal-image">'));
$modal.modal({});
});
})
</script>
{% endblock %}
添加滑行
Glide 是一个可以做我们想做的事情的库——按需调整图像大小。 让我们安装它。
composer require league/glide
接下来,让我们在应用程序中注册它。 我们通过添加一个新服务来做到这一点 src/Services
内容如下:
<?php
namespace AppService;
use LeagueGlide;
class GlideServer
{
private $server;
public function __construct(FileManager $fm)
{
$this->server = $server = GlideServerFactory::create([
'source' => $fm->getUploadsDirectory(),
'cache' => $fm->getUploadsDirectory().'/cache',
]);
}
public function getGlide()
{
return $this->server;
}
}
该服务使用已声明的 FileManager 服务,该服务是由于 Symfony 的新自动装配方法而自动注入的。 我们将输入和输出路径都声明为 uploads
dir,给输出目录a cache
后缀,并添加返回服务器的方法。 服务器基本上是 Glide 的实例,它执行调整大小并返回调整大小的图像。
我们需要让 getUploadsDirectory
中的方法 FileManager
公开的,因为它是目前 private
:
public function getUploadsDirectory()
{
return $this->path;
}
最后,让我们修改 ImageController 的 serveImageAction
方法,使其看起来像这样:
/**
* @Route("/image/{id}/raw", name="image.serve")
*/
public function serveImageAction(Request $request, $id, GlideServer $glide)
{
$idFragments = explode('--', $id);
$id = $idFragments[0];
$size = $idFragments[1] ?? null;
$image = $this->em->getRepository(Image::class)->find($id);
if (empty($image)) {
throw new NotFoundHttpException('Image not found');
}
$fullPath = $this->fileManager->getFilePath($image->getFilename());
if ($size) {
$info = pathinfo($fullPath);
$file = $info['filename'] . '.' . $info['extension'];
$newfile = $info['filename'] . '-' . $size . '.' . $info['extension'];
$fullPathNew = str_replace($file, $newfile, $fullPath);
if (file_exists($fullPath) && ! file_exists($fullPathNew)) {
$fullPath = $fullPathNew;
$img = $glide->getGlide()->getImageAsBase64($file,
['w' => $size]);
$ifp = fopen($fullPath, 'wb');
$data = explode(',', $img);
fwrite($ifp, base64_decode($data[1]));
fclose($ifp);
}
}
$response = new BinaryFileResponse($fullPath);
$response->headers->set('Content-type',
mime_content_type($fullPath));
$response->headers->set('Content-Disposition',
'attachment; filename="' . $image->getOriginalFilename() . '";');
return $response;
}
此方法现在通过双破折号分解图像 ID,将大小与图像 ID 分开。 一旦 Doctrine 从数据库中获取图像的文件路径,如果传入了文件名,则大小将重新附加到文件名,否则将使用原始图像。 如果此图像不存在,则从原始路径生成一个并保存以备后用。
出于演示目的,我们在这里采取更长的方式并通过将大小附加到文件并将它们保存到文件中来手动生成文件 uploads
文件夹。 应该注意的是,您还可以使用 outputImage
Glide 的方法直接输出图像,它会直接从 cache
子文件夹,而不是在主文件夹中使用后缀保存它 upload
文件夹。 您还可以使用 makeImage
方法来创建图像并让获取图像的旧逻辑接管。 这是我们在下面选择的方法:
/**
* @Route("/image/{id}/raw", name="image.serve")
*/
public function serveImageAction(Request $request, $id, GlideServer $glide)
{
$idFragments = explode('--', $id);
$id = $idFragments[0];
$size = $idFragments[1] ?? null;
$image = $this->em->getRepository(Image::class)->find($id);
if (empty($image)) {
throw new NotFoundHttpException('Image not found');
}
$fullPath = $this->fileManager->getFilePath($image->getFilename());
if ($size) {
$info = pathinfo($fullPath);
$file...