Yii 2 组件(Widget)完全指南:复用视图构建块的原理、用法与最佳实践
2026/9/23 16:30:47 网站建设 项目流程

Yii 2 组件(Widget)完全指南:复用视图构建块的原理、用法与最佳实践

【免费下载链接】yii2Yii 2: The Fast, Secure and Professional PHP Framework项目地址: https://gitcode.com/gh_mirrors/yi/yii2

本文基于仓库内 structure-widgets 指南(含对应的 英文版)编写,并对照框架核心类 framework/base/Widget.php 与测试用例 tests/framework/base/WidgetTest.php 进行源码级佐证。Widget(组件)是 Yii 2 中面向对象地复用视图代码的核心机制,本篇将带你掌握其两种使用方式(widget()begin()/end())、如何自定义属于自己的 Widget、如何用视图文件承载大段渲染内容,以及自包含设计等最佳实践。

什么是 Widget

Widget 是用于在 视图(views) 中创建复杂、可配置用户界面元素的可复用构建块,以面向对象的方式组织视图代码。例如,一个日期选择器(date picker)Widget 可以生成漂亮的日历控件,让用户挑选日期作为表单输入——你只需要在视图里插入一行代码:

<?php use yii\jui\DatePicker; ?> <?= DatePicker::widget(['name' => 'date']) ?>

Yii 2 框架内置了大量开箱即用的 Widget,例如:

  • yii\widgets\ActiveForm(Active Form,活动表单)
  • yii\widgets\Menu(菜单)
  • yii\widgets\Breadcrumbs(面包屑)
  • yii\widgets\ListView/yii\widgets\DetailView(列表与详情视图)
  • yii\widgets\LinkPager/yii\widgets\LinkSorter(分页与排序)
  • yii\widgets\Pjaxyii\widgets\Blockyii\widgets\FragmentCacheyii\widgets\Spaceless

你可以在 framework/widgets 目录下看到这些内置 Widget 的完整源码。此外,官方还提供了 jQuery UI Widget 与 Twitter Bootstrap Widget 等扩展包。下面我们先介绍 Widget 的基础知识;如需了解某个特定 Widget 的详细用法,请查阅其类 API 文档。

关于 MVC 中视图的角色,可参阅 structure-views 指南;Widget 的初始化参数本质上是配置(configuration)数组。

使用 Widget:两种调用范式

Widget 主要在视图(views)中使用,Yii 提供了两种调用范式:自包含式Widget::widget()成对包裹式Widget::begin()/Widget::end()

方式一:widget()方法

调用[[yii\base\Widget::widget()]]即可在视图中使用一个 Widget。该方法接收一个配置(configuration)数组来初始化 Widget,并返回该 Widget 的渲染结果

例如,下面的代码插入一个配置为俄语界面、并把所选日期写入$modelfrom_date属性的日期选择器:

<?php use yii\jui\DatePicker; ?> <?= DatePicker::widget([ 'model' => $model, 'attribute' => 'from_date', 'language' => 'ru', 'clientOptions' => [ 'dateFormat' => 'yy-mm-dd', ], ]) ?>

源码视角widget()的实现位于 framework/base/Widget.php。其执行流程为:

  1. 调用ob_start()开启输出缓冲,防止run()内直接echo的内容污染返回值;
  2. 把调用类名写入$config['class'],通过Yii::createObject($config)创建并配置实例;
  3. 依次调用beforeRun()run()afterRun($result)
  4. 返回ob_get_clean() . $out,即「缓冲内输出 + run() 返回值」拼接的结果;
  5. 若执行过程中抛出异常,会先清理输出缓冲再重新抛出,避免破坏外层输出。

其中beforeRun()/afterRun()会触发EVENT_BEFORE_RUN/EVENT_AFTER_RUN事件(见下文「Widget 生命周期与事件钩子」),这也是 2.0.11 引入的可扩展点。

方式二:begin()end()方法

有些 Widget 需要包裹一段内容块,此时应把内容放在[[yii\base\Widget::begin()]][[yii\base\Widget::end()]]之间。例如,下面的代码使用[[yii\widgets\ActiveForm]]生成一个登录表单:Widget 会在begin()end()被调用的位置分别生成<form>的开闭标签,两者之间的所有内容原样渲染在表单内部。

<?php use yii\widgets\ActiveForm; use yii\helpers\Html; ?> <?php $form = ActiveForm::begin(['id' => 'login-form']); ?> <?= $form->field($model, 'username') ?> <?= $form->field($model, 'password')->passwordInput() ?> <div class="form-group"> <?= Html::submitButton('Login') ?> </div> <?php ActiveForm::end(); ?>

注意与widget()不同:[[yii\base\Widget::begin()]]返回的是 Widget 实例本身,你可以用这个实例继续构建内容(例如上面的$form->field()),而end()会把run()的渲染结果直接echo出来。

源码视角(framework/base/Widget.php):

  • begin():把调用类写入配置后通过Yii::createObject()创建实例,并压入静态栈Widget::$stack
  • end():从栈顶弹出实例,校验类名匹配后执行beforeRun()run()afterRun(),并将结果echo输出;
  • begin()end()未正确配对(例如交叉嵌套),end()会抛出yii\base\InvalidCallException。测试 tests/framework/base/WidgetTest.php 中的testStackTrackingtestStackTrackingDisorder分别验证了「无 begin 直接 end」与「嵌套顺序错乱」两种异常场景。

重要提示:部分 Widget 会在end()时借助 PHP 的输出缓冲(output buffering)来调整被包裹的内容,因此begin()end()应当写在同一个视图文件中;违反这一规则可能导致意外的输出结果。

使用 DI 容器配置 Widget 的全局默认值

某种 Widget 的全局默认配置可以通过依赖注入(DI)容器统一设置。例如,让所有LinkPager分页组件默认最多显示 5 个按钮:

\Yii::$container->set('yii\widgets\LinkPager', ['maxButtonCount' => 5]);

这样,应用中任何地方渲染LinkPager时都会套用该默认值,除非在具体调用处显式覆盖。这正是「依赖注入容器指南中的实际用法一节」所描述的场景——因为widget()begin()内部都是通过Yii::createObject()创建实例,所以 DI 容器的定义在创建阶段就会生效。

创建自己的 Widget

根据需求,自定义 Widget 有两条创建路径,二者都要求继承[[yii\base\Widget]]并重写init()和/或run()方法

  • init():通常放置属性初始化/归一化的代码(在构造函数末尾被调用,见 framework/base/Widget.php);
  • run():通常放置生成渲染结果的代码,结果可以直接echo,也可以作为字符串返回。

路径一:基于widget()的自包含 Widget

下面的HelloWidget会对message属性做 HTML 编码后输出;若未设置该属性,则默认显示 "Hello World":

namespace app\components; use yii\base\Widget; use yii\helpers\Html; class HelloWidget extends Widget { public $message; public function init() { parent::init(); if ($this->message === null) { $this->message = 'Hello World'; } } public function run() { return Html::encode($this->message); } }

在视图中使用它:

<?php use app\components\HelloWidget; ?> <?= HelloWidget::widget(['message' => 'Good morning']) ?>

路径二:基于begin()/end()的包裹式 Widget

下面的变体把begin()end()之间的内容捕获下来,经 HTML 编码后输出:

namespace app\components; use yii\base\Widget; use yii\helpers\Html; class HelloWidget extends Widget { public function init() { parent::init(); ob_start(); } public function run() { $content = ob_get_clean(); return Html::encode($content); } }

可以看到:init()中启动 PHP 输出缓冲,于是init()run()之间的任何输出都会被捕获,在run()中统一处理并返回。

提示:调用begin()时,会创建 Widget 的新实例,并在构造函数的末尾立即调用init();调用end()时,run()会被执行,其返回值由end()直接 echo 出来。

使用这个新变体:

<?php use app\components\HelloWidget; ?> <?php HelloWidget::begin(); ?> 这里可以是任意内容,例如包含一个或多个 <strong>HTML</strong> <pre>标签</pre> 如果内容过大,请考虑拆分成子视图: <?php echo $this->render('viewfile'); // 注意:这里的 render() 属于 \yii\base\View,因为此代码位于视图文件中,而非 Widget 类文件中 ?> <?php HelloWidget::end(); ?>

用视图文件承载大段内容

有时 Widget 需要渲染大段内容。虽然可以把所有内容写进run(),但更佳实践是放入一个视图文件,再用[[yii\base\Widget::render()]]渲染

public function run() { return $this->render('hello'); }

