Blade 模版

简介

Blade 是 Laravel 内置的模板引擎,简单又强大。和有些 PHP 模板引擎不同,Blade 不限制你在模板里写原生 PHP 代码。实际上,所有 Blade 模板最终都会编译成原生 PHP 代码并缓存起来,直到被修改才重新编译——也就是说 Blade 对你的应用几乎没有性能开销。Blade 模板文件用 .blade.php 扩展名,通常放在 resources/views 目录下。

可以用全局 view() 辅助函数从路由或控制器返回 Blade 视图。如视图文档所述,可以通过 view() 的第二个参数传数据:

Route::get('/', function () {
    return view('greeting', ['name' => 'Finn']);
});

用 Livewire 增强 Blade

想把 Blade 模板玩到更高境界、轻松构建动态界面?看看 Laravel Livewire。Livewire 让你写出带有动态功能的 Blade 组件——这些功能以前通常只有 React、Svelte 或 Vue 等前端框架才能做到。它提供了一种很好的方式来构建现代化的响应式前端,不用经历 JavaScript 框架的复杂性、客户端渲染和构建步骤。

显示数据

把变量放在双花括号里就能显示传给 Blade 视图的数据。比如有这样一个路由:

Route::get('/', function () {
    return view('welcome', ['name' => 'Samantha']);
});

在视图里显示 name 变量的内容:

Hello, {{ $name }}.

信息

Blade 的 {{ }} 输出语句会自动经过 PHP 的 htmlspecialchars() 函数处理,防止 XSS 攻击。

你不只能显示传给视图的变量内容,也可以输出任何 PHP 函数的结果。实际上,Blade 输出语句里可以放任何 PHP 代码:

The current UNIX timestamp is {{ time() }}.

HTML 实体编码

默认情况下,Blade(和 Laravel 的 e() 函数)会对 HTML 实体做双重编码。如果你想关闭双重编码,在 AppServiceProvider 的 boot() 方法里调用 Blade::withoutDoubleEncoding():

<?php

namespace App\Providers;

use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * 启动应用服务。
     */
    public function boot(): void
    {
        Blade::withoutDoubleEncoding();
    }
}

显示未转义数据

默认情况下,Blade 的 {{ }} 语句会自动经过 htmlspecialchars() 处理以防 XSS 攻击。如果不想让数据被转义,用以下语法:

Hello, {!! $name !!}.

注意

输出用户提供的内容时要格外小心。显示用户数据时应该使用转义的双花括号语法,防止 XSS 攻击。

Blade 与 JavaScript 框架

因为很多 JavaScript 框架也用花括号来标记要显示的表达式,你可以用 @ 符号告诉 Blade 引擎这个表达式不要动。比如:

<h1>Laravel</h1>

Hello, @{{ name }}.

这个例子里 @ 符号会被 Blade 移除,但 {{ name }} 表达式原样保留,交给你的 JavaScript 框架去渲染。

@ 符号也可以用来转义 Blade 指令:

{{-- Blade 模板 --}}
@@if()

<!-- HTML 输出 -->
@if()

渲染 JSON

有时候你会传一个数组给视图,打算把它渲染成 JSON 来初始化一个 JavaScript 变量。比如:

<script>
    var app = <?php echo json_encode($array); ?>;
</script>

不过不用手动调 json_encode(),可以用 Illuminate\Support\Js::from() 方法。from() 方法接受和 PHP 的 json_encode() 相同的参数,但会确保生成的 JSON 被正确转义,可以安全地放在 HTML 引号里。from() 方法返回一个 JSON.parse JavaScript 语句字符串,把给定的对象或数组转成有效的 JavaScript 对象:

<script>
    var app = {{ Illuminate\Support\Js::from($array) }};
</script>

最新版的 Laravel 应用骨架包含了 Js 门面,在 Blade 模板里可以更方便地使用:

<script>
    var app = {{ Js::from($array) }};
</script>

注意

Js::from() 方法只应用于渲染已有变量为 JSON。Blade 模板基于正则表达式,尝试传复杂表达式给这个指令可能导致意外错误。

@verbatim 指令

如果模板里大段区域都要显示 JavaScript 变量,可以用 @verbatim 指令把整块 HTML 包起来,这样就不用在每个 Blade 输出语句前都加 @ 符号了:

@verbatim
    <div class="container">
        Hello, {{ name }}.
    </div>
@endverbatim

Blade 指令

除了模板继承和数据显示,Blade 还为常见的 PHP 控制结构(条件语句、循环等)提供了便捷的指令。这些指令的用法和 PHP 对应的语法一致,但写起来更干净、更简洁。

If 语句

用 @if、@elseif、@else、@endif 指令构建 if 语句,用法和 PHP 完全一样:

@if (count($records) === 1)
    I have one record!
@elseif (count($records) > 1)
    I have multiple records!
@else
    I don't have any records!
@endif

为方便起见,Blade 还提供了 @unless 指令:

@unless (Auth::check())
    You are not signed in.
@endunless

此外,@isset 和 @empty 指令分别是对应 PHP 函数的快捷写法:

@isset($records)
    // $records 已定义且不为 null...
@endisset

@empty($records)
    // $records 为"空"...
@endempty

认证指令

@auth 和 @guest 指令可以快速判断当前用户是否已认证或是访客:

@auth
    // 用户已认证...
@endauth

@guest
    // 用户未认证...
@endguest

需要的话,可以指定 @auth 和 @guest 指令检查的认证 guard:

@auth('admin')
    // 用户已认证...
@endauth

@guest('admin')
    // 用户未认证...
@endguest

环境指令

用 @production 指令检查应用是否运行在生产环境:

@production
    // 生产环境专属内容...
@endproduction

或者用 @env 指令判断是否运行在指定环境:

@env('staging')
    // 应用运行在 "staging" 环境...
@endenv

@env(['staging', 'production'])
    // 应用运行在 "staging" 或 "production" 环境...
@endenv

Section 指令

用 @hasSection 指令判断模板继承的某个 section 是否有内容:

@hasSection('navigation')
    <div class="pull-right">
        @yield('navigation')
    </div>

    <div class="clearfix"></div>
@endif

