跳到内容

AssetMapper:简单、现代的 CSS & JS 管理

编辑此页

AssetMapper 组件让你可以编写现代 JavaScript 和 CSS,而无需使用打包工具的复杂性。浏览器已经支持许多现代 JavaScript 功能,例如 import 语句和 ES6 类。并且 HTTP/2 协议意味着合并你的资源以减少 HTTP 连接不再是当务之急。此组件是一个轻量级层,可帮助直接将你的文件提供给浏览器。

该组件有两个主要功能

  • 映射 & 版本控制资源assets/ 内部的所有文件都公开可用并进行版本控制。你可以在 Twig 模板中使用 {{ asset('images/product.jpg') }} 引用文件 assets/images/product.jpg。最终的 URL 将包含版本哈希,例如 /assets/images/product-3c16d92m.jpg
  • Importmaps:一种原生浏览器功能,可以更轻松地使用 JavaScript import 语句(例如 import { Modal } from 'bootstrap'),而无需构建系统。它在所有浏览器中都受支持(感谢 shim),并且是 HTML 标准的一部分。

安装

要安装 AssetMapper 组件,请运行

1
$ composer require symfony/asset-mapper symfony/asset symfony/twig-pack

除了 symfony/asset-mapper 之外,这也确保你拥有 Asset 组件 和 Twig。

如果你正在使用 Symfony Flex,那么你就完成了!配方刚刚添加了一些文件

  • assets/app.js 你的主要 JavaScript 文件;
  • assets/styles/app.css 你的主要 CSS 文件;
  • config/packages/asset_mapper.yaml 你在其中定义资源 "路径" 的位置;
  • importmap.php 你的 importmap 配置文件。

它还更新templates/base.html.twig 文件

1
2
3
{% block javascripts %}
+    {% block importmap %}{{ importmap('app') }}{% endblock %}
{% endblock %}

如果你没有使用 Flex,你需要手动创建和更新这些文件。请参阅 最新的 asset-mapper 配方,以获取这些文件的确切内容。

映射和引用资源

AssetMapper 组件的工作原理是定义你想要公开的资源的目录/路径。然后对这些资源进行版本控制,并且易于引用。感谢 asset_mapper.yaml 文件,你的应用从一个映射路径开始:assets/ 目录。

如果你创建一个 assets/images/duck.png 文件,你可以在模板中使用以下代码引用它

1
<img src="{{ asset('images/duck.png') }}">

路径 - images/duck.png - 相对于你的映射目录 (assets/)。这被称为资源的逻辑路径

如果你查看页面中的 HTML,URL 将类似于:/assets/images/duck-3c16d92m.png。如果你更改文件,URL 的版本部分也会自动更改。

在开发环境与生产环境提供资源

dev 环境中,URL /assets/images/duck-3c16d92m.png 由你的 Symfony 应用程序处理和返回。

对于 prod 环境,在部署之前,你应该运行

1
$ php bin/console asset-map:compile

这将物理复制所有文件从你的映射目录到 public/assets/,以便它们由你的 Web 服务器直接提供服务。有关更多详细信息,请参阅 部署

警告

如果你在开发机器上运行 asset-map:compile 命令,则在重新加载页面时,你不会看到对资源所做的任何更改。要解决此问题,请删除 public/assets/ 目录的内容。这将允许你的 Symfony 应用程序再次动态提供这些资源。

提示

如果你需要将编译后的资源复制到不同的位置(例如,将它们上传到 S3),请创建一个实现 Symfony\Component\AssetMapper\Path\PublicAssetsFilesystemInterface 的服务,并将其服务 ID(或别名)设置为 asset_mapper.local_public_assets_filesystem(以替换内置服务)。

调试:查看所有已映射的资源

要查看你的应用中所有已映射的资源,请运行

1
$ php bin/console debug:asset-map

这将显示所有映射的路径以及每个路径内的资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
AssetMapper Paths
------------------

--------- ------------------
 Path      Namespace prefix
--------- ------------------
assets

Mapped Assets
-------------

------------------ ----------------------------------------------------
 Logical Path       Filesystem Path
------------------ ----------------------------------------------------
 app.js             assets/app.js
 styles/app.css     assets/styles/app.css
 images/duck.png    assets/images/duck.png

"逻辑路径" 是引用资源时要使用的路径,例如从模板。

debug:asset-map 命令提供多个选项来过滤结果

1
2
3
4
5
6
7
8
9
10
11
12
13
# provide an asset name or dir to only show results that match it
$ php bin/console debug:asset-map bootstrap.js
$ php bin/console debug:asset-map style/

# provide an extension to only show that file type
$ php bin/console debug:asset-map --ext=css

# you can also only show assets in vendor/ dir or exclude any results from it
$ php bin/console debug:asset-map --vendor
$ php bin/console debug:asset-map --no-vendor

# you can also combine all filters (e.g. find bold web fonts in your own asset dirs)
$ php bin/console debug:asset-map bold --no-vendor --ext=woff2

7.2

过滤 debug:asset-map 结果的选项在 Symfony 7.2 中引入。

Importmaps & 编写 JavaScript

所有现代浏览器都支持 JavaScript import 语句 和现代 ES6 功能,例如类。因此,此代码 "可以直接工作"

1
2
3
4
5
// assets/app.js
import Duck from './duck.js';

const duck = new Duck('Waddles');
duck.quack();
1
2
3
4
5
6
7
8
9
// assets/duck.js
export default class {
    constructor(name) {
        this.name = name;
    }
    quack() {
        console.log(`${this.name} says: Quack!`);
    }
}

感谢 {{ importmap('app') }} Twig 函数调用,你将在本节中学习它,assets/app.js 文件被浏览器加载并执行。

提示

当导入相对文件时,请务必包含 .js 文件扩展名。与 Node.js 不同,此扩展名在浏览器环境中是必需的。

导入第三方 JavaScript 包

假设你想使用 npm 包,例如 bootstrap。从技术上讲,这可以通过导入其完整 URL 来完成,例如从 CDN

1
import { Alert } from 'https://cdn.jsdelivr.net.cn/npm/[email protected]/+esm';

但是,哎呀!需要包含该 URL 真是太麻烦了!相反,我们可以通过 importmap:require 命令将此包添加到我们的 "importmap" 中。此命令可用于添加任何 npm 包

1
$ php bin/console importmap:require bootstrap

这会将 bootstrap 包添加到你的 importmap.php 文件中

1
2
3
4
5
6
7
8
9
10
// importmap.php
return [
    'app' => [
        'path' => './assets/app.js',
        'entrypoint' => true,
    ],
    'bootstrap' => [
        'version' => '5.3.0',
    ],
];

注意

有时,一个包(例如 bootstrap)将具有一个或多个依赖项,例如 @popperjs/coreimportmap:require 命令将同时添加主包其依赖项。如果一个包包含一个主要的 CSS 文件,那么该文件也将被添加(请参阅 处理第三方 CSS)。

注意

如果收到 404 错误,则 JavaScript 包可能存在一些问题,阻止 jsDelivr CDN 提供它。例如,该包可能在其 package.json 配置文件中缺少诸如 mainmodule 之类的属性。尝试联系包维护者,要求他们修复这些问题。

现在你可以像往常一样导入 bootstrap

1
2
import { Alert } from 'bootstrap';
// ...

importmap.php 中的所有包都下载到 assets/vendor/ 目录中,该目录应被 git 忽略(Flex 配方会为你将其添加到 .gitignore 中)。如果缺少某些文件,则需要在其他计算机上运行以下命令来下载文件

1
$ php bin/console importmap:install

你可以通过运行以下命令将第三方包更新到当前版本

1
2
3
4
5
6
7
8
# lists outdated packages and shows their latest versions
$ php bin/console importmap:outdated
# updates all the outdated packages
$ php bin/console importmap:update

# you can also run the commands only for the given list of packages
$ php bin/console importmap:update bootstrap lodash
$ php bin/console importmap:outdated bootstrap lodash

importmap 如何工作?

这个 importmap.php 文件如何允许你导入 bootstrap?这要归功于 base.html.twig 中的 {{ importmap() }} Twig 函数,它输出一个 importmap

1
2
3
4
5
6
7
<script type="importmap">{
    "imports": {
        "app": "/assets/app-4e986c1a.js",
        "/assets/duck.js": "/assets/duck-1b7a64b3.js",
        "bootstrap": "/assets/vendor/bootstrap/bootstrap.index-f093544d.js"
    }
}</script>

Import maps 是一种原生浏览器功能。当你从 JavaScript 导入 bootstrap 时,浏览器将查看 importmap 并查看它是否应该从关联的路径获取包。

但是,/assets/duck.js 导入条目来自哪里?它不在 importmap.php 中。好问题!

