Yii2学习笔记系列13——Application Structure-Views(应用结构之视图)

二 10 五月 2016

视图

视图是MVC设计模式的一部分,它是负责向终端用户展示数据的代码。在一个Web应用程序中,通常需要根据视图模板来创建视图,视图模板是包含HTML代码和展示类的PHP代码的PHP脚本文件。它通过yii\web\View应用组件来管理,该组件主要提供通用方法帮助视图构造和渲染,简单起见,我们称视图模板或者视图模板文件为视图。

创建视图

如上面所述,视图是包含HTML和PHP代码的PHP脚本文件,下面就是一个展示登录表单的视图,如你所见,PHP代码用来生成动态的内容例如页面标题和表单,HTML代码则负责把它组织成一个漂亮的HTML页面。

<?php
use yii\helpers\Html;
use yii\widgets\ActiveForm;

/* @var $this yii\web\View */
/* @var $form yii\widgets\ActiveForm */
/* @var $model app\models\LoginForm */

$this->title = 'Login';
?>
<h1><?= Html::encode($this->title) ?></h1>

<p>Please fill out the following fields to login:</p>

<?php $form = ActiveForm::begin(); ?>
    <?= $form->field($model, 'username') ?>
    <?= $form->field($model, 'password')->passwordInput() ?>
    <?= Html::submitButton('Login') ?>
<?php ActiveForm::end(); ?>

在视图中,可以通过访问$this(指向yii\web\View)来管理和渲染这个视图文件。

除了$this之外,上述示例中的视图有其它预定义变量如$model,这些变量代表从控制器或其它触发视图渲染的对象传入到视图的数据。

小技巧:将预定义变量列在视图文件头部注释处,这样儿可以被IDE编辑器识别到,也是生成视图文档的好方法。

安全

当创建生成HTML页面的视图时,在显示之前将用户输入数据进行转码和过滤是非常重要的,否则你的应用可能会被跨站脚本攻击。

要显示纯文本,先调用yii\helpers\Html::encode()进行转码,例如如下代码将用户名在显示前先进行转码操作:

<?php
use yii\helpers\Html;
?>

<div class="username">
    <?= Html::encode($user->name) ?>
</div>

要显示HTML内容,先调用yii\helpers\HtmlPurifier过滤内容,例如如下代码将提交内容在显示前先过滤:

<?php
use yii\helpers\HtmlPurifier;
?>

<div class="post">
    <?= HtmlPurifier::process($post->text) ?>
</div>

小技巧:HTMLPurifier在保证输出数据的安全性上做的不错,但是性能不佳,如果你的应用需要高性能可能需要考虑缓存过滤后的结果。

组织视图

与控制器和模型类似,在组织视图上有一些约定:

  • 控制器渲染的视图文件默认放在@app/views/ControllerID目录下,其中ControllerID对应控制器ID,例如控制器类为PostController,则对应的视图文件目录应该为@app/views/post,控制器类PostCommentController对应的目录应该为@app/views/post-comment,如果是模块中的控制器,目录应该为yii\base\Module::basePath模块目录下的views/ControllerID目录;
  • 对于小部件徐然的视图文件默认放在WidgetPath/views目录,其中WidgetPath代表小部件类文件所在的目录;
  • 对于其他对象渲染的视图文件,建议遵循和小部件相似的规则

可以通过覆盖控制器或者小部件的yii\base\ViewContextInterface::getViewPath()方法来自定义视图文件默认目录。

渲染视图

我们可以在控制器、小部件,或者其他地方调用渲染视图的方法来渲染视图,该方法类似以下的格式:

/**
 * @param string $view 命名视图或文件路径,由实际的渲染方法决定
 * @param array $params 传递给视图的数据
 * @return string 渲染结果
 */
methodName($view, $params = [])
在控制器中渲染