用 @sectionMissing 指令判断某个 section 是否没有内容:

@sectionMissing('navigation')
    <div class="pull-right">
        @include('default-navigation')
    </div>
@endif

Session 指令

@session 指令用于判断某个会话值是否存在。如果存在,@session 和 @endsession 之间的模板内容会被执行。在这个指令内部,你可以用 $value 变量输出会话值:

@session('status')
    <div class="p-4 bg-green-100">
        {{ $value }}
    </div>
@endsession

Context 指令

@context 指令用于判断某个上下文值是否存在。如果存在,@context 和 @endcontext 之间的模板内容会被执行。在这个指令内部,你可以用 $value 变量输出上下文值:

@context('canonical')
    <link href="{{ $value }}" rel="canonical">
@endcontext

Switch 语句

用 @switch、@case、@break、@default、@endswitch 指令构建 switch 语句:

@switch($i)
    @case(1)
        First case...
        @break

    @case(2)
        Second case...
        @break

    @default
        Default case...
@endswitch

循环

除了条件语句,Blade 还为 PHP 的循环结构提供了简单的指令,用法和 PHP 对应语法一致:

@for ($i = 0; $i < 10; $i++)
    The current value is {{ $i }}
@endfor

@foreach ($users as $user)
    <p>This is user {{ $user->id }}</p>
@endforeach

@forelse ($users as $user)
    <li>{{ $user->name }}</li>
@empty
    <p>No users</p>
@endforelse

@while (true)
    <p>I'm looping forever.</p>
@endwhile

信息

在 foreach 循环中,你可以使用循环变量获取有用的信息,比如是否是第一次或最后一次迭代。

循环中可以用 @continue 跳过当前迭代,用 @break 结束循环:

@foreach ($users as $user)
    @if ($user->type == 1)
        @continue
    @endif

    <li>{{ $user->name }}</li>

    @if ($user->number == 5)
        @break
    @endif
@endforeach

也可以把条件直接写在指令声明里:

@foreach ($users as $user)
    @continue($user->type == 1)

    <li>{{ $user->name }}</li>

    @break($user->number == 5)
@endforeach

循环变量

在 foreach 循环中,循环内可以使用 $loop 变量。它提供了一些有用的信息,比如当前循环索引、是否是第一次或最后一次迭代:

@foreach ($users as $user)
    @if ($loop->first)
        This is the first iteration.
    @endif

    @if ($loop->last)
        This is the last iteration.
    @endif

    <p>This is user {{ $user->id }}</p>
@endforeach

如果是嵌套循环,可以通过 parent 属性访问父循环的 $loop 变量:

@foreach ($users as $user)
    @foreach ($user->posts as $post)
        @if ($loop->parent->first)
            This is the first iteration of the parent loop.
        @endif
    @endforeach
@endforeach

$loop 变量还包含其他有用的属性:

属性 说明
$loop->index 当前循环的索引(从 0 开始)。
$loop->iteration 当前循环的迭代次数(从 1 开始)。
$loop->remaining 循环中剩余的迭代次数。
$loop->count 被迭代的数组总项数。
$loop->first 是否是循环的第一次迭代。
$loop->last 是否是循环的最后一次迭代。
$loop->even 是否是偶数迭代。
$loop->odd 是否是奇数迭代。
$loop->depth 当前循环的嵌套层级。
$loop->parent 嵌套循环中父循环的 $loop 变量。

条件类名和样式

@class 指令可以条件性地编译 CSS 类名字符串。它接受一个数组,数组的键是要添加的类名,值是布尔表达式。如果数组元素的键是数字,它会被始终包含在渲染的类名列表中:

@php
    $isActive = false;
    $hasError = true;
@endphp

<span @class([
    'p-4',
    'font-bold' => $isActive,
    'text-gray-500' => ! $isActive,
    'bg-red' => $hasError,
])></span>

<span class="p-4 text-gray-500 bg-red"></span>

同样,@style 指令可以条件性地给 HTML 元素添加内联 CSS 样式:

@php
    $isActive = true;
@endphp

<span @style([
    'background-color: red',
    'font-weight: bold' => $isActive,
])></span>

<span style="background-color: red; font-weight: bold;"></span>

额外属性

用 @checked 指令可以轻松标记 HTML 复选框是否"选中"。如果条件为 true,指令会输出 checked:

<input
    type="checkbox"
    name="active"
    value="active"
    @checked(old('active', $user->active))
/>

用 @selected 指令标记下拉选项是否"选中":

<select name="version">
    @foreach ($product->versions as $version)
        <option value="{{ $version }}" @selected(old('version') == $version)>
            {{ $version }}
        </option>
    @endforeach
</select>

用 @disabled 指令标记元素是否"禁用":

<button type="submit" @disabled($errors->isNotEmpty())>Submit</button>

用 @readonly 指令标记元素是否"只读":

<input
    type="email"
    name="email"
    value="email@laravel.com"
    @readonly($user->isNotAdmin())
/>

用 @required 指令标记元素是否"必填":

<input
    type="text"
    name="title"
    value="title"
    @required($user->isAdmin())
/>

包含子视图

信息

虽然你可以自由使用 @include 指令,但 Blade 组件提供了类似功能,且在数据和属性绑定方面比 @include 更有优势。

Blade 的 @include 指令可以在一个视图中包含另一个 Blade 视图。父视图中所有可用的变量都会传给被包含的视图:

<div>
    @include('shared.errors')

    <form>
        <!-- 表单内容 -->
    </form>
</div>

虽然被包含的视图会继承父视图中的所有数据,你也可以传一个额外数据数组:

@include('view.name', ['status' => 'complete'])

如果 @include 一个不存在的视图,Laravel 会报错。如果视图可能存在也可能不存在,用 @includeIf 指令:

@includeIf('view.name', ['status' => 'complete'])

如果想根据布尔条件来决定是否包含视图,用 @includeWhen 和 @includeUnless 指令:

@includeWhen($boolean, 'view.name', ['status' => 'complete'])

@includeUnless($boolean, 'view.name', ['status' => 'complete'])

要从一组视图中包含第一个存在的,用 @includeFirst 指令:

@includeFirst(['custom.admin', 'admin'], ['status' => 'complete'])

如果你想包含视图但不继承父视图的任何变量,用 @includeIsolated 指令。被包含的视图只能访问你显式传入的变量:

@includeIsolated('view.name', ['user' => $user])

注意

不要在 Blade 视图中使用 __DIR__ 和 __FILE__ 常量,因为它们会指向缓存后的编译视图的位置。

为集合渲染视图

Blade 的 @each 指令把循环和包含合二为一:

@each('view.name', $jobs, 'job')

@each 指令的第一个参数是为每个元素渲染的视图。第二个参数是要遍历的数组或集合。第三个参数是在视图中分配给当前迭代的变量名。比如遍历 jobs 数组时,通常希望视图中用 job 变量访问每个元素。当前迭代的数组键会作为 key 变量在视图中可用。

@each 指令还接受第四个参数,指定数组为空时渲染的视图:

@each('view.name', $jobs, 'job', 'view.empty')

注意

通过 @each 渲染的视图不会继承父视图的变量。如果子视图需要这些变量,应该改用 @foreach 和 @include 指令。

@once 指令

@once 指令让你定义一段模板内容,在每个渲染周期中只执行一次。这在用堆栈往页面头部推送 JavaScript 时很有用。比如在循环中渲染某个组件时,你可能只想在第一次渲染组件时推送 JavaScript:

@once
    @push('scripts')
        <script>
            // Your custom JavaScript...
        </script>
    @endpush
@endonce

因为 @once 经常和 @push 或 @prepend 一起用,所以提供了 @pushOnce 和 @prependOnce 指令更方便:

@pushOnce('scripts')
    <script>
        // Your custom JavaScript...
    </script>
@endPushOnce

如果从两个不同的 Blade 模板推送重复内容,应该给 @pushOnce 传第二个参数作为唯一标识符,确保内容只渲染一次:

<!-- pie-chart.blade.php -->
@pushOnce('scripts', 'chart.js')
    <script src="/chart.js"></script>
@endPushOnce

<!-- line-chart.blade.php -->
@pushOnce('scripts', 'chart.js')
    <script src="/chart.js"></script>
@endPushOnce

原生 PHP

有些情况下在视图里嵌入 PHP 代码很有用。用 Blade 的 @php 指令在模板中执行一段原生 PHP:

@php
    $counter = 1;
@endphp

如果只是需要导入一个类,用 @use 指令:

@use('App\Models\Flight')

@use 指令还接受第二个参数给导入的类起别名:

@use('App\Models\Flight', 'FlightModel')

如果同一命名空间下有多个类,可以分组导入:

@use('App\Models\{Flight, Airport}')

@use 指令也支持导入 PHP 函数和常量,在导入路径前加 function 或 const 修饰符:

@use(function App\Helpers\format_currency)
@use(const App\Constants\MAX_ATTEMPTS)

函数和常量也支持别名:

@use(function App\Helpers\format_currency, 'formatMoney')
@use(const App\Constants\MAX_ATTEMPTS, 'MAX_TRIES')

函数和常量的分组导入同样支持:

@use(function App\Helpers\{format_currency, format_date})
@use(const App\Constants\{MAX_ATTEMPTS, DEFAULT_TIMEOUT})

字体

使用 Laravel 的 Vite 字体优化时,可以用 @fonts 指令在应用布局中渲染配置好的字体预加载链接和内联字体 CSS:

<!doctype html>
<head>
    {{-- ... --}}

    @fonts
    @vite('resources/js/app.js')
</head>

@fonts 指令渲染 vite.config.js 文件中配置的所有字体族。这个指令通常应放在应用根布局的 <head> 中,在使用这些字体的任何内容之前。

如果某个页面只需要部分配置的字体,可以给指令传一个或多个字体别名:

{{-- 加载单个字体别名... --}}
@fonts('sans')

{{-- 加载多个字体别名... --}}
@fonts(['sans', 'mono'])

字体别名在 Vite 配置中定义字体时通过 alias 选项设置。@fonts 指令调用 Vite 门面提供的 fonts() 方法,也可以直接调用:

{{ Vite::fonts(['sans', 'mono']) }}

注释

Blade 也允许你在视图中定义注释。不过和 HTML 注释不同,Blade 注释不会出现在应用返回的 HTML 中:

{{-- This comment will not be present in the rendered HTML --}}

组件

组件和插槽提供了和 section、布局、include 类似的好处,但有些人觉得组件和插槽的心智模型更容易理解。写组件有两种方式:基于类的组件和匿名组件。

创建基于类的组件,用 make:component Artisan 命令。为了演示组件的用法,我们创建一个简单的 Alert 组件。make:component 命令会把组件放在 app/View/Components 目录下:

php artisan make:component Alert

make:component 命令还会创建组件的视图模板,放在 resources/views/components 目录下。为自己应用写组件时,app/View/Components 目录和 resources/views/components 目录下的组件会被自动发现,通常不需要额外注册。

也可以在子目录中创建组件:

php artisan make:component Forms/Input

上面的命令会在 app/View/Components/Forms 目录下创建 Input 组件,视图放在 resources/views/components/forms 目录下。

手动注册包组件

为自己应用写组件时,app/View/Components 目录和 resources/views/components 目录下的组件会被自动发现。

但如果你在开发一个使用 Blade 组件的包,就需要手动注册组件类及其 HTML 标签别名。通常在包的服务提供者的 boot() 方法中注册:

use Illuminate\Support\Facades\Blade;

/**
 * 启动包的服务。
 */
public function boot(): void
{
    Blade::component('package-alert', Alert::class);
}

注册后,就可以用标签别名渲染组件:

<x-package-alert/>

或者,用 componentNamespace() 方法按约定自动加载组件类。比如一个 Nightshade 包可能有 Calendar 和 ColorPicker 组件,位于 Package\Views\Components 命名空间下:

use Illuminate\Support\Facades\Blade;

/**
 * 启动包的服务。
 */
public function boot(): void
{
    Blade::componentNamespace('Nightshade\\Views\\Components', 'nightshade');
}

这样就可以通过 package-name:: 语法按供应商命名空间使用包组件:

<x-nightshade::calendar />
<x-nightshade::color-picker />

Blade 会自动把组件名转成 PascalCase 来检测对应的类。子目录也支持用点号表示。

渲染组件

要显示组件,在 Blade 模板中使用 Blade 组件标签。组件标签以 x- 开头,后跟组件类的 kebab-case 名称:

<x-alert/>

<x-user-profile/>

如果组件类嵌套在 app/View/Components 目录更深层,用 . 表示目录嵌套。比如组件位于 app/View/Components/Inputs/Button.php,可以这样渲染:

<x-inputs.button/>

如果想条件性渲染组件,在组件类上定义 shouldRender() 方法。如果返回 false,组件不会被渲染:

use Illuminate\Support\Str;

/**
 * 组件是否应该被渲染
 */
public function shouldRender(): bool
{
    return Str::length($this->message) > 0;
}

索引组件

有时候组件是一组相关组件的一部分,你可能想把它们放在同一个目录下。比如一个 "card" 组件,类结构如下:

App\Views\Components\Card\Card
App\Views\Components\Card\Header
App\Views\Components\Card\Body

因为根 Card 组件嵌套在 Card 目录里,你可能以为需要通过 <x-card.card> 来渲染。但当组件的文件名与组件目录名相同时,Laravel 会自动认为该组件是"根"组件,允许你不重复目录名来渲染:

<x-card>
    <x-card.header>...</x-card.header>
    <x-card.body>...</x-card.body>
</x-card>

往组件传数据

你可以用 HTML 属性给 Blade 组件传数据。硬编码的原始值用简单的 HTML 属性字符串传递,PHP 表达式和变量用 : 前缀的属性传递:

<x-alert type="error" :message="$message"/>

组件的所有数据属性应该在类的构造函数中定义。组件的所有公共属性会自动在组件视图中可用。不需要在 render() 方法中手动传数据给视图:

<?php

namespace App\View\Components;

use Illuminate\View\Component;
use Illuminate\View\View;

class Alert extends Component
{
    /**
     * 创建组件实例。
     */
    public function __construct(
        public string $type,
        public string $message,
    ) {}

    /**
     * 获取表示组件的视图/内容。
     */
    public function render(): View
    {
        return view('components.alert');
    }
}

组件渲染时,你可以通过变量名输出组件的公共属性:

<div class="alert alert-{{ $type }}">
    {{ $message }}
</div>

大小写

组件构造函数参数用 camelCase 命名,而在 HTML 属性中引用时用 kebab-case。比如有这样一个构造函数:

/**
 * 创建组件实例。
 */
public function __construct(
    public string $alertType,
) {}

$alertType 参数可以这样传给组件:

<x-alert alert-type="danger" />

简写属性语法

给组件传属性时,还可以用"简写属性"语法。属性名经常和对应的变量名一样,这样写很方便:

{{-- 简写属性语法... --}}
<x-profile :$userId :$name />

{{-- 等价于... --}}
<x-profile :user-id="$userId" :name="$name" />

转义属性渲染

因为 Alpine.js 等 JavaScript 框架也用冒号前缀的属性,你可以用双冒号(::)前缀告诉 Blade 这个属性不是 PHP 表达式。比如:

<x-button ::class="{ danger: isDeleting }">
    Submit
</x-button>

Blade 渲染出来的 HTML 是:

<button :class="{ danger: isDeleting }">
    Submit
</button>

组件方法

除了公共变量可以在组件模板中使用,组件的任何公共方法也可以被调用。比如组件有一个 isSelected() 方法:

/**
 * 判断给定的选项是否是当前选中的选项。
 */
public function isSelected(string $option): bool
{
    return $option === $this->selected;
}

你可以在组件模板中通过与方法名匹配的变量来执行这个方法:

<option {{ $isSelected($value) ? 'selected' : '' }} value="{{ $value }}">
    {{ $label }}
</option>

在组件类中访问属性和插槽

Blade 组件还允许在类的 render() 方法中访问组件名称、属性和插槽。要访问这些数据,你需要从 render() 方法返回一个闭包:

use Closure;

/**
 * 获取表示组件的视图/内容。
 */
public function render(): Closure
{
    return function () {
        return '<div {{ $attributes }}>Components content</div>';
    };
}

render() 方法返回的闭包还可以接受一个 $data 数组作为唯一参数,包含组件的几项信息:

return function (array $data) {
    // $data['componentName'];
    // $data['attributes'];
    // $data['slot'];

    return '<div {{ $attributes }}>Components content</div>';
}

注意

$data 数组中的元素不应该直接嵌入 render() 方法返回的 Blade 字符串中,否则可能通过恶意属性内容导致远程代码执行。

componentName 等于 HTML 标签中 x- 前缀后面的名称。所以 <x-alert /> 的 componentName 是 alert。attributes 元素包含 HTML 标签上存在的所有属性。slot 元素是包含组件插槽内容的 Illuminate\Support\HtmlString 实例。

闭包应返回一个字符串。如果返回的字符串对应一个已存在的视图,则渲染该视图;否则,返回的字符串会作为内联 Blade 视图被执行。

额外依赖

如果组件需要 Laravel服务容器的依赖,可以列在组件数据属性之前,容器会自动注入:

use App\Services\AlertCreator;

/**
 * 创建组件实例。
 */
public function __construct(
    public AlertCreator $creator,
    public string $type,
    public string $message,
) {}

隐藏属性和方法

如果想阻止某些公共方法或属性作为变量暴露给组件模板,可以把它们加到组件的 $except 数组属性中:

<?php

namespace App\View\Components;

use Illuminate\View\Component;

class Alert extends Component
{
    /**
     * 不应暴露给组件模板的属性/方法。
     *
     * @var array
     */
    protected $except = ['type'];

    /**
     * 创建组件实例。
     */
    public function __construct(
        public string $type,
    ) {}
}

组件属性

前面已经看了如何给组件传数据属性,但有时你可能需要指定额外的 HTML 属性(如 class),这些不是组件运行所需的数据。通常你想把这些额外属性传到组件模板的根元素上。比如渲染一个 alert 组件:

<x-alert type="error" :message="$message" class="mt-4"/>

