Blade 是 Laravel 自带的简单而强大的模板引擎。与其他 PHP 模板引擎不同,Blade 不限制你在模板中使用纯 PHP 代码,同时提供了优雅的语法和强大的功能。本文将全面介绍 Blade 模板引擎的使用方法和最佳实践。
Blade 模板引擎简介
核心特性
- 零开销:所有 Blade 模板都会被编译成纯 PHP 代码并缓存
- 简洁语法:提供清晰、易读的模板语法
- 模板继承:支持强大的布局继承系统
- 组件系统:现代化的组件开发方式
- 安全性:自动防止 XSS 攻击
文件结构
Blade 模板文件使用 .blade.php 扩展名,通常存储在 resources/views 目录中:
1 2 3 4 5 6 7 8 9 10 11 12 13
| resources/views/ ├── layouts/ │ ├── app.blade.php │ └── guest.blade.php ├── components/ │ ├── button.blade.php │ └── card.blade.php ├── pages/ │ ├── home.blade.php │ └── about.blade.php └── partials/ ├── header.blade.php └── footer.blade.php
|
基础语法
显示数据
1 2 3 4 5 6 7 8 9 10 11
| {{-- 基本数据输出(自动转义) --}} <h1>Hello, {{ $name }}!</h1>
{{-- 输出 PHP 函数结果 --}} <p>当前时间:{{ date('Y-m-d H:i:s') }}</p>
{{-- 三元运算符 --}} <p>用户状态:{{ $user->isActive() ? '活跃' : '非活跃' }}</p>
{{-- 空值合并 --}} <p>用户名:{{ $user->name ?? '游客' }}</p>
|
原始数据输出
1 2 3 4 5 6 7 8 9
| {{-- 输出未转义的 HTML(谨慎使用) --}} <div class="content"> {!! $htmlContent !!} </div>
{{-- 在 JavaScript 中使用 --}} <script> var config = {!! json_encode($config) !!}; </script>
|
与 JavaScript 框架兼容
1 2 3 4 5 6 7 8 9 10 11 12
| {{-- 使用 @ 符号避免 Blade 解析 --}} <div id="app"> <h1>Laravel</h1> <p>Hello, @{{ name }}!</p> {{-- Vue.js 语法 --}} </div>
{{-- 使用 @verbatim 指令 --}} @verbatim <div class="container"> Hello, {{ name }}. </div> @endverbatim
|
Blade 指令
条件语句
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
| {{-- if 语句 --}} @if ($user->isAdmin()) <p>欢迎,管理员!</p> @elseif ($user->isModerator()) <p>欢迎,版主!</p> @else <p>欢迎,普通用户!</p> @endif
{{-- unless 语句(if 的反向) --}} @unless ($user->isGuest()) <p>你已登录</p> @endunless
{{-- isset 和 empty 检查 --}} @isset($records) <p>记录存在</p> @endisset
@empty($records) <p>没有记录</p> @endempty
{{-- 认证检查 --}} @auth <p>用户已认证</p> @endauth
@guest <p>用户未认证</p> @endguest
{{-- 环境检查 --}} @production <script src="{{ asset('js/app.min.js') }}"></script> @endproduction
@env('local') <script src="{{ asset('js/app.js') }}"></script> @endenv
|
循环语句
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
| {{-- foreach 循环 --}} @foreach ($users as $user) <div class="user-card"> <h3>{{ $user->name }}</h3> <p>{{ $user->email }}</p> </div> @endforeach
{{-- forelse 循环(带空值处理) --}} @forelse ($posts as $post) <article> <h2>{{ $post->title }}</h2> <p>{{ $post->excerpt }}</p> </article> @empty <p>暂无文章</p> @endforelse
{{-- for 循环 --}} @for ($i = 0; $i < 10; $i++) <p>当前值:{{ $i }}</p> @endfor
{{-- while 循环 --}} @while (true) <p>这是一个无限循环</p> @break @endwhile
|
循环变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @foreach ($users as $user) <div class="user-item {{ $loop->first ? 'first' : '' }} {{ $loop->last ? 'last' : '' }}"> <span class="index">{{ $loop->iteration }}</span> <span class="name">{{ $user->name }}</span> @if ($loop->remaining > 0) <span class="remaining">还有 {{ $loop->remaining }} 个用户</span> @endif {{-- 嵌套循环 --}} @foreach ($user->posts as $post) <div class="post {{ $loop->parent->first ? 'first-user-post' : '' }}"> {{ $post->title }} </div> @endforeach </div> @endforeach
|
Switch 语句
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @switch($user->role) @case('admin') <p>管理员权限</p> @break @case('moderator') <p>版主权限</p> @break @case('user') <p>普通用户权限</p> @break @default <p>未知权限</p> @endswitch
|
模板继承
定义布局
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
| {{-- resources/views/layouts/app.blade.php --}} <!DOCTYPE html> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="csrf-token" content="{{ csrf_token() }}"> <title>@yield('title', config('app.name'))</title> {{-- 样式文件 --}} @vite(['resources/css/app.css']) @stack('styles') </head> <body> <div id="app"> {{-- 导航栏 --}} @include('partials.navbar') {{-- 主要内容区域 --}} <main class="main-content"> @yield('content') </main> {{-- 侧边栏(可选) --}} @hasSection('sidebar') <aside class="sidebar"> @yield('sidebar') </aside> @endif {{-- 页脚 --}} @include('partials.footer') </div> {{-- JavaScript 文件 --}} @vite(['resources/js/app.js']) @stack('scripts') </body> </html>
|
继承布局
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
| {{-- resources/views/posts/index.blade.php --}} @extends('layouts.app')
@section('title', '文章列表')
@push('styles') <link rel="stylesheet" href="{{ asset('css/posts.css') }}"> @endpush
@section('content') <div class="container"> <h1>文章列表</h1> @forelse ($posts as $post) <article class="post-card"> <h2><a href="{{ route('posts.show', $post) }}">{{ $post->title }}</a></h2> <p class="meta"> 作者:{{ $post->author->name }} | 发布时间:{{ $post->created_at->format('Y-m-d') }} </p> <p class="excerpt">{{ $post->excerpt }}</p> </article> @empty <p class="no-posts">暂无文章</p> @endforelse {{ $posts->links() }} </div> @endsection
@section('sidebar') <div class="widget"> <h3>热门标签</h3> <div class="tags"> @foreach ($popularTags as $tag) <a href="{{ route('tags.show', $tag) }}" class="tag">{{ $tag->name }}</a> @endforeach </div> </div> @endsection
@push('scripts') <script src="{{ asset('js/posts.js') }}"></script> @endpush
|
组件系统
创建组件
1 2 3 4 5
| php artisan make:component Button
php artisan make:component forms.input --view
|
组件类
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 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| <?php
namespace App\View\Components;
use Illuminate\View\Component; use Illuminate\View\View;
class Button extends Component {
public string $type;
public string $size;
public bool $disabled;
public function __construct( string $type = 'button', string $size = 'md', bool $disabled = false ) { $this->type = $type; $this->size = $size; $this->disabled = $disabled; }
public function classes(): string { $classes = ['btn']; $classes[] = match($this->type) { 'primary' => 'btn-primary', 'secondary' => 'btn-secondary', 'danger' => 'btn-danger', default => 'btn-default' }; $classes[] = match($this->size) { 'sm' => 'btn-sm', 'lg' => 'btn-lg', default => 'btn-md' }; if ($this->disabled) { $classes[] = 'btn-disabled'; } return implode(' ', $classes); }
public function render(): View { return view('components.button'); } }
|
组件视图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| {{-- resources/views/components/button.blade.php --}} @props([ 'type' => 'button', 'href' => null, 'disabled' => false ])
@if($href) <a href="{{ $href }}" {{ $attributes->merge(['class' => $classes()]) }}> {{ $slot }} </a> @else <button type="{{ $type }}" {{ $disabled ? 'disabled' : '' }} {{ $attributes->merge(['class' => $classes()]) }} > {{ $slot }} </button> @endif
|
使用组件
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
| {{-- 基本使用 --}} <x-button type="primary">保存</x-button>
{{-- 传递属性 --}} <x-button type="danger" size="lg" :disabled="$user->cannot('delete', $post)" onclick="confirmDelete()" > 删除文章 </x-button>
{{-- 作为链接 --}} <x-button type="secondary" :href="route('posts.edit', $post)" > 编辑 </x-button>
{{-- 使用插槽 --}} <x-button type="primary"> <x-icon name="save" /> 保存文章 </x-button>
|
匿名组件
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
| {{-- resources/views/components/forms/input.blade.php --}} @props([ 'type' => 'text', 'name', 'label' => null, 'required' => false, 'error' => null ])
<div class="form-group"> @if($label) <label for="{{ $name }}" class="form-label"> {{ $label }} @if($required) <span class="text-red-500">*</span> @endif </label> @endif <input type="{{ $type }}" name="{{ $name }}" id="{{ $name }}" {{ $required ? 'required' : '' }} {{ $attributes->merge([ 'class' => 'form-input' . ($error ? ' border-red-500' : '') ]) }} value="{{ old($name, $attributes->get('value')) }}" > @if($error) <p class="text-red-500 text-sm mt-1">{{ $error }}</p> @endif </div>
|
组件插槽
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
| {{-- 组件定义 --}} {{-- resources/views/components/card.blade.php --}} <div class="card"> @isset($header) <div class="card-header"> {{ $header }} </div> @endisset <div class="card-body"> {{ $slot }} </div> @isset($footer) <div class="card-footer"> {{ $footer }} </div> @endisset </div>
{{-- 使用组件 --}} <x-card> <x-slot:header> <h3>用户信息</h3> </x-slot:header> <p>这是卡片的主要内容</p> <p>用户名:{{ $user->name }}</p> <x-slot:footer> <x-button type="primary">编辑</x-button> <x-button type="secondary">取消</x-button> </x-slot:footer> </x-card>
|
高级特性
包含子视图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| {{-- 基本包含 --}} @include('partials.navbar')
{{-- 传递数据 --}} @include('partials.user-card', ['user' => $user])
{{-- 条件包含 --}} @includeIf('partials.admin-panel')
{{-- 根据条件包含不同视图 --}} @includeWhen($user->isAdmin(), 'partials.admin-panel') @includeUnless($user->isGuest(), 'partials.user-menu')
{{-- 包含第一个存在的视图 --}} @includeFirst(['partials.custom-header', 'partials.default-header'])
|
堆栈
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| {{-- 在布局中定义堆栈 --}} @stack('styles') @stack('scripts')
{{-- 在子视图中推送内容 --}} @push('styles') <link rel="stylesheet" href="{{ asset('css/custom.css') }}"> @endpush
@push('scripts') <script src="{{ asset('js/custom.js') }}"></script> @endpush
{{-- 前置推送 --}} @prepend('styles') <link rel="stylesheet" href="{{ asset('css/critical.css') }}"> @endprepend
|
服务注入
1 2 3 4 5 6 7 8
| {{-- 注入服务 --}} @inject('metrics', 'App\Services\MetricsService')
<div class="dashboard"> <h1>仪表板</h1> <p>总用户数:{{ $metrics->getTotalUsers() }}</p> <p>今日访问量:{{ $metrics->getTodayVisits() }}</p> </div>
|
@once 指令
1 2 3 4 5 6
| {{-- 确保代码只执行一次 --}} @once @push('scripts') <script src="{{ asset('js/chart.js') }}"></script> @endpush @endonce
|
原始 PHP
1 2 3 4 5 6 7
| {{-- 使用原始 PHP 代码 --}} @php $isWeekend = date('N') >= 6; $greeting = $isWeekend ? '周末愉快' : '工作日快乐'; @endphp
<h1>{{ $greeting }}!</h1>
|
表单处理
CSRF 保护
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <form method="POST" action="{{ route('posts.store') }}"> @csrf <x-forms.input name="title" label="标题" :required="true" :error="$errors->first('title')" /> <x-forms.textarea name="content" label="内容" :required="true" :error="$errors->first('content')" /> <x-button type="submit">发布文章</x-button> </form>
|
方法伪造
1 2 3 4 5 6
| <form method="POST" action="{{ route('posts.update', $post) }}"> @csrf @method('PUT') {{-- 表单字段 --}} </form>
|
验证错误显示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| {{-- 显示所有错误 --}} @if ($errors->any()) <div class="alert alert-danger"> <ul> @foreach ($errors->all() as $error) <li>{{ $error }}</li> @endforeach </ul> </div> @endif
{{-- 显示特定字段错误 --}} @error('email') <div class="text-red-500 text-sm">{{ $message }}</div> @enderror
|
性能优化
视图缓存
1 2 3 4 5
| php artisan view:cache
php artisan view:clear
|
组件缓存
1 2 3 4 5 6 7 8
| public function boot() { if (app()->environment('production')) { Blade::componentNamespace('App\\View\\Components', 'app'); } }
|
最佳实践
- 保持模板简洁:避免在模板中编写复杂的业务逻辑
- 合理使用组件:将可重用的 UI 元素封装成组件
- 遵循命名约定:使用清晰、一致的文件和变量命名
- 安全第一:始终使用
{{ }} 输出用户数据,避免 XSS 攻击
- 性能考虑:在生产环境中启用视图缓存
- 代码复用:使用
@include 和组件避免重复代码
总结
Blade 模板引擎是 Laravel 框架的重要组成部分,它提供了强大而优雅的模板解决方案。通过掌握 Blade 的基础语法、模板继承、组件系统和高级特性,你可以构建出结构清晰、易于维护的前端界面。合理使用 Blade 的各种功能,将大大提高你的开发效率和代码质量。