核心概念

理解 Magento 2 的模板,首先要理解几个核心概念,它们共同构成了模板系统的工作流程。

PHTML (PHP HTML) 文件

这是模板文件本身,它包含了 HTML 标记和 PHP 代码块,PHP 代码主要用于:

  • 获取数据:从 Block 对象中获取要展示的数据。
  • 输出安全:使用 <?= $block->getSomeData() ?> 这样的语法来输出数据,Magento 2 会自动对这些输出进行转义,以防止 XSS 攻击。
  • 包含子模板:使用 <?= $block->getChildHtml('child_block_name') ?> 来渲染子块。

示例: catalog/product/list.phtml

<?php
/**
 * @var $block \Magento\Catalog\Block\Product\ListProduct
 */
?>
<?php $_productCollection = $block->getLoadedProductCollection(); ?>
<?php if (!$_productCollection->count()): ?>
    <div class="message info empty"><<?= /* @escapeNotVerified */ __('We can\'t find products matching the selection.') ?></div>
<?php else: ?>
    <!-- ... 产品列表的 HTML 结构 ... -->
    <?php foreach ($_productCollection as $_product): ?>
        <div class="product-item">
            <a href="<?= /* @escapeNotVerified */ $_product->getProductUrl() ?>">
                <img src="<?= /* @escapeNotVerified */ $block->getImage($_product, 'category_page_list')->getImageUrl() ?>" />
            </a>
            <h3 class="product name">
                <a href="<?= /* @escapeNotVerified */ $_product->getProductUrl() ?>">
                    <?= $block->escapeHtml($_product->getName()) ?>
                </a>
            </h3>
            <!-- ... 更多产品信息 ... -->
        </div>
    <?php endforeach; ?>
<?php endif; ?>

Block (块)

Block 是 PHP 类,负责业务逻辑数据准备,它不直接输出 HTML,而是为 PHTML 模板提供数据。

  • 职责:获取数据、处理逻辑、调用服务等。
  • 与模板的关联:每个 Block 类都有一个对应的模板文件,这个关联通常在布局文件中定义。
  • 获取数据:通过 getSomeData() 方法,在模板中通过 $block->getSomeData() 调用。

示例: Magento\Catalog\Block\Product\ListProduct 这个类负责获取产品集合,然后将其传递给 list.phtml 模板进行渲染。

Layout (布局)

Layout 是连接 Block 和模板的蓝图,它定义了页面上各个 Block 的位置、层级以及它们使用的模板。

  • XML 文件:布局文件是 XML 格式的,通常位于 view/frontend/layout/ 目录下。
  • 指令
    • <block>:定义一个 Block,并指定其类名、模板文件和名称。
    • <referenceBlock>:引用一个已存在的 Block,并可以修改其属性(如模板)。
    • <move>:移动 Block 到页面的另一个位置。

示例: catalog_product_view.xml (产品详情页布局)

<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <!-- 在 "content" 区域添加一个产品列表块 -->
        <block class="Magento\Catalog\Block\Product\View" name="product.info" template="product/view.phtml" />
        <!-- 引用 product.info 块,并修改它的模板 -->
        <referenceBlock name="product.info">
            <action method="setTemplate">
                <argument name="template" xsi:type="string">Magento_Catalog::product/custom-view.phtml</argument>
            </action>
        </referenceBlock>
    </body>
</page>

UI Component (UI 组件)

这是 Magento 2 更现代、推荐的构建用户界面的方式,它将数据提供者、布局和视图逻辑解耦。

  • XML 定义:通过 XML 文件(.xml)定义组件的结构和属性。
  • JavaScript 渲染:大部分 UI 组件由 Knockout.js 在客户端动态渲染,这极大地提升了页面的交互性和性能。
  • 视图文件:UI 组件的视图文件是 .html 文件,位于 view/frontend/web/templates/ 目录下,它们使用 Knockout 的 data-bind 属性来绑定数据和方法。

示例: product-info/main.xml (产品信息主容器)

<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
    <argument name="data" xsi:type="array">
        <item name="js_config" xsi:type="array">
            <item name="provider" xsi:type="string">productInfoProvider</item>
        </item>
        <!-- ... 其他配置 ... -->
    </argument>
    <settings>
        <!-- ... 组件设置 ... -->
    </settings>
    <!-- 在这里定义子组件,比如价格、库存等 -->
    <container name="productInfoContainer">
        <block class="Magento\Catalog\Block\Product\View" name="product.info" template="product/view/attributes.phtml"/>
    </container>
</listing>

模板文件结构

Magento 2 的模板文件遵循严格的命名和目录结构,这被称为 Themes(主题)

主题层级

Magento 2 支持主题继承,你可以创建一个子主题来继承并修改父主题的文件,而无需复制所有文件。

app/design/frontend/ ├── Vendor/parent-theme/ (父主题) │ ├── Magento_Theme/ │ │ └── web/ │ │ └── templates/ │ │ └── header.phtml │ └── Magento_Catalog/ │ └── web/ │ └── templates/ │ └── product/ │ └── list.phtml │ └── Vendor/child-theme/ (子主题) ├── Magento_Theme/ │ └── web/ │ └── templates/ │ └── header.phtml <-- 如果存在,会覆盖父主题的 header.phtml └── Magento_Catalog/ └── web/ └── templates/ └── product/ └── list.phtml <-- 如果不存在,则从父主题继承

标准路径

  • PHTML 模板app/design/frontend/<Vendor>/<theme>/Magento_<Module>/templates/<path/to/template>.phtml
  • UI 组件模板app/design/frontend/<Vendor>/<theme>/Magento_<Module>/web/templates/<path/to/template>.html
  • 静态文件 (CSS, JS, 图片)app/design/frontend/<Vendor>/<theme>/web/<css|js|images>/...

如何覆盖模板(自定义)

这是开发中最常见的操作,Magento 2 提供了多种覆盖方式,推荐从最安全、最推荐的方式开始。

方式一:使用主题覆盖 (最推荐)

这是官方推荐的方式,最安全,且升级时不会被覆盖。

  • 步骤
    1. 在你的主题目录下创建与原模块相同的文件路径。
    2. 复制你想要修改的模板文件到你的主题目录中。
    3. 修改你的主题目录下的文件。

示例:修改产品列表页的模板

  1. 原始文件:vendor/magento/module-catalog/view/frontend/templates/product/list.phtml
  2. 在你的主题下创建文件:app/design/frontend/Vendor/my-theme/Magento_Catalog/templates/product/list.phtml
  3. 所有使用这个模板的地方都会自动使用你主题中的版本。

方式二:通过布局文件覆盖

如果你只想修改某个特定页面(如某个分类页)的模板,而不是全局修改,这种方式更精确。

  • 步骤
    1. 在你的主题的 layout 目录下创建一个布局文件。
    2. 使用 <referenceBlock> 标签找到目标 Block,然后用 <action method="setTemplate"> 修改其模板。

示例:修改 "分类 ID 为 5" 的产品列表模板

  1. 创建布局文件:app/design/frontend/Vendor/my-theme/Magento_Catalog/layout/catalog_category_view_id_5.xml
  2. 编辑文件内容:
    <?xml version="1.0"?>
    <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
        <body>
            <referenceBlock name="category.products.list">
                <action method="setTemplate">
                    <argument name="template" xsi:type="string">Magento_Catalog::product/custom-list.phtml</argument>
                </action>
            </referenceBlock>
        </body>
    </page>
  3. 只有 ID 为 5 的分类页会使用 custom-list.phtml,其他分类页不受影响。

方式三:重写 Block 类 (不推荐用于模板修改)

这种方式用于修改 Block 的PHP 逻辑,而不是直接替换模板,虽然你可以在重写的 Block 中 setTemplate,但这会破坏布局的清晰度,通常不推荐。

示例:重写 ListProduct Block

  1. etc/di.xml 中定义重写:
    <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework/ObjectManager/etc/config.xsd">
        <preference for="Magento\Catalog\Block\Product\ListProduct" type="Vendor\Module\Block\Catalog\Product\ListProduct" />
    </config>
  2. 创建新的 Block 类 Vendor/Module/Block/Catalog/Product/ListProduct.php,你可以在这里修改 getLoadedProductCollection() 方法,或者 setTemplate()

最佳实践

  1. 优先使用主题覆盖:这是最干净、最安全的方式。
  2. 最小化修改:只修改你真正需要改变的文件,避免不必要的文件复制。
  3. 利用 Knockout.js:对于动态、交互性强的部分,优先考虑使用 UI Components 和 Knockout.js,而不是在 PHTML 中写大量 JavaScript。
  4. 使用 Escape Helper:永远不要直接输出变量,使用 <?= $block->escapeHtml($variable) ?> 或其他 escape... 方法来防止 XSS 攻击。
  5. 遵循命名规范:保持文件和类的命名与 Magento 2 的标准一致。
  6. 清除缓存:修改了模板、布局或主题后,一定要清除缓存 (bin/magento cache:clean) 才能看到效果。

特性 PHTML 模块 UI 组件
用途 、页面结构、表单等 动态列表、复杂交互、数据网格
技术 PHP + HTML XML + Knockout.js + HTML
数据流 服务端渲染,数据从 Block 传递到模板 客户端渲染,数据通过 Data Provider 和 XML 配置
性能 页面加载时渲染,可能较重 按需渲染,交互更流畅
覆盖方式 主题覆盖、布局覆盖 通过 XML 重新配置或覆盖模板

对于 Magento 2 你需要同时掌握这两种技术:

  • 使用 PHTML 和 Layout 来构建页面的基本骨架和静态部分。
  • 使用 UI Components 来构建页面的动态、可交互的数据展示部分。

希望这份详细的指南能帮助你全面理解 Magento 2 的模板系统!