所有不在组件构造函数中的属性会自动加到组件的"属性袋"中。这个属性袋通过 $attributes 变量自动在组件中可用。在组件中输出这个变量就能渲染所有属性:

<div {{ $attributes }}>
    <!-- Component content -->
</div>

注意

目前不支持在组件标签内使用 @env 等指令。比如 <x-alert :live="@env('production')"/> 不会被编译。

默认/合并属性

有时需要给属性指定默认值或合并额外值。用属性袋的 merge() 方法。这个方法特别适合定义一组默认 CSS 类:

<div {{ $attributes->merge(['class' => 'alert alert-'.$type]) }}>
    {{ $message }}
</div>

假设组件这样使用:

<x-alert type="error" :message="$message" class="mb-4"/>

最终渲染的 HTML 是:

<div class="alert alert-error mb-4">
    <!-- Contents of the $message variable -->
</div>

条件合并类名

有时你想在条件为 true 时合并类名。用 class() 方法,它接受一个数组,键是类名,值是布尔表达式。数字键的元素会始终包含在渲染的类名列表中:

<div {{ $attributes->class(['p-4', 'bg-red' => $hasError]) }}>
    {{ $message }}
</div>

如果还需要合并其他属性,可以把 merge() 链在 class() 后面:

<button {{ $attributes->class(['p-4'])->merge(['type' => 'button']) }}>
    {{ $slot }}
</button>

信息

如果需要在其他不需要合并属性的 HTML 元素上条件编译类名,可以使用 @class 指令。

非类名属性合并

合并非 class 属性时,merge() 方法提供的值被视为属性的"默认值"。和 class 属性不同,这些属性不会和传入的值合并,而是被覆盖。比如一个 button 组件的实现:

<button {{ $attributes->merge(['type' => 'button']) }}>
    {{ $slot }}
</button>

使用自定义 type 渲染按钮组件时,可以指定 type。不指定则用 button:

<x-button type="submit">
    Submit
</x-button>

渲染的 HTML 是:

<button type="submit">
    Submit
</button>

如果想让 class 以外的属性也把默认值和传入值拼在一起,用 prepends() 方法。这个例子里 data-controller 属性始终以 profile-controller 开头,其他传入的 data-controller 值会跟在后面:

<div {{ $attributes->merge(['data-controller' => $attributes->prepends('profile-controller')]) }}>
    {{ $slot }}
</div>

获取和过滤属性

用 filter() 方法过滤属性。它接受一个闭包,返回 true 表示保留该属性:

{{ $attributes->filter(fn (string $value, string $key) => $key == 'foo') }}

用 whereStartsWith() 方法获取所有以指定字符串开头的属性:

{{ $attributes->whereStartsWith('wire:model') }}

反过来,用 whereDoesntStartWith() 方法排除所有以指定字符串开头的属性:

{{ $attributes->whereDoesntStartWith('wire:model') }}

用 first() 方法渲染属性袋中的第一个属性:

{{ $attributes->whereStartsWith('wire:model')->first() }}

用 has() 方法检查组件上是否存在某个属性,返回布尔值:

@if ($attributes->has('class'))
    <div>Class attribute is present</div>
@endif

如果给 has() 传数组,会判断所有给定属性是否都存在:

@if ($attributes->has(['name', 'class']))
    <div>All of the attributes are present</div>
@endif

用 hasAny() 方法判断给定属性中是否有任一个存在:

@if ($attributes->hasAny(['href', ':href', 'v-bind:href']))
    <div>One of the attributes is present</div>
@endif

用 get() 方法获取特定属性的值:

{{ $attributes->get('class') }}

用 only() 方法只获取指定键的属性:

{{ $attributes->only(['class']) }}

用 except() 方法获取除指定键以外的所有属性:

{{ $attributes->except(['class']) }}

保留关键字

默认情况下,有些关键字是 Blade 渲染组件时内部使用的保留字,不能在组件中定义为公共属性或方法名:

  • data
  • render
  • resolve
  • resolveView
  • shouldRender
  • view
  • withAttributes
  • withName

插槽

你经常需要通过"插槽"给组件传额外内容。组件插槽通过输出 $slot 变量来渲染。假设一个 alert 组件的标记如下:

<!-- /resources/views/components/alert.blade.php -->

<div class="alert alert-danger">
    {{ $slot }}
</div>

我们可以通过在组件标签内注入内容来传递插槽内容:

<x-alert>
    <strong>Whoops!</strong> Something went wrong!
</x-alert>

有时组件需要在不同位置渲染多个不同的插槽。修改 alert 组件,允许注入一个 "title" 插槽:

<!-- /resources/views/components/alert.blade.php -->

<span class="alert-title">{{ $title }}</span>

<div class="alert alert-danger">
    {{ $slot }}
</div>

用 x-slot 标签定义命名插槽的内容。不在 x-slot 标签内的内容会传给 $slot 变量:

<x-alert>
    <x-slot:title>
        Server Error
    </x-slot>

    <strong>Whoops!</strong> Something went wrong!
</x-alert>

用插槽的 isEmpty() 方法判断插槽是否有内容:

<span class="alert-title">{{ $title }}</span>

<div class="alert alert-danger">
    @if ($slot->isEmpty())
        This is default content if the slot is empty.
    @else
        {{ $slot }}
    @endif
</div>

此外,hasActualContent() 方法可以判断插槽是否包含非 HTML 注释的"实际"内容:

@if ($slot->hasActualContent())
    The scope has non-comment content.
@endif

作用域插槽

如果你用过 Vue 等 JavaScript 框架,可能对"作用域插槽"很熟悉——它允许你在插槽中访问组件的数据或方法。在 Laravel 中可以通过在组件上定义公共方法或属性,然后在插槽中通过 $component 变量访问组件来实现类似效果。假设 x-alert 组件的类上定义了公共 formatAlert() 方法:

<x-alert>
    <x-slot:title>
        {{ $component->formatAlert('Server Error') }}
    </x-slot>

    <strong>Whoops!</strong> Something went wrong!
</x-alert>

插槽属性

和 Blade 组件一样,你可以给插槽分配额外的属性,比如 CSS 类名:

<x-card class="shadow-sm">
    <x-slot:heading class="font-bold">
        Heading
    </x-slot>

    Content

    <x-slot:footer class="text-sm">
        Footer
    </x-slot>
</x-card>

要操作插槽属性,可以访问插槽变量的 attributes 属性。关于属性操作的更多信息,请参考组件属性文档:

@props([
    'heading',
    'footer',
])

<div {{ $attributes->class(['border']) }}>
    <h1 {{ $heading->attributes->class(['text-lg']) }}>
        {{ $heading }}
    </h1>

    {{ $slot }}

    <footer {{ $footer->attributes->class(['text-gray-700']) }}>
        {{ $footer }}
    </footer>
</div>

内联组件视图

对于很小的组件,同时管理组件类和视图模板可能感觉很繁琐。因此,你可以直接从 render() 方法返回组件的标记:

/**
 * 获取表示组件的视图/内容。
 */
public function render(): string
{
    return <<<'blade'
        <div class="alert alert-danger">
            {{ $slot }}
        </div>
    blade;
}

生成内联视图组件

创建渲染内联视图的组件,在执行 make:component 命令时加 inline 选项:

php artisan make:component Alert --inline

动态组件

有时你需要渲染组件,但直到运行时才知道该渲染哪个组件。这时可以用 Laravel 内置的 dynamic-component 组件,根据运行时值或变量来渲染:

// $componentName = "secondary-button";

<x-dynamic-component :component="$componentName" class="mt-4" />

手动注册组件

注意

以下关于手动注册组件的文档主要适用于编写包含视图组件的 Laravel 包的开发者。如果你不是在写包,这部分内容可能与你无关。

为自己应用写组件时,app/View/Components 目录和 resources/views/components 目录下的组件会被自动发现。

但如果你在开发一个使用 Blade 组件的包,或者把组件放在非约定目录下,就需要手动注册组件类及其 HTML 标签别名,让 Laravel 知道去哪找组件。通常在包的服务提供者的 boot() 方法中注册:

use Illuminate\Support\Facades\Blade;
use VendorPackage\View\Components\AlertComponent;

/**
 * 启动包的服务。
 */
public function boot(): void
{
    Blade::component('package-alert', AlertComponent::class);
}

注册后,就可以用标签别名渲染组件:

<x-package-alert/>

