CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Laravel 实战入门/课时/Blade 模板
RawBlame
课时目录12 节

Laravel 实战入门

  1. 01Laravel 概览
  2. 02目录结构
  3. 03路由
  4. 04控制器
  5. 05Blade 模板
  6. 06Eloquent ORM
  7. 07认证与授权
  8. 08验证与异常
  9. 09队列与事件
  10. 10运维与工具
  11. 11测试
  12. 12综合练习

Blade 模板

布局、组件与 Livewire。

30 分钟

Blade 模板引擎与视图

这一课你将能做到

  1. 用 Blade 输出变量、条件渲染、循环渲染,并理解转义与安全
  2. 用 @extends / @section 组织布局,用组件(class-based + anonymous)封装可复用 UI
  3. 在 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')

讲清楚要点

  1. Blade 编译为纯 PHP:指令是编译期语法糖,没有运行时模板解释开销。改文件自动失效,部署时可用 view:cache 预热。
  2. 转义默认开启:{{ }} 转义,{!! !!} 不转义;永远不要对用户输入使用后者。这是 XSS 的最后一道防线。
  3. 布局两种体系:继承(@extends/@yield/@section)适合整页骨架;组件(<x-*>)适合可复用小块。新项目更推荐组件化,Blade 组件是 Laravel 7 后的首选。
  4. 指令覆盖面:认证态 @auth/@guest、权限 @can/@cannot(第 7 课)、语言 @lang、注入 @inject、栈 @push/@stack(子页面向母版追加脚本)。
  5. $attributes 是组件的生命线:外部传入的非 props 属性(class、id、data-*)都收集在这里,merge() 可做 CSS 类智能合并。
  6. 分页样式可换:默认使用 Tailwind 分页视图,可用 php artisan vendor:publish --tag=laravel-pagination 发布后改 Bootstrap 风格。
  7. 邮件模板也是 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 }}
表单漏写 @csrfPOST 返回 419所有非 GET 表单加 @csrf
验证失败后用户要重填全部字段体验差value 绑定 old()
每个页面重复写 <head> 和导航维护灾难母版 + @extends
在 Blade 里直接 DB::table(...)数据层泄漏到视图控制器准备好数据再传入
组件 props 与普通属性混用不清取值意外显式声明 @props,其余走 $attributes

卡点急救

  1. 页面显示「Invalid argument supplied for foreach()」:传入的不是集合。控制器用 collect() 包装;模板里用 @forelse 并在调试时 dd($orders) 确认类型。
  2. 改了 Blade 没生效:检查是否改错文件(路径与 view() 名称要对应点斜杠规则);删 storage/framework/views 下编译文件;确认 storage 可写。
  3. 组件报 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)。

自测三问

  1. 要把每个页面相同的导航和 <head> 抽出来,你会创建什么文件、子页面如何声明?
  2. 用户提交表单后某个字段校验失败,怎样让错误提示显示在该字段下方、并保留用户刚输入的值?
  3. 为什么 {{ $content }} 是安全的而 {!! $content !!} 可能不安全?各举一个合理使用场景。

小结一句话

Blade 用编译型指令把变量输出、布局继承、组件插槽和表单脚手架收拢成一套默认安全的服务端渲染方案,写后台和邮件模板的效率远超裸 PHP。

延伸

  • Blade 模板文档
  • Blade 组件
  • Vite 前端资源
  • Thymeleaf 对照文档
  • 下一课:数据库与 Eloquent ORM——进入数据层,学习迁移、模型关系、查询构造器与 N+1 治理。
加载本节练习…
← 上一课控制器下一课 →Eloquent ORM