跳到内容

Asset 组件

编辑此页

Asset 组件管理 Web Asset(如 CSS 样式表、JavaScript 文件和图像文件)的 URL 生成和版本控制。

过去,Web 应用程序通常会硬编码 Web Asset 的 URL。例如:

1
2
3
4
5
<link rel="stylesheet" type="text/css" href="/css/main.css">

<!-- ... -->

<a href="/"><img src="/images/logo.png" alt="logo"></a>

除非 Web 应用程序非常简单,否则不再建议这种做法。硬编码 URL 可能是一个缺点,因为:

  • 模板变得冗长:您必须为每个 Asset 编写完整路径。使用 Asset 组件时,您可以将 Asset 分组到包中,以避免重复其路径的公共部分;
  • 版本控制很困难:它必须为每个应用程序自定义管理。向 Asset URL 添加版本(例如 main.css?v=5)对于某些应用程序至关重要,因为它允许您控制 Asset 的缓存方式。Asset 组件允许您为每个包定义不同的版本控制策略;
  • 移动 Asset 的位置 繁琐且容易出错:它要求您仔细更新所有模板中包含的所有 Asset 的 URL。Asset 组件允许您轻松移动 Asset,只需更改与 Asset 包关联的基础路径值即可;
  • 几乎不可能使用多个 CDN:此技术要求您为每个请求随机更改 Asset 的 URL。Asset 组件为任意数量的多个 CDN 提供开箱即用的支持,包括常规 CDN (http://) 和安全 CDN (https://)。

安装

1
$ composer require symfony/asset

注意

如果您在 Symfony 应用程序之外安装此组件,则必须在代码中 require vendor/autoload.php 文件,以启用 Composer 提供的类自动加载机制。阅读 这篇文章 以获取更多详细信息。

用法

Asset 包

Asset 组件通过包管理 Asset。包对共享相同属性的所有 Asset 进行分组:版本控制策略、基础路径、CDN 主机等。在以下基本示例中,创建一个包来管理没有任何版本控制的 Asset:

1
2
3
4
5
6
7
8
9
10
11
12
use Symfony\Component\Asset\Package;
use Symfony\Component\Asset\VersionStrategy\EmptyVersionStrategy;

$package = new Package(new EmptyVersionStrategy());

// Absolute path
echo $package->getUrl('/image.png');
// result: /image.png

// Relative path
echo $package->getUrl('image.png');
// result: image.png

包实现了 PackageInterface,它定义了以下两个方法:

getVersion()
返回 Asset 的 Asset 版本。
getUrl()
返回绝对路径或根相对公共路径。

使用包,您可以:

A) 版本化 Asset;B) 为 Asset 设置 通用基础路径(例如 /css);C) 为 Asset 配置 CDN

版本化 Asset

Asset 组件的主要功能之一是管理应用程序 Asset 的版本控制能力。Asset 版本通常用于控制这些 Asset 的缓存方式。

Asset 组件不依赖于简单的版本机制,而是允许您通过 PHP 类定义高级版本控制策略。两个内置策略是 EmptyVersionStrategy(它不会向 Asset 添加任何版本)和 StaticVersionStrategy(它允许您使用格式字符串设置版本)。

在此示例中,StaticVersionStrategy 用于将 v1 后缀附加到任何 Asset 路径:

1
2
3
4
5
6
7
8
9
10
11
12
use Symfony\Component\Asset\Package;
use Symfony\Component\Asset\VersionStrategy\StaticVersionStrategy;

$package = new Package(new StaticVersionStrategy('v1'));

// Absolute path
echo $package->getUrl('/image.png');
// result: /image.png?v1

// Relative path
echo $package->getUrl('image.png');
// result: image.png?v1

如果您想修改版本格式,请传递一个 sprintf 兼容的格式字符串作为 StaticVersionStrategy 构造函数的第二个参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// puts the 'version' word before the version value
$package = new Package(new StaticVersionStrategy('v1', '%s?version=%s'));

echo $package->getUrl('/image.png');
// result: /image.png?version=v1

// puts the asset version before its path
$package = new Package(new StaticVersionStrategy('v1', '%2$s/%1$s'));

echo $package->getUrl('/image.png');
// result: /v1/image.png

echo $package->getUrl('image.png');
// result: v1/image.png

JSON 文件清单

管理 Asset 版本控制的一种流行策略(被 Webpack 等工具使用)是生成一个 JSON 文件,将所有源文件名映射到其对应的输出文件:

1
2
3
4
5
{
    "css/app.css": "build/css/app.b916426ea1d10021f3f17ce8031f93c2.css",
    "js/app.js": "build/js/app.13630905267b809161e71d0f8a0c017b.js",
    "...": "..."
}

