laravel Blade 模板引擎


視圖文件緊密關聯的就是模板代碼,我們在視圖文件中通過模板代碼和 HTML 代碼結合實現視圖的渲染。和很多其他后端語言不同,PHP 本身就可以當做模板語言來使用,但是這種方式有很多缺點,比如安全上的隱患、容易產生業務邏輯與視圖模板的耦合,而且在視圖文件中到處使用 <?php 內聯代碼一點都不優雅,甚至是 ugly code,所以你會看到絕大多數現代框架都會提供一套模板引擎,比如 SmartyTwig,以及 Laravel 使用的 Blade

注:不同於其他基於 Symfony 的 PHP 框架,Laravel 沒有使用 Twig 模板引擎,不過你想要使用的話,可以借助 TwigBridge 擴展包來實現。

Blade 模板引擎是由 Laravel 框架提供的自有實現,借鑒了 .NET 的 Razor 引擎語法,其語法簡潔,易於上手,同時提供了強大而直觀的繼承模型,而且方便擴展。下面是一個簡單的 Blade 模板代碼示例:

<h1>{{ $group->title }}</h1> {!! $group->imageHtml() !!} @forelse ($users as $user) {{ $user->username }} {{ $user->nickname }}<br> @empty 該組中沒有任何用戶 @endforelse

正如你所看到的,Blade 模板引擎有三種常見的語法:

  • 通過 {{ }} 渲染 PHP 變量(最常用)
  • 通過 {!! !!} 渲染原生 HTML 代碼(用於富文本數據渲染)
  • 通過以 @ 作為前綴的 Blade 指令執行一些控制結構和繼承、引入之類的操作

下面我們就來逐一介紹這些語法。

注:Blade 模板代碼存放在以 .blade.php 后綴結尾的視圖文件中,最終會被編譯為原生 PHP 代碼,並緩存起來,直到視圖模板有修改才會再次編譯,所以擁有與原生 PHP 幾乎一致的性能,這些編譯后的代碼位於 storage/framework/views 目錄下。你當然可以在 Blade 模板中使用原生 PHP 代碼,但是不建議這么做,如果你非要這么做的話,可以通過 @php 指令引入。

渲染數據

首先我們來看一下 {{}} 語法,我們通過通過該語法包裹需要渲染的 PHP 變量,如 {{ $variable }},你可以將其類比為 <?php echo $variable; ?>,但是 Blade 模板代碼的功能要更強大,通過 {{}} 語法包裹渲染的 PHP 變量會通過 htmlentities() 方法進行 HTML 字符轉義,從而避免類似 XSS 這種攻擊,提高了代碼的安全性,所以 {{ $variable }} 編譯后的最終代碼是:

<?php echo htmlentities($variable); ?>

但是某些情況下不能對變量中 HTML 字符進行轉義,比如我們在表單通過富文本編輯器編輯后提交的表單數據,這種場景就需要通過 {!! !!} 來包裹待渲染數據了:

{!! $variable !!}

這樣編譯后的代碼就是 <?php echo $variable; ?> 了。

注:對於富文本數據 XSS 攻擊防護,可以參考這篇教程

最后,關於數據變量渲染,我們還要注意的是,很多前端框架也是通過 {{}} 來輸出 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

和原生 PHP 中的用法如出一轍。

@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

后面兩個都是語法糖,如果你不想記太多東西,不防都用 @if 來實現對應的邏輯了。

@switch

顧名思義,Blade 中的 @switch 指令和 PHP 中的 switch 語句等價,我們可以通過 @switch@case@break@default 和 @enswitch 指令構建對應邏輯:

@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 在布局文件中定義插槽

 

在理解 Blade 模板繼承的時候,我們可以類比類的繼承機制:在父類中定義抽象方法或公共方法,然后在子類中實現抽象方法或重寫公共方法。在視圖文件中,這個「父類」一般對應布局文件,不同的功能模塊往往有不同的頁面布局,比如前台、后台、用戶中心,頁面布局往往不一樣。而「子類」則對應不同功能模塊的各個子視圖頁面,比如首頁、文章詳情頁、文章編輯頁等等。

 

我們先來看一個布局文件的示例:

 

<!-- 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> 

 

引入組件的時候可以通過傳遞第二個參數指定組件中需要用到的變量。

 

注:你也可以不顯式指定要傳遞的參數,組件視圖可以訪問引入它的視圖中的所有變量,但是不推薦這些做,如果被多個視圖引入的話容易引起混亂。

 

通過 @each 指令循環引入單個組件

 

在某些場景下,你可能需要遍歷一個集合並循環引入單個組件,這可以通過 @each 指令快速實現。比如我們的側邊欄由多個模塊組成(每個模塊 DOM 結構一樣,可以通過單個組件多次復用實現),我們需要循環引入模塊組件,並且為它們設置不同的標題,通過 @each 指令,我們可以這么做:

 

<!-- resources/views/sidebar.blade.php --> <div class="sidebar"> @each('partials.module', $modules, 'module', 'partials.empty-module') </div> <!-- resources/views/partials/module.blade.php --> <div class="sidebar-module"> <h1>{{ $module->title }}</h1> </div> <!-- resources/views/partials/empty-module.blade.php --> <div class="sidebar-module"> No modules :( </div>

 

@each 指令支持多個參數,第一個參數用於指定要循環引入的組件名,第二個參數是要遍歷的集合變量,第三個參數是在引入組件中使用的變量名(對應 $modules 集合中單個元素),最后一個參數是集合數據為空時引入的默認組件。

 

通過 @slot 和 @component 實現更加靈活的內容分發

 

從 Laravel 5.4 開始,除了通過 @include 引入組件之外,還可以通過 @slot 和 @component 指令在 Blade 中實現更加靈活的內容分發,關於這個功能,應該是借鑒自 Vue.js,Vue 組件中也有使用插槽分發內容的功能。

 

要在 Blade 中使用插槽分發內容,首先需要創建相應的組件:

 

<!-- /resources/views/alert.blade.php --> <div class="alert alert-danger"> {{ $slot }} </div>

 

然后在需要引入該組件的地方通過 @component 引入:

 

@component('alert') <strong>哎呦!</strong> 出錯啦! @endcomponent 

 

@component 第一個參數對應要引入的組件名,引入組件中 $slot 變量的值通過在引入時 @component 和 @endcomponent 之間的區塊內容指定。這種通過插槽分發內容功能的靈活之處在於可以在引入組件的地方定義要渲染的區塊內容,換句話說,就是 $slot 的作用域在引入它的父視圖中,組件要顯示什么內容由引入它的視圖決定。

 

和 @include 一樣,@component 也支持傳遞額外的變量參數到組件中,比如我們修改組件文件如下:

 

<!-- /resources/views/alert.blade.php --> <div class="alert alert-danger"> <div class="alert-title">{{ $title }}</div> {{ $slot }} </div>

 

然后就可以在引入它的地方這樣傳遞參數指定 $title 的值:

 

@component('alert', ['title' => $title]) <strong>哎呦!</strong> 出錯啦! @endcomponent

 

通過 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,但是除非預設邏輯很復雜,否則推薦使用閉包函數方式來實現,一則簡潔,二則減少了不必要的類初始化和方法調用對性能的損耗。

在視圖中注入服務

我們在 Blade 模板引擎入門教程中演示了如何在視圖模板中處理基本變量、集合數據以及對象數據,除此之外,還可以通過服務注入指令 @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 移除。

除此之外,我們還可以通過 Blade::if 方法在 Blade 模板中實現自定義的 if 指令,具體實現方式請參考官方文檔


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM