laravel 入门(4)视图
Laravel 视图概述#
在实际开发中,除了 API 路由返回指定格式数据对象外,大部分 Web 路由返回的都是视图,以便实现更加复杂的页面交互
return view('以.分隔的视图模板路径');
视图是 MVC 模式中的 View 部分,大部分视图都应该是 HTML 格式文本,在 Laravel 中,支持三种格式的视图文件解析:CSS 文件,原生 PHP 和 Blade 模板。从文件名扩展就可以区分它们,CSS 文件后缀是 .css,通过文件引擎解析,原生 PHP 文件后缀是 .php,通过 PHP 引擎解析,Blade 模板文件后缀是 .blade.php,通过 Blade 引擎解析(底层实现逻辑可参考 vendor/laravel/framework/src/Illuminate/View/ViewServiceProvider.php 中的 registerEngineResolver 方法)。
视图返回与参数传递#
Laravel 提供了多个语法糖在路由中返回视图,如辅助函数 view 或 View::make 方法,还可以注入 Illuminate\View\View Factory 类(最底层实现),通常我们使用辅助函数 view,因为最简洁:
// 使用 view 辅助函数
Route::get('/', function () {
// 该函数会在 resources/views 目录下查找 home.blade.php 或 home.php 视图文件,
// 加载文件内容并解析 PHP 变量或语句,然后传递给响应,最终呈现给用户
return view('home');
});
如果要传递数据给视图,可以这么做(多个数据以数组方式传递),这样就可以将 tasks 数据变量传递到视图以便在视图中进行引用:
return view('home')->with('tasks', Task::all());
还可以这么做:
return view('home', ['tasks' => Task::all()]);
推荐使用后者,因为简单。
在视图间共享变量#
有时候在不同视图间传递同一个数据变量很麻烦,是否可以做到一次定义,多处使用呢?答案是可以,通过视图对象提供的 share 方法即可实现,我们可以在某个服务提供者如 AppServiceProvider 的 boot 方法中定义共享的视图变量:
view()->share('siteName', 'Laravel学院');
view()->share('siteUrl', 'https://xueyuanjun.com');
然后就可以在各个视图中使用 $siteName 和 $siteUrl 这两个变量了(其它变量定义方式类似)
对于那些位于页头、页尾、面包屑或导航条中的数据变量,使用共享变量的方式定义将会很方便。
Blade 概述#
与视图文件紧密关联的就是模板代码,我们在视图文件中通过模板代码和 HTML 代码结合实现视图的渲染。和很多其他后端语言不同,PHP 本身就可以当做模板语言来使用,但是这种方式有很多缺点,比如安全上的隐患、容易产生业务逻辑与视图模板的耦合,而且在视图文件中到处使用 <?php 内联代码一点都不优雅,甚至是 ugly code,所以你会看到绝大多数现代框架都会提供一套模板引擎,比如 Smarty,Twig,以及 Laravel 使用的 Blade。
Blade 模板引擎有三种常见的语法:
- 通过 {{ }} 渲染 PHP 变量(最常用)
- 通过 {!! !!} 渲染原生 HTML 代码(用于富文本数据渲染)
- 通过以 @ 作为前缀的 Blade 指令执行一些控制结构和继承、引入之类的操作
注:Blade 模板代码存放在以 .blade.php 后缀结尾的视图文件中,最终会被编译为原生 PHP 代码,并缓存起来,直到视图模板有修改才会再次编译,所以拥有与原生 PHP 几乎一致的性能,这些编译后的代码位于 storage/framework/views 目录下。你当然可以在 Blade 模板中使用原生 PHP 代码,但是不建议这么做,如果你非要这么做的话,可以通过 @php 指令引入。
渲染数据#
首先我们来看一下 {{}} 语法,我们通过通过该语法包裹需要渲染的 PHP 变量,如 {{ $variable }},你可以将其类比为 ,但是 Blade 模板代码的功能要更强大,通过 {{}} 语法包裹渲染的 PHP 变量会通过 htmlentities() 方法进行 HTML 字符转义,从而避免类似 XSS 这种攻击,提高了代码的安全性,所以 {{ $variable }} 编译后的最终代码是:
<?php echo htmlentities($variable); ?>
但是某些情况下不能对变量中 HTML 字符进行转义,比如我们在表单通过富文本编辑器编辑后提交的表单数据,这种场景就需要通过 {!! !!} 来包裹待渲染数据了:
{!! $variable !!}
这样编译后的代码就是 <?php echo $variable; ?>
了。
最后,关于数据变量渲染,我们还要注意的是,很多前端框架也是通过 {{}} 来输出 JavaScript 变量数据的,比如 Laravel 的好基友 Vue.js 就是,对于这种情况,我们需要在渲染前端 JavaScript 变量的 {{}} 前面加上 @ 前缀,这样,Blade 模板引擎在编译模板代码的时候会跳过带 @ 前缀的 {{}} 数据渲染,并将 @ 移除从而可以后续执行对应的 JavaScript 框架渲染逻辑:
// Blade 引擎会将其编译为对应的 PHP 代码
{{ $phpData }}
// Blade 引擎编译时会移除 @,保留 {{ $vueData }} 结构
@{{ $vueData }}
如果要注释一段 PHP 代码,可以通过 {{-- 注释内容 --}} 实现。
控制结构#
Blade 中的控制结构语法和 PHP 大同小异,学习成本几乎为零,不过 Blade 为我们额外提供了一些有用的辅助变量和方法,方便我们进行条件判断。
条件语句#
@if、@else、@elseif
Blade 模板中的 @if 等价于 PHP 的 <?php if ($condition):,@else 和 @elseif 依次类推,最后以一个 @endif 收尾:
@if (count($students) === 1)
操场上只有一个同学
@elseif (count($students) === 0)
操场上一个同学也没有
@else
操场上有 {{ count($students) }} 个同学
@endif
@unless
@unless 是 Blade 提供的一个 PHP 中没有的语法,用于表示和 @if 条件相反的条件,@unless($condition) 可以理解为 <?php if (!$condition):,然后以 @endunless 收尾:
@unless ($user->hasPaid())
用户没有支付
@endunless
@isset、@empty
这两个指令和 PHP 中的 isset() 和 empty() 方法等价:
@isset($records)
// 记录被设置
@endisset
@empty($records)
// 记录为空
@endempty
@switch
顾名思义,Blade 中的 @switch 指令和 PHP 中的 switch 语句等价,我们可以通过 @switch、@case、@break、@default 和 @endswitch 指令构建对应逻辑:
@switch($i)
@case(1)
// $i = 1 做什么
@break
@case(2)
// $i = 2 做什么
@break
@default
// 默认情况下做什么
@endswitch
循环结构#
@for、@foreach 和 @while
和 PHP 一样,在 Laravel 中,我们可以通过与之等价的 @for、@foreach 和 @while 实现循环控制结构,使用语法和 PHP 代码相仿:
// for 循环
@for ($i = 0; $i < $talk->slotsCount(); $i++)
The number is {{ $i }}<br>
@endfor
// foreach 循环
@foreach ($talks as $talk)
{{ $talk->title }} ({{ $talk->length }} 分钟)<br>
@endforeach
// while 循环
@while ($item = array_pop($items))
{{ $item->orSomething() }}<br>
@endwhile
@forelse
这个指令是 PHP 中具备的,可以理解为处理以下 PHP 代码逻辑:
<?php
if ($students) {
foreach ($students as $student) {
// do something ...
}
} else {
// do something else ...
}
在 Blade 模板中我们可以使用 @forelse 指令通过以下代码实现上述逻辑:
@forelse ($students as $student)
// do something ...
@empty
// do something else ...
@endforelse
@foreach 和 @forelse 中的 $loop 变量
在循环控制结构中,我们要重磅介绍的就是 Blade 模板为 @foreach 和 @forelse 循环结构提供的 $loop
变量了,通过该变量,我们可以在循环体中轻松访问该循环体的很多信息,而不用自己编写那些恼人的面条式代码,比如当前迭代索引、嵌套层级、元素总量、当前索引在循环中的位置等,$loop 实例上有以下属性可以直接访问:
下面是一个简单的使用示例:
<ul>
@foreach ($pages as $page)
@if ($loop->first)
// 第一个循环迭代
@endif
<li>{{ $loop->iteration }}: {{ $page->title }}
@if ($page->hasChildren())
<ul> @foreach ($page->children() as $child)
<li>{{ $loop->parent->iteration }}. {{ $loop->iteration }}: {{ $child->title }}</li>
@endforeach
</ul>
@endif
</li>
@if ($loop->last)
// 最后一个循环迭代
@endif
@endforeach
</ul>
有了这个 $loop 变量,确实能够帮我们节省很多重复的逻辑判断和编码工作,推荐使用。
模板继承#
Blade 还提供了模板继承和组件引入功能,从而允许视图模板之间继承、覆盖及引入。
通过 @yield 和 @section/@show 在布局文件中定义插槽#
我们先来看一个布局文件的示例:
<!-- resources/views/layouts/master.blade.php -->
<html>
<head>
<title>Laravel学院 | @yield('title', '首页')</title>
</head>
<body>
<div class="container">
@yield('content')
</div>
@section('footerScripts')
<script src="{{ asset('js/app.js') }}"></script>
@show
</body>
</html>
在这个布局文件中我们使用了两个 Blade 指令,@yield 用于指定需要子视图继承实现的内容区块,我们可以通过传递第二个参数给该指令用于指定子视图未继承时的默认值,@section/@show 也用于指定子视图需要继承实现的内容区块,并且提供了默认区块内容,与 @yield 不同之处在于,@section/@show 指定的默认内容子视图可以通过 @parent 访问,而 @yield 指定的默认内容对子视图不可见。
通过 @extends 和 @section/@endsection 在子视图实现继承#
定义好布局文件后,接下来我们来定义继承布局文件的子视图:
<!-- resources/views/dashboard.blade.php -->
@extends('layouts.master')
@section('title', '管理后台')
@section('content')
环境访问 Laravel 学院后台!
@endsection
@section('footerScripts')
@parent
<script src="{{ asset('js/dashboard.js') }}"></script>
@endsection
在子视图中,我们一一实现了布局文件中定义的、需要子视图继承实现的区块内容:
- 首先,通过 @extends 指令指定要继承的布局文件,通过目录名和文件名并以「.」分隔来指定布局文件(Blade 都是通过这种方式指定视图文件,前提是这些视图文件都位于 resources/views 目录中)
- 然后通过 @section 指令依次实现布局文件中需要子视图继承实现的区块内容,两者通过 @section 指令第一个参数建立关联(可以类比为类的继承中的方法名),不同的继承方式实现也略有不同。对于 title 这种比较简单的区块元素我们直接通过传递第二个参数简单实现即可,content 部分是页面主体内容,所以需要通过完整的 @section/@endsection 来实现,最后是 footerScripts 区块,由于布局文件中通过 @section/@show 定义,所以我们可以在子视图中通过 @parent 渲染布局文件中指定的默认区块内容(类比于 PHP 类中通过 parent:: 调用父类方法),并添加该视图中需要的新区块内容。
- 最终子视图页面将是布局文件根据子视图实现填充完所有待继承插槽后呈献给用户。
通过 @include 引入其他视图组件#
和 PHP 类除了通过单一继承机制外,还可以通过 Trait 横向扩展功能一样,Blade 视图也可以借助 @include 指令引入其他组件完善页面功能,同时这些组件可以在不同视图文件中共用,提高了代码的复用性。比如我们定义一个点击按钮组件:
<!-- resources/views/sign-up-button.blade.php -->
<a class="button button--callout" data-page-name="{{ $pageName }}">
<i class="exclamation-icon"></i> {{ $text }}
</a>
然后就可以在其他视图中通过 @include 引入这个组件:
<!-- resources/views/home.blade.php -->
<div class="content" data-page-name="{{ $pageName }}">
<p>为什么要注册 Laravel 学院: <strong>能提供更多服务</strong></p>
@include('sign-up-button', ['text' => '看看到底有哪些服务'])
</div>
引入组件的时候可以通过传递第二个参数指定组件中需要用到的变量。
注:你也可以不显式指定要传递的参数,组件视图可以访问引入它的视图中的所有变量,但是不推荐这些做,如果被多个视图引入的话容易引起混乱。
通过 View Composer 预设视图组件数据变量#
我们的视图有很多公共部分,比如导航菜单、侧边栏、底部信息等,通常我们会以单独的视图组件来处理这些元素区块,但是如何从后端传递这些组件需要的数据变量是个问题,因为这些组件在多个页面中共用,从后端角度来看,会涉及到多个路由/控制器方法,难道我们要每次都重复获取并传递这些数据吗?有没有一种方式可以支持一处定义,多处复用?
答案是有,在 Laravel 中,我们可以通过 View Composer 功能来实现上述需求,我们可以在后端通过 View Composer 将数据绑定到指定视图,从而避免在路由定义或控制器方法中重复获取以及显式传递这些视图组件所需的数据。
废话不多说,接下来我们就来演示 View Composer 的使用,假设我们有一个侧边栏视图组件 resources/views/partials/sidebar.blade.php 用于显示网站最新发布的五篇文章,该组件会在每个视图中引入,如果不使用 View Composer 的话,需要在每个路由定义(或者控制器方法)中这么传递数据:
Route::get('home', function () {
return view('home')->with('posts', Post::recent());
});
Route::get('about', function () {
return view('about')->with('posts', Post::recent());
});
这些数据变量最终会在每个视图中通过引入 partials.sidebar 组件传递到侧边栏。这样的写法两三个还能忍,十个八个的话就让人抓狂了,好在我们还可以全局「预设」这些视图变量,通常这个工作需要在某个服务提供者的 boot 方法中进行,现在我们将其定义到 app/Providers/AppServiceProvider.php 的 boot 方法:
view()->share('posts', Post::recent());
如果不指定视图组件的话,上述代码的含义是在所有视图中共享 posts 变量(,这当然是有点浪费了,不推荐这么做,我们通常会以闭包方式通过 View Composer 指定视图作用域来预设共享「变量」:
view()->composer('partials.sidebar', function ($view) {
$view->with('posts', Post::recent());
});
这样,我们就可以在 resources/views/partials/sidebar.blade.php 中使用 posts 变量,而不必在定义路由或实现控制器方法的时候显式传递它了。
你甚至还可以通过数组/通配符的方式指定多个视图作用域:
// 通过数组指定多个视图组件
view()->composer(['partials.header', 'partials.footer'], function ($view) {
$view->with('posts', Post::recent());
});
// 通过通配符指定多个视图组件
view()->composer('partials.*', function ($view) {
$view->with('posts', Post::recent());
});
通过自定义类实现更加灵活的数据预设#
除了常见的闭包方式外,你还可以通过自定义类的方式为 View Composer 实现更加灵活的数据预设。
首先,我们在 app/Http/ViewComposers 目录下创建一个自定义 View Composer 类 RecentPostsComposer.php:
<?php
namespace App\Http\ViewComposers;
use App\Post;
use Illuminate\Contracts\View\View;
class RecentPostsComposer
{
private $posts;
public function __construct(Post $posts) {
$this->posts = $posts;
}
public function compose(View $view) {
$view->with('posts', $this->posts->recent());
}
}
我们在 RecentPostsComposer 类的构造函数中注入了一个 Post 模型类,该模型类会在实例化的时候自动注入,然后我们将变量预设逻辑定义在 compose 方法中。这样,当我们在 View Composer 中调用 RecentPostsComposer 类的时候,compose 方法会被自动调用从而完成变量预设:
view()->composer( 'partials.sidebar', \App\Http\ViewComposers\RecentPostsComposer::class );
我们可以通过这段代码取代之前的闭包函数定义的 View Composer,但是除非预设逻辑很复杂,否则推荐使用闭包函数方式来实现,一则简洁,二则减少了不必要的类初始化和方法调用对性能的损耗。
在视图中注入服务#
在视图模板中处理基本变量、集合数据以及对象数据,除此之外,还可以通过服务注入指令 @inject 在视图模板中注入服务,以便快捷使用服务中提供的方法,该功能的初衷和 View Composer 差不多,都是为了避免每次从路由定义/控制器方法中显式重复传递变量到视图模板,提高开发人员的工作效率:
@inject('analytics', 'App\Services\Analytics')
<div class="finances-display">
{{ $analytics->getBalance() }} / {{ $analytics->getBudget() }}
</div>
其原理就是将注册到服务容器中的服务解析出来,然后就可以调用该服务提供的方法:
$analytics = app('App\Services\Analytics');
在实际生产环境中,学院君不推荐使用这个服务注入功能,因为这很容易将业务逻辑混合到视图模板中,视图层干好数据渲染的事情就好了,数据的处理和获取交由服务端去完成。
自定义 Blade 指令#
前面我们已经见识过很多基于 Blade 指令实现的功能了,比如控制结构、模板继承、服务注入等,Blade 指令的强大之处不止于此,还提供了接口让我们可以自定义满足自己特定需求的指令。我们可以通过自定义 Blade 指令替换那些在多处重复编写的、实现同样功能的代码,从而提高代码的可读性和复用性。
比如视图模板中一个很常见的功能就是格式化显示时间,我们可以通过 Blade::directive 方法为其编写一个自定义指令。和 View Composer 一样,需要在 AppServiceProvider 的 boot 方法中注册这个指令:
Blade::directive('datetime', function($expression) {
return "<?php echo ($expression)->format('Y/m/d H:i:s'); ?>";
});
第一个参数是方法名,第二个参数是一个闭包函数,用于定义指定实现逻辑。这样,我们就可以在视图模板中通过 @datetime($time) 指令统一显示指定格式的日期时间了。
注:更新完 Blade 指令逻辑后,必须删除所有的 Blade 缓存视图指令才能生效。缓存的 Blade 视图可以通过 Artisan 命令 view:clear 移除。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?