自动加载包组件 {#autoloading-package-components}

或者,用 componentNamespace() 方法按约定自动加载组件类。比如一个 Nightshade 包可能有 Calendar 和 ColorPicker 组件,位于 Package\Views\Components 命名空间下:

use Illuminate\Support\Facades\Blade;

/**
 * 启动包的服务。
 */
public function boot(): void
{
    Blade::componentNamespace('Nightshade\\Views\\Components', 'nightshade');
}

这样就可以通过 package-name:: 语法按供应商命名空间使用包组件:

<x-nightshade::calendar />
<x-nightshade::color-picker />

Blade 会自动把组件名转成 PascalCase 来检测对应的类。子目录也支持用点号表示。

匿名组件

和内联组件类似,匿名组件也通过单个文件来管理组件。不过匿名组件只使用一个视图文件,没有关联的类。定义匿名组件只需在 resources/views/components 目录下放一个 Blade 模板。比如在 resources/views/components/alert.blade.php 定义了组件后,可以直接这样渲染:

<x-alert/>

用 . 表示组件在 components 目录更深层的位置。比如组件定义在 resources/views/components/inputs/button.blade.php,可以这样渲染:

<x-inputs.button/>

通过 Artisan 创建匿名组件,在 make:component 命令时加 --view 标志:

php artisan make:component forms.input --view

上面的命令会在 resources/views/components/forms/input.blade.php 创建一个 Blade 文件,可以通过 <x-forms.input /> 渲染。

匿名索引组件

有时一个组件由多个 Blade 模板组成,你可能想把它们放在同一个目录下。比如一个 "accordion" 组件的目录结构如下:

/resources/views/components/accordion.blade.php
/resources/views/components/accordion/item.blade.php

这个目录结构让你可以这样渲染 accordion 组件及其 item:

<x-accordion>
    <x-accordion.item>
        ...
    </x-accordion.item>
</x-accordion>

不过,要通过 x-accordion 渲染 accordion 组件,我们不得不把"索引"accordion 组件模板放在 resources/views/components 目录下,而不是和其他 accordion 相关模板一起嵌套在 accordion 目录里。

好在 Blade 允许在组件目录内放一个与目录同名的文件。当这个模板存在时,即使嵌套在目录内,也可以作为组件的"根"元素来渲染。所以我们可以继续用上面的 Blade 语法,但调整目录结构如下:

/resources/views/components/accordion/accordion.blade.php
/resources/views/components/accordion/item.blade.php

数据属性

因为匿名组件没有关联的类,你可能想知道怎么区分哪些数据应该作为变量传给组件、哪些属性应该放在组件的属性袋中。

在组件 Blade 模板顶部用 @props 指令指定哪些属性应被视为数据变量。组件上的其他所有属性通过属性袋可用。如果想给数据变量设默认值,用变量名做键、默认值做值:

<!-- /resources/views/components/alert.blade.php -->

@props(['type' => 'info', 'message'])

<div {{ $attributes->merge(['class' => 'alert alert-'.$type]) }}>
    {{ $message }}
</div>

根据上面的组件定义,可以这样渲染:

<x-alert type="error" :message="$message" class="mb-4"/>

访问父组件数据

有时你可能想在子组件中访问父组件的数据。这种情况下可以用 @aware 指令。比如构建一个由父组件 <x-menu> 和子组件 <x-menu.item> 组成的复杂菜单组件:

<x-menu color="purple">
    <x-menu.item>...</x-menu.item>
    <x-menu.item>...</x-menu.item>
</x-menu>

<x-menu> 组件的实现可能如下:

<!-- /resources/views/components/menu/index.blade.php -->

@props(['color' => 'gray'])

<ul {{ $attributes->merge(['class' => 'bg-'.$color.'-200']) }}>
    {{ $slot }}
</ul>

因为 color prop 只传给了父组件(<x-menu>),它在 <x-menu.item> 内部不可用。但如果用 @aware 指令,就可以让它在 <x-menu.item> 中也可用:

<!-- /resources/views/components/menu/item.blade.php -->

@aware(['color' => 'gray'])

<li {{ $attributes->merge(['class' => 'text-'.$color.'-800']) }}>
    {{ $slot }}
</li>

注意

@aware 指令不能访问未通过 HTML 属性显式传给父组件的父组件数据。未显式传给父组件的 @props 默认值无法被 @aware 指令访问。

匿名组件路径

如前所述,匿名组件通常通过在 resources/views/components 目录下放 Blade 模板来定义。不过,你偶尔可能想在默认路径之外注册其他匿名组件路径。

anonymousComponentPath() 方法接受匿名组件位置的"路径"作为第一个参数,可选的"命名空间"作为第二个参数。通常应该在应用的某个服务提供者的 boot() 方法中调用:

/**
 * 启动应用服务。
 */
public function boot(): void
{
    Blade::anonymousComponentPath(__DIR__.'/../components');
}

注册路径时不指定前缀,组件渲染时也不需要前缀。比如上面注册的路径下有个 panel.blade.php 组件,可以这样渲染:

<x-panel />

可以给 anonymousComponentPath() 的第二个参数传一个前缀"命名空间":

Blade::anonymousComponentPath(__DIR__.'/../components', 'dashboard');

提供了前缀后,该"命名空间"下的组件在渲染时需要加上命名空间前缀:

<x-dashboard::panel />

构建布局

用组件构建布局

大多数 Web 应用在不同页面间保持相同的基本布局。如果每个视图都要重复整个布局 HTML,那将非常繁琐且难以维护。把布局定义为一个单独的 Blade 组件,然后在整个应用中使用,就很方便了。

定义布局组件

比如构建一个"待办事项"应用,可以定义一个 layout 组件:

<!-- resources/views/components/layout.blade.php -->

<html>
    <head>
        <title>{{ $title ?? 'Todo Manager' }}</title>
    </head>
    <body>
        <h1>Todos</h1>
        <hr/>
        {{ $slot }}
    </body>
</html>

应用布局组件

定义好 layout 组件后,可以创建一个使用该组件的 Blade 视图。定义一个简单的视图来显示任务列表:

<!-- resources/views/tasks.blade.php -->

<x-layout>
    @foreach ($tasks as $task)
        <div>{{ $task }}</div>
    @endforeach
</x-layout>

注入组件的内容会提供给 layout 组件中的默认 $slot 变量。如你所见,layout 还支持 $title 插槽——如果提供了就显示自定义标题,否则显示默认标题。可以用组件文档中讨论的标准插槽语法从任务列表视图注入自定义标题:

<!-- resources/views/tasks.blade.php -->

<x-layout>
    <x-slot:title>
        Custom Title
    </x-slot>

    @foreach ($tasks as $task)
        <div>{{ $task }}</div>
    @endforeach
</x-layout>

定义好布局和任务列表视图后,只需从路由返回 task 视图:

use App\Models\Task;

Route::get('/tasks', function () {
    return view('tasks', ['tasks' => Task::all()]);
});

用模板继承构建布局

定义布局

布局也可以通过"模板继承"来创建。这是引入组件之前构建应用的主要方式。

先看一个简单的例子。首先是页面布局。因为大多数 Web 应用在不同页面间保持相同的基本布局,把它定义为一个单独的 Blade 视图很方便:

<!-- resources/views/layouts/app.blade.php -->

<html>
    <head>
        <title>App Name - @yield('title')</title>
    </head>
    <body>
        @section('sidebar')
            This is the master sidebar.
        @show

        <div class="container">
            @yield('content')
        </div>
    </body>
</html>

这个文件包含典型的 HTML 标记。注意 @section 和 @yield 指令。@section 指令定义一个内容区块,@yield 指令用来显示给定区块的内容。

定义好布局后,来定义一个继承该布局的子页面。

继承布局

定义子视图时,用 @extends Blade 指令指定子视图要"继承"哪个布局。继承 Blade 布局的视图可以用 @section 指令向布局的区块注入内容。如上面例子所示,这些区块的内容在布局中通过 @yield 显示:

<!-- resources/views/child.blade.php -->

@extends('layouts.app')

@section('title', 'Page Title')

@section('sidebar')
    @@parent

    <p>This is appended to the master sidebar.</p>
@endsection

@section('content')
    <p>This is my body content.</p>
@endsection

这个例子中,sidebar 区块用 @@parent 指令向布局的侧边栏追加(而不是覆盖)内容。视图渲染时 @@parent 会被布局的内容替换。

信息

和上面的例子不同,这个 sidebar 区块以 @endsection 结束而不是 @show。@endsection 指令只定义区块,而 @show 会定义并立即输出区块。

@yield 指令还接受第二个参数作为默认值。如果被输出的区块未定义,就渲染这个默认值:

@yield('content', 'Default content')

表单

CSRF 字段

在应用中定义 HTML 表单时,都应该包含一个隐藏的 CSRF 令牌字段,让 CSRF 保护中间件可以验证请求。用 @csrf Blade 指令生成令牌字段:

<form method="POST" action="/profile">
    @csrf

    ...
</form>

Method 字段

因为 HTML 表单不能发 PUT、PATCH 或 DELETE 请求,需要添加一个隐藏的 _method 字段来伪装这些 HTTP 动词。@method Blade 指令可以创建这个字段:

<form action="/foo/bar" method="POST">
    @method('PUT')

    ...
</form>

校验错误

@error 指令可以快速检查某个属性是否存在校验错误信息。在 @error 指令内,你可以用 $message 变量输出错误信息:

<!-- /resources/views/post/create.blade.php -->

<label for="title">Post Title</label>

<input
    id="title"
    type="text"
    class="@error('title') is-invalid @enderror"
/>

@error('title')
    <div class="alert alert-danger">{{ $message }}</div>
@enderror

因为 @error 指令编译成 "if" 语句,可以用 @else 指令在属性没有错误时渲染内容:

<!-- /resources/views/auth.blade.php -->

<label for="email">Email address</label>

<input
    id="email"
    type="email"
    class="@error('email') is-invalid @else is-valid @enderror"
/>

在包含多个表单的页面上,可以给 @error 指令传特定错误包的名称作为第二个参数来获取校验错误信息:

<!-- /resources/views/auth.blade.php -->

<label for="email">Email address</label>

<input
    id="email"
    type="email"
    class="@error('email', 'login') is-invalid @enderror"
/>

@error('email', 'login')
    <div class="alert alert-danger">{{ $message }}</div>
@enderror

堆栈

Blade 允许你往命名堆栈中推送内容,这些内容可以在另一个视图或布局的其他位置渲染。这对于指定子视图需要的 JavaScript 库特别有用:

@push('scripts')
    <script src="/example.js"></script>
@endpush

如果想在布尔条件为 true 时才推送内容,用 @pushIf 指令:

@pushIf($shouldPush, 'scripts')
    <script src="/example.js"></script>
@endPushIf

可以按需多次推送到同一个堆栈。用 @stack 指令渲染完整的堆栈内容:

<head>
    <!-- Head Contents -->

    @stack('scripts')
</head>

如果想把内容推到堆栈开头,用 @prepend 指令:

@push('scripts')
    This will be second...
@endpush

// Later...

@prepend('scripts')
    This will be first...
@endprepend

用 @hasstack 指令判断堆栈是否为空:

@hasstack('list')
    <ul>
        @stack('list')
    </ul>
@endif

服务注入

@inject 指令可以从 Laravel服务容器中获取服务。第一个参数是服务要放入的变量名,第二个参数是要解析的类或接口名:

@inject('metrics', 'App\Services\MetricsService')

<div>
    Monthly Revenue: {{ $metrics->monthlyRevenue() }}.
</div>

渲染内联 Blade 模板

有时你可能需要把一段 Blade 模板字符串转成有效 HTML。用 Blade 门面的 render() 方法。render() 方法接受 Blade 模板字符串和可选的数据数组:

use Illuminate\Support\Facades\Blade;

return Blade::render('Hello, {{ $name }}', ['name' => 'Julian Bashir']);

Laravel 通过把内联 Blade 模板写入 storage/framework/views 目录来渲染。如果你希望 Laravel 在渲染后删除这些临时文件,可以传入 deleteCachedView 参数:

return Blade::render(
    'Hello, {{ $name }}',
    ['name' => 'Julian Bashir'],
    deleteCachedView: true
);

渲染 Blade 片段

使用 Turbo 和 htmx 等前端框架时,你可能偶尔只需要在 HTTP 响应中返回 Blade 模板的一部分。Blade "片段"就能做到。把 Blade 模板的一部分放在 @fragment 和 @endfragment 指令之间:

@fragment('user-list')
    <ul>
        @foreach ($users as $user)
            <li>{{ $user->name }}</li>
        @endforeach
    </ul>
@endfragment

渲染使用这个模板的视图时,调用 fragment() 方法指定只包含指定片段:

return view('dashboard', ['users' => $users])->fragment('user-list');

fragmentIf() 方法可以根据条件有条件地返回视图片段。否则返回整个视图:

return view('dashboard', ['users' => $users])
    ->fragmentIf($request->hasHeader('HX-Request'), 'user-list');

fragments() 和 fragmentsIf() 方法可以在响应中返回多个视图片段,片段会被拼接在一起:

view('dashboard', ['users' => $users])
    ->fragments(['user-list', 'comment-list']);

view('dashboard', ['users' => $users])
    ->fragmentsIf(
        $request->hasHeader('HX-Request'),
        ['user-list', 'comment-list']
    );

扩展 Blade

Blade 允许你用 directive() 方法定义自定义指令。当 Blade 编译器遇到自定义指令时,会调用提供的回调,传入指令包含的表达式。

下面的例子创建一个 @datetime($var) 指令,格式化给定的 $var(应该是 DateTime 实例):

<?php

namespace App\Providers;

use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * 注册应用服务。
     */
    public function register(): void
    {
        // ...
    }

    /**
     * 启动应用服务。
     */
    public function boot(): void
    {
        Blade::directive('datetime', function (string $expression) {
            return "<?php echo ($expression)->format('m/d/Y H:i'); ?>";
        });
    }
}

如你所见,我们会把 format() 方法链到传入指令的表达式上。所以这个例子中,指令最终生成的 PHP 是:

<?php echo ($var)->format('m/d/Y H:i'); ?>

注意

更新 Blade 指令的逻辑后,需要删除所有缓存的 Blade 视图。可以用 view:clear Artisan 命令清除。

自定义输出处理器

如果你尝试用 Blade "输出"一个对象,对象的 __toString() 方法会被调用。__toString 是 PHP 的"魔术方法"之一。但有时你可能无法控制某个类的 __toString() 方法,比如这个类属于第三方库。

这些情况下,Blade 允许你为特定类型的对象注册自定义输出处理器。调用 Blade 的 stringable() 方法即可。stringable() 方法接受一个闭包,闭包应类型提示它负责渲染的对象类型。通常在 AppServiceProvider 的 boot() 方法中调用:

use Illuminate\Support\Facades\Blade;
use Money\Money;

/**
 * 启动应用服务。
 */
public function boot(): void
{
    Blade::stringable(function (Money $money) {
        return $money->formatTo('en_GB');
    });
}

定义好自定义输出处理器后,在 Blade 模板中直接输出对象即可:

Cost: {{ $money }}

自定义 If 语句

定义简单的自定义条件语句时,编写自定义指令有时过于复杂。因此 Blade 提供了 Blade::if() 方法,可以用闭包快速定义自定义条件指令。比如定义一个检查应用默认"磁盘"的自定义条件,在 AppServiceProvider 的 boot() 方法中:

use Illuminate\Support\Facades\Blade;

/**
 * 启动应用服务。
 */
public function boot(): void
{
    Blade::if('disk', function (string $value) {
        return config('filesystems.default') === $value;
    });
}

定义好自定义条件后,就可以在模板中使用:

@disk('local')
    <!-- The application is using the local disk... -->
@elsedisk('s3')
    <!-- The application is using the s3 disk... -->
@else
    <!-- The application is using some other disk... -->
@enddisk

@unlessdisk('local')
    <!-- The application is not using the local disk... -->
@enddisk