laravel Blade 模板引擎
与视图文件紧密关联的就是模板代码,我们在视图文件中通过模板代码和 HTML 代码结合实现视图的渲染。和很多其他后端语言不同,PHP 本身就可以当做模板语言来使用,但是这种方式有很多缺点,比如安全上的隐患、容易产生业务逻辑与视图模板的耦合,而且在视图文件中到处使用 <?php
内联代码一点都不优雅,甚至是 ugly code,所以你会看到绝大多数现代框架都会提供一套模板引擎,比如 Smarty,Twig,以及 Laravel 使用的 Blade。
注:不同于其他基于 Symfony 的 PHP 框架,Laravel 没有使用 Twig 模板引擎,不过你想要使用的话,可以借助 TwigBridge 扩展包来实现。
Blade 模板引擎是由 Laravel 框架提供的自有实现,借鉴了 .NET 的 Razor 引擎语法,其语法简洁,易于上手,同时提供了强大而直观的继承模型,而且方便扩展。下面是一个简单的 Blade 模板代码示例:
<h1>{{ $group->title }}</h1>
{!! $group->imageHtml() !!}
@forelse ($users as $user)
{{ $user->username }} {{ $user->nickname }}<br>
@empty
该组中没有任何用户
@endforelse
正如你所看到的,Blade 模板引擎有三种常见的语法:
- 通过
{{ }}
渲染 PHP 变量(最常用) - 通过
{!! !!}
渲染原生 HTML 代码(用于富文本数据渲染) - 通过以
@
作为前缀的 Blade 指令执行一些控制结构和继承、引入之类的操作
下面我们就来逐一介绍这些语法。
注:Blade 模板代码存放在以
.blade.php
后缀结尾的视图文件中,最终会被编译为原生 PHP 代码,并缓存起来,直到视图模板有修改才会再次编译,所以拥有与原生 PHP 几乎一致的性能,这些编译后的代码位于storage/framework/views
目录下。你当然可以在 Blade 模板中使用原生 PHP 代码,但是不建议这么做,如果你非要这么做的话,可以通过 @php 指令引入。
渲染数据
首先我们来看一下 {{}}
语法,我们通过通过该语法包裹需要渲染的 PHP 变量,如 {{ $variable }}
,你可以将其类比为 <?php echo $variable; ?>
,但是 Blade 模板代码的功能要更强大,通过 {{}}
语法包裹渲染的 PHP 变量会通过 htmlentities()
方法进行 HTML 字符转义,从而避免类似 XSS 这种攻击,提高了代码的安全性,所以 {{ $variable }}
编译后的最终代码是:
<?php echo htmlentities($variable); ?>
但是某些情况下不能对变量中 HTML 字符进行转义,比如我们在表单通过富文本编辑器编辑后提交的表单数据,这种场景就需要通过 {!! !!}
来包裹待渲染数据了:
{!! $variable !!}
这样编译后的代码就是 <?php echo $variable; ?>
了。
注:对于富文本数据 XSS 攻击防护,可以参考这篇教程。
最后,关于数据变量渲染,我们还要注意的是,很多前端框架也是通过 {{}}
来输出 JavaScript 变量数据的,比如 Laravel 的好基友 Vue.js 就是,对于这种情况,我们需要在渲染前端 JavaScript 变量的 {{}}
前面加上 @
前缀,这样,Blade 模板引擎在编译模板代码的时候会跳过带 @
前缀的 {{}}
数据渲染,并将 @
移除从而可以后续执行对应的 JavaScript 框架渲染逻辑:
// Blade 引擎会将其编译为对应的 PHP 代码
{{ $phpData }}
// Blade 引擎编译时会移除 @,保留 {{ $vueData }} 结构
@{{ $vueData }}
如果要注释一段 PHP 代码,可以通过 {{-- 注释内容 --}}
实现。
控制结构
Blade 中的控制结构语法和 PHP 大同小异,学习成本几乎为零,不过 Blade 为我们额外提供了一些有用的辅助变量和方法,方便我们进行条件判断。
条件语句
@if、@else、@elseif
Blade 模板中的 @if
等价于 PHP 的 <?php if ($condition):
,@else
和 @elseif
依次类推,最后以一个 @endif
收尾:
@if (count($students) === 1)
操场上只有一个同学
@elseif (count($students) === 0)
操场上一个同学也没有
@else
操场上有 {{ count($students) }} 个同学
@endif
和原生 PHP 中的用法如出一辙。
@unless
@unless
是 Blade 提供的一个 PHP 中没有的语法,用于表示和 @if
条件相反的条件,@unless($condition)
可以理解为 <?php if (!$condition):
,然后以 @endunless
收尾:
@unless ($user->hasPaid())
用户支付之后才能享受该服务
@endunless
@isset、@empty
这两个指令和 PHP 中的 isset()
和 empty()
方法等价:
@isset($records)
// 记录被设置
@endisset
@empty($records)
// 记录为空
@endempty
后面两个都是语法糖,如果你不想记太多东西,不防都用 @if
来实现对应的逻辑了。
@switch
顾名思义,Blade 中的 @switch
指令和 PHP 中的 switch
语句等价,我们可以通过 @switch
、@case
、@break
、@default
和 @enswitch
指令构建对应逻辑:
@switch($i)
@case(1)
// $i = 1 做什么
@break
@case(2)
// $i = 2 做什么
@break
@default
// 默认情况下做什么
@endswitch
循环结构
@for、@foreach 和 @while
和 PHP 一样,在 Laravel 中,我们可以通过与之等价的 @for
、@foreach
和 @while
实现循环控制结构,使用语法和 PHP 代码相仿:
// for 循环
@for ($i = 0; $i < $talk->slotsCount(); $i++)
The number is {{ $i }}<br>
@endfor
// foreach 循环
@foreach ($talks as $talk)
{{ $talk->title }} ({{ $talk->length }} 分钟)<br>
@endforeach
// while 循环
@while ($item = array_pop($items))
{{ $item->orSomething() }}<br>
@endwhile
@forelse
这个指令是 PHP 中具备的,可以理解为处理以下 PHP 代码逻辑:
<?php
if ($students) {
foreach ($students as $student) {
// do something ...
}
} else {
// do something else ...
}
在 Blade 模板中我们可以使用 @forelse
指令通过以下代码实现上述逻辑:
@forelse ($students as $student)
// do something ...
@empty
// do something else ...
@endforelse
@foreach 和 @forelse 中的 $loop 变量
在循环控制结构中,我们要重磅介绍的就是 Blade 模板为 @foreach
和 @forelse
循环结构提供的 $loop
变量了,通过该变量,我们可以在循环体中轻松访问该循环体的很多信息,而不用自己编写那些恼人的面条式代码,比如当前迭代索引、嵌套层级、元素总量、当前索引在循环中的位置等,$loop
实例上有以下属性可以直接访问:
下面是一个简单的使用示例:
<ul>
@foreach ($pages as $page)
@if ($loop->first)
// 第一个循环迭代
@endif
<li>{{ $loop->iteration }}: {{ $page->title }}
@if ($page->hasChildren())
<ul> @foreach ($page->children() as $child)
<li>{{ $loop->parent->iteration }}. {{ $loop->iteration }}: {{ $child->title }}</li>
@endforeach
</ul>
@endif
</li>
@if ($loop->last)
// 最后一个循环迭代
@endif
@endforeach
</ul>
有了这个 $loop
变量,确实能够帮我们节省很多重复的逻辑判断和编码工作,推荐使用。
除了基本的数据渲染及控制结构指令之外,Blade 还提供了模板继承和组件引入功能,从而允许视图模板之间继承、覆盖及引入。
通过 @yield
和 @section/@show
在布局文件中定义插槽
在理解 Blade 模板继承的时候,我们可以类比类的继承机制:在父类中定义抽象方法或公共方法,然后在子类中实现抽象方法或重写公共方法。在视图文件中,这个「父类」一般对应布局文件,不同的功能模块往往有不同的页面布局,比如前台、后台、用户中心,页面布局往往不一样。而「子类」则对应不同功能模块的各个子视图页面,比如首页、文章详情页、文章编辑页等等。
我们先来看一个布局文件的示例:
<!-- resources/views/layouts/master.blade.php -->
<html>
<head>
<title>Laravel学院 | @yield('title', '首页')</title>
</head>
<body>
<div class="container">
@yield('content')
</div>
@section('footerScripts')
<script src="{{ asset('js/app.js') }}"></script>
@show
</body>
</html>
在这个布局文件中我们使用了两个 Blade 指令,@yield
用于指定需要子视图继承实现的内容区块,我们可以通过传递第二个参数给该指令用于指定子视图未继承时的默认值,@section/@show
也用于指定子视图需要继承实现的内容区块,并且提供了默认区块内容,与 @yield
不同之处在于,@section/@show
指定的默认内容子视图可以通过 @parent
访问,而 @yield
指定的默认内容对子视图不可见。
通过 @extends
和 @section/@endsection
在子视图实现继承
定义好布局文件后,接下来我们来定义继承布局文件的子视图:
<!-- resources/views/dashboard.blade.php -->
@extends('layouts.master')
@section('title', '管理后台')
@section('content')
环境访问 Laravel 学院后台!
@endsection
@section('footerScripts')
@parent
<script src="{{ asset('js/dashboard.js') }}"></script>
@endsection
在子视图中,我们一一实现了布局文件中定义的、需要子视图继承实现的区块内容:
- 首先,通过
@extends
指令指定要继承的布局文件,通过目录名和文件名并以「.」分隔来指定布局文件(Blade 都是通过这种方式指定视图文件,前提是这些视图文件都位于resources/views
目录中) - 然后通过
@section
指令依次实现布局文件中需要子视图继承实现的区块内容,两者通过@section
指令第一个参数建立关联(可以类比为类的继承中的方法名),不同的继承方式实现也略有不同。对于title
这种比较简单的区块元素我们直接通过传递第二个参数简单实现即可,content
部分是页面主体内容,所以需要通过完整的@section
/@endsection
来实现,最后是footerScripts
区块,由于布局文件中通过@section
/@show
定义,所以我们可以在子视图中通过@parent
渲染布局文件中指定的默认区块内容(类比于 PHP 类中通过parent::
调用父类方法),并添加该视图中需要的新区块内容。 - 最终子视图页面将是布局文件根据子视图实现填充完所有待继承插槽后呈献给用户。
通过 @include
引入其他视图组件
和 PHP 类除了通过单一继承机制外,还可以通过 Trait 横向扩展功能一样,Blade 视图也可以借助 @include
指令引入其他组件完善页面功能,同时这些组件可以在不同视图文件中共用,提高了代码的复用性。比如我们定义一个点击按钮组件:
<!-- resources/views/sign-up-button.blade.php -->
<a class="button button--callout" data-page-name="{{ $pageName }}">
<i class="exclamation-icon"></i> {{ $text }}
</a>
然后就可以在其他视图中通过 @include
引入这个组件:
<!-- resources/views/home.blade.php -->
<div class="content" data-page-name="{{ $pageName }}">
<p>为什么要注册 Laravel 学院: <strong>能提供更多服务</strong></p>
@include('sign-up-button', ['text' => '看看到底有哪些服务'])
</div>
引入组件的时候可以通过传递第二个参数指定组件中需要用到的变量。
注:你也可以不显式指定要传递的参数,组件视图可以访问引入它的视图中的所有变量,但是不推荐这些做,如果被多个视图引入的话容易引起混乱。
通过 @each
指令循环引入单个组件
在某些场景下,你可能需要遍历一个集合并循环引入单个组件,这可以通过 @each
指令快速实现。比如我们的侧边栏由多个模块组成(每个模块 DOM 结构一样,可以通过单个组件多次复用实现),我们需要循环引入模块组件,并且为它们设置不同的标题,通过 @each
指令,我们可以这么做:
<!-- resources/views/sidebar.blade.php -->
<div class="sidebar">
@each('partials.module', $modules, 'module', 'partials.empty-module')
</div>
<!-- resources/views/partials/module.blade.php -->
<div class="sidebar-module">
<h1>{{ $module->title }}</h1>
</div>
<!-- resources/views/partials/empty-module.blade.php -->
<div class="sidebar-module">
No modules :(
</div>
@each
指令支持多个参数,第一个参数用于指定要循环引入的组件名,第二个参数是要遍历的集合变量,第三个参数是在引入组件中使用的变量名(对应 $modules
集合中单个元素),最后一个参数是集合数据为空时引入的默认组件。
通过 @slot
和 @component
实现更加灵活的内容分发
从 Laravel 5.4 开始,除了通过 @include
引入组件之外,还可以通过 @slot
和 @component
指令在 Blade 中实现更加灵活的内容分发,关于这个功能,应该是借鉴自 Vue.js,Vue 组件中也有使用插槽分发内容的功能。
要在 Blade 中使用插槽分发内容,首先需要创建相应的组件:
<!-- /resources/views/alert.blade.php -->
<div class="alert alert-danger">
{{ $slot }}
</div>
然后在需要引入该组件的地方通过 @component
引入:
@component('alert')
<strong>哎呦!</strong> 出错啦!
@endcomponent
@component
第一个参数对应要引入的组件名,引入组件中 $slot
变量的值通过在引入时 @component
和 @endcomponent
之间的区块内容指定。这种通过插槽分发内容功能的灵活之处在于可以在引入组件的地方定义要渲染的区块内容,换句话说,就是 $slot
的作用域在引入它的父视图中,组件要显示什么内容由引入它的视图决定。
和 @include
一样,@component
也支持传递额外的变量参数到组件中,比如我们修改组件文件如下:
<!-- /resources/views/alert.blade.php -->
<div class="alert alert-danger">
<div class="alert-title">{{ $title }}</div>
{{ $slot }}
</div>
然后就可以在引入它的地方这样传递参数指定 $title
的值:
@component('alert', ['title' => $title])
<strong>哎呦!</strong> 出错啦!
@endcomponent
通过 View Composer 预设视图组件数据变量
我们已经在视图使用这篇教程演示了如何从后端传递数据给视图模板,但是这里有个场景需要拉出来讨论,我们的视图有很多公共部分,比如导航菜单、侧边栏、底部信息等,通常我们会以单独的视图组件来处理这些元素区块,但是如何从后端传递这些组件需要的数据变量是个问题,因为这些组件在多个页面中共用,从后端角度来看,会涉及到多个路由/控制器方法,难道我们要每次都重复获取并传递这些数据吗?有没有一种方式可以支持一处定义,多处复用?
答案是有,在 Laravel 中,我们可以通过 View Composer 功能来实现上述需求,我们可以在后端通过 View Composer 将数据绑定到指定视图,从而避免在路由定义或控制器方法中重复获取以及显式传递这些视图组件所需的数据。
废话不多说,接下来我们就来演示 View Composer 的使用,假设我们有一个侧边栏视图组件 resources/views/partials/sidebar.blade.php
用于显示网站最新发布的五篇文章,该组件会在每个视图中引入,如果不使用 View Composer 的话,需要在每个路由定义(或者控制器方法)中这么传递数据:
Route::get('home', function () {
return view('home')->with('posts', Post::recent());
});
Route::get('about', function () {
return view('about')->with('posts', Post::recent());
});
这些数据变量最终会在每个视图中通过引入 partials.sidebar
组件传递到侧边栏。这样的写法两三个还能忍,十个八个的话就让人抓狂了,好在我们还可以全局「预设」这些视图变量,通常这个工作需要在某个服务提供者的 boot
方法中进行,现在我们将其定义到 app/Providers/AppServiceProvider.php
的 boot
方法:
view()->share('posts', Post::recent());
如果不指定视图组件的话,上述代码的含义是在所有视图中共享 posts
变量(该用法在视图入门教程中已经提及),这当然是有点浪费了,不推荐这么做,我们通常会以闭包方式通过 View Composer 指定视图作用域来预设共享「变量」:
view()->composer('partials.sidebar', function ($view) {
$view->with('posts', Post::recent());
});
这样,我们就可以在 resources/views/partials/sidebar.blade.php
中使用 posts
变量,而不必在定义路由或实现控制器方法的时候显式传递它了。
你甚至还可以通过数组/通配符的方式指定多个视图作用域:
// 通过数组指定多个视图组件
view()->composer(['partials.header', 'partials.footer'], function ($view) {
$view->with('posts', Post::recent());
});
// 通过通配符指定多个视图组件
view()->composer('partials.*', function ($view) {
$view->with('posts', Post::recent());
});
通过自定义类实现更加灵活的数据预设
除了常见的闭包方式外,你还可以通过自定义类的方式为 View Composer 实现更加灵活的数据预设。
首先,我们在 app/Http/ViewComposers
目录下创建一个自定义 View Composer 类 RecentPostsComposer.php
:
<?php
namespace App\Http\ViewComposers;
use App\Post;
use Illuminate\Contracts\View\View;
class RecentPostsComposer
{
private $posts;
public function __construct(Post $posts) {
$this->posts = $posts;
}
public function compose(View $view) {
$view->with('posts', $this->posts->recent());
}
}
我们在 RecentPostsComposer
类的构造函数中注入了一个 Post
模型类,该模型类会在实例化的时候自动注入,然后我们将变量预设逻辑定义在 compose
方法中。这样,当我们在 View Composer 中调用 RecentPostsComposer
类的时候,compose
方法会被自动调用从而完成变量预设:
view()->composer( 'partials.sidebar', \App\Http\ViewComposers\RecentPostsComposer::class );
我们可以通过这段代码取代之前的闭包函数定义的 View Composer,但是除非预设逻辑很复杂,否则推荐使用闭包函数方式来实现,一则简洁,二则减少了不必要的类初始化和方法调用对性能的损耗。
在视图中注入服务
我们在 Blade 模板引擎入门教程中演示了如何在视图模板中处理基本变量、集合数据以及对象数据,除此之外,还可以通过服务注入指令 @inject
在视图模板中注入服务,以便快捷使用服务中提供的方法,该功能的初衷和 View Composer 差不多,都是为了避免每次从路由定义/控制器方法中显式重复传递变量到视图模板,提高开发人员的工作效率:
@inject('analytics', 'App\Services\Analytics')
<div class="finances-display">
{{ $analytics->getBalance() }} / {{ $analytics->getBudget() }}
</div>
其原理就是将注册到服务容器中的服务解析出来,然后就可以调用该服务提供的方法:
$analytics = app('App\Services\Analytics');
如果你还不了解服务容器及其工作原理,可以等到后面讲完服务容器后再回来看这个功能,而且在实际生产环境中,学院君不推荐使用这个服务注入功能,因为这很容易将业务逻辑混合到视图模板中,视图层干好数据渲染的事情就好了,数据的处理和获取交由服务端去完成。
自定义 Blade 指令
前面我们已经见识过很多基于 Blade 指令实现的功能了,比如控制结构、模板继承、服务注入等,Blade 指令的强大之处不止于此,还提供了接口让我们可以自定义满足自己特定需求的指令。我们可以通过自定义 Blade 指令替换那些在多处重复编写的、实现同样功能的代码,从而提高代码的可读性和复用性。
比如视图模板中一个很常见的功能就是格式化显示时间,我们可以通过 Blade::directive
方法为其编写一个自定义指令。和 View Composer 一样,需要在 AppServiceProvider
的 boot
方法中注册这个指令:
Blade::directive('datetime', function($expression) {
return "<?php echo ($expression)->format('Y/m/d H:i:s'); ?>";
});
第一个参数是方法名,第二个参数是一个闭包函数,用于定义指定实现逻辑。这样,我们就可以在视图模板中通过 @datetime($time)
指令统一显示指定格式的日期时间了。
注:更新完 Blade 指令逻辑后,必须删除所有的 Blade 缓存视图指令才能生效。缓存的 Blade 视图可以通过 Artisan 命令
view:clear
移除。
除此之外,我们还可以通过 Blade::if
方法在 Blade 模板中实现自定义的 if
指令,具体实现方式请参考官方文档。