上面的 assets/app.js 文件导入了 ./duck.js。当你使用相对路径导入文件时,你的浏览器会相对于导入它的文件查找该文件。因此,它会查找 /assets/duck.js。该 URL 是正确的,除了 duck.js 文件是版本控制的。幸运的是,AssetMapper 组件看到了导入,并添加了从 /assets/duck.js 到正确的、版本控制的文件名的映射。结果:导入 ./duck.js 就可以直接工作!

importmap() 函数还输出一个 ES module shim,以便 旧浏览器 理解 importmaps(请参阅 polyfill 配置)。

"app" 入口点 & 预加载

"入口点" 是浏览器加载的主要 JavaScript 文件,你的应用默认从一个入口点开始

1
2
3
4
5
6
7
8
// importmap.php
return [
    'app' => [
        'path' => './assets/app.js',
        'entrypoint' => true,
    ],
    // ...
];

除了 importmap 之外,base.html.twig 中的 {{ importmap('app') }} 还输出了一些其他内容,包括

1
<script type="module">import 'app';</script>

此行告诉浏览器加载 app importmap 条目,这会导致执行 assets/app.js 中的代码。

importmap() 函数还输出一组 "预加载"

1
2
<link rel="modulepreload" href="/assets/app-4e986c1a.js">
<link rel="modulepreload" href="/assets/duck-1b7a64b3.js">

这是一种性能优化,你可以在下面的 性能:添加预加载 中了解更多信息。

从第三方包导入特定文件

有时你需要从包中导入特定文件。例如,假设你正在集成 highlight.js,并且只想导入核心和特定语言

1
2
3
4
5
import hljs from 'highlight.js/lib/core';
import javascript from 'highlight.js/lib/languages/javascript';

hljs.registerLanguage('javascript', javascript);
hljs.highlightAll();

在这种情况下,将 highlight.js 包添加到你的 importmap.php 文件将不起作用:你导入的任何内容(例如 highlight.js/lib/core)都需要完全匹配 importmap.php 文件中的条目。

相反,请使用 importmap:require 并将你需要的确切路径传递给它。这也展示了如何一次性 require 多个包

1
$ php bin/console importmap:require highlight.js/lib/core highlight.js/lib/languages/javascript

全局变量,例如 jQuery

你可能习惯于依赖全局变量 - 例如 jQuery 的 $ 变量

1
2
3
4
5
// assets/app.js
import 'jquery';

// app.js or any other file
$('.something').hide(); // WILL NOT WORK!

但是在模块环境(例如使用 AssetMapper),当你导入像 jquery 这样的库时,它不会创建全局变量。相反,你应该导入它并在你需要它的每个文件中将其设置为变量

1
2
import $ from 'jquery';
$('.something').hide();

你甚至可以从内联脚本标签执行此操作

1
2
3
4
<script type="module">
    import $ from 'jquery';
    $('.something').hide();
</script>

如果你确实需要将某些内容变成全局变量,你可以从 app.js 内部手动执行此操作

1
2
3
import $ from 'jquery';
// things on "window" become global variables
window.$ = $;

处理 CSS

可以通过从 JavaScript 文件导入 CSS 将 CSS 添加到你的页面。默认的 assets/app.js 已经导入了 assets/styles/app.css

1
2
3
4
// assets/app.js
import '../styles/app.css';

// ...

当你在 base.html.twig 中调用 importmap('app') 时,AssetMapper 会解析 assets/app.js(以及它导入的任何 JavaScript 文件),以查找 CSS 文件的 import 语句。最终的 CSS 文件集合作为 link 标签按照它们被导入的顺序呈现到页面上。

注意

导入 CSS 文件不是 JavaScript 模块原生支持的功能。AssetMapper 通过为每个 CSS 文件添加一个特殊的 importmap 条目来使其工作。这些特殊条目是有效的,但没有任何作用。AssetMapper 为每个 CSS 文件添加一个 <link> 标签,但是当 JavaScript 执行 import 语句时,不会发生任何额外的事情。

处理第三方 CSS

有时,JavaScript 包将包含一个或多个 CSS 文件。例如,bootstrap 包具有 dist/css/bootstrap.min.css 文件

你可以像 require JavaScript 文件一样 require CSS 文件

1
$ php bin/console importmap:require bootstrap/dist/css/bootstrap.min.css

要在页面上包含它,请从 JavaScript 文件中导入它

1
2
3
4
// assets/app.js
import 'bootstrap/dist/css/bootstrap.min.css';

// ...

提示

某些包(例如 bootstrap)声明它们包含 CSS 文件。在这些情况下,当你 importmap:require bootstrap 时,为了方便起见,CSS 文件也会添加到 importmap.php 中。如果某些包未在 package.json 配置文件style 属性中声明其 CSS 文件,请尝试联系包维护者,要求他们添加该属性。