在这些情况下,请使用 JsonManifestVersionStrategy

1
2
3
4
5
6
7
8
use Symfony\Component\Asset\Package;
use Symfony\Component\Asset\VersionStrategy\JsonManifestVersionStrategy;

// assumes the JSON file above is called "rev-manifest.json"
$package = new Package(new JsonManifestVersionStrategy(__DIR__.'/rev-manifest.json'));

echo $package->getUrl('css/app.css');
// result: build/css/app.b916426ea1d10021f3f17ce8031f93c2.css

如果您请求的 Asset 在 rev-manifest.json 文件中未找到,则将返回原始的 - 未修改的 - Asset 路径。$strictMode 参数有助于调试问题,因为它会在 Asset 未在清单中列出时抛出异常:

1
2
3
4
5
6
7
8
9
10
use Symfony\Component\Asset\Package;
use Symfony\Component\Asset\VersionStrategy\JsonManifestVersionStrategy;

// The value of $strictMode can be specific per environment "true" for debugging and "false" for stability.
$strictMode = true;
// assumes the JSON file above is called "rev-manifest.json"
$package = new Package(new JsonManifestVersionStrategy(__DIR__.'/rev-manifest.json', null, $strictMode));

echo $package->getUrl('not-found.css');
// error:

如果您的 JSON 文件不在本地文件系统上,但可以通过 HTTP 访问,请将 JsonManifestVersionStrategyHttpClient 组件 一起使用:

1
2
3
4
5
6
7
use Symfony\Component\Asset\Package;
use Symfony\Component\Asset\VersionStrategy\JsonManifestVersionStrategy;
use Symfony\Component\HttpClient\HttpClient;

$httpClient = HttpClient::create();
$manifestUrl = 'https://cdn.example.com/rev-manifest.json';
$package = new Package(new JsonManifestVersionStrategy($manifestUrl, $httpClient));

自定义版本策略

使用 VersionStrategyInterface 定义您自己的版本控制策略。例如,您的应用程序可能需要将当前日期附加到其所有 Web Asset,以便每天清除缓存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use Symfony\Component\Asset\VersionStrategy\VersionStrategyInterface;

class DateVersionStrategy implements VersionStrategyInterface
{
    private string $version;

    public function __construct()
    {
        $this->version = date('Ymd');
    }

    public function getVersion(string $path): string
    {
        return $this->version;
    }

    public function applyVersion(string $path): string
    {
        return sprintf('%s?v=%s', $path, $this->getVersion($path));
    }
}

分组 Asset

通常,许多 Asset 位于公共路径下(例如 /static/images)。如果是这种情况,请将默认的 Package 类替换为 PathPackage,以避免一遍又一遍地重复该路径:

1
2
3
4
5
6
7
8
9
10
11
use Symfony\Component\Asset\PathPackage;
// ...

$pathPackage = new PathPackage('/static/images', new StaticVersionStrategy('v1'));

echo $pathPackage->getUrl('logo.png');
// result: /static/images/logo.png?v1

// Base path is ignored when using absolute paths
echo $pathPackage->getUrl('/logo.png');
// result: /logo.png?v1

请求上下文感知 Asset

如果您还在项目中使用 HttpFoundation 组件(例如,在 Symfony 应用程序中),则 PathPackage 类可以考虑当前请求的上下文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use Symfony\Component\Asset\Context\RequestStackContext;
use Symfony\Component\Asset\PathPackage;
// ...

$pathPackage = new PathPackage(
    '/static/images',
    new StaticVersionStrategy('v1'),
    new RequestStackContext($requestStack)
);

echo $pathPackage->getUrl('logo.png');
// result: /somewhere/static/images/logo.png?v1

// Both "base path" and "base url" are ignored when using absolute path for asset
echo $pathPackage->getUrl('/logo.png');
// result: /logo.png?v1

现在设置了请求上下文,PathPackage 将预先添加当前请求的基础 URL。因此,例如,如果您的整个站点托管在 Web 服务器根目录的 /somewhere 目录下,并且配置的基础路径是 /static/images,则所有路径都将以 /somewhere/static/images 为前缀。

绝对 Asset 和 CDN

将其 Asset 托管在不同域和 CDN(内容分发网络)上的应用程序应使用 UrlPackage 类为其 Asset 生成绝对 URL:

1
2
3
4
5
6
7
8
9
10
use Symfony\Component\Asset\UrlPackage;
// ...

$urlPackage = new UrlPackage(
    'https://static.example.com/images/',
    new StaticVersionStrategy('v1')
);