默认情况下,Widget 的视图文件应存放在WidgetPath/views目录下(WidgetPath即存放 Widget 类文件的目录)。因此上例会渲染@app/components/views/hello.php(假设 Widget 类位于@app/components目录)。

源码视角:目录的确定逻辑在getViewPath()(framework/base/Widget.php):通过反射取得类文件所在目录,拼接DIRECTORY_SEPARATOR . 'views'。你可以重写该方法来自定义 Widget 视图目录。此外:

  • render()内部委托给getView()返回的视图对象(默认为应用组件Yii::$app->getView(),见 framework/base/Widget.php);
  • render()支持的视图名称格式包括路径别名(如@app/views/site/index)、以//开头的应用内绝对路径、以/开头的模块内绝对路径,以及相对viewPath的相对路径;未写扩展名时默认补.php(见 framework/base/Widget.php)。

Widget 生命周期与事件钩子

从源码可以确认,Yii 2.0.11+ 为 Widget 内置了三个事件(常量定义见 framework/base/Widget.php):

事件触发时机说明
EVENT_INITinitinit()被调用时在构造函数末尾触发,可用于初始化逻辑
EVENT_BEFORE_RUNbeforeRun执行run()之前事件处理器可将WidgetEvent::$isValid置为false取消本次执行
EVENT_AFTER_RUNafterRun执行run()之后事件处理器可修改WidgetEvent::$result改写渲染结果

事件参数对象为yii\base\WidgetEvent(源码见 framework/base/WidgetEvent.php),其中$isValid默认true$result保存 Widget 返回值。

测试 tests/framework/base/WidgetTest.php 中的testEventstestPreventRun验证了这两个钩子的实际行为:前者依次输出<init><before-run>、run 结果与<after-run>的拼接;后者通过把isValid置为false使 Widget 完全不执行(输出为空字符串)。

其他实用机制

  • 自动 ID 生成:未显式指定id时,Widget 会以static::$autoIdPrefix(默认'w')加自增计数器生成形如w0w1的 ID,见 framework/base/Widget.php。
  • DI 与类名解析begin()会把「调用类 → 实际创建类」的映射记录在静态变量中,使end()在「通过 DI 容器将某 Widget 类替换为子类」时仍能正确配对(对应测试testDependencyInjection,见 tests/framework/base/WidgetTest.php)。
  • 框架内置 Widget 目录:全部内置实现位于 framework/widgets,包括ActiveFormMenuListViewDetailViewBreadcrumbsLinkPagerPjax等;以Menu为例,其类注释给出了多级菜单的用法示例(含itemsurlvisible等配置),源码见 framework/widgets/Menu.php。

最佳实践

Widget 是面向对象地复用视图代码的方式。创建 Widget 时应遵循以下原则:

  1. 遵循 MVC 模式:逻辑放在 Widget 类中,表现(呈现)放在视图(views)中,二者职责分离。
  2. 设计为自包含(self-contained):使用一个 Widget 时,应当能「即插即用」——把它放进视图即可,无需额外做任何事。这一点在 Widget 依赖外部资源(CSS、JavaScript、图片等)时会变得棘手;幸运的是,Yii 提供了资源包(asset bundles)机制来解决,Widget 可以通过资源包声明并自动加载自己所需的静态资源,从而保持自包含。
  3. 纯视图型 Widget 与视图的关系:当 Widget 只包含视图代码时,它与一个视图(view)非常相似。二者的唯一区别在于:Widget 是一个可分发(redistributable)的类,而视图只是一段更愿意保留在应用内部的普通 PHP 脚本。因此,如果你希望把一段视图能力打包成可复用的分发单元,就做成 Widget;否则直接用视图即可。

小结

Widget 是 Yii 2 视图层复用的基石:widget()适合自包含、返回字符串的场景;begin()/end()适合包裹内容块的场景;继承yii\base\Widget并重写init()/run()即可创建自定义 Widget;大段渲染内容推荐放入WidgetPath/views目录并由render()渲染。结合 DI 容器配置全局默认值、beforeRun/afterRun事件钩子以及资源包机制,可以让 Widget 既灵活又自包含。以上原理均可在 framework/base/Widget.php 及 tests/framework/base/WidgetTest.php 中得到验证,建议读者在编写自己的 Widget 前通读这两份文件。

【免费下载链接】yii2Yii 2: The Fast, Secure and Professional PHP Framework项目地址: https://gitcode.com/gh_mirrors/yi/yii2

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询