在控制器中,我们可以通过调用以下控制器方法来渲染视图:

  • yii\base\Controller::render():渲染一个命名视图并使用一个布局渲染结果
  • yii\base\Controller::renderPartial():渲染一个命名的视图并且不使用布局
  • yii\web\Controller::renderAjax():渲染一个命名视图并且不适用布局,并且注入所有注册的JS/CSS脚本和文件等,通常在响应AJAX网页请求的情况下使用
  • yii\base\Controller::renderFile():渲染一个视图文件目录或别名下的视图文件

例如:

namespace app\controllers;

use Yii;
use app\models\Post;
use yii\web\Controller;
use yii\web\NotFoundHttpException;

class PostController extends Controller
{
    public function actionView($id)
    {
        $model = Post::findOne($id);
        if ($model === null) {
            throw new NotFoundHttpException;
        }

        // 渲染一个名称为"view"的视图并使用布局
        return $this->render('view', [
            'model' => $model,
        ]);
    }
}
在小部件中渲染

在小部件中,我们可以调用以下的小部件方法来渲染视图:

  • yii\base\Widget::render():渲染一个命名的视图
  • yii\base\Widget::renderFile():渲染一个视图文件目录或者别名下的视图文件

例如:

namespace app\components;

use yii\base\Widget;
use yii\helpers\Html;

class ListWidget extends Widget
{
    public $items = [];

    public function run()
    {
        // 渲染一个名为 "list" 的视图
        return $this->render('list', [
            'items' => $this->items,
        ]);
    }
}
在视图中渲染

我们可以通过视图组件提供的如下方法在一个视图中调用另一个视图:

  • yii\base\View::render():渲染一个命名视图
  • yii\web\View::renderAjax():渲染一个命名视图并注入所有注册的JS/CSS脚本和文件,通常在响应AJAX网页请求的情况下使用
  • yii\base\View::renderFile():渲染一个视图文件目录或者别名下的视图文件

例如,视图中的如下代码会渲染该视图所在目录下的_overview.php视图文件,这里需要记住的是视图中的$this对应的是yii\base\View组件:

<?= $this->render('_overview') ?>
在其他地方渲染

在任何地方都可以通过表达式Yii::$app->view来访问yii\base\View应用组件,调用它的如上所述的方法来渲染视图,例如:

// 显示视图文件 "@app/views/site/license.php"
echo \Yii::$app->view->renderFile('@app/views/site/license.php');
命名视图

当我们渲染一个视图的时候,我们可以通过两种方式来指定要渲染的视图,要么指定视图名,要么指定视图文件的路径/别名,在大多数情况下,我们更多的是使用前者因为它更加简洁和灵活。我们可以将这些通过名字来指定的视图为命名视图。

通过如下规则可以将一个视图名与相应的视图文件路径对应起来:

  • 视图名可以省略文件扩展名,这种情况下使用.php作为扩展名,视图名about对应到about.php文件名;
  • 视图名以双斜杠//开头,对应的视图文件路径为@app/views/ViewName,也就是说视图文件在yii\base\Application::viewPath路径下找,例如//site/about对应到@app/views/site/about.php
  • 视图名以单斜杠/开始,视图文件路径以当前使用模块的yii\base\Module::viewPath开始,如果不存在模块,使用@app/views/ViewName开始,例如,如果当前模块是user/user/create对应@app/modules/user/views/create.php,如果模块不存在,/user/create则对应@app/views/user/create.php
  • 如果是通过yii\base\View::context的方式渲染视图并且实现了yii\base\ViewContextInterface接口,视图文件的路径由Context上下文的yii\base\ViewContextInterface::getViewPath()开始,这种方式主要用在控制器和小部件中渲染视图,例如如果上下文为控制器SiteControllersite/about对应到@app/views/site/about.php
  • 如果A视图是在B视图中被调用渲染,那么B视图所在的目录会被作为A视图所对应的路径的前缀。例如,如果item视图在@app/views/post/index.php视图中被调用渲染,那么item视图对应的就是@app/views/post/item.php

