7. 合理划分子视图
介绍
这是一个比较容易让人忽略的问题,而有时候这个问题却比数据库 N+1 问题还要严重。
接下来我们探讨下视图划分时,需要注意的性能问题。
做个实验
首先在 Debugbar 上打开模板加载视图,并记录响应时间,加载 9 个视图的情况下:
假如我们对 resources/views/topics/_topic_list.blade.php
做优化,将话题显示的内容存放到独自的子模板中,以便后面可以复用此子模板,修改后如下:
resources/views/topics/_topic_list.blade.php
@if (count($topics))
<ul class="list-unstyled">
@foreach ($topics as $topic)
@include('topics._topic')
@if ( ! $loop->last)
<hr>
@endif
@endforeach
</ul>
@else
<div class="empty-block">暂无数据 ~_~ </div>
@endif
新建视图文件 topics._topic
:
resources/views/topics/_topic.blade.php
<li class="media">
<div class="media-left">
<a href="{{ route('users.show', [$topic->user_id]) }}">
<img class="media-object img-thumbnail mr-3" style="width: 52px; height: 52px;" src="{{ $topic->user->avatar }}" title="{{ $topic->user->name }}">
</a>
</div>
<div class="media-body">
<div class="media-heading mt-0 mb-1">
<a href="{{ $topic->link() }}" title="{{ $topic->title }}">
{{ $topic->title }}
</a>
<a class="float-right" href="{{ route('topics.show', [$topic->id]) }}">
<span class="badge badge-secondary badge-pill"> {{ $topic->reply_count }} </span>
</a>
</div>
<small class="media-body meta text-secondary">
<a class="text-secondary" href="{{ route('categories.show', $topic->category_id) }}" title="{{ $topic->category->name }}">
<i class="far fa-folder"></i>
{{ $topic->category->name }}
</a>
<span> • </span>
<a class="text-secondary" href="{{ route('users.show', [$topic->user_id]) }}" title="{{ $topic->user->name }}">
<i class="far fa-user"></i>
{{ $topic->user->name }}
</a>
<span> • </span>
<i class="far fa-clock"></i>
<span class="timeago" title="最后活跃于:{{ $topic->updated_at }}">{{ $topic->updated_at->diffForHumans() }}</span>
</small>
</div>
</li>
这个时候刷新页面,可以看见一些轻微的变化:
以上是输出 20 条数据的情况,为了更好的示范,接下来我们改为 100 条,打开控制器文件 app/Http/Controllers/TopicsController.php
,如下修改:
刷新页面,可以看到加载 100 多个模板文件,响应时间也接近 400 毫秒:
为了方便维护,我们决定,头像部分的 HTML 会在项目的其他地方用到,也要创建子模板,新建子模板:
resources/views/topics/_user_avatar.blade.php
<img class="media-object img-thumbnail mr-3" style="width: 52px; height: 52px;" src="{{ $topic->user->avatar }}" title="{{ $topic->user->name }}">
修改 topics._topic
文件加载模板:
resources/views/topics/_topic.blade.php
<li class="media">
<div class="media-left">
<a href="{{ route('users.show', [$topic->user_id]) }}">
@include('topics._user_avatar')
</a>
</div>
<div class="media-body">
<div class="media-heading mt-0 mb-1">
<a href="{{ $topic->link() }}" title="{{ $topic->title }}">
{{ $topic->title }}
</a>
<a class="float-right" href="{{ route('topics.show', [$topic->id]) }}">
<span class="badge badge-secondary badge-pill"> {{ $topic->reply_count }} </span>
</a>
</div>
<small class="media-body meta text-secondary">
<a class="text-secondary" href="{{ route('categories.show', $topic->category_id) }}" title="{{ $topic->category->name }}">
<i class="far fa-folder"></i>
{{ $topic->category->name }}
</a>
<span> • </span>
<a class="text-secondary" href="{{ route('users.show', [$topic->user_id]) }}" title="{{ $topic->user->name }}">
<i class="far fa-user"></i>
{{ $topic->user->name }}
</a>
<span> • </span>
<i class="far fa-clock"></i>
<span class="timeago" title="最后活跃于:{{ $topic->updated_at }}">{{ $topic->updated_at->diffForHumans() }}</span>
</small>
</div>
</li>
子模板里加载子模板的情况,注意加载视图数量和响应时间,已经是超过 500 毫秒了:
重点:子模板嵌套子模板是 比较危险 的操作,有时会写出来类似于数据库 N+1 的性能问题。例如说一个页面罗列 100 篇文章,每篇文章又有各自的 5 个标签,每个标签的加载都是使用子模板形式加载,那么渲染出来就是 100 * 5 + 100 = 600 文件包含,响应时间一下子可能会慢几倍。
总结
可以看到,这种模板嵌套的方式,如果规划时只考虑 复用性 而不考虑 性能的局限性 的话,有时候会付出高昂的性能代价。
至于可复用性与性能的抉择,建议你从项目的商业逻辑上入手,商业项目建议优先选择性能。因为随着功能不断累积,积年累月项目里每个地方都消耗一点点性能,累积在一起整个项目会响应如蜗牛。
不局限于 Laravel,所有 PHP 项目中文件包含,都是非常昂贵的操作,平时编写代码时,要小心谨慎,养成不滥用的好习惯。
最后为了方便后续文章,我们清理下项目代码:
$ git checkout . && git clean -f