Symfony+Twig模块式资源加载异常:CSS数组为空如何解决?
问题描述
在Symfony项目中拆分Twig模板为模块化组件,每个组件配置独立的CSS/JS文件,通过模板片段中的add_resource函数自动收集资源:
{# ui-button.twig #} <button class="ui-button"> {{ icon }} {{ text }} </button> {# 声明组件依赖资源 #} {% do add_resource('ui-button') %}
自定义Twig扩展实现资源收集逻辑,基础模板中循环渲染资源列表:
namespace App\Twig; use Twig\Extension\AbstractExtension; use Twig\TwigFunction; class AppExtension extends AbstractExtension { protected $scripts = []; protected $styles = []; public function getFunctions() { return [ new TwigFunction('get_resources', [$this, 'get_resources']), new TwigFunction('add_resouce', [$this, 'add_resouce']), ]; } public get_resources ($type) { if ($type == 'js') return $this->$scripts; if ($type == 'css') return $this->$styles; } public add_resouce ($resourse) { $css_file = '\/bundle\/' . $resourse . '.css'; $js_file = '\/bundle\/' . $resourse . '.js'; if (!is_array($js_file, $this->get_resources('js'))) $this->$scripts[] = $js_file; if (!is_array($css_file, $this->get_resources('css'))) $this->$styles[] = $css_file; } }
基础模板资源加载代码:
<!DOCTYPE html> <html lang="en"> <head> <title>Document</title> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> {% for resource in get_resources('css') %} <link rel="stylesheet" href="{{ asset(resource) }}"> {% endfor %} </head> <body> {% block body %}{% endblock body %} {% for resource in get_resources('js') %} <script src="{{ asset(resource) }}"></script> {% endfor %} </body> </html>
当前JS资源可正常收集加载,但CSS数组始终为空,已知是Twig模板渲染顺序导致的问题,尝试过embed、block等结构未解决,需实现模块化资源加载方案。
解决方案
第一步:修正Twig扩展代码错误
原扩展存在语法和逻辑错误,先修复这些问题:
namespace App\Twig; use Twig\Extension\AbstractExtension; use Twig\TwigFunction; class AppExtension extends AbstractExtension { protected $scripts = []; protected $styles = []; public function getFunctions() { return [ new TwigFunction('get_resources', [$this, 'getResources']), new TwigFunction('add_resource', [$this, 'addResource']), ]; } public function getResources($type) { if ($type === 'js') { return $this->scripts; } if ($type === 'css') { return $this->styles; } return []; } public function addResource($resource) { $cssFile = '/bundle/' . $resource . '.css'; $jsFile = '/bundle/' . $resource . '.js'; // 避免重复添加资源 if (!in_array($jsFile, $this->getResources('js'))) { $this->scripts[] = $jsFile; } if (!in_array($cssFile, $this->getResources('css'))) { $this->styles[] = $cssFile; } } }
修复点说明:
- 修正方法名拼写错误(
add_resouce→addResource、get_resources→getResources) - 修复变量引用错误(
$this->$scripts→$this->scripts,去掉多余的$符号) - 替换错误的
is_array为in_array(正确检查元素是否存在于数组) - 移除路径中不必要的转义字符
- 添加默认空数组返回逻辑,避免无匹配类型时报错
第二步:解决Twig渲染顺序问题
核心问题:Twig是从上到下线性渲染,基础模板的<head>部分先执行,此时add_resource还未在body块中被调用,导致CSS资源未被收集。
方案1:使用Twig延迟渲染标签(推荐,Twig 3.10+)
修改基础模板的CSS渲染部分,用{% deferred %}标签延迟渲染,直到所有模板片段执行完成:
<!DOCTYPE html> <html lang="en"> <head> <title>Document</title> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> {% deferred %} {% for resource in get_resources('css') %} <link rel="stylesheet" href="{{ asset(resource) }}"> {% endfor %} {% enddeferred %} </head> <body> {% block body %}{% endblock body %} {% for resource in get_resources('js') %} <script src="{{ asset(resource) }}"></script> {% endfor %} </body> </html>
{% deferred %}会推迟这段代码的渲染时机,确保所有模板片段的add_resource执行完毕后再渲染CSS资源。
方案2:兼容旧版Twig的请求作用域Runtime
如果使用Twig版本低于3.10,可将资源收集逻辑放到请求作用域的Runtime类中,避免单例扩展的状态污染,同时结合事件监听实现延迟收集:
- 创建Runtime类:
namespace App\Twig\Runtime; use Twig\Extension\RuntimeExtensionInterface; class AppRuntime implements RuntimeExtensionInterface { private $scripts = []; private $styles = []; public function getResources(string $type): array { return match($type) { 'js' => $this->scripts, 'css' => $this->styles, default => [] }; } public function addResource(string $resource): void { $cssFile = '/bundle/' . $resource . '.css'; $jsFile = '/bundle/' . $resource . '.js'; if (!in_array($jsFile, $this->scripts)) { $this->scripts[] = $jsFile; } if (!in_array($cssFile, $this->styles)) { $this->styles[] = $cssFile; } } }
- 修改Twig扩展关联Runtime:
namespace App\Twig; use App\Twig\Runtime\AppRuntime; use Twig\Extension\AbstractExtension; use Twig\TwigFunction; class AppExtension extends AbstractExtension { public function getFunctions() { return [ new TwigFunction('get_resources', [AppRuntime::class, 'getResources']), new TwigFunction('add_resource', [AppRuntime::class, 'addResource']), ]; } public function getRuntimeClass() { return AppRuntime::class; } }
- 在
services.yaml中配置Runtime为请求作用域:
services: App\Twig\Runtime\AppRuntime: scope: request tags: - { name: twig.runtime }
之后在基础模板中使用{% block styles %}配合子模板的扩展,或者通过Symfony的KernelEvents::RESPONSE事件在模板渲染完成后注入CSS资源。
第三步:验证模板调用
确保组件模板通过{% include %}或{% embed %}在基础模板的body块中调用,例如:
{% extends 'base.html.twig' %} {% block body %} {% include 'ui-button.twig' with {icon: '🔘', text: '点击按钮'} %} {% endblock %}
内容的提问来源于stack exchange,提问作者Кепри де Лармо
相关产品推荐
相关产品推荐

