5. 视图与模板引擎

5.视图与模板引擎

在 MVC 架构中,视图负责将数据以用户友好的方式呈现。Phalcon 提供了强大的视图系统和高效的 Volt 模板引擎,让开发者能够轻松创建动态且美观的页面。作为日常开发中与前端交互最直接的部分,掌握视图技术对构建优质用户体验至关重要。

Volt 模板引擎介绍

Volt 是 Phalcon 框架内置的高性能模板引擎,采用 C 语言编写,编译速度极快。它的语法借鉴了 Jinja2,对于熟悉 Python 或其他现代模板引擎的开发者来说上手非常容易。Volt 模板会被编译成原生 PHP 代码执行,这意味着它既能提供优雅的语法,又不会损失性能。

在 Phalcon 应用中启用 Volt 非常简单,只需在依赖注入容器中注册即可:

use Phalcon\Mvc\View;
use Phalcon\Mvc\View\Engine\Volt;

$di->setShared('view', function () {
    $view = new View();
    $view->setViewsDir('../app/views/');
    $view->registerEngines([
        '.volt' => function ($view) use ($di) {
            $volt = new Volt($view, $di);
            $volt->setOptions([
                'path' => '../app/cache/volt/',
                'separator' => '_'
            ]);
            return $volt;
        }
    ]);
    return $view;
});

这段代码配置了视图组件,指定了模板文件存放目录和 Volt 引擎的缓存路径。缓存路径非常重要,因为 Volt 会将模板编译成 PHP 文件并保存在这里,以提高后续请求的执行速度。

Volt 的主要优势在于:

  • 语法简洁直观,减少模板中的冗余代码
  • 编译型模板,执行效率高
  • 支持模板继承,促进代码复用
  • 内置丰富的过滤器和函数
  • 与 Phalcon 其他组件深度集成

变量输出与过滤器

在 Volt 模板中,使用双花括号 {{ }} 来输出变量。这与许多现代模板引擎的语法类似,降低了学习成本。

<h1>{{ title }}</h1>
<p>{{ content }}</p>

上面的代码会输出控制器中传递的 titlecontent 变量的值。如果需要访问对象的属性或数组元素,可以使用点语法或方括号语法:

{{ user.name }}
{{ user['email'] }}
{{ products[0].name }}

变量输出时,经常需要进行格式化或转换。Volt 提供了过滤器机制,使用竖线 | 来应用过滤器:

{{ price | number_format(2) }}
{{ content | striptags | truncate(100) }}
{{ username | escape }}

这里展示了几个常用过滤器:number_format 用于格式化数字,striptags 用于去除 HTML 标签,truncate 用于截断长文本,escape 用于 HTML 转义防止 XSS 攻击。

Phalcon 提供了丰富的内置过滤器,涵盖了字符串处理、数字格式化、数组操作等常见需求:

  • eescape:HTML 转义
  • striptags:去除 HTML 标签
  • trim:去除首尾空白字符
  • lower/upper:转换大小写
  • capitalize:首字母大写
  • nl2br:将换行符转换为标签
  • json_encode/json_decode:JSON 编解码
  • length:获取字符串长度或数组元素个数
  • date:日期格式化

例如,格式化日期可以这样写:

{{ create_time | date('Y-m-d H:i:s') }}

如果需要多个过滤器组合使用,只需链式调用即可:

{{ article.content | striptags | trim | truncate(200) }}

这行代码会先去除 HTML 标签,再修剪空白字符,最后截断为 200 个字符,非常适合显示文章摘要。

控制结构语法

Volt 提供了完整的控制结构,包括条件判断、循环等,语法简洁清晰,比原生 PHP 模板更易读。

条件判断

if 语句的使用方式与 PHP 类似,但语法更简洁:

{% if user.role == 'admin' %}
    <a href="/admin">管理后台</a>
{% elseif user.role == 'editor' %}
    <a href="/editor">编辑面板</a>
{% else %}
    <a href="/profile">个人中心</a>
{% endif %}