echo $urlPackage->getUrl('/logo.png');
// result: https://static.example.com/images/logo.png?v1

您还可以传递一个 schema-agnostic URL:

1
2
3
4
5
6
7
8
9
10
use Symfony\Component\Asset\UrlPackage;
// ...

$urlPackage = new UrlPackage(
    '//static.example.com/images/',
    new StaticVersionStrategy('v1')
);

echo $urlPackage->getUrl('/logo.png');
// result: //static.example.com/images/logo.png?v1

这很有用,因为如果访问者通过 HTTPS 访问您的站点,则 Asset 将自动通过 HTTPS 请求。如果要使用此功能,请确保您的 CDN 主机支持 HTTPS。

如果您从多个域提供 Asset 以提高应用程序性能,请将 URL 数组作为 UrlPackage 构造函数的第一个参数传递:

1
2
3
4
5
6
7
8
9
10
11
12
13
use Symfony\Component\Asset\UrlPackage;
// ...

$urls = [
    'https://static1.example.com/images/',
    'https://static2.example.com/images/',
];
$urlPackage = new UrlPackage($urls, new StaticVersionStrategy('v1'));

echo $urlPackage->getUrl('/logo.png');
// result: https://static1.example.com/images/logo.png?v1
echo $urlPackage->getUrl('/icon.png');
// result: https://static2.example.com/images/icon.png?v1

对于每个 Asset,将随机使用一个 URL。但是,选择是确定性的,这意味着每个 Asset 将始终由同一域提供服务。此行为简化了 HTTP 缓存的管理。

请求上下文感知 Asset

与应用程序相对 Asset 类似,绝对 Asset 也可以考虑当前请求的上下文。在这种情况下,仅考虑请求方案,以便选择适当的基础 URL(HTTPS 请求的 HTTPS 或协议相对 URL,HTTP 请求的任何基础 URL):

1
2
3
4
5
6
7
8
9
10
11
12
13
use Symfony\Component\Asset\Context\RequestStackContext;
use Symfony\Component\Asset\UrlPackage;
// ...

$urlPackage = new UrlPackage(
    ['http://example.com/', 'https://example.com/'],
    new StaticVersionStrategy('v1'),
    new RequestStackContext($requestStack)
);

echo $urlPackage->getUrl('/logo.png');
// assuming the RequestStackContext says that we are on a secure host
// result: https://example.com/logo.png?v1

命名包

管理大量不同 Asset 的应用程序可能需要将它们分组到具有相同版本控制策略和基础路径的包中。Asset 组件包含一个 Packages 类,以简化多个包的管理。

在以下示例中,所有包都使用相同的版本控制策略,但它们都具有不同的基础路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use Symfony\Component\Asset\Package;
use Symfony\Component\Asset\Packages;
use Symfony\Component\Asset\PathPackage;
use Symfony\Component\Asset\UrlPackage;
// ...

$versionStrategy = new StaticVersionStrategy('v1');

$defaultPackage = new Package($versionStrategy);

$namedPackages = [
    'img' => new UrlPackage('https://img.example.com/', $versionStrategy),
    'doc' => new PathPackage('/somewhere/deep/for/documents', $versionStrategy),
];

$packages = new Packages($defaultPackage, $namedPackages);

Packages 类允许定义一个默认包,该默认包将应用于未定义要使用的包名称的 Asset。此外,此应用程序定义了一个名为 img 的包,用于从外部域提供图像,以及一个 doc 包,以避免在模板中链接到文档时重复长路径:

1
2
3
4
5
6
7
8
echo $packages->getUrl('/main.css');
// result: /main.css?v1

echo $packages->getUrl('/logo.png', 'img');
// result: https://img.example.com/logo.png?v1

echo $packages->getUrl('resume.pdf', 'doc');
// result: /somewhere/deep/for/documents/resume.pdf?v1

本地文件和其他协议

除了 HTTP,此组件还支持其他协议(例如 file://ftp://)。例如,这允许提供本地文件以提高性能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use Symfony\Component\Asset\UrlPackage;
// ...

$localPackage = new UrlPackage(
    'file:///path/to/images/',
    new EmptyVersionStrategy()
);

$ftpPackage = new UrlPackage(
    'ftp://example.com/images/',
    new EmptyVersionStrategy()
);

echo $localPackage->getUrl('/logo.png');
// result: file:///path/to/images/logo.png

echo $ftpPackage->getUrl('/logo.png');
// result: ftp://example.com/images/logo.png
本作品,包括代码示例,根据 Creative Commons BY-SA 3.0 许可获得许可。
目录
    版本