根据上述规则,在控制器中app\controllers\PostController调用$this->render('view'),实际上会渲染@app/views/post/view.php视图文件,而在@app/views/post/view.php视图文件中调用$this->render('_overview')会渲染@app/views/post/_overview.php视图文件。

在视图中访问数据

在视图中有两种方式可以访问数据:推送和拉取。

推送方式是通过视图渲染方法的第二个参数传递数据来实现的,数据格式应为名称-值的数组,视图渲染时,调用PHP的extract()方法将该数组转换为视图可以访问的变量。例如,如下控制器的渲染视图代码推送2个变量到report视图:$foo=1$bar=2

<?php
echo $this->render('report', [
    'foo' => 1,
    'bar' => 2,
]);

拉取方式可以让视图从yii\base\View视图组件或其他对象中主动获得数据(如Yii::$app),在视图中使用如下表达式$this->context可以获取到控制器ID,由此可以让你在report视图中获取控制器的任意属性或方法,如下代码用来获取控制器ID:

The controller ID is: <?= $this->context->id ?>

推送方式让视图可以更少的依赖上下文对象,是视图获取数据的优先使用方式,缺点是需要手动构建数组,这点儿比较繁琐,在不同的地方渲染的时候容易出错。

视图间共享数据

yii\base\View视图组件提供yii\base\View:params参数属性来让不同视图共享数据

例如在about视图中,我们可以使用如下代码指定面包屑导航的当前部分:

$this->params['breadcrumbs'][] = 'About Us';

然后,在布局文件(也是一个视图)中,我们可以将面包屑导航显示出来:

<?= yii\widgets\Breadcrumbs::widget([
    'links' => isset($this->params['breadcrumbs']) ? $this->params['breadcrumbs'] : [],
]) ?>

布局

布局是一种特殊的视图,代表多个视图的公共部分,例如,大多数Web应用共享相同的页头和页尾,在每个视图中重复相同的页头和页尾,更好的方式是将这些公共部分放到一个布局中,渲染内容视图后在合适的地方嵌入到布局中。

创建布局

由于布局也是视图,它可以像普通视图一样创建,布局默认存储在@app/views/layouts路径下,模块中使用的布局应存储在yii\base\Module::basePath模块目录下的views/layouts路径下,可配置yii\base\Module::layoutPath来自定义应用或模块的布局默认路径。

如下示例是一个布局的大致内容,而该示例简化了很多代码,在实际中可能我们想要添加更多内容,如头部标签、主菜单等。

<?php
use yii\helpers\Html;

/* @var $this yii\web\View */
/* @var $content string 字符串 */
?>
<?php $this->beginPage() ?>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <?= Html::csrfMetaTags() ?>
    <title><?= Html::encode($this->title) ?></title>
    <?php $this->head() ?>
</head>
<body>
<?php $this->beginBody() ?>
    <header>My Company</header>
    <?= $content ?>
    <footer>&copy; 2016 by My Company</footer>
<?php $this->endBody() ?>
</body>
</html>
<?php $this->endPage() ?>

如上所示,布局生成每个页面通用的HTML标签,在<body>标签中,打印$content变量,$content变量代表党yii\base\Controller::render()控制器渲染方法调用时传递到布局的内容视图渲染结果。

大多数视图应调用上述代码中的如下方法,这些方法触发关于渲染过程的事件,这样儿其他地方注册的脚本和标签会添加到这些方法调用的地方。

  • yii\base\View::beginPage():该方法应在布局的开始处调用,它触发表明页面开始的yii\base\View::EVENT_BEGIN_PAGE事件。
  • yii\base\View::endPage():该方法应在布局的结尾处调用,它触发表明页面结尾的yii\base\View::EVENT_END_PAGE事件。
  • yii\web\View::head():该方法应在HTML页面的<head>标签中调用,它生成一个占位符,在页面渲染结束时会被注册的头部HTML代码(如link标签,meta标签)替换。
  • yii\web\View::beginBody():该方法应在<body>标签的开始处调用,它触发yii\web\View::EVENT_BEGIN_BODY事件并生成一个占位符,会被注册的HTML代码(如JavaScript)在页面body体开始处替换。
  • yii\web\View::endBody():该方法应在<body>标签的结尾处调用,它触发yii\web\View::EVENT_END_BODY事件并生成一个占位符,会被注册的HTML代码(如JavaScript)在页面body体结尾处替换。
