• 浅析Yii2的view层设计


    Yii2.0的view层提供了若干重要的功能:assets资源管理,widgets小组件,layouts布局...

    下面将通过对Yii2.0代码直接进行分析,看一下上述功能都是如何实现的,当然细枝末节的东西不会过多赘述,如果你对此感兴趣又懒得自己去翻代码和文档,那么这篇博客可以快速的给你一个系统的认识。

    基础渲染

    这一节要谈的是view层是如何完成基础工作的,也就是根据用户传入的参数渲染出一个html页面。

    用法

    我们在controller里调用$this->render方法,第一个参数是要套用的模板文件(别名),第二个参数是用户数据用于填充模板。

    1
    2
    3
    4
    public function actionIndex()
    {
        return $this->render('index', ['param' => 'hello world']);
    }  

    布局模板和子模板的关系

    controller会直接将请求代理给view,这个view也就是mvc的中的v,在整个框架中是一个单例对象。首先通过view->render方法渲染出index这个模板得到的结果保存到$content,接着调用了controller->renderContent($content),这是做什么呢?

    1
    2
    3
    4
    5
    public function render($view$params = [])
    {
        $content $this->getView()->render($view$params$this);
        return $this->renderContent($content);
    }

    原来,renderContent会找到controller对应的布局layouts文件,并将$content填充到布局文件中,最终才能渲染出完整的页面。其实,layouts布局本身也是一个模板文件,它需要的参数就是content,代表了子模板文件渲染后的结果,这个设计很巧妙。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public function renderContent($content)
    {
        $layoutFile $this->findLayoutFile($this->getView());
        if ($layoutFile !== false) {
            return $this->getView()->renderFile($layoutFile, ['content' => $content], $this);
        else {
            return $content;
        }
    }

    上述代码很简单,先找到布局文件(1个controller可以配置1个),然后调用view->renderFile渲染布局模板,传入子模板的渲染结果,就得到了完整页面。

    特别提一下,上面子模板渲染用的view->render,而布局模板用的view->renderFile,其区别是render传入的模板是一个别名(这里是index),而renderFile是直接传入模板的文件路径,这里的设计哲学是:view只负责查找模板文件&渲染模板,而布局文件是controller自己设计的概念,所以布局模板的查找是controller负责的,而模板按别名查找是view的职责。

    填充模板

    无论是布局还是子模板,在填充时都是通过view->renderPhpFile方法实现的,它用到了php的ob库实现数据的捕捉,实现非常简单:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public function renderPhpFile($_file_$_params_ = [])
    {
        ob_start();
        ob_implicit_flush(false);
        extract($_params_, EXTR_OVERWRITE);
        require($_file_);
     
        return ob_get_clean();
    }
    • ob_start():创建1个新的用户级输出缓冲区,捕获输出的内容到内存。
    • ob_implicit_flush(false):设置SAPI级的输出缓冲区模式,不自动刷新。
    • ob_get_clean():得到当前用户缓冲区的内容并删除当前的用户缓冲区。

    如果你对ob系列函数不了解,可以点访问官方文档。如果你对用户,SAPI缓冲区不了解,可以访问这里

    总之,ob_start后所有echo输出都会被缓存起来,然后通过extract方法可以将用户参数params解开为局部变量,最后通过require包含模板文件,这样模板文件就可以直接按局部变量$var1,$var2的方式访问方便的访问$params里的数据了,这个函数最后将缓冲的数据全部取出返回,完成了模板的渲染。

    举个例子

    这里拿布局文件为例(因为它本身也是一个模板),看看模板文件可以做什么事情:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    <?php
     
    /* @var $this yiiwebView */
    /* @var $content string */
     
    use yiihelpersHtml;
    use yiiootstrapNav;
    use yiiootstrapNavBar;
    use yiiwidgetsBreadcrumbs;
    use frontendassetsAppAsset;
    use commonwidgetsAlert;
     
    ?>
     
    AppAsset::register($this);
     
    <?php $this->beginPage() ?>
    <!DOCTYPE html>
    <html lang="<?= Yii::$app->language ?>">
    <head>
        <meta charset="<?= Yii::$app->charset ?>">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title><?= Html::encode($this->title) ?></title>
        <?php $this->head() ?>
    </head>
    <body>
    <?php $this->beginBody() ?>
     
    <?php
    NavBar::begin([
        'brandLabel' => 'My Company',
        'brandUrl' => Yii::$app->homeUrl,
        'options' => [
            'class' => 'navbar-inverse navbar-fixed-top',
        ],
    ]);
    ?>
     
    <div class="wrap">
        <?php echo $content ?>
    </div>
     
    <?php
    echo Nav::widget([
        'options' => ['class' => 'navbar-nav navbar-right'],
        'items' => $menuItems,
    ]);
     
    NavBar::end();
     
    ?>
     
    <footer class="footer">
        <?php echo "yuerblog.cc" ?>
    </footer>
     
    <?php $this->endBody() ?>
    </body>
    </html>
    <?php $this->endPage() ?>

    可见,模板文件也是一个普通php文件,只不过它写了很多html标签而已。在模板文件中可以直接访问$this,它代表了view对象,因为模板文件是在view对象的方法里require进来的,因此是可以直接访问的,PHP脚本语言的确够灵活。

    对于布局模板来说,可以直接访问$content获取子模板的渲染结果,上面有所体现。另外,beginXXX和endXXX是很核心的函数,后续在assets和widget中会看到具体作用。

     assets资源管理

    我们开发各种页面的时候,一般都需要引入css和js文件,普通的做法就是在模板文件中直接通过<link>和<scipt>来引入就可以了。

    现在假想一个问题:如果我们使用了布局文件的话,整个html的head部分是共用同一份代码的,每个子模板依赖的css和js各不相同,这该怎么引入呢?

    这其实就是beginPage,head,beginBody,endBody,endPage存在的意义了,它们相当于在布局文件的合适位置"先占上坑",以便子模板可以通过代码控制向"坑"里填充需要的东西,也就是实现了父子模板之间的沟通,同时也是一种延迟填充的策略:先占坑,后填坑,从程序角度来讲就是先写占位符,后替换字符串。

    知道了beginXXX,endXXX的意义后,那么assets的意义又是什么呢?它其实就是基于上述机制,通过创建assets类的方式,简化引入css和js的工作,也就是不需要你再去写<script>和<link>这种代码了,这就是资源管理。

    实现

    回头看上面的布局文件,里面有一行:

    1
    AppAsset::register($this);
    在布局文件里直接引入这个assets类,说明它引入的资源是所有子模板都需要的,当然你可以在某个子模板里引入其他的assets。

    AppAssets是自定义的,它继承了基类AssetsBundle,配置了引入的资源:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class AppAsset extends AssetBundle
    {
        public $basePath '@webroot';
        public $baseUrl '@web';
        public $css = [
            'css/site.css',
        ];
        public $js = [
        ];
        public $depends = [
            'yiiwebYiiAsset',
            'yiiootstrapBootstrapAsset',
        ];
    }

    可见,这里指定了css和js文件是相对于@webroot的,这里也就是相对于frontend/web,因此css文件应该部署在fronend/web/css/site.css,并且这里还依赖了2个其他的资源也会被递归包含。

    那么register方法做了什么呢?最终结果,就是拼装出site.css的url作为key,然后<link ...>标签作为value,保存到view对象的一个属性里暂存,用于后续"填坑"备用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public function registerAssetFiles($view)
    {
        $manager $view->getAssetManager();
        foreach ($this->js as $js) {
            if (is_array($js)) {
                $file array_shift($js);
                $options = ArrayHelper::merge($this->jsOptions, $js);
                $view->registerJsFile($manager->getAssetUrl($this$file), $options);
            else {
                $view->registerJsFile($manager->getAssetUrl($this$js), $this->jsOptions);
            }
        }
        foreach ($this->css as $css) {
            if (is_array($css)) {
                $file array_shift($css);
                $options = ArrayHelper::merge($this->cssOptions, $css);
                $view->registerCssFile($manager->getAssetUrl($this$file), $options);
            else {
                $view->registerCssFile($manager->getAssetUrl($this$css), $this->cssOptions);
            }
        }
    }

    如果追踪代码,会发现上述AppAssets::register最终进入了AssetsBundle基类的这个方法,它将自己的css和js逐个注册到view方法中,这样view中就采集了模板文件中所有assets引入的css和js文件,能够做一个去重避免重复引入相同的文件,因为不同的assets可能引入相同的css or js文件,可以想到这样也可以实现布局模板和子模板之间的相同资源去重,非常聪明。

    另外,$js[]里的每个js文件可以通过position选项配置其引入的位置,也就是可以引入在beginBody之后,或者endBody之前,或者header里,这就体现了此前beginXXX的另外一个存在意义。

    Widget

    组件,这个东西其实和现在前端开发提倡的组件化开发是一个道理,只不过在PHP里是服务端渲染,因此组件是PHP代码来实现的,最终运行时widget类输出的其实就是html代码了。

    组件当然是为了复用性考虑,比如:封装一个列表组件,然后通过传入一个数组就可以渲染出<ul>列表了。

    组件也有高度的内聚性,它内部可以使用其他widget,可以通过assets引入所需的css/js资源,它是自治的。

    实现

    回到之前的布局文件,里面用到了2个widget,一个是NavBar是导航列表,一个是Nav是导航项,前者体现了widget::begin,widget::end的widget用法,后者体现了widget::widget的用法,我们分别看看原理既可。

    NavBar

    当我们调用NavBar::begin()的时候,Widget基类会创建一个NavBar对象并推到数据结构stack中维护,这是因为begin和end是配对使用的,是允许嵌套出现的,例如NavBar中再嵌套一个NavBar,因此必须用stack维护,以便end和begin可以配对。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public static function begin($config = [])
    {
        $config['class'] = get_called_class();
        /* @var $widget Widget */
        $widget = Yii::createObject($config);
        static::$stack[] = $widget;
     
        return $widget;
    }
    这里注意,createObject实际上会创建NavBar对象并调用它的init,因此NavBar会在自己的init函数中输出自己的开始标签,比如:<ul>,同时也可以引入各种需要的assets或者注册一些head信息到view,这样后续"填坑"阶段可以替换到html中,保证组件想要的东西都可以引入。

    在NavBar::end()调用的时候,Widget基类会调用NavBar对象的run()方法,这时候NavBar会输出自己的结束标签,例如:</ul>。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public static function end()
    {
        if (!empty(static::$stack)) {
            $widget array_pop(static::$stack);
            if (get_class($widget) === get_called_class()) {
                echo $widget->run();
                return $widget;
            else {
                throw new InvalidCallException('Expecting end() of ' . get_class($widget) . ', found ' . get_called_class());
            }
        else {
            throw new InvalidCallException('Unexpected ' . get_called_class() . '::end() call. A matching begin() is not found.');
        }
    }

    Nav

    当我们调用Nav::widget()的时候,Widget基类会立即分配一个Nav对象,调用它的run方法,用ob_start捕获它的输出,通过返回值返回到模板文件中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public static function widget($config = [])
     {
         ob_start();
         ob_implicit_flush(false);
         try {
             /* @var $widget Widget */
             $config['class'] = get_called_class();
             $widget = Yii::createObject($config);
             $out $widget->run();
         catch (Exception $e) {
             // close the output buffer opened above if it has not been closed already
             if (ob_get_level() > 0) {
                 ob_end_clean();
             }
             throw $e;
         }
     
         return ob_get_clean() . $out;
     }

    最后的填坑

    当我们知道了view,assets,widget的原理之后,我们最后看一下"填坑阶段",view是如何把此前在布局文件、子模板文件以及组件中注册的css、js、head信息填充到最终html页面中的吧。

    占坑部分

    简单看一下占坑的原理。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    /**
       * Marks the beginning of a page.
       */
      public function beginPage()
      {
          ob_start();
          ob_implicit_flush(false);
     
          $this->trigger(self::EVENT_BEGIN_PAGE);
      }

    此前,renderPhpFile中是在开启了ob_start后require模板文件的,为什么view->beginPage再次开启了ob捕获呢?我想这主要是因为view需要在endPage的时候对html进行"填坑",因此需要在renderPhpFile之前捕捉到输出。而renderPhpFile能不能免去ob_start()调用呢?不能,因为模板文件可以不使用beginXXX,endXXX,这种情况下输出的捕捉还是要renderPhpFile来完成。

    填坑部分

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public function endPage($ajaxMode = false)
     {
         $this->trigger(self::EVENT_END_PAGE);
     
         $content = ob_get_clean();
     
         echo strtr($content, [
             self::PH_HEAD => $this->renderHeadHtml(),
             self::PH_BODY_BEGIN => $this->renderBodyBeginHtml(),
             self::PH_BODY_END => $this->renderBodyEndHtml($ajaxMode),
         ]);
     
         $this->clear();
     }

    在endPage里,从ob取出完整的html输出后,对$content进行了一次内容替换,也就是"填坑"。它将html中的PH_HEAD,PH_BODY_BEGIN,PH_BODY_END三个占位符替换成了模板渲染过程中注册到view中的js,css资源和head信息,那么PHP_HEAD这些占位符其实就是通过此前在布局文件中见到的head(),beginBody(),endBody()调用输出的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    /**
     * This is internally used as the placeholder for receiving the content registered for the head section.
     */
    const PH_HEAD = '<![CDATA[YII-BLOCK-HEAD]]>';
    /**
     * This is internally used as the placeholder for receiving the content registered for the beginning of the body section.
     */
    const PH_BODY_BEGIN = '<![CDATA[YII-BLOCK-BODY-BEGIN]]>';
    /**
     * This is internally used as the placeholder for receiving the content registered for the end of the body section.
     */
    const PH_BODY_END = '<![CDATA[YII-BLOCK-BODY-END]]>';
     
        /**
     * Marks the position of an HTML head section.
     */
    public function head()
    {
        echo self::PH_HEAD;
    }
     
    /**
     * Marks the beginning of an HTML body section.
     */
    public function beginBody()
    {
        echo self::PH_BODY_BEGIN;
        $this->trigger(self::EVENT_BEGIN_BODY);
    }
  • 相关阅读:
    网页背景音乐
    CSS 实现背景半透明
    实战中总结出来的CSS常见问题及解决办法
    AsyncTask的简单使用
    ORMLiteDatabase的简单使用并且与其他的表相互联系
    传递消息--第三方开源--EventBus的简单使用
    通过messenger实现activity与service的相互通信
    通过Messenger与后台连接(单向操作,activity向service发送数据)
    怎么做QQ、微信等消息气泡
    通过bind实现activity与service的交互
  • 原文地址:https://www.cnblogs.com/xieqing/p/6520151.html
Copyright © 2020-2023  润新知