CodeRoad Map 程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。
由 程序员柏风 独立维护 · 关于作者
学习 更多
Blade 模板引擎与视图
这一课你将能做到
用 Blade 输出变量、条件渲染、循环渲染,并理解转义与安全
用 @extends / @section 组织布局,用组件(class-based + anonymous)封装可复用 UI
在 Blade 中处理表单、CSRF、验证错误回显和分页链接
为什么要学
虽然很多新项目采用前后端分离,但服务端模板从未消失:管理后台、邮件模板、PDF 渲染、内容站、SEO 敏感的落地页,仍然首选服务端渲染。Laravel 的 Blade 是这一层的工具,定位相当于 Spring 生态的 Thymeleaf(而不是 JSP):语法简洁、支持继承与组件、编译为纯 PHP 缓存执行。
学会 Blade 还有一个隐性收益:Breeze、Jetstream、大部分后台界面包的代码你都能读懂并改造。本课用一个「订单管理后台」把布局、列表、表单、错误回显完整串起来。
先建立图像
Controller
│ return view('orders.index', ['orders' => $orders]);
▼
resources/views/
├── layouts/app.blade.php ← 母版:<head>、导航、@yield('content')
├── components/
│ ├── alert.blade.php ← 匿名组件:<x-alert type="success"/>
│ └── stat-card.blade.php
└── orders/
├── index.blade.php ← @extends('layouts.app') + @section
├── create.blade.php
└── show.blade.php
编译机制:Blade 文件第一次被访问时编译为 storage/framework/views/*.php(纯 PHP),原文件改动后自动重编译。等价 Thymeleaf 的模板缓存机制,但开发期零配置自动失效。
跟着做
步骤 1:渲染第一个视图
新建 resources/views/hello.blade.php:
<h1>你好,{{ $name }}</h1>
<p>当前时间:{{ now()->format('Y-m-d H:i') }}</p>
在 routes/web.php:
Route::get('/hello', function () {
return view('hello', ['name' => '小李']);
});
要点:{{ $name }} 等价 htmlspecialchars($name, ENT_QUOTES),输出自动转义,防 XSS。确认不需要转义(内容本身就是受信 HTML)才用 {!! $html !!}。
步骤 2:条件与循环
{{-- 这是 Blade 注释,不会出现在 HTML 源码里 --}}
@if($orders->isEmpty())
<x-alert type="info">还没有订单,去<a href="{{ route('orders.create') }}">下一单</a>吧。</x-alert>
@else
<p>共 {{ $orders->total() }} 笔订单。</p>
@endif
@isset($user)
<p>欢迎,{{ $user->name }}</p>
@endisset
@unless(auth()->check())
<a href="/login">登录</a>
@endunless
<ul>
@foreach($orders as $order)
<li class="@if($loop->first) font-bold @endif">
{{ $loop->iteration }}.
<a href="{{ route('orders.show', $order) }}">{{ $order->order_no }}</a>
— ¥{{ number_format($order->amount / 100, 2) }}
@switch($order->status)
@case('paid') <span class="text-green">已支付</span> @break
@case('cancelled') <span class="text-gray">已取消</span> @break
@default <span>{{ $order->status }}</span>
@endswitch
</li>
@else
<li>无数据</li>
@endforeach
</ul>
{{-- 复杂循环用 forelse 更省心(空态内置) --}}
@forelse($orders as $order)
...
@empty
<p>暂无订单</p>
@endforelse
$loop 变量提供 index/iteration/first/last/remaining/depth/parent,比手写计数器清晰。
步骤 3:布局继承 母版 resources/views/layouts/app.blade.php:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>@yield('title', '商家后台') - Shop</title>
<link rel="stylesheet" href="{{ asset('css/app.css') }}">
</head>
<body>
<nav>
<a href="{{ route('home') }}">首页</a>
<a href="{{ route('orders.index') }}">订单</a>
@auth
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit">退出({{ auth()->user()->name }})</button>
</form>
@endauth
</nav>
{{-- 闪存消息 --}}
@if(session('success'))
<x-alert type="success">{{ session('success') }}</x-alert>
@endif
<main>
@yield('content')
</main>
</body>
</html>
子页面 resources/views/orders/index.blade.php:
@extends('layouts.app')
@section('title', '订单列表')
@section('content')
<h1>订单列表</h1>
{{-- 列表内容见步骤 2 --}}
@endsection
步骤 4:组件封装 匿名组件(推荐简单 UI):新建 resources/views/components/alert.blade.php:
@props([
'type' => 'info', // success / danger / warning / info
])
<div {{ $attributes->merge(['class' => "alert alert-{$type}"]) }}>
{{ $slot }}
</div>
<x-alert type="danger" class="mt-4">库存不足,请补货</x-alert>
php artisan make:component StatCard
app/View/Components/StatCard.php:
<?php
namespace App\View\Components;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class StatCard extends Component
{
public function __construct(
public string $label,
public int|float $value,
public ?string $trend = null,
) {}
public function render(): View|Closure|string
{
return view('components.stat-card');
}
}
resources/views/components/stat-card.blade.php:
<div class="stat-card">
<div class="label">{{ $label }}</div>
<div class="value">{{ $value }}</div>
@isset($trend)<div class="trend">{{ $trend }}</div>@endisset
</div>
使用:<x-stat-card label="今日成交额" :value="number_format($todayAmount / 100, 2)" trend="+12%"/>,注意属性前加 : 表示按 PHP 表达式传参。
步骤 5:表单、CSRF 与错误回显 resources/views/orders/create.blade.php:
@extends('layouts.app')
@section('content')
<form method="POST" action="{{ route('orders.store') }}">
@csrf
{{-- 用 PUT/PATCH 时:@method('PUT') --}}
<div>
<label>支付渠道</label>
<select name="channel">
@foreach(['alipay' => '支付宝', 'wechat' => '微信'] as $value => $text)
<option value="{{ $value }}" @selected(old('channel') === $value)>{{ $text }}</option>
@endforeach
</select>
@error('channel') <p class="error">{{ $message }}</p> @enderror
</div>
<div>
<label>备注</label>
<input type="text" name="remark" value="{{ old('remark') }}">
@error('remark') <p class="error">{{ $message }}</p> @enderror
</div>
<button type="submit">提交订单</button>
</form>
@endsection
@csrf 生成隐藏的 token 输入框,缺失会得到 419。
old('field') 在验证失败重定向后回填用户上次输入,避免用户重填。
@error('field') 直接拿到该字段的第一条错误信息;$errors 是所有页面自动可用的错误包(ViewErrorBag)。
@checked、@selected、@disabled 是 Laravel 9+ 的便捷指令。
步骤 6:分页与 URL 资源 {{-- 控制器:Order::paginate(20);模板输出分页条 --}}
{{ $orders->withQueryString()->links() }}
{{-- 静态资源(带版本 query 防缓存) --}}
<img src="{{ asset('images/logo.png') }}" alt="logo">
{{-- 混合 Vite 构建资源(Laravel 官方前端方案) --}}
@vite(['resources/css/app.css', 'resources/js/app.js'])
{{-- 引入子视图(可传数据) --}}
@include('orders._filters', ['status' => $status ?? null])
{{-- 一次性渲染:每个 item 都 include(循环集合) --}}
@each('orders._card', $orders, 'order', 'orders._empty')
讲清楚要点
Blade 编译为纯 PHP :指令是编译期语法糖,没有运行时模板解释开销。改文件自动失效,部署时可用 view:cache 预热。
转义默认开启 :{{ }} 转义,{!! !!} 不转义;永远不要对用户输入使用后者。这是 XSS 的最后一道防线。
布局两种体系 :继承(@extends/@yield/@section)适合整页骨架;组件(<x-*>)适合可复用小块。新项目更推荐组件化,Blade 组件是 Laravel 7 后的首选。
指令覆盖面 :认证态 @auth/@guest、权限 @can/@cannot(第 7 课)、语言 @lang、注入 @inject、栈 @push/@stack(子页面向母版追加脚本)。
$attributes 是组件的生命线 :外部传入的非 props 属性(class、id、data-*)都收集在这里,merge() 可做 CSS 类智能合并。
分页样式可换 :默认使用 Tailwind 分页视图,可用 php artisan vendor:publish --tag=laravel-pagination 发布后改 Bootstrap 风格。
邮件模板也是 Blade :php artisan make:mail OrderShipped --markdown 生成 Markdown + Blade 邮件,通知类内容复用同一套技能。
你可能会懵的地方
@yield、@section、@stack、@include 到底怎么选?
@yield 定义可被子页面覆盖的内容位;@section 在子页面填充,配合 @parent 可追加;@stack 收集多处 @push(适合页底脚本,列表页每行都可能 push);@include 是直接引入一个固定子视图。
为什么 old() 有时返回 null?
只有验证失败后 back() 的请求才有闪存的旧输入;直接 GET 访问表单页本来就没有。默认值写法:old('remark', $order->remark ?? '')。
组件名和文件路径如何对应?
resources/views/components/stat-card.blade.php → <x-stat-card>;子目录 components/form/input.blade.php → <x-form.input>。
Blade 里能写业务逻辑吗?
简单展示逻辑可以(格式化、条件);但凡涉及查库、多步计算都应放控制器、Service 或视图组件类里,保持模板可扫读。
和 Vue/React 是什么关系?
Blade 负责首屏 HTML;Vue/React 可作为页面内的交互岛(Inertia 或直接挂载),Blade 仍然输出容器与初始数据。Livewire 则是「用 Blade 写交互」的另一流派,不需要写 JS。
错法对照 错法 问题 正解 {!! $userInput !!} 渲染用户内容XSS 漏洞 {{ $userInput }}表单漏写 @csrf POST 返回 419 所有非 GET 表单加 @csrf 验证失败后用户要重填全部字段 体验差 value 绑定 old() 每个页面重复写 <head> 和导航 维护灾难 母版 + @extends 在 Blade 里直接 DB::table(...) 数据层泄漏到视图 控制器准备好数据再传入 组件 props 与普通属性混用不清 取值意外 显式声明 @props,其余走 $attributes
卡点急救
页面显示「Invalid argument supplied for foreach()」 :传入的不是集合。控制器用 collect() 包装;模板里用 @forelse 并在调试时 dd($orders) 确认类型。
改了 Blade 没生效 :检查是否改错文件(路径与 view() 名称要对应点斜杠规则);删 storage/framework/views 下编译文件;确认 storage 可写。
组件报 Unable to locate a class or view component :确认组件路径/命名;类组件执行过 php artisan view:clear;匿名组件文件必须在 resources/views/components 下。
面试常问
Blade 如何防止 XSS?什么场景会绕过防护?
{{ }} 编译为 htmlspecialchars(ENT_QUOTES | UTF-8);风险场景是显式使用 {!! !!} 输出不可信内容,以及在 JavaScript 上下文(<script> 内拼 JSON)——此时应使用 @json($data) 或 JSON_HEX_TAG 等安全标志输出,不能直接拼字符串。
Blade 组件和布局继承有什么区别,如何取舍?
继承是整页层级的模板复用(母版占位 + 子页面填充);组件是带属性与插槽的独立 UI 单元,可跨页面复用、可携带类逻辑。现代实践以组件为复用主力,继承仅保留一两层页面骨架;与 React/Vue 的组件心智一致。
服务端渲染与前后端分离各自的适用场景?
SSR 利于首屏与 SEO、部署简单、适合后台和内容站;分离架构利于交互复杂度、前后端独立伸缩、多端复用 API。选型看团队结构与产品形态,没有绝对优劣;也可用 Inertia 取中间路线(路由与控制器仍在 Laravel,页面组件用 Vue/React)。
自测三问
要把每个页面相同的导航和 <head> 抽出来,你会创建什么文件、子页面如何声明?
用户提交表单后某个字段校验失败,怎样让错误提示显示在该字段下方、并保留用户刚输入的值?
为什么 {{ $content }} 是安全的而 {!! $content !!} 可能不安全?各举一个合理使用场景。
小结一句话 Blade 用编译型指令把变量输出、布局继承、组件插槽和表单脚手架收拢成一套默认安全的服务端渲染方案,写后台和邮件模板的效率远超裸 PHP。
延伸