CSS 文件中的路径

在 CSS 内部,你可以使用普通的 CSS url() 函数和目标文件的相对路径来引用其他文件

1
2
3
4
5
/* assets/styles/app.css */
.quack {
    /* file lives at assets/images/duck.png */
    background-image: url('../images/duck.png');
}

最终 app.css 文件中的路径将自动包含 duck.png 的版本控制 URL

1
2
3
4
/* public/assets/styles/app-3c16d92m.css */
.quack {
    background-image: url('../images/duck-3c16d92m.png');
}

使用 Tailwind CSS

要将 Tailwind CSS 框架与 AssetMapper 组件一起使用,请查看 symfonycasts/tailwind-bundle

使用 Sass

要将 Sass 与 AssetMapper 组件一起使用,请查看 symfonycasts/sass-bundle

从 JavaScript 文件延迟导入 CSS

如果你有一些想要延迟加载的 CSS,你可以通过正常的 "动态" 导入语法来完成

1
2
3
4
// assets/any-file.js
import('./lazy.css');

// ...

在这种情况下,lazy.css 将被异步下载,然后添加到页面。如果你使用动态导入来延迟加载 JavaScript 文件,并且该文件导入了一个 CSS 文件(使用非动态 import 语法),那么该 CSS 文件也将被异步下载。

问题和调试

你可能会遇到一些常见的错误和问题。

缺少 importmap 条目

最常见的错误之一将来自浏览器的控制台,并且看起来像这样

Failed to resolve module specifier " bootstrap". Relative references must start with either "/", "./", or "../".

指定符 “bootstrap” 是一个裸指定符,但未被重映射到任何内容。相对模块指定符必须以 “./”、“../” 或 “/” 开头。

这意味着,在你的 JavaScript 代码中的某个地方,你正在导入一个第三方包 - 例如 import 'bootstrap'。浏览器尝试在你的 importmap 文件中找到这个包,但它不在那里。

几乎所有的修复方法都是将其添加到你的 importmap

1
$ php bin/console importmap:require bootstrap

注意

一些浏览器,如 Firefox,会显示这个 “import” 代码位于何处,而其他浏览器,如 Chrome,目前不会显示。

JavaScript、CSS 或图像文件 404 未找到

有时,你正在导入的 JavaScript 文件(例如 import './duck.js'),或者你正在引用的 CSS/图像文件将无法找到,你将在浏览器的控制台中看到 404 错误。你还会注意到,404 URL 中缺少文件名中的版本哈希(例如,指向 /assets/duck.js 的 404 错误,而不是像 /assets/duck-1b7a64b3.js 这样的路径)。

这通常是因为路径错误。如果你直接在 Twig 模板中引用该文件

1
<img src="{{ asset('images/duck.png') }}">

那么你传递给 asset() 的路径应该是文件的“逻辑路径”。使用 debug:asset-map 命令查看你的应用程序中所有有效的逻辑路径。

更可能的情况是,你正在从 CSS 文件(例如 @import url('other.css'))或 JavaScript 文件中导入失败的 asset

1
2
// assets/controllers/farm-controller.js
import '../farm/chicken.js';

执行此操作时,路径应相对于导入它的文件(并且,在 JavaScript 文件中,应以 ./../ 开头)。在这种情况下,../farm/chicken.js 将指向 assets/farm/chicken.js。要查看你的应用程序中所有无效导入的列表,请运行

1
2
$ php bin/console cache:clear
$ php bin/console debug:asset-map

任何无效的导入都将显示在屏幕顶部的警告中(请确保你已安装 symfony/monolog-bundle

1
2
WARNING   [asset_mapper] Unable to find asset "../images/ducks.png" referenced in "assets/styles/app.css".
WARNING   [asset_mapper] Unable to find asset "./ducks.js" imported from "assets/app.js".

注释代码中缺少资源警告

AssetMapper 组件会在你的 JavaScript 文件中查找 import 行,以便它可以 自动将它们添加到你的 importmap。这是通过正则表达式完成的,效果非常好,但并非完美。如果你注释掉一个 import,它仍然会被找到并添加到你的 importmap 中。这不会造成任何危害,但可能会令人惊讶。

如果找不到导入的路径,你将在构建该 asset 时看到警告日志,你可以忽略它。

使用 AssetMapper 组件部署

当你准备部署时,通过运行以下命令“编译”你的 assets

1
$ php bin/console asset-map:compile

这将把所有版本化的 asset 文件写入 public/assets/ 目录,以及一些 JSON 文件(manifest.jsonimportmap.json 等),以便可以闪电般快速地渲染 importmap

优化性能

为了使你的由 AssetMapper 驱动的站点飞速运行,你需要做一些事情。如果你想走捷径,可以使用像 Cloudflare 这样的服务,它会自动为你完成大部分这些事情

  • 使用 HTTP/2:你的 Web 服务器应运行 HTTP/2 或 HTTP/3,以便浏览器可以并行下载 assets。HTTP/2 在 Caddy 中自动启用,并且可以在 Nginx 和 Apache 中激活。或者,通过像 Cloudflare 这样的服务代理你的站点,它将自动为你启用 HTTP/2。
  • 压缩你的 assets:你的 Web 服务器应在将 assets(JavaScript、CSS、图像)发送到浏览器之前对其进行压缩(例如,使用 gzip)。这在 Caddy 中自动启用,并且可以在 Nginx 和 Apache 中激活。在 Cloudflare 中,默认情况下会压缩 assets。
  • 设置长期缓存过期时间:你的 Web 服务器应在你的 assets 上设置长期 Cache-Control HTTP 标头。由于 AssetMapper 组件在每个 asset 的文件名中都包含版本哈希,因此你可以安全地将 max-age 设置为非常长的时间(例如,1 年)。这在任何 Web 服务器中都不是自动的,但可以轻松启用。

完成这些操作后,你可以使用像 Lighthouse 这样的工具来检查你的站点的性能。

性能:理解预加载

Lighthouse 可能会报告的一个问题是

避免链接关键请求

为了理解这个问题,想象一下以下理论设置

  • assets/app.js 导入 ./duck.js
  • assets/duck.js 导入 bootstrap

在没有预加载的情况下,当浏览器下载页面时,将发生以下情况

  1. 浏览器下载 assets/app.js
  2. 然后,它看到 ./duck.js 导入并下载 assets/duck.js
  3. 然后,它看到 bootstrap 导入并下载 assets/bootstrap.js

浏览器将被迫一个接一个地下载它们,而不是并行下载所有 3 个文件,因为它会在发现它们时才下载。这将损害性能。

AssetMapper 通过输出 “preload” link 标签来避免这个问题。逻辑如下所示

A) 当你在模板中调用 importmap('app'),AssetMapper 组件会查看 assets/app.js 文件,并查找它导入的所有 JavaScript 文件或这些文件导入的文件等。

B) 然后,它为每个文件输出一个 link 标签,其中包含 rel="preload" 属性。这告诉浏览器立即开始下载这些文件,即使它尚未看到它们的 import 语句。

此外,如果 WebLink 组件 在你的应用程序中可用,Symfony 将在响应中添加 Link 标头以预加载 CSS 文件。

常见问题解答

AssetMapper 组件会合并资源吗?

不!但这因为这不再是必要的了!

过去,通常会合并 assets 以减少发出的 HTTP 请求数量。由于 HTTP/2 等 Web 服务器的进步,通常保持 assets 分开并让浏览器并行下载它们不是问题。实际上,通过保持它们分开,当你更新一个 asset 时,浏览器可以继续使用所有其他 assets 的缓存版本。

有关更多详细信息,请参见 优化

AssetMapper 组件会压缩资源吗?

不!在大多数情况下,这完全可以。Web 服务器在发送 assets 之前执行的 Web asset 压缩通常就足够了。但是,如果你认为你可以从缩小 assets(除了稍后压缩它们之外)中受益,则可以使用 SensioLabs Minify Bundle

此 bundle 与 AssetMapper 无缝集成,并在运行 asset-map:compile 命令时自动缩小所有 Web assets(如 在生产环境中提供 assets 部分中所述)。

有关更多详细信息,请参见 优化

AssetMapper 组件是否已准备好用于生产环境?性能如何?

是的!非常棒!AssetMapper 组件利用了浏览器技术(如 importmaps 和原生 import 支持)和 Web 服务器(如 HTTP/2,它允许并行下载 assets)的进步。有关最小化和组合以及 优化 的其他问题,请参见其他问题。

https://ux.symfony.ac.cn 站点在 AssetMapper 组件上运行,并且 Google Lighthouse 得分为 99%。

AssetMapper 组件是否适用于所有浏览器?

是的!所有现代浏览器都支持诸如 importmaps 和 import 语句之类的功能,但是 AssetMapper 组件附带了一个 ES 模块 shim,以支持旧浏览器中的 importmap。因此,它在任何地方都有效(请参见下面的注释)。

在你自己的代码中,如果你依赖现代 ES6 JavaScript 功能,例如 class 语法,则除最旧的浏览器外,所有浏览器都支持此功能。如果你确实需要支持非常旧的浏览器,则应使用像 Encore 而不是 AssetMapper 组件这样的工具。

注意

import 语句 无法进行 polyfill 或 shim 处理,以使其在每个浏览器上都有效。但是,只有最旧的浏览器不支持它 - 基本上是 IE 11(Microsoft 不再支持它,并且全球使用率不到 0.4%)。

importmap 功能通过 AssetMapper 组件进行 shim 处理,以在所有浏览器中工作。但是,shim 不适用于“动态”导入

1
2
3
4
5
6
7
// this works
import { add } from './math.js';

// this will not work in the oldest browsers
import('./math.js').then(({ add }) => {
    // ...
});

如果你想使用动态导入并且需要支持某些较旧的浏览器 (https://caniuse.cn/import-maps),则可以使用 shim 中的 importShim() 函数:https://npmjs.net.cn/package/es-module-shims#user-content-polyfill-edge-case-dynamic-import

我可以将其与 JSX 或 Vue 一起使用吗?

可能不会。并且,如果你正在用 React,Svelte 或另一个前端框架编写应用程序,则最好直接使用它们的工具。

JSX 可以直接编译为原生 JavaScript 文件,但是如果你使用大量的 JSX,则可能需要使用像 Encore 这样的工具。有关将其与 AssetMapper 组件一起使用的更多详细信息,请参见 UX React 文档

Vue 文件可以用原生 JavaScript 编写,并且这些文件与 AssetMapper 组件一起使用。但是你不能使用组件编写单文件组件(即 .vue 文件),因为这些组件必须在构建系统中使用。有关将其与 AssetMapper 组件一起使用的更多详细信息,请参见 UX Vue.js 文档

我可以 Lint 和格式化我的代码吗?

不能使用 AssetMapper,但是你可以在你的项目中安装 kocal/biome-js-bundle 来 lint 和格式化你的前端 assets。它比 Prettier 等替代方案快得多,并且无需配置即可处理你的 JavaScript,TypeScript 和 CSS 文件。

使用 TypeScript

要将 TypeScript 与 AssetMapper 组件一起使用,请查看 sensiolabs/typescript-bundle

第三方扩展包 & 自定义资源路径

所有具有 Resources/public/public/ 目录的 bundles 都将自动将该目录添加为“asset 路径”,使用命名空间:bundles/<BundleName>。例如,如果你正在使用 BabdevPagerfantaBundle 并且你运行 debug:asset-map 命令,你将看到一个 asset,其逻辑路径为 bundles/babdevpagerfanta/css/pagerfanta.css

这意味着你可以在模板中使用 asset() 函数渲染这些 assets

1
<link rel="stylesheet" href="{{ asset('bundles/babdevpagerfanta/css/pagerfanta.css') }}">

实际上,此路径 - bundles/babdevpagerfanta/css/pagerfanta.css - 已经在没有 AssetMapper 组件的应用程序中工作,因为 assets:install 命令将 assets 从 bundles 复制到 public/bundles/。但是,当启用 AssetMapper 组件时,pagerfanta.css 文件将自动进行版本控制!它将输出类似以下内容

1
<link rel="stylesheet" href="/assets/bundles/babdevpagerfanta/css/pagerfanta-ea64fc9c.css">

覆盖第三方资源

如果要覆盖第三方 asset,可以通过在你的 assets/ 目录中创建一个同名文件来做到这一点。例如,如果你要覆盖 pagerfanta.css 文件,请在 assets/bundles/babdevpagerfanta/css/pagerfanta.css 中创建一个文件。将使用此文件代替原始文件。

注意

如果 bundle 渲染它们自己的 assets,但是它们使用非默认的 asset 包,则不会使用 AssetMapper 组件。例如,EasyAdminBundle 就是这种情况。

导入 assets/ 目录之外的 Assets

可以导入位于你的 asset 路径之外(即 assets/ 目录)的 assets。例如

1
2
3
4
/* assets/styles/app.css */

/* you can reach above assets/ */
@import url('../../vendor/babdev/pagerfanta-bundle/Resources/public/css/pagerfanta.css');

但是,如果你收到类似这样的错误

“app” importmap 条目包含路径 “vendor/some/package/assets/foo.js”,但它似乎不在你的任何 asset 路径中。

这意味着你指向的是一个有效文件,但是该文件不在你的任何 asset 路径中。你可以通过将路径添加到你的 asset_mapper.yaml 文件来解决此问题

1
2
3
4
5
6
# config/packages/asset_mapper.yaml
framework:
    asset_mapper:
        paths:
            - assets/
            - vendor/some/package/assets

然后再次尝试该命令。

配置选项

你可以通过运行以下命令查看每个可用的配置选项和一些信息

1
$ php bin/console config:dump framework asset_mapper

下面介绍了一些更重要的选项。

framework.asset_mapper.paths

此配置包含将扫描 assets 的所有目录。这可以是一个简单的列表

1
2
3
4
5
framework:
    asset_mapper:
        paths:
            - assets/
            - vendor/some/package/assets

或者你可以为每个路径指定一个“命名空间”,该命名空间将在 asset map 中使用

1
2
3
4
5
framework:
    asset_mapper:
        paths:
            assets/: ''
            vendor/some/package/assets/: 'some-package'

在这种情况下,vendor/some/package/assets/ 目录中所有文件的“逻辑路径”将以 some-package 为前缀 - 例如 some-package/foo.js

framework.asset_mapper.excluded_patterns

这是将从 asset map 中排除的 glob 模式列表

1
2
3
4
framework:
    asset_mapper:
        excluded_patterns:
            - '*/*.scss'

你可以使用 debug:asset-map 命令来仔细检查你期望的文件是否包含在 asset map 中。

framework.asset_mapper.exclude_dotfiles

是否从 asset mapper 中排除任何以 . 开头的文件。如果你想避免在 asset mapper 发布的文件中泄漏敏感文件(如 .env.gitignore),这将很有用。

1
2
3
framework:
    asset_mapper:
        exclude_dotfiles: true

默认情况下启用此选项。

framework.asset_mapper.importmap_polyfill

为旧浏览器配置 polyfill。默认情况下,ES 模块 shim 通过 CDN 加载(即,此设置的默认值为 es-module-shims

1
2
3
4
5
6
7
8
9
framework:
    asset_mapper:
        # set this option to false to disable the shim entirely
        # (your website/web app won't work in old browsers)
        importmap_polyfill: false

        # you can also use a custom polyfill by adding it to your importmap.php file
        # and setting this option to the key of that file in the importmap.php file
        # importmap_polyfill: 'custom_polyfill'

提示

你可以通过使用以下命令告诉 AssetMapper 在本地加载 ES 模块 shim,而无需更改你的配置

1
$ php bin/console importmap:require es-module-shims

framework.asset_mapper.importmap_script_attributes

这是将添加到 {{ importmap() }} Twig 函数渲染的 <script> 标签的属性列表

1
2
3
4
framework:
    asset_mapper:
        importmap_script_attributes:
            crossorigin: 'anonymous'

页面特定的 CSS & JavaScript

有时,你可能选择仅在某些页面上包含 CSS 或 JavaScript 文件。对于 JavaScript,一种简单的方法是使用 动态导入 加载文件

1
2
3
4
5
6
7
const someCondition = '...';
if (someCondition) {
    import('./some-file.js');

    // or use async/await
    // const something = await import('./some-file.js');
}

另一种选择是创建一个单独的 入口点。例如,创建一个 checkout.js 文件,其中包含你需要的任何 JavaScript 和 CSS

1
2
3
4
// assets/checkout.js
import './checkout.css';

// ...

接下来,将其添加到 importmap.php 并将其标记为入口点

1
2
3
4
5
6
7
8
9
// importmap.php
return [
    // the 'app' entrypoint ...

    'checkout' => [
        'path' => './assets/checkout.js',
        'entrypoint' => true,
    ],
];

最后,在需要此 JavaScript 的页面上,调用 importmap() 并传递 appcheckout

1
2
3
4
5
6
7
8
9
10
{# templates/products/checkout.html.twig #}
{#
    Override an "importmap" block from base.html.twig.
    If you don't have that block, add it around the {{ importmap('app') }} call.
#}
{% block importmap %}
    {# do NOT call parent() #}

    {{ importmap(['app', 'checkout']) }}
{% endblock %}

通过同时传递 appcheckoutimportmap() 函数将输出 importmap,并添加一个加载 app.js 文件 checkout.js 文件的 <script type="module"> 标签。重要的是不要importmap 块中调用 parent()。每个页面只能有一个 importmap,因此 importmap() 必须只调用一次。

如果出于某种原因,你只想执行 checkout.js执行 app.js,请仅将 checkout 传递给 importmap()

使用内容安全策略 (CSP)

如果你正在使用 内容安全策略(CSP)来防止跨站点脚本攻击,则 importmap() 函数渲染的内联 <script> 标签可能会违反该策略,并且不会被浏览器执行。

为了允许这些脚本在不禁用 CSP 提供的安全性的情况下运行,你可以为每个请求生成一个安全的随机字符串(称为 nonce),并将其包含在 CSP 标头和 <script> 标签的 nonce 属性中。importmap() 函数接受一个可选的第二个参数,该参数可用于将属性传递给渲染的 <script> 标签。你可以使用 NelmioSecurityBundle 生成 nonce 并将其包含在 CSP 标头中,然后将相同的 nonce 传递给 Twig 函数

1
2
{# the csp_nonce() function is defined by the NelmioSecurityBundle #}
{{ importmap('app', {'nonce': csp_nonce('script')}) }}

内容安全策略和 CSS 文件

如果你的 importmap 包含 CSS 文件,AssetMapper 会使用一个技巧来加载它们,方法是将 data:application/javascript 添加到渲染的 importmap(请参见 处理 CSS)。

这可能会导致浏览器报告 CSP 违规并阻止 CSS 文件加载。为了防止这种情况,你可以将 strict-dynamic 添加到你的内容安全策略的 script-src 指令中,以告知浏览器允许 importmap 加载其他资源。

注意

当使用 strict-dynamic 时,浏览器将忽略 script-src 中的任何其他来源,例如 'self''unsafe-inline',因此任何其他 <script> 标签也需要通过 nonce 受信任。

开发环境中 AssetMapper 组件的缓存系统

在调试模式下开发你的应用程序时,AssetMapper 组件将计算每个 asset 文件的内容并将其缓存。每当该文件更改时,该组件将自动重新计算内容。

该系统还考虑了“依赖关系”:如果 app.css 包含 @import url('other.css'),则每当 other.css 更改时,也会重新计算 app.css 文件内容。这是因为 other.css 的版本哈希将更改...这将导致 app.css 的最终内容发生更改,因为它在内部包含最终的 other.css 文件名。

在大多数情况下,此系统可以正常工作。但是,如果你有一个文件在你期望时没有被重新计算,则可以运行

1
$ php bin/console cache:clear

这将强制 AssetMapper 组件重新计算所有文件的内容。

在你的依赖项上运行安全审计

npm 类似,AssetMapper 组件捆绑了一个命令,该命令检查你的应用程序依赖项中的安全漏洞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ php bin/console importmap:audit

--------  ---------------------------------------------  ---------  -------  ----------  -----------------------------------------------------
Severity  Title                                          Package    Version  Patched in  More info
--------  ---------------------------------------------  ---------  -------  ----------  -----------------------------------------------------
Medium    jQuery Cross Site Scripting vulnerability      jquery     3.3.1    3.5.0       https://api.github.com/advisories/GHSA-257q-pV89-V3xv
High      Prototype Pollution in JSON5 via Parse Method  json5      1.0.0    1.0.2       https://api.github.com/advisories/GHSA-9c47-m6qq-7p4h
Medium    semver vulnerable to RegExp Denial of Service  semver     4.3.0    5.7.2       https://api.github.com/advisories/GHSA-c2qf-rxjj-qqgw
Critical  Prototype Pollution in minimist                minimist   1.1.3    1.2.6       https://api.github.com/advisories/GHSA-xvch-5gv4-984h
Medium    ESLint dependencies are vulnerable             minimist   1.1.3    1.2.2       https://api.github.com/advisories/GHSA-7fhm-mqm4-2wp7
Medium    Bootstrap Vulnerable to Cross-Site Scripting   bootstrap  4.1.3    4.3.1       https://api.github.com/advisories/GHSA-9v3M-8fp8-mi99
--------  ---------------------------------------------  ---------  -------  ----------  -----------------------------------------------------

7 packages found: 7 audited / 0 skipped
6 vulnerabilities found: 1 Critical / 1 High / 4 Medium

如果未发现漏洞,该命令将返回 0 退出代码,否则返回 1 退出代码。这意味着你可以将此命令无缝集成到你的 CI 中,以便在新漏洞被发现时发出警告。

提示

该命令采用 --format 选项来选择 txtjson 之间的输出格式。

这项工作,包括代码示例,均根据 Creative Commons BY-SA 3.0 许可获得许可。
目录
    版本