除了常见的比较运算符,Volt 还支持 is 关键字进行类型检查和特殊判断:

{% if products is empty %}
    <p>暂无产品数据</p>
{% endif %}

{% if user.age is even %}
    <p>年龄为偶数</p>
{% endif %}

{% if total is divisibleby(3) %}
    <p>总数能被3整除</p>
{% endif %}

常用的测试条件包括 empty(为空)、defined(已定义)、even(偶数)、odd(奇数)、iterable(可迭代)、numeric(数字)等。

对于多条件判断,还可以使用 switch 语句:

{% switch status %}
    {% case 'pending' %}
        <span class="label pending">待处理</span>
    {% case 'processing' %}
        <span class="label processing">处理中</span>
    {% case 'completed' %}
        <span class="label completed">已完成</span>
    {% default %}
        <span class="label unknown">未知状态</span>
{% endswitch %}

循环结构

for 循环是遍历数组或集合的主要方式:

<ul class="product-list">
    {% for product in products %}
        <li>{{ product.name }} - ${{ product.price }}</li>
    {% endfor %}
</ul>

循环中可以使用 loop 变量获取循环信息:

<table>
    {% for user in users %}
        {% if loop.first %}
        <thead>
            <tr>
                <th>序号</th>
                <th>姓名</th>
                <th>邮箱</th>
            </tr>
        </thead>
        <tbody>
        {% endif %}

        <tr class="{{ loop.index is odd ? 'odd' : 'even' }}">
            <td>{{ loop.index }}</td>
            <td>{{ user.name }}</td>
            <td>{{ user.email }}</td>
        </tr>

        {% if loop.last %}
        </tbody>
        {% endif %}
    {% else %}
        <tr><td colspan="3">暂无用户数据</td></tr>
    {% endfor %}
</table>

这里展示了几个实用的 loop 变量属性:

  • loop.index:当前循环索引(从 1 开始)
  • loop.index0:当前循环索引(从 0 开始)
  • loop.first:是否为第一个元素
  • loop.last:是否为最后一个元素
  • loop.length:总元素个数

注意循环结构中的 else 子句,当循环的数组为空时会执行,这比在循环外单独判断要简洁得多。

循环中还可以使用 breakcontinue 控制流程:

{% for item in items %}
    {% if item.skip %}
        {% continue %}
    {% endif %}

    {{ item.name }}

    {% if item.stop %}
        {% break %}
    {% endif %}
{% endfor %}

赋值语句

在模板中有时需要定义或修改变量,可以使用 set 语句:

{% set title = '产品列表' %}
{% set total = products | length %}
{% set is_empty = total == 0 %}

也可以同时定义多个变量:

{% set name = 'John', age = 30, active = true %}

Volt 还支持复合赋值运算符:

{% set count += 1 %}
{% set total -= price %}
{% set score *= 2 %}

模板继承

模板继承是 Volt 最强大的特性之一,它允许创建一个基础模板(母版页),然后在子模板中重用这个基础模板并覆盖特定部分。这极大地提高了代码复用率,使页面维护更加容易。

基础模板

首先创建一个基础模板(通常命名为 base.volt),定义页面的整体结构和可替换的区块:

{# app/views/base.volt #}
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}我的网站{% endblock %}</title>
    <link rel="stylesheet" href="/css/style.css">
    {% block styles %}{% endblock %}
</head>
<body>
    <header>
        <nav>
            <a href="/">首页</a>
            <a href="/products">产品</a>
            <a href="/about">关于我们</a>
        </nav>
    </header>

    <main>
        {% block content %}{% endblock %}
    </main>

    <footer>
        <p>&copy; {{ date('Y') }} 我的网站. 保留所有权利.</p>
        {% block footer %}{% endblock %}
    </footer>

    <script src="/js/jquery.js"></script>
    {% block scripts %}{% endblock %}
