使用Yii2的Pjax和片段缓存来大大提升页面速度

在开发网站时,经常会发现最占用大多数访问量的页面却是列表展示页,这是很常见的情况。对于博客而言,书签收藏量最多的通常是首页,而它的加载速度可能也是最慢的。而且还有页面分页、按分类展示等等…

如果您正在使用Yii2,那么可以使用Pjax和片段缓存使这个页面飞快起来。

Pjax 简略版本如下。

Yii 1.1 的 CGridView/CListView 是一个使用 Ajax 默认更新 DOM 内容的小部件。它在分页时避免了不必要的重新加载,速度很快,这是很好的。但是对于初学者来说,他们可能会对这种特殊的行为感到困惑。

Yii 2 的 GridView/ListView 导航现在输出常规的超链接。它不再是一个特殊处理的小部件,而是输出普通的HTML,这样不仅理解起来更简单,而且执行起来也没有了冗余。

哎呀,不过1.1版的功能是不是下降了呢?不,不用担心。Yii 2中,GridView/ListView从DOM部分更新和操纵浏览器历史的history.pushState中剥离出来,并作为一个通用的Pjax实现。只要使用它,即使不使用特殊的小部件,也能实现通过Ajax获取的HTML来进行DOM的部分更新的设计和构建列表页面。

假设只有以下区域进行页面切换的情况下:

    <div class="posts">
        <?php foreach($postsQuery->all() as $post): ?>
            <div class="post">
                ...
            </div>
        <?php endforeach; ?>
    </div>
    <div class="pagination">
        <ul>
            <?php foreach($pages as $page): ?>
                <li><?= Html::a(Html::encode($page), ['index', 'page'=>$page]) ?></li>
            <?php endforeach; ?>
        </ul>
    </div>

使用Pjax的分页方法如下。

<?php
use yii\widgets\Pjax;
?>

<?php Pjax::begin([
    'linkSelector' => '#posts-pjax-region .pagination a',
    'options' => [
        'id' => '#posts-pjax-region'
    ]
]); /* ここから */ ?>
    <div class="posts">
        <?php foreach($postsQuery->all() as $post): ?>
            <div class="post">
                ...
            </div>
        <?php endforeach; ?>
    </div>
    <div class="pagination">
        ...
    </div>
<?php Pjax::end(); /* ここまで */ ?>

Pjax小工具会劫持linkSelector选择符所指示的元素的点击事件,并通过Ajax将请求发送到真实的链接目标,然后用返回的HTML重写DOM。不会重新请求CSS、JS或图像。侧边栏的Twitter小工具和引人注目的动画广告也不会重新加载。(哦,广告最好改变一下→见后文)

在示例中为了简单明了起见写出了选项,但只要简单地使用 也能够运行。在区域内的 标签中,具有相同动作的标签将自动转化为 Pjax 链接。

如果服务器端的请求处理正在渲染包含布局的整个HTML页面,并且检测到是Pjax请求,则从HTML中提取与该区域对应的节点(以及TITLE标签),然后将其作为响应返回。

不仅仅是在客户端上更快。只使用相关部分的话,相反地,如果 Yii::$app->request->isPjax 是开启的话,可以不用太过介意 Pjax 区域之外的 DOM 部分。如果在 Pjax 区域之外有一个向数据库发起重查询的小部件,可以跳过该操作。对于页面上有登录用户高级功能的头部和底部,跳过它们是非常值得的。

片段缓存

在主要内容很重的情况下,仅使用Pjax来减轻负载是困难的。这时候缓存视图就发挥作用了。缓存视图是指如果能够缓存整个HTML页面,那么定期地导出静态内容也没关系,而且可以使用Nginx或者Vernish。Yii 2中有一种叫做”片段缓存”的方法,可以缓存HTML片段。

请先确认 Yii::$app->cache 是否已经实现了缓存功能,无论使用何种方式实现,只要能够实现缓存即可,如果没有 memcache,也可以使用文件缓存。

如果可以的话,我将尝试使用片段缓存来解决之前的复杂示例。这样做就是这样的。

<?php if ($this->beginCache('post_list', [
    'variations' => [
        'page' => Yii::$app->request->get('page', 1)
    ],
    'dependency' => [
        'class' => 'yii\caching\DbDependency',
        'sql' => 'SELECT MAX(updated_at) FROM posts',
    ],
    'duration' => 180, // sec
])): /* ここから */ ?>
    <div class="posts">
        <?php foreach($postsQuery->all() as $post): ?>
            <div class="post">
                ...
            </div>
        <?php endforeach; ?>
    </div>
    <div class="pagination">
        ...
    </div>
    <?php $this->endCache(); ?>
<?php endif; /* ここまで */ ?>

只需简单地摘要即可。太容易了。这个片段缓存是一项非常受欢迎的功能,因为View直接支持。

缓存的键由第一个参数和变量的组合确定。它需要一个唯一的名称来表示“这个页面的这一部分”,以及一个与查询字符串分开的单独的缓存。如果还有其他变量,也请全部添加到变量中。

依赖性用来指定“在预定期限之前即使缓存失效的条件”。例如,如果在数据库查询中出现与先前不同的值,缓存将失效。当然,也可以查询其他不同于数据库的数据源。

全体都是一个if语句,这是关键。如果缓存命中,整个处理过程会被跳过,包括看起来最重的$postsQuery->all()部分。太好了。

中似乎还有许多会产生频繁访问相关表的元素。

当我在简单的页面上进行测试时,第一次查询的响应时间是16个查询,耗时140毫秒,而后续的查询则变为6个查询,耗时88毫秒。这是一个实际工作中的例子,我成功将相当复杂页面的响应时间从200至400毫秒稳定地降低到70毫秒左右。

在这里,请记住 Yii 使用查询或数据提供程序等方式,直到实际渲染到视图之前不会访问数据库。Yii 的 ActiveRecord 也是默认延迟加载的。

在某种将所有数据都从控制器传送到模板引擎中,并且最终在 MVC 框架中对数据库进行同样多次查询的情况下,即使视图中有片段缓存,也无法实现关键的数据库访问避免。

通过在业务逻辑中创建查询,并坚持 Yii 的风格,在真正显示之前不获取,就不会出现需要关注缓存是否存在并引入 if 条件到逻辑中的情况。

Pjax + 片段缓存 = 极速

然后将这两个元素结合在一起,就能够迅速地完成网页。

<?php
use yii\widgets\Pjax;
?>

<?php Pjax::begin([
    'linkSelector' => '#posts-pjax-region .pagination a',
    'options' => [
        'id' => '#posts-pjax-region'
    ]
]); ?>
    <?php if ($this->beginCache('post_list', [
        'variations' => [
            'page' => Yii::$app->request->get('page', 1)
        ],
        'dependency' => [
            'class' => 'yii\caching\DbDependency',
            'sql' => 'SELECT MAX(updated_at) FROM posts',
        ],
        'duration' => 180, // sec
    ])): ?>
        <div class="posts">
            <?php foreach($postsQuery->all() as $post): ?>
                <div class="post">
                    ...
                </div>
            <?php endforeach; ?>
        </div>
        <div class="pagination">
            ...
        </div>
        <?php $this->endCache(); ?>
    <?php endif; ?>
<?php Pjax::end(); ?>

我认为,如果是个还算不错的服务器,即使是 PHP,每秒钟也可以输出大约 50 到 80 个页面吧。(因为理论上只需要从缓存中返回 HTML)

即使没有使用用于单页面应用程序的技术,也没有做任何特殊处理,普通的应用程序开发者也可以通过普通的 Web 页面应用程序创建,然后只需添加行来实现这一点,而不会破坏它,这就是 Yii 的出色之处。

有各种各样的补充信息

Pjax 事件

从我粗略阅读源代码的感觉来看,Pjax 事件似乎有以下内容。

    • pjax:click
    • pjax:clicked
    • pjax:beforeSend
    • pjax:timeout
    • pjax:complete
    • pjax:end
    • pjax:error
    • pjax:beforeReplace
    • pjax:success
    • pjax:start
    • pjax:send
    pjax:popstate

可以通过以下方式捕获jQuery事件。

<?php $this->registerJs(<<<JS
jQuery('#posts-pjax-region').on('pjax:success', function() {
    console.log('pjax success');
});
JS
) ?>

如果使用这个,当页面变化时,可以将广告换成不同的,甚至可以进行一些小花招。

Pjax 表单提交

Pjax 不只可用于导航,还可用于表单提交。当您希望能够在无需页面跳转的情况下频繁输入数据时,Pjax 将非常有用。

<?php Pjax::begin([
    'formSelector' => '#data-editor form',
    'options' => [
        'id' => '#data-pjax-region'
    ]
]); ?>
    <?= GridView::widget([
        'dataProvider' => $dataProvider,
        'columns' => [
            'name',
            'value',
        ]
    ]) ?>
<?php Pjax::end(); ?>

<div class="form-inline" id="data-editor">
    <?php $form = ActiveForm::begin(); ?>
    <?= $form->field($editorModel, 'name') ?>
    <?= $form->field($editorModel, 'value') ?>
    <?= Html::submitButton('Add', ['class' => 'btn btn-primary']) ?>
    <?php $form->end(); ?>
</div>

Pjax通过formSelector来拦截表单提交,然后使用Ajax进行POST请求,并使用响应来更新Pjax区域。这样就可以在同一页上不断添加数据。

如果保持这样的状态,表单不会被清空,所以可以使用上面的发送成功事件。

在接收方,如果URL是通过POST方法传递过来的,需要添加一个选项来允许添加。对于Pjax请求,不能像通常的重定向一样,而是应该返回更新后的HTML。这里有点不同寻常。

public function actionIndex()
{
    $editorModel = new DataModel();
    if (Yii::$app->request->isPost) {
        if ($editorModel->load(Yii::$app->request->post()) &&
            $editorModel->save()
        ) {
            if (!Yii::$app->request->isPjax) { // Pjaxでない場合のみ
                return $this->redirect(...);
            }
        } else {
            $errorMessage = $editorModel->...; // なんかうまいことやる
        }
    }
    $dataProvider = ...;
    $this->render(...);
}

也许使用REST控制器会更好。

如果你想做到正确的验证,不推荐这种方式。如果写入失败,需要将错误响应放入Pjax区域内,无法在表单中反映。不要太过努力,因为客户端验证可以控制提交按钮,所以就做到这个程度吧。

各种缓存 (gè

在缓存中,不仅可以使用片段缓存,还可以选择将纯数据缓存或者缓存整个HTML文件。

另外,我做了各种尝试,但如果用户没有登录会话,最好优先考虑使用Last-Modified和ETag来利用浏览器缓存。

广告
将在 10 秒后关闭
bannerAds