资讯动态

iTop 邮件模板 CSS 内联化实战:Emogrifier 库(CssInliner 全家桶)原理与完整用法

发布时间:2026/10/16 2:44:43 来源:尧图企业网站定制
后端企业应用【免费下载链接】iTopA simple, web based CMDB IT Service Management tool项目地址https://gitcode.com/gh_mirrors/it/iTop点击查看免费下载导读Emogrifier 是随 iTop 开源仓库一并分发、托管于lib/pelago/emogrifier目录下的 PHP 工具库核心使命是把 HTML 中的外部 CSS含style块与外部传入的样式表转换为元素上的内联style属性从而让 Outlook、Gmail 等不支持或部分不支持style/link样式机制的邮件客户端也能正确渲染邮件。iTop 在自身邮件模块中直接使用CssInlinerHtmlPrunerCssToAttributeConverter组合管线处理通知邮件正文见 sources/Core/Email/EmailSymfony.php并有对应的单元测试守护其行为见 tests/php-unit-tests/unitary-tests/sources/core/Email/EmailSymfonyTest.php。读完本文你将掌握 Emogrifier 的完整 API 家族、CSS 内联的底层处理流水线、支持与不支持的 CSS 选择器边界、各配置选项的语义以及如何在自己基于 iTop 的邮件与富文本渲染场景中正确组合这些类。Emogrifier 是什么解决的问题与工作方式邮件 HTML 与网页 HTML 的最大差异在于大量邮件客户端尤其是 Outlook 2007 与 Gmail 的某些历史版本只认内联样式会丢弃style元素以及link引用的外部样式表。Emogrifier 就是为解决这一历史顽疾而生的工具——它解析 CSS根据选择器把声明块写到匹配元素的style属性上。从源码结构看整个库由以下部分构成位于 lib/pelago/emogrifier/src文件/类职责CssInliner.php核心把 CSS 内联进 HTMLfinal class CssInliner extends AbstractHtmlProcessorHtmlProcessor/AbstractHtmlProcessor.php所有 HTML 处理类的公共基类负责 DOM 构建、渲染HtmlProcessor/HtmlNormalizer.php规范化 HTMLHtmlProcessor/CssToAttributeConverter.php把部分 CSS 样式转成传统 HTML 视觉属性HtmlProcessor/CssVariableEvaluator.php求值 CSS 自定义属性变量HtmlProcessor/HtmlPruner.php精简 HTML删display:none元素、冗余 classCss/CssDocument.php基于sabberworm/php-css-parser解析 CSS 文档Utilities/*声明块解析、CSS 拼接、正则封装等内部工具从composer.jsonlib/pelago/emogrifier/composer.json可以看到它的运行前提与依赖PHP~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0需要ext-dom、ext-libxml扩展依赖sabberworm/php-css-parserCSS 语法解析与symfony/css-selector把 CSS 选择器翻译为 XPath用于在 DOM 中定位元素。安装与引入官方推荐的安装方式是 Composercomposer require pelago/emogrifier在 iTop 仓库中它已作为第三方依赖随lib/目录分发源码位于 lib/pelago/emogrifierPSR-4 自动加载映射为Pelago\\Emogrifier\\: src/。因此 iTop 内部直接以命名空间方式使用use Pelago\Emogrifier\CssInliner; use Pelago\Emogrifier\HtmlProcessor\CssToAttributeConverter; use Pelago\Emogrifier\HtmlProcessor\HtmlPruner;核心用法把 CSS 内联进 HTML基本调用CssInliner最基础的用法是以原始 HTML 创建实例 → 内联外部 CSS → 渲染结果use Pelago\Emogrifier\CssInliner; $visualHtml CssInliner::fromHtml($html)-inlineCss($css)-render();如果所有样式都位于 HTML 内部的style块中、没有外部 CSS 文件可以省略$css参数$visualHtml CssInliner::fromHtml($html)-inlineCss()-render();inlineCss()的完整执行流水线见 CssInliner.php值得展开清空内部缓存与已访问节点记录归一化所有节点已有的style属性把DISPLAY: none这样的写法规范为小写DeclarationBlockParser::normalizePropertyName对标准属性统一strtolower对--开头的自定义属性保留大小写见 Utilities/DeclarationBlockParser.php若开启style块解析则把所有style节点的内容追加到传入的 CSS 之后追加而非前置是为了让 HTML 内嵌样式对冲突样式拥有更高优先级然后移除这些style节点见getCssFromAllStyleNodes用CssDocument内部走sabberworm/php-css-parser解析合并后的 CSS通过symfony/css-selector把每个可内联规则的选择器转成 XPath用DOMXPath::query命中元素将声明块合入元素的style属性若开启了内联样式属性解析把保存的原始内联样式合并回各节点移除所有内联样式上的!important注解必要时重排声明顺序见下方“优先级处理”把不可内联但会实际影响文档的规则如media、:hover、::after相关规则复制回一个style元素。只取body内容如果只想拿body内的内容而非完整 HTML 文档用renderBodyContent()$bodyContent CssInliner::fromHtml($html)-inlineCss()-renderBodyContent();源码实现上renderBodyContent()先对 body 元素执行saveHTML()再剥除body标签本身见 AbstractHtmlProcessor.php。内联前的选项设置所有选项方法都必须在调用inlineCss()之前链式调用例如$visualHtml CssInliner::fromHtml($html)-disableStyleBlocksParsing() -inlineCss($css)-render();多处理器共享同一个 DOMDocumentCssInliner与所有 HTML 处理类都继承自AbstractHtmlProcessor因此它们可以在同一个DOMDocument实例上接力工作。这是 iTop 邮件管线实际采用的组合方式use Pelago\Emogrifier\CssInliner; use Pelago\Emogrifier\HtmlProcessor\CssToAttributeConverter; use Pelago\Emogrifier\HtmlProcessor\HtmlPruner; $cssInliner CssInliner::fromHtml($html)-inlineCss($css); $domDocument $cssInliner-getDomDocument(); HtmlPruner::fromDomDocument($domDocument)-removeElementsWithDisplayNone() -removeRedundantClassesAfterCssInlined($cssInliner); $finalHtml CssToAttributeConverter::fromDomDocument($domDocument) -convertCssToVisualAttributes()-render();iTop 的实际实现EmailSymfony.php与之高度一致protected static function InlineCssIntoBodyContent($sBody, $sCustomStyles): string { if (is_null($sCustomStyles)) { return $sBody; } $oDomDocument CssInliner::fromHtml($sBody)-inlineCss($sCustomStyles)-getDomDocument(); HtmlPruner::fromDomDocument($oDomDocument)-removeElementsWithDisplayNone(); $sBody CssToAttributeConverter::fromDomDocument($oDomDocument)-convertCssToVisualAttributes()-render(); return $sBody; }这正说明了 iTop 邮件 HTML 部分的处理策略先内联自定义样式再剪除display: none的隐藏元素最后把能映射的 CSS 属性转成老式 HTML 属性兜底。HTML 规范化HtmlNormalizerHtmlNormalizer对给定 HTML 做四件事见 HtmlProcessor/HtmlNormalizer.php 的类注释缺失时补一个 HTML5 文档类型!DOCTYPE html理清错误嵌套的标签缺失时补head与body元素重排/格式化 HTML。用法use Pelago\Emogrifier\HtmlProcessor\HtmlNormalizer; $cleanHtml HtmlNormalizer::fromHtml($rawHtml)-render();CSS 样式转视觉 HTML 属性CssToAttributeConverterCssToAttributeConverter把少量 CSS 样式值映射为传统 HTML 视觉属性为 CSS 支持很差的客户端兜底。例如stylewidth: 100px会转出width100。用法use Pelago\Emogrifier\HtmlProcessor\CssToAttributeConverter; $visualHtml CssToAttributeConverter::fromHtml($rawHtml) -convertCssToVisualAttributes()-render();也可以在 DOMDocument 上工作$visualHtml CssToAttributeConverter::fromDomDocument($domDocument) -convertCssToVisualAttributes()-render();从源码看CssToAttributeConverter.php映射表是显式硬编码的且很多映射附带节点与取值的白名单约束CSS 属性映射的 HTML 属性限制条件background-colorbgcolor无text-alignalign仅p、div、td、th且值为left/right/center/justifyfloatalign仅table、img且值为left/rightborder-spacingcellspacing仅tablebackgroundbgcolor值首 token 为颜色非数字、非url(...)时width/height同名属性仅接受px或%数值忽略auto等marginaligncenter仅table/img且左右外边距均为autoborderborder0仅table/img且值为none或0实现细节该方法只新增属性原style属性保持不动映射前会剥离值中的!important。CSS 自定义属性求值CssVariableEvaluatorCssVariableEvaluator负责把内联样式属性中定义、被var()引用的 CSS 变量就地求值替换。例如给定:root { --text-color: green; } p { color: var(--text-color); }经过CssInliner::inlineCss()作用在htmlbodyp/p/body/html上之后HTML 会变成html style--text-color: green; body p stylecolor: var(--text-color); /body /htmlCssVariableEvaluator::evaluateVariables()会把--text-color的值代入使段落style变为color: green;。用法use Pelago\Emogrifier\HtmlProcessor\CssVariableEvaluator; $evaluatedHtml CssVariableEvaluator::fromHtml($html) -evaluateVariables()-render();DOMDocument 版本$evaluatedHtml CssVariableEvaluator::fromDomDocument($domDocument) -evaluateVariables()-render();源码实现要点CssVariableEvaluator.php从html根元素开始递归遍历后代每层把“本元素定义的变量 祖先已收集的变量定义”合并为当前可见的变量表用正则匹配var(--name[, fallback])语法替换引用fallback 值内若再引用变量会递归求值未定义且无 fallback 的引用原样保留。这一机制与内联 CSS 配合可为支持自定义属性的客户端提供变量注入能力。精简 HTMLHtmlPrunerHtmlPruner通过两件事减小 HTML 体积见 HtmlProcessor/HtmlPruner.php移除带display: none声明的元素移除class属性中不再被引用的冗余类名。移除display: none元素use Pelago\Emogrifier\HtmlProcessor\HtmlPruner; $prunedHtml HtmlPruner::fromHtml($html)-removeElementsWithDisplayNone() -removeRedundantClasses($classesToKeep)-render();removeElementsWithDisplayNone()用一条 XPath 1.0 查询定位元素源码通过translate()把属性值中的NONE大小写归一再匹配display:none。注意两点带-emogrifier-keep类的元素不会被移除。典型场景是某元素默认display: none但会被media规则在特定视口下显示例如预头部 preheader 文案。即使内联后它带着display: none只要加了这个类就能保留p classpreheader -emogrifier-keep styledisplay: none; Hello World! /p在removeElementsWithDisplayNone()之后调用removeRedundantClasses...系列方法时会顺带把-emogrifier-keep类去掉。移除冗余 classremoveRedundantClasses(array $classesToKeep [])接受一个保留白名单若传空数组则会直接移除所有class属性。removeRedundantClassesAfterCssInlined(CssInliner $cssInliner)是内联后的专用变体它读取CssInliner记录的“不可内联但已复制回style元素”的规则中仍然引用的类名只保留这些类。源码中通过getMatchingUninlinableSelectors()拿到未内联规则的选择器再正则抽取其中.className形式见 HtmlPruner.php。该系列方法还有个附带收益会顺带压缩class属性中多余的空白。CssInliner的完整选项清单方法默认行为说明disableStyleBlocksParsing()默认解析 HTML 内全部style块内联后移除这些块关闭后style块保留在 HTML 中且不被解析所需 CSS 必须通过inlineCss($css)传入disableInlineStyleAttributesParsing()默认保留元素上已有的style属性关闭后应用 CSS 前会丢弃所有既有内联样式addAllowedMediaType(string $mediaName)默认只保留all、screen、print三种媒体类型追加需要保留的媒体类型例如brailleremoveAllowedMediaType(string $mediaName)—从保留列表中移除某媒体类型addExcludedSelector(string $selector)—排除匹配该选择器的 HTML 节点使其不受内联影响。注意只排除精确匹配的元素本身不含后代排除整个子树需同时给出如.message-preview *的通用选择器addExcludedCssSelector(string $selector)—排除某个 CSS 选择器本身不被内联例如不想让 reset 规则* { margin: 0; padding: 0; font-size: 100% }内联到每个节点时排除*。注意排除必须精确匹配排除.example不会排除p .exampleremoveExcludedCssSelector(string $selector)—撤销之前添加的排除选择器示例排除选择器与排除 CSS 选择器的区别// 排除 HTML 节点需要同时覆盖子树 $cssInliner-addExcludedSelector(.message-preview); $cssInliner-addExcludedSelector(.message-preview *); // 排除 CSS 选择器不内联 reset 规则 $cssInliner-addExcludedCssSelector(*); $cssInliner-addExcludedCssSelector(form); $cssInliner-removeExcludedCssSelector(form);优先级specificity与!important处理内联是“把多条规则合并到同一个style属性”因此必须遵循 CSS 优先级语义。源码在collateCssRules()中对可内联规则做了排序CssInliner.php排序依据getCssSelectorPrecedence()计算的选择器权重权重表在类常量中明确定义ID 选择器#权重 10000类、属性、静态伪类:not除外权重 100元素/伪元素权重 1。优先级低的选择器先处理、优先级高的后处理从而让后者覆盖前者优先级相同时按 CSS 中出现的行号顺序决定见sortBySelectorPrecedence。权重计算与合并样式字符串都带有进程内缓存CACHE_KEY_SELECTOR/CACHE_KEY_COMBINED_STYLES避免重复计算。!important语义同样被保留generateStyleStringFromDeclarationsArrays()在合并新旧声明时若旧值带!important而新值不带则新值让位unsetremoveImportantAnnotationFromAllInlineStyles()最终会从内联样式中剥掉!important注解并在必要时重排声明顺序——例如font: 12px serif !important; font-size: 13px;会被重排为font-size: 13px; font: 12px serif;以保持正确见源码注释与removeImportantAnnotationFromNodeInlineStyle。支持的 CSS 选择器Emogrifier 目前支持以下选择器类别类型选择器typep、div等类选择器class.fooID 选择器ID#bar通用选择器universal*属性选择器attribute含以下取值形式presence存在[attr]精确值[attrv]~空格分隔词表中的某词[attr~word]|精确值或带连字符前缀[attr|v]^前缀[attr^pre]$后缀[attr$suf]*子串[attr*sub]相邻兄弟adjacentA B通用兄弟general siblingA ~ B子元素childA B后代descendantA B静态伪类static pseudo-classes:empty:first-child:first-of-type需带类型如p:first-of-type不支持*:first-of-type:last-child:last-of-type需带类型:not(...):nth-child(...):nth-last-child(...):nth-last-of-type(...)需带类型:nth-of-type(...)需带类型:only-child:only-of-type需带类型:root尚未实现的选择器大小写不敏感属性值case-insensitive attribute value即[attrv i]README 明确列出的部分静态伪类:any-link、不带类型的:first-of-type、不带类型的:last-of-type、不带类型的:nth-last-of-type()、不带类型的:nth-of-type()、不带类型的:only-of-type()、:optional、:required。涉及这些规则时规则本身不会丢失而是被保留并复制到 HTML 的style元素中。无法内联但会被保留的选择器以下选择器无法以内联样式呈现但相关规则会被保留并复制到style元素动态伪类dynamic pseudo-classes如:hover伪元素pseudo-elements如::after。这一“复制回style”的动作在inlineCss()尾部由copyUninlinableCssToStyleNode()完成CssInliner.php只复制确实能匹配到文档中元素的不可内联规则避免无谓膨胀media规则会被正确排序import、font-face、keyframes等非条件 at-rule 也会原样前置于style元素。注意与坑Caveats字符编码必须 UTF-8。HTML 与 CSS 都要求 UTF-8ISO8859-1、ISO8859-15等编码不受支持。AbstractHtmlProcessor在构建 DOM 时会强制加入Content-Typemeta 标签charsetutf-8见 AbstractHtmlProcessor.php。media规则会被完整保留在响应式邮件设计中非常有用但要想让media内声明覆盖内联样式往往需要给其中的声明加!important。例如下面的 CSS 中若无!importantmedia内的font-size: 14px无法覆盖已被内联成p stylefont-size: 16px;的声明即便不内联时不需要!importantp { font-size: 16px; } media (max-width: 640px) { p { font-size: 14px !important; } }定义在media规则内的 CSS 自定义属性变量无法作用于已经内联并求值的属性值不过对于支持自定义属性的客户端media内使用var()的规则仍能从内联定义或media规则中取到值。伪元素::after与动态伪类:hover规则无法内联但会被保留并复制到style元素注意事项同上。Emogrifier 会抓取 HTML 中已有的内联样式与style块但不会抓取link引用的 CSS 文件与import规则中的 CSS会原样保留供支持它们的客户端使用。即便样式已内联部分邮件客户端仍会忽略某些 CSS 属性如邮件标准推进缓慢的历史现实参见 email-standards.org、Campaign Monitor 与 MailChimp 的客户端 CSS 兼容性资料。所有命中元素的 CSS 属性都会被写上哪怕冗余。例如同时定义了font与font-size两个属性都会出现在元素上更具体的属性不会被折叠进更通用的属性。HTML 不规范时可能遇到问题DOMDocument会抱怨。若出现解析问题可考虑先用 Tidy 清洗 HTML 再交给 Emogrifier。Emogrifier 会把传入的 (X)HTML 自动转换为 HTML5——自闭合标签会失去斜杠例如br/变为br。要保持 HTML 合法建议直接使用 HTML5 而非 XHTML 变体。从旧版Emogrifier类迁移到CssInliner老版本库的核心类是Emogrifier新版以CssInliner 各HtmlProcessor取代。迁移示例最小示例旧代码$emogrifier new Emogrifier($html); $html $emogrifier-emogrify();新代码$html CssInliner::fromHtml($html)-inlineCss()-render();注意旧代码默认会移除display: none;的元素新代码不会——新旧默认行为在这一细节上有差异如需保持旧行为要显式调用HtmlPruner::removeElementsWithDisplayNone()。复杂示例含 CSS 到 HTML 属性映射旧代码$emogrifier new Emogrifier($html, $css); $emogrifier-enableCssToHtmlMapping(); $html $emogrifier-emogrify();新代码$domDocument CssInliner::fromHtml($html)-inlineCss($css)-getDomDocument(); HtmlPruner::fromDomDocument($domDocument)-removeElementsWithDisplayNone(); $html CssToAttributeConverter::fromDomDocument($domDocument) -convertCssToVisualAttributes()-render();在 iTop 中的落地与验证Emogrifier 在 iTop 中并非孤立的第三方库而是邮件通知链路的正式组成部分调用入口sources/Core/Email/EmailSymfony.php 的InlineCssIntoBodyContent()先CssInliner::inlineCss($sCustomStyles)内联自定义样式再HtmlPruner::removeElementsWithDisplayNone()剪除隐藏元素最后CssToAttributeConverter::convertCssToVisualAttributes()做传统属性兜底样式来源core/action.class.inc.php 在拼装通知正文时把 HTML 字段内容包进div classemail-is-html-content以便通过css/email.scss编译出的邮件样式经 Emogrifier 作用于其中——这是仓库中注释明确点名的协作方式测试守护tests/php-unit-tests/unitary-tests/sources/core/Email/EmailSymfonyTest.php 中的testSetBodyHtmlPartContainsBodyAndInlinedCss断言 HTML 邮件部分必须同时包含原始正文文本与内联后的color: red同时同文件的其他用例如testSetBodyHtmlPartContainsPlainTextWithoutCss见 同一测试文件验证纯文本部分不含style与 CSS 属性。这套“内联 剪枝 属性映射”的管线正是邮件客户端兼容性问题的标准解法内联保证基础样式可靠呈现剪枝控制体积属性映射为最老旧的客户端兜底。理解 Emogrifier 的类族与这些边界行为你就能够在自己编写的 iTop 扩展或独立 PHP 项目中复刻同样稳健的邮件渲染流程。赞分享后端企业应用【免费下载链接】iTopA simple, web based CMDB IT Service Management tool项目地址https://gitcode.com/gh_mirrors/it/iTop点击查看免费下载相关推荐listmonk邮件模板CSS内联性能构建时vs运行时listmonk邮件模板CSS内联性能构建时vs运行时 你是否曾遇到过邮件模板在不同客户端显示错乱的问题或者发送大批量邮件时系统响应缓慢本文将深入探讨li后端MJML mj-style 组件完全指南向邮件模板注入自定义 CSS 的两种模式与底层实现原理MJML mj style 组件完全指南向邮件模板注入自定义 CSS 的两种模式与底层实现原理 导读 mj style 是 MJML 头部 mj head前端CLIApache Ossie验证体系完全指南Schema、唯一性、引用与SQL语法四层校验Apache Ossie验证体系完全指南Schema、唯一性、引用与SQL语法四层校验 Apache Ossie 是一套厂商中立vendor agnosti后端数据建模数据集成上一篇30分钟跑通Label Studio数据标注全流程从装好工具到交付训练集下一篇机器翻译质量评估怎么做COMET 框架把感觉变成 0 到 1 的分数创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