</body>
</html>

在基础模板中,使用 {% block 名称 %} 定义可替换的区块。子模板可以覆盖这些区块来提供自定义内容。

子模板

创建子模板时,使用 extends 指令指定要继承的基础模板,然后使用 block 指令覆盖需要自定义的部分:

{# app/views/products/index.volt #}
{% extends 'base.volt' %}

{% block title %}产品列表 - 我的网站{% endblock %}

{% block styles %}
    <link rel="stylesheet" href="/css/products.css">
{% endblock %}

{% block content %}
    <h1>产品列表</h1>
    <ul class="product-grid">
        {% for product in products %}
            <li class="product-item">
                <h3>{{ product.name }}</h3>
                <p class="price">${{ product.price }}</p>
                <a href="/products/{{ product.id }}">查看详情</a>
            </li>
        {% endfor %}
    </ul>
{% endblock %}

{% block scripts %}
    <script src="/js/product-filter.js"></script>
    <script>
        $(document).ready(function() {
            initProductFilter();
        });
    </script>
{% endblock %}

这个子模板继承了 base.volt 并覆盖了 titlestylescontentscripts 区块。未被覆盖的区块(如 footer)将使用基础模板中的默认内容。

区块嵌套与超级块

区块可以嵌套,形成更复杂的结构。有时子模板可能需要在父模板区块内容的基础上添加内容,而不是完全替换,这时可以使用 super() 函数:

{% block styles %}
    {{ super() }}
    <link rel="stylesheet" href="/css/product-detail.css">
{% endblock %}

super() 函数会输出父模板中该区块的内容,这样就可以在其基础上添加新的样式表,而不是完全替换。这在需要扩展父模板功能时非常有用。

视图组件使用

Phalcon 的视图系统不仅支持模板渲染,还提供了多种组件来帮助构建复杂的页面结构。

局部视图

对于页面中重复出现的部分(如页头、页脚、侧边栏),可以将其提取为局部视图,然后在需要的地方引入。Volt 提供了两种引入局部视图的方式:partial() 函数和 include 指令。

使用 partial() 函数:

<div class="sidebar">
    {{ partial('shared/sidebar') }}
</div>

使用 include 指令:

<div class="footer">
    {% include 'shared/footer' %}
</div>

两者的主要区别在于:partial() 是在运行时动态加载并渲染,支持传递变量,适合内容经常变化的情况;而 include 是在编译时将局部视图内容嵌入到当前模板中,性能更好,但不支持动态路径。

传递参数给局部视图:

{{ partial('shared/menu', ['items': menu_items, 'active': 'products']) }}

{% include 'shared/pagination' with ['current': page, 'total': total_pages] %}

模板布局

除了模板继承,Phalcon 还支持通过控制器设置布局。布局本质上是一种特殊的视图文件,用于包裹控制器对应的视图内容。

在控制器中设置布局:

use Phalcon\Mvc\Controller;

class ProductsController extends Controller
{
    public function initialize()
    {
        $this->view->setLayout('main');
    }
}

这会将 app/views/layouts/main.phtml(或 .volt)作为布局文件。布局文件中使用 $this->getContent() 输出控制器视图的内容:

{# app/views/layouts/main.volt #}
<div class="container">
    <header>
        <h1>产品管理系统</h1>
    </header>
    <nav>
        <!-- 导航菜单 -->
    </nav>
    <div class="content">
        {{ content() }}
    </div>
    <footer>
        <!-- 页脚内容 -->
    </footer>
</div>

视图组件

Phalcon 提供了 Phalcon\Mvc\View\Simple 作为轻量级替代方案,它不支持视图层次结构,适合需要完全控制渲染过程的场景:

$di->setShared('view', function () {
    $view = new \Phalcon\Mvc\View\Simple();
    $view->setViewsDir('../app/views/');
    return $view;
});

在控制器中使用:

public function showAction($id)
{
    $product = Products::findFirstById($id);
    $this->view->setVar('product', $product);
    echo $this->view->render('products/show');
}

这种方式需要手动调用 render() 方法,并显式输出结果。

实用技巧与最佳实践

变量赋值与作用域

Volt 模板中的变量有其作用域规则。在循环或条件语句内部定义的变量,在外部无法访问:

{% for item in items %}
    {% set current = item.id %}
{% endfor %}
{{ current }} {# 这里会报错,current未定义 #}

如需在循环外部使用变量,应在循环前定义:

{% set current = null %}
{% for item in items %}
    {% if item.active %}
        {% set current = item.id %}
    {% endif %}
{% endfor %}
{{ current }} {# 正确 #}

避免在模板中编写复杂逻辑

虽然 Volt 支持复杂的表达式和控制结构,但良好的实践是保持模板简洁,只包含与展示相关的逻辑。复杂的业务逻辑应放在控制器或模型中处理。

不好的做法:

{% for user in users %}
    {% if user.registered_at > '2023-01-01' and user.status == 'active' and (user.role == 'editor' or user.role == 'admin') %}
        <li>{{ user.name }}</li>
    {% endif %}
{% endfor %}

好的做法:在控制器中预处理数据

// 控制器中
$this->view->setVar('eligibleUsers', $users->filter(function($user) {
    return $user->registered_at > '2023-01-01' &&
           $user->status == 'active' &&
           in_array($user->role, ['editor', 'admin']);
}));
{# 模板中 #}
{% for user in eligibleUsers %}
    <li>{{ user.name }}</li>
{% endfor %}

使用宏封装重复 HTML 片段

对于重复出现的 HTML 结构(如表单元素、卡片组件),可以使用 Volt 宏来封装:

{% macro input_field(name, label, value = '', type = 'text') %}
<div class="form-group">
    <label for="{{ name }}">{{ label }}</label>
    <input type="{{ type }}" id="{{ name }}" name="{{ name }}" value="{{ value | e }}" class="form-control">
</div>
{% endmacro %}

{# 使用宏 #}
{{ input_field('username', '用户名', user.username) }}
{{ input_field('email', '邮箱', user.email, 'email') }}
{{ input_field('password', '密码', '', 'password') }}

宏可以放在单独的文件中,然后在需要的模板中导入:

{% import 'macros/forms.volt' as forms %}

{{ forms.input_field('username', '用户名') }}
{{ forms.select_field('role', '角色', roles, user.role) }}

转义输出防止 XSS 攻击

虽然 Volt 默认不会自动转义输出,但为了防止 XSS 攻击,应始终对用户提供的内容进行转义。使用 e 过滤器或 escape 过滤器:

{{ user_input | e }}
{{ comment.content | escape }}

对于可信的 HTML 内容(如管理员编辑的富文本),可以不转义,但要确保内容来源可靠:

{{ article.content | raw }}

raw 过滤器会禁用转义,直接输出原始 HTML。使用时务必谨慎,确保内容安全。

调试技巧

开发过程中,经常需要查看变量内容进行调试。Volt 提供了 dump 函数:

{{ dump(user) }}
{{ dump(products) }}

这会输出变量的详细信息,包括类型和值,非常有助于调试。

总结

视图与模板引擎是构建用户界面的关键部分。Phalcon 的 Volt 模板引擎通过简洁的语法和高效的编译机制,让开发者能够轻松创建动态页面。本章介绍了 Volt 的基本语法、变量输出、过滤器、控制结构、模板继承和视图组件使用等核心知识点。

掌握模板继承和布局技术可以显著提高代码复用率,保持项目结构清晰。合理使用局部视图和宏可以减少重复代码,提高开发效率。同时,遵循最佳实践,如保持模板逻辑简单、正确转义输出内容等,能够确保应用的可维护性和安全性。

下一章将介绍模型基础与数据库操作,学习如何在 Phalcon 中与数据库交互,实现数据的增删改查功能。