Laravel: Laravel视图与Blade模板引擎
最后更新:2026-08-26
Blade 是 Laravel 的"排版车间"——数据在这里变成用户看到的页面,布局、组件、继承让页面不再重复。
1. 你将学到
- Blade 布局继承:@extends/@section/@yield
- 控制结构:@if/@foreach/@switch/@auth/@guest
- Blade 组件:Class Components vs Anonymous Components
- 视图合成器:View Composers 共享数据
- 前端资产:Vite 编译 Tailwind CSS / Alpine.js
2. 一个前端开发者的真实故事
(1) 痛点:每个页面都重复写导航栏
Charlie 为 ShopMetrics 写了 10 个页面,每个页面都手动复制了 60 行的导航栏 HTML。当 Bob 要求把"定价"链接从导航栏移到下拉菜单时,Charlie 不得不逐个修改 10 个文件——改到第 7 个时漏了一个 class,线上导航栏样式不一致,被 Alice 投诉了 3 次。
(2) Blade 布局的解法
Blade 用 @extends 继承布局,@section 填充内容,导航栏只写一次,全站自动同步。
PHP
// resources/views/layouts/app.blade.php — Define layout once
<body>
<nav>...</nav> <!-- Navigation appears on every page -->
@yield('content') <!-- Each page fills this slot -->
</body>
// resources/views/shops/index.blade.php — Extend and fill
@extends('layouts.app')
@section('content')
<h1>All Shops</h1>
@endsection
(3) 收益
Charlie 用 Blade 布局后,导航栏只需维护 1 个文件,Bob 的修改请求从"改 10 个文件"变成"改 1 个文件",0 遗漏。
3. Blade 布局继承
(1) 定义布局
HTML
<!-- resources/views/layouts/app.blade.php -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>@yield('title', 'ShopMetrics')</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body>
@include('partials.navbar')
<main class="container mx-auto">
@yield('content')
</main>
@include('partials.footer')
</body>
</html>
(2) 继承布局
HTML
<!-- resources/views/shops/index.blade.php -->
@extends('layouts.app')
@section('title', 'All Shops')
@section('content')
<div class="grid grid-cols-3 gap-4">
@foreach($shops as $shop)
<div class="card">{{ $shop->name }}</div>
@endforeach
</div>
@endsection
| 指令 | 作用 | 类比 |
|---|---|---|
@extends |
指定继承的布局 | 选择模板 |
@section |
定义内容块 | 填充模板中的空位 |
@yield |
输出内容块 | 模板中的空位 |
@include |
引入子视图 | 插入公共片段 |
▶ 示例:ShopMetrics 双栏布局
HTML
<!-- resources/views/layouts/dashboard.blade.php -->
@extends('layouts.app')
@section('content')
<div class="flex">
@include('partials.sidebar')
<div class="flex-1 p-6">
@yield('dashboard-content')
</div>
</div>
@endsection
<!-- resources/views/dashboard/analytics.blade.php -->
@extends('layouts.dashboard')
@section('title', 'Analytics Dashboard')
@section('dashboard-content')
<h1>Analytics Overview</h1>
<div id="chart"></div>
@endsection
输出:
TEXT
📖 仅展示
// 执行成功
4. 控制结构
(1) 条件指令
PHP
@if($shop->is_active)
<span class="badge-green">Active</span>
@elseif($shop->is_suspended)
<span class="badge-yellow">Suspended</span>
@else
<span class="badge-gray">Inactive</span>
@endif
@auth
<a href="{{ route('dashboard') }}">Dashboard</a>
@endauth
@guest
<a href="{{ route('login') }}">Login</a>
@endguest
(2) 循环指令
PHP
@foreach($shops as $shop)
<tr>
<td>{{ $shop->name }}</td>
<td>{{ $shop->revenue }}</td>
</tr>
@if($loop->last)
</tbody></table>
@endif
@endforeach
@forelse($orders as $order)
<li>{{ $order->total }}</li>
@empty
<li>No orders yet.</li>
@endforelse
| 循环变量 | 说明 |
|---|---|
$loop->index |
当前索引(从 0 开始) |
$loop->iteration |
当前轮次(从 1 开始) |
$loop->first |
是否第一个 |
$loop->last |
是否最后一个 |
$loop->count |
总数 |
▶ 示例:ShopMetrics 仪表盘条件渲染
HTML
<!-- resources/views/dashboard/index.blade.php -->
@extends('layouts.dashboard')
@section('dashboard-content')
@auth
<h1>Welcome, {{ auth()->user()->name }}!</h1>
@endauth
@if($shops->count() > 0)
<div class="grid grid-cols-3 gap-4">
@foreach($shops as $shop)
<div class="card {{ $loop->first ? 'border-blue-500' : '' }}">
<h3>{{ $shop->name }}</h3>
<p>Revenue: ${{ number_format($shop->revenue, 2) }}</p>
</div>
@endforeach
</div>
@else
<p>No shops yet. <a href="{{ route('shops.create') }}">Create one!</a></p>
@endif
@endsection
输出:
TEXT
📖 仅展示
// 执行成功
5. Blade 组件
(1) Anonymous Components
HTML
<!-- resources/views/components/shop-card.blade.php -->
@props(['shop', 'highlight' => false])
<div class="card {{ $highlight ? 'border-gold' : '' }}">
<h3>{{ $shop->name }}</h3>
<p>{{ $shop->description }}</p>
{{ $slot }}
</div>
<!-- Usage -->
<x-shop-card :shop="$shop" :highlight="true">
<p>Featured this week!</p>
</x-shop-card>
(2) Class Components
BASH
php artisan make:component Alert
# Creates: app/View/Components/Alert.php
# And: resources/views/components/alert.blade.php
PHP
// app/View/Components/Alert.php
class Alert extends Component
{
public function __construct(
public string $type = 'info',
public ?string $message = null,
) {}
public function render(): View
{
return view('components.alert');
}
}
HTML
<!-- resources/views/components/alert.blade.php -->
<div class="alert alert-{{ $type }}">
{{ $message ?? $slot }}
</div>
<!-- Usage -->
<x-alert type="success" message="Shop created!" />
<x-alert type="warning">Check your settings.</x-alert>
| 维度 | Anonymous | Class |
|---|---|---|
| 文件 | 仅 Blade 模板 | PHP 类 + Blade 模板 |
| 逻辑 | 无/极简 | 可包含业务逻辑 |
| Props | @props() 声明 |
构造函数参数 |
| 适合 | 简单展示组件 | 需要逻辑的组件 |
▶ 示例:ShopMetrics 统计卡片组件
HTML
<!-- resources/views/components/stat-card.blade.php -->
@props(['label', 'value', 'icon', 'trend' => null])
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">{{ $label }}</p>
<p class="text-2xl font-bold">{{ $value }}</p>
@if($trend)
<p class="text-sm {{ $trend > 0 ? 'text-green-500' : 'text-red-500' }}">
{{ $trend > 0 ? '+' : '' }}{{ $trend }}%
</p>
@endif
</div>
<span class="text-3xl">{{ $icon }}</span>
</div>
</div>
<!-- Usage in dashboard -->
<x-stat-card label="Total Revenue" value="$12,450" icon="$" :trend="15.3" />
<x-stat-card label="Active Shops" value="23" icon="#" :trend="-2.1" />
输出:
TEXT
📖 仅展示
// 执行成功
6. 视图合成器
View Composers 自动为视图注入共享数据,避免在每个控制器方法中重复传递。
PHP
// app/Providers/AppServiceProvider.php
public function boot(): void
{
// Share tenant info with all dashboard views
View::composer('dashboard.*', function ($view) {
$view->with('tenant', Tenant::current());
});
// Share navigation data with specific views
View::composer(['layouts.app', 'shops.*'], NavigationComposer::class);
}
| 类型 | 何时执行 | 用途 |
|---|---|---|
View::composer() |
每次渲染视图时 | 动态计算数据 |
View::creator() |
视图实例化时 | 更早绑定数据 |
View::share() |
所有视图 | 全局常量 |
▶ 示例:ShopMetrics 导航视图合成器
PHP
// app/View/Composers/NavigationComposer.php
class NavigationComposer
{
public function compose(View $view): void
{
$view->with([
'navShops' => auth()->check()
? auth()->user()->shops()->take(5)->get()
: collect(),
'navNotifications' => auth()->check()
? auth()->user()->unreadNotifications()->count()
: 0,
]);
}
}
// Register in AppServiceProvider
View::composer('layouts.app', NavigationComposer::class);
输出:
TEXT
📖 仅展示
// 执行成功
7. 前端资产:Vite
Laravel 11 默认使用 Vite 编译前端资产。
(1) 安装 Tailwind CSS 和 Alpine.js
BASH
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
npm install alpinejs
(2) 配置 Vite
JAVASCRIPT
// vite.config.js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
export default defineConfig({
plugins: [
laravel({
input: [
'resources/css/app.css',
'resources/js/app.js',
],
refresh: true,
}),
],
});
(3) Blade 中引入
HTML
<!-- In layout head -->
@vite(['resources/css/app.css', 'resources/js/app.js'])
| 命令 | 作用 |
|---|---|
npm run dev |
启动 Vite 开发服务器(热更新) |
npm run build |
编译生产资产(压缩) |
▶ 示例:ShopMetrics 带 Alpine.js 交互的导航
HTML
<!-- resources/views/partials/navbar.blade.php -->
<nav class="bg-white shadow" x-data="{ open: false }">
<div class="container mx-auto px-4">
<div class="flex justify-between h-16">
<a href="{{ route('home') }}" class="font-bold text-xl">ShopMetrics</a>
<div class="flex items-center">
@auth
<div class="relative" @click="open = !open">
<button class="flex items-center">
{{ auth()->user()->name }}
@if($navNotifications > 0)
<span class="badge-red">{{ $navNotifications }}</span>
@endif
</button>
<div x-show="open" x-transition class="dropdown">
<a href="{{ route('dashboard') }}">Dashboard</a>
<a href="{{ route('shops.index') }}">Shops</a>
<form action="{{ route('logout') }}" method="POST">
@csrf
<button type="submit">Logout</button>
</form>
</div>
</div>
@else
<a href="{{ route('login') }}">Login</a>
@endauth
</div>
</div>
</div>
</nav>
输出:
TEXT
📖 仅展示
// 执行成功
8. 综合示例:ShopMetrics 仪表盘页面
HTML
<!-- ============================================
Comprehensive: ShopMetrics Dashboard Page
Covers: layout inheritance, components, directives, Alpine.js
============================================ -->
<!-- resources/views/dashboard/index.blade.php -->
@extends('layouts.dashboard')
@section('title', 'Dashboard — ShopMetrics')
@section('dashboard-content')
<div class="space-y-6" x-data="{ period: '7d' }">
<!-- Period Selector -->
<div class="flex gap-2">
<button @click="period = '7d'"
:class="period === '7d' ? 'btn-primary' : 'btn-secondary'">7 Days</button>
<button @click="period = '30d'"
:class="period === '30d' ? 'btn-primary' : 'btn-secondary'">30 Days</button>
<button @click="period = '90d'"
:class="period === '90d' ? 'btn-primary' : 'btn-secondary'">90 Days</button>
</div>
<!-- Stat Cards -->
<div class="grid grid-cols-4 gap-4">
<x-stat-card label="Total Revenue" value="${{ number_format($totalRevenue, 0) }}" icon="$" :trend="$revenueTrend" />
<x-stat-card label="Orders" value="{{ number_format($orderCount) }}" icon="#" :trend="$orderTrend" />
<x-stat-card label="Active Shops" value="{{ $activeShops }}" icon="#" :trend="$shopTrend" />
<x-stat-card label="Conversion Rate" value="{{ number_format($conversionRate, 1) }}%" icon="%" :trend="$conversionTrend" />
</div>
<!-- Recent Orders -->
<div class="bg-white rounded-lg shadow">
<h2 class="p-4 border-b font-semibold">Recent Orders</h2>
@forelse($recentOrders as $order)
<div class="p-4 border-b flex justify-between">
<span>{{ $order->product_name }}</span>
<span>${{ number_format($order->total, 2) }}</span>
</div>
@empty
<p class="p-4 text-gray-500">No orders in this period.</p>
@endforelse
</div>
</div>
@endsection
❓ 常见问题
Q Blade 的 {{ }} 和 {!! !!} 有什么区别?
A {{ }} 会自动转义 HTML(防 XSS),{!! !!} 输出原始 HTML 不转义。只在信任内容时用 {!! !!},如富文本编辑器输出。
Q @include 和 @component 有什么区别?
A @include 是简单的视图片段引入,没有 props 隔离;@component 有明确的 props 声明和 slot 机制,更适合可复用的 UI 组件。简单片段用 @include,独立组件用 @component。
Q Blade 组件和 Vue 组件有什么区别?
A Blade 组件在服务端渲染,生成静态 HTML;Vue 组件在客户端渲染,支持响应式数据绑定。Laravel 项目通常 Blade 做页面结构,Alpine.js 或 Vue 做交互。
Q Vite 和 Mix 有什么区别?
A Vite 是 Laravel 9+ 默认的前端构建工具,使用 ES Module 开发服务器,启动快热更新快;Mix(Laravel Mix)基于 Webpack,配置简单但速度慢。新项目用 Vite。
Q 如何在 Blade 中调试变量?
A 用
{{ dd($variable) }} 直接在页面 dump 并终止;或安装 Laravel Debugbar 扩展包查看所有变量、SQL 查询和内存使用。Q 视图合成器和控制器传数据有什么区别?
A 控制器用
view('xxx', ['key' => $value]) 显式传数据,适合该视图专有的数据;视图合成器自动注入共享数据,适合多个视图都需要的数据(如导航栏、通知数)。📖 小节
- Blade 用 @extends/@section/@yield 实现布局继承,消除重复 HTML
- 控制指令 @if/@foreach/@auth/@guest 在模板中处理条件逻辑
- Anonymous Components 适合简单组件,Class Components 适合有逻辑的组件
- View Composers 自动为视图注入共享数据,避免控制器重复传值
- Vite 是 Laravel 11 默认前端构建工具,支持热更新
- {{ }} 自动转义防 XSS,{!! !!} 输出原始 HTML 需谨慎
📝 作业
-
基础题(⭐):为 ShopMetrics 创建
layouts/app.blade.php布局文件,包含导航栏和页脚,然后创建首页home/index.blade.php继承该布局并填充内容。 -
进阶题(⭐⭐):创建一个
<x-alert>Blade 组件,支持 type(success/warning/error)和 slot 内容,在 ShopMetrics 的创建成功/验证失败场景中使用。 -
挑战题(⭐⭐⭐):使用 Alpine.js 实现一个可折叠的侧边栏组件,在仪表盘布局中集成,支持移动端自动折叠,状态保存到 localStorage。