在布局中访问数据

在布局中可以访问两个预定义变量:$this$content,前者对应和普通视图类似的yii\base\View视图组件,后者包含调用yii\base\Controller::render()方法渲染内容视图的结果。

如果想在布局中访问其他数据,必须使用在视图中访问数据一节中介绍的数据拉取方式,如果想从内容视图中传递数据到布局,可以使用视图间共享数据一节中的方法。

使用布局

正如控制器中渲染一节中描述的,当我们在控制器中通过调用yii\base\Controller::render()方法渲染视图时,布局文件也会同时被应用到渲染结果中。默认情况下会使用@app/views/layouts/main.php作为布局文件。

我们可以配置yii\base\Application::layout或yii\base\Controller::layout使用其他布局文件,前者管理所有控制器的布局,后者覆盖前者来控制单个控制器的布局。例如,如下的代码使post控制器渲染视图时使用@app/views/layouts/post.php作为布局文件,假如layout属性没改变,控制器默认使用@app/views/layouts/main.php作为布局文件。

namespace app\controllers;

use yii\web\Controller;

class PostController extends Controller
{
    public $layout = 'post';

    // ...
}

对于模块中的控制器,我们可以配置模块的yii\base\Module::layout属性指定布局文件,以此应用到该模块下的所有控制器。

由于layout可以在不同层级(控制器、模块、应用)进行配置,所以在幕后,Yii通过以下两步来判断控制器实际使用的布局文件:

第一步,判断布局的值和上下文模块:

  • 如果控制器的yii\base\Controller::$layout属性不为空,则使用该属性作为布局的值,使用控制器所在的模块作为上下文模块。
  • 如果$layout为空,则从控制器的所有上级模块(ancestor modules,包括应用)开始查找,查找到的第一个yii\base\Module::layout属性不为空的模块作为上下文模块,并将它的yii\base\Module::layout的值作为布局的值,如果都没有找到,则表示不使用布局。

第二步,根据第一步中布局的值和上下文模块判断实际的布局文件,布局文件的值可能为:

  • 路径别名(例如@app/views/layouts/main
  • 绝对路径(例如/main):布局的值以斜杠开始。实际的布局文件位于应用的布局文件路径下,默认是@app/views/layouts
  • 相对路径(如main):在上下文模块的yii\base\Module::layoutPath布局文件路径中查找实际的布局文件,默认是yii\base\Module::basePath模块目录下的views/layouts目录。
  • 布尔值false:不使用布局。

布局的值没有包含文件的扩展名,默认的扩展名是.php

嵌套布局

有时候我们可能需要将一个布局嵌套到另一个布局中,例如,在Web站点的不同地方,想要使用不同的布局,而这些布局共享相同的生成全局HTML5页面结构的基本布局,这时候我们可以通过在子布局中调用yii\base\View::beginContent()和yii\base\View::endContent()方法,如下所示:

<?php $this->beginContent('@app/views/layouts/base.php'); ?>

...child layout content here...

<?php $this->endContent(); ?>

如上所示,自布局内容应在yii\base\View::beginContent()和yii\bse\View::endContent()方法之间,传给yii\base\View::beginContent()的参数指定父布局,父布局可以是布局文件或者别名。

使用该方式可以多层嵌套布局。

使用数据块

数据块可以在一个地方指定视图内容在另一个地方显示,通常它需要和布局一起使用,例如,我们可以在内容视图中定义数据块,然后在布局中显示它。

我们可以通过yii\base\View::beginBlock()和yii\base\View::endBlock()来定义数据块,并且通过$view->blocks[$blockID]来访问它,其中$blockID是定义数据块时指定的唯一标识ID。

下面的示例演示了如何在一个内容视图中使用数据块来自定义布局。

首先,在内容视图中,定义一个或多个数据块:

...

<?php $this->beginBlock('block1'); ?>

...数据块block1的内容...

<?php $this->endBlock(); ?>

...

<?php $this->beginBlock('block3'); ?>

...数据块block3的内容...

<?php $this->endBlock(); ?>

然后,在布局视图中,如果数据块可用的话就渲染数据块,如果数据块未定义则显示默认内容。

...
<?php if (isset($this->blocks['block1'])): ?>
    <?= $this->blocks['block1'] ?>
<?php else: ?>
    ... block1的默认内容 ...
<?php endif; ?>

...

<?php if (isset($this->blocks['block2'])): ?>
    <?= $this->blocks['block2'] ?>
<?php else: ?>
    ... block2的默认内容 ...
<?php endif; ?>

...

<?php if (isset($this->blocks['block3'])): ?>
    <?= $this->blocks['block3'] ?>
<?php else: ?>
    ... block3的默认内容 ...
<?php endif; ?>
...

使用视图组件

yii\base\View视图组件提供了许多视图相关的特性,我们可以通过创建yii\base\View或它的子类实例来获取视图组件,大多数情况下我们主要使用view应用组件,我们可以通过如下所示的方式在配置文件中来配置该组件:

[
    // ...
    'components' => [
        'view' => [
            'class' => 'app\components\View',
        ],
        // ...
    ],
]

视图组件提供如下实用的视图相关的特性,每项详情将会在后面的独立章节中介绍:

当然了,在我们开发Web页面的时候,我们可能也会频繁的用到下面的一些实用的小特性。

设置页面标题

每个Web页面都应该有一个标题,正常情况下标题的标签显示在布局中,但实际上标题大多由内容视图而不是布局来决定,为了解决这个问题,yii\web\View提供了yii\web\View::title标题属性,可以让标题信息从内容视图传递到布局中。

为了使用该特性,在每个内容视图中,我们都可以通过如下所示的代码来设置页面标题:

<?php
$this->title = '我的页面标题';
?>

然后在布局中,确保我们在<head>区域有如下的代码:

<title><?= Html::encode($this->title) ?></title>
注册Meta元标签

Web页面通常需要生成各种元标签用以提供给不同的浏览器,如<head>中的页面标题。元标签通常在布局中生成。

如果想要在内容视图中生成原标签,我们可以通过在内容视图中调用yii\web\View::registerMetaTag()方法,如下所示:

<?php
$this->registerMetaTag(['name' => 'keywords', 'content' => 'yii, framework, php']);
?>

上述代码会通过视图组件注册一个“keywords”元标签,注册的元标签通常会在布局渲染结束之后渲染。当我们在某处调用yii\web\View::head()方法时,如下HTML代码将会生成并且插入调用yii\web\View::head()方法的地方:

<meta name="keywords" content="yii, framework, php">

这里需要注意的是,如果我们多次调用yii\web\View::registerMetaTag()方法,则会多次注册元标签,不论后面注册的元标签是否会重复。

所以为了确保每种元标签只有一个,我们可以在调用方法时指定键作为第二个参数,例如,如下代码注册了两次“description”元标签,但只有第二次会渲染:

$this->registerMetaTag(['name' => 'description', 'content' => 'This is my cool website made with Yii!'], 'description');
$this->registerMetaTag(['name' => 'description', 'content' => 'This website is about funny raccoons.'], 'description');
注册链接标签

和Meta标签类似,链接标签有时候也很实用,如自定义网站图标,指定Rss订阅,或授权OpenID到其他服务器,使用方法也和元标签类似,通过调用yii\web\View::registerLinkTag(),例如,在内容视图中注册链接标签,如下所示:

$this->registerLinkTag([
    'title' => 'Live News for Yii',
    'rel' => 'alternate',
    'type' => 'application/rss+xml',
    'href' => 'http://www.yiiframework.com/rss.xml/',
]);

上述代码会转换成:

<link title="Live News for Yii" rel="alternate" type="application/rss+xml" href="http://www.yiiframework.com/rss.xml/">

和yii\web\View::registerMetaTag()类似,我们可以通过指定键来避免生成重复的链接标签。

视图事件

yii\base\View视图组件会在视图渲染过程中触发几个事件,我们可以在内容发送给终端用户前,通过响应这些事件来添加内容到视图中或是处理相应的渲染结果。

  • yii\base\View:EVENT_BEFORE_RENDER:在控制器渲染文件开始时触发,该事件可以设置yii\base\ViewEvent::isValid为false来取消视图渲染
  • yii\base\View::EVENT_AFTER_RENDER:在布局中调用yii\base\View::beginPage()时触发,该事件可以获取yii\base\ViewEvent::output的渲染结果,可以通过修改该属性来修改渲染结果
  • yii\base\View::EVENT_BEGIN_PAGE:在布局调用yii\base\View::beginPage()时触发
  • yii\base\View::EVENT_END_PAGE:在布局调用yii\base\View::endPage()时触发
  • yii\base\View::EVENT_BEGIN_BODY:在布局调用yii\base\View::beginBody()时触发
  • yii\base\View::EVENT_END_BODY:在布局调用yii\base\View::endBody()时触发

例如,如下代码将当前日期添加到页面结尾处:

\Yii::$app->view->on(View::EVENT_END_BODY, function () {
    echo date('Y-m-d');
});

渲染静态页面

静态页面指的是大部分内容为静态的不需要控制器传递动态数据的Web页面

我们可以将HTML代码放在视图中,然后在控制器中使用如下代码输出静态页面:

public function actionAbout()
{
    return $this->render('about');
}

如果Web站点包含很多静态页面,多次重复相似的代码会显得很繁琐,为了解决这个问题,我们可以使用控制器中的一个被称为yii\web\ViewAction的独立操作。例如:

namespace app\controllers;

use yii\web\Controller;

class SiteController extends Controller
{
    public function actions()
    {
        return [
            'page' => [
                'class' => 'yii\web\ViewAction',
            ],
        ];
    }
}

现在,如果你在@app/views/site/pages目录下创建名为about的视图,可以通过如下的URL显示该视图:

http://hostname/index.php?r=site/page&view=about

GET中的view参数告知yii\web\ViewAction操作,确定要请求哪个视图,然后操作在@app/views/site/pages目录下寻找该视图,可以通过配置yii\web\WebAction::viewPrefix来修改搜索视图的目录。

最佳实践

视图负责将模型的数据以用户想要的格式展示,总的来说,视图:

  • 应该主要包含展示代码,如HTML,和简单的PHP代码来控制、格式化和渲染数据
  • 不应该包含执行数据查询的代码,这种类型的代码最好放在模型中
  • 应该避免直接访问请求数据,如$_GET$_POST,这种应该放在控制器中执行,如果需要请求数据,应该由控制器推送到视图
  • 可以读取模型属性,但不应该修改它们

为了使模型更加易于维护,避免创建太复杂或者包含太多冗余代码的视图,为了达到这个目标我们可以遵循以下方法:

  • 使用布局来展示公共代码(如,页面头部、尾部)
  • 将复杂的视图拆分成几个小视图,可以使用上面描述的渲染方法将这些小视图渲染并组装成大视图
  • 创建并使用小部件作为视图的数据块
  • 创建并使用助手类在视图中转换和格式化数据

Category: PHP Develop

Comments