From 2581e335af626706b5ab5d99b6e9031c81c9de28 Mon Sep 17 00:00:00 2001 From: webjoin111 <455457521@qq.com> Date: Tue, 11 Nov 2025 21:42:01 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(renderer):=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=20Jinja2=20`inline=5Fasset`=20=E5=85=A8=E5=B1=80?= =?UTF-8?q?=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 `RendererService._inline_asset_global` 方法,并注册为 Jinja2 全局函数 `inline_asset`。 - 允许模板通过 `{{ inline_asset('@namespace/path/to/asset.svg') }}` 直接内联已注册命名空间下的资源文件内容。 - 主要用于解决内联 SVG 时可能遇到的跨域安全问题。 - 【重构】优化 `ResourceResolver.resolve_asset_uri` 中对命名空间资源 (以 `@` 开头) 的解析逻辑,确保能够正确获取文件绝对路径并返回 URI。 - 改进 `RenderableComponent.get_extra_css`,使其在组件定义 `component_css` 时自动返回该 CSS 内容。 - 清理 `Renderable` 协议和 `RenderableComponent` 基类中已存在方法的 `[新增]` 标记。 --- zhenxun/services/renderer/protocols.py | 2 +- zhenxun/services/renderer/service.py | 18 ++++++++- zhenxun/services/renderer/theme.py | 53 ++++++++++++++++++-------- zhenxun/ui/models/core/base.py | 8 ++-- 4 files changed, 59 insertions(+), 22 deletions(-) diff --git a/zhenxun/services/renderer/protocols.py b/zhenxun/services/renderer/protocols.py index 3255b0d3..619cdc48 100644 --- a/zhenxun/services/renderer/protocols.py +++ b/zhenxun/services/renderer/protocols.py @@ -40,7 +40,7 @@ class Renderable(ABC): @abstractmethod def get_children(self) -> Iterable["Renderable"]: """ - [新增] 返回一个包含所有直接子组件的可迭代对象。 + 返回一个包含所有直接子组件的可迭代对象。 这使得渲染服务能够递归地遍历整个组件树,以执行依赖收集(CSS、JS)等任务。 非容器组件应返回一个空列表。 diff --git a/zhenxun/services/renderer/service.py b/zhenxun/services/renderer/service.py index 0e2a1413..d050aa37 100644 --- a/zhenxun/services/renderer/service.py +++ b/zhenxun/services/renderer/service.py @@ -75,6 +75,7 @@ class RendererService: self._custom_globals: dict[str, Callable] = {} self.filter("dump_json")(self._pydantic_tojson_filter) + self.global_function("inline_asset")(self._inline_asset_global) def _create_jinja_env(self) -> Environment: """ @@ -176,9 +177,24 @@ class RendererService: return decorator + async def _inline_asset_global(self, namespaced_path: str) -> str: + """ + 一个Jinja2全局函数,用于读取并内联一个已注册命名空间下的资源文件内容。 + 主要用于内联SVG,以解决浏览器的跨域安全问题。 + """ + if not self._jinja_env or not self._jinja_env.loader: + return f"" + try: + source, _, _ = self._jinja_env.loader.get_source( + self._jinja_env, namespaced_path + ) + return source + except TemplateNotFound: + return f"" + async def initialize(self): """ - [新增] 延迟初始化方法,在 on_startup 钩子中调用。 + 延迟初始化方法,在 on_startup 钩子中调用。 负责初始化截图引擎和主题管理器,确保在首次渲染前所有依赖都已准备就绪。 使用锁来防止并发初始化。 diff --git a/zhenxun/services/renderer/theme.py b/zhenxun/services/renderer/theme.py index 410176b2..2e372668 100644 --- a/zhenxun/services/renderer/theme.py +++ b/zhenxun/services/renderer/theme.py @@ -172,24 +172,45 @@ class ResourceResolver: if asset_path.startswith("@"): try: - full_asset_path = self.theme_manager.jinja_env.join_path( - asset_path, current_template_name - ) - _source, file_abs_path, _uptodate = ( - self.theme_manager.jinja_env.loader.get_source( - self.theme_manager.jinja_env, full_asset_path + if "/" not in asset_path: + raise TemplateNotFound(f"无效的命名空间路径: {asset_path}") + + namespace, rel_path = asset_path.split("/", 1) + + loader = self.theme_manager.jinja_env.loader + if ( + isinstance(loader, ChoiceLoader) + and loader.loaders + and isinstance(loader.loaders[0], PrefixLoader) + ): + prefix_loader = loader.loaders[0] + if namespace in prefix_loader.mapping: + loader_for_namespace = prefix_loader.mapping[namespace] + if isinstance(loader_for_namespace, FileSystemLoader): + base_path = Path(loader_for_namespace.searchpath[0]) + file_abs_path = (base_path / rel_path).resolve() + + if file_abs_path.is_file(): + logger.debug( + f"Resolved namespaced asset" + f" '{asset_path}' -> '{file_abs_path}'" + ) + return file_abs_path.as_uri() + else: + raise TemplateNotFound(asset_path) + else: + raise TemplateNotFound( + f"Unsupported loader type for namespace '{namespace}'." + ) + else: + raise TemplateNotFound(f"Namespace '{namespace}' not found.") + else: + raise TemplateNotFound( + f"无法解析命名空间资源 '{asset_path}',加载器结构不符合预期。" ) - ) - if file_abs_path: - logger.debug( - f"Jinja Loader resolved asset '{asset_path}'->'{file_abs_path}'" - ) - return Path(file_abs_path).absolute().as_uri() + except TemplateNotFound: - logger.warning( - f"资源文件在命名空间中未找到: '{asset_path}'" - f"(在模板 '{current_template_name}' 中引用)" - ) + logger.warning(f"资源文件在命名空间中未找到: '{asset_path}'") return "" search_paths: list[tuple[str, Path]] = [] diff --git a/zhenxun/ui/models/core/base.py b/zhenxun/ui/models/core/base.py index 09d14862..aad9f942 100644 --- a/zhenxun/ui/models/core/base.py +++ b/zhenxun/ui/models/core/base.py @@ -64,13 +64,13 @@ class RenderableComponent(BaseModel, Renderable): @compat_computed_field def inline_style_str(self) -> str: - """[新增] 一个辅助属性,将内联样式字典转换为CSS字符串""" + """一个辅助属性,将内联样式字典转换为CSS字符串""" if not self.inline_style: return "" return "; ".join(f"{k}: {v}" for k, v in self.inline_style.items()) def get_extra_css(self, context: Any) -> str | Awaitable[str]: - return "" + return self.component_css or "" class ContainerComponent(RenderableComponent, ABC): @@ -86,7 +86,7 @@ class ContainerComponent(RenderableComponent, ABC): raise NotImplementedError def get_required_scripts(self) -> list[str]: - """[新增] 聚合所有子组件的脚本依赖。""" + """聚合所有子组件的脚本依赖。""" scripts = set(super().get_required_scripts()) for child in self.get_children(): if child: @@ -94,7 +94,7 @@ class ContainerComponent(RenderableComponent, ABC): return list(scripts) def get_required_styles(self) -> list[str]: - """[新增] 聚合所有子组件的样式依赖。""" + """聚合所有子组件的样式依赖。""" styles = set(super().get_required_styles()) for child in self.get_children(): if child: