上次提到過,模板引擎一般是要做三件事情:
變數值的輸出(echo)
條件判斷和迴圈(if ... else、for、foreach、while)
引入或繼承其他檔案
現在就來看看 Laravel 的模板引擎是如何來處理這三件事情的。我是在 Laravel 5.1 的實現上來寫這篇文章的。
1. 視圖解析流程
Laravel 的 View 部分是內建了兩套輸出系統:直接輸出和使用 Blade 引擎“編譯”後輸出,預設情況下它們通過檔案名稱尾碼來選擇:.blade.php 尾碼的認為是模板視圖檔案,其他的 .php 檔案按照 PHP 本身的方式執行。雖然 Blade 模板檔案中也可以隨意嵌入 PHP 代碼,但如果並沒有使用,系統還去進行文法解析和替換也是沒有必要的,這樣可以提高效率。
在使用 View 組件輸出時,不管是調用 helpers 中提供的 view 函數還是使用 Facades 提供靜態介面 View::make(),實際上執行的都是 Illuminate\View\Factory 中的 make 方法。以此為入口,很容易就能知道視圖解析輸出的流程:
查詢檢視表檔案;
根據檔案名稱尾碼從 Container 中取出響應的引擎;
載入視圖檔案或編譯後載入編譯後的檔案執行,同時將需要解析的資料暴露在視圖檔案環境中。
Factory 中的一些方法完成了以上第一步的過程,檔案尋找是調用的 FileViewFinder,其中使用了一些 Illuminate\Filesystem\Filesystem 中的方法,這個類中還有一些方法是跟 events 相關的,這裡就忽略不表了。
在以上步驟中,如果中擷取到的視圖檔案是需要“編譯”的,引擎會調用 “Blade 編譯器”將原視圖進行“編譯”並儲存在 cache 目錄中然後載入輸出。下次調用時如果發現源檔案並沒有被修改過就不再重新編譯而是直接擷取快取檔案並輸出。
CompilerEngine 調用的編譯器是 CompilerInterface 介面的實現,預設情況下也就只有 BladeCompiler(如果不知道解析器是如何注入的,你需要去瞭解 Laravel 的服務容器,這裡就不細表)。
2. Blade 引擎
接下來就是本文的重點:Blade 是如何“編譯”的。我一直給“編譯”兩個字加引號,因為這顯然不是真正意義上的代碼編譯的過程,只是一些正則替換的過程。
我們知道 Laravel 的模板引擎是很簡潔的,使用時並不需要掌握太多東西,基本上只需要知道以下兩點:
{{ 與 }} 之間是要輸出的內容,也有擴充的兩個方法 {{{ ... }}} 和 {!! .. !!} 分別用於轉義輸出和不轉義輸出,5.0 以後的版本中 {{ ... }} 之間的預設情況下也是轉義處處的;
@ 符號開頭的都是指令,包括 PHP 本身有的 if else foreach 以及擴充的 include yield stop 等等;
而 Blade 對於解析的處理實際上是分了四種情況:
Extensions -> 擴充部分
Statements -> 語句塊(就是 @ 開頭的指令)
Comments -> 注釋部分({{-- ... --}} 的寫法,解析之後是 PHP 的注釋而不是 HTML的注釋)
Echos -> 輸出
在解析(解析是在 cache 不存在的情況下)過程中,Blade 會先使用 token_get_all 函數擷取到視圖檔案中的被 PHP 解譯器認為是 HTML(T_INLINE_HTML)的部分,然後依次進行以上四種情況的解析。
擴充部分是調用使用者自訂的編譯器解析字串。BladeCompiler 中提供了的 extend 方法來添加可擴充。
注釋部分也很簡單,就是將 {{-- ... --}} 替換成 。
輸出部分提供了三個方法,分別對應上文提到的三種情況:
compileRawEchos -> 輸出未經轉義的內容 ({!! ... !!})
compileEscapedEchos -> 輸出轉義之後的內容 ({{{ ... }}})
compileRegularEchos -> 正常輸出 ({{ ... }})
預設情況下經過字元替換之後 compileEscapedEchos 和 compileRegularEchos 的函數體其實是完全一樣的,在輸出的時候都是調用一個 e() 的輔助函數來輸出:
toHtml(); } return htmlentities($value, ENT_QUOTES, 'UTF-8', false); }
這貌似是 5.0 之後的版本才改的,之前的版本裡 compileRegularEchos 執行的是 compileRawEchos 的行為。不過兩個函數還是有一個區別:compileRegularEchos 的轉義函數是可以通過 setEchoFormat 自訂的(只是預設是 e()),但是 compileEscapedEchos 不允許自訂。
echo 後的內容也是經過正則替換的:
從Regex中可以看出來輸出提供了一個 or 的關鍵字,$a or $b 的寫法會被替換成 isset($a) ? $a : $b。
語句塊部分可以分成三種情況:
和 PHP 本身一樣的 if else foreach 以及擴充的 unless 等流程和迴圈控制的關鍵字;
include yield 等模板檔案引入、內容替換的部分;
lang choice can 等涉及到 Laravel 其他組件的功能性關鍵字。
第一種情況是很簡單的替換過程,本身 PHP 為了在 HMTL 和 PHP 混合書寫方便就提供了 if foreach 等幾個關鍵字使用冒號和 endif 等關鍵字代替大括弧來控制流程程的方法。
第二種情況稍微複雜一點,比如下面的函數:
yieldContent{$expression}; ?>"; }
解析之後的語句是調用了一個名為 $_env 的執行個體中的方法。這個執行個體其實就是 Illuminate\View\Factory 的執行個體:
Factory 的建構函式:
share('__env', $this); }
Illuminate\View\View 中:
engine->get($this->path, $this->gatherData()); } /** * Get the data bound to the view instance. * * @return array */ protected function gatherData() { $data = array_merge($this->factory->getShared(), $this->data); ... return $data; }
由此也可以看出 each yield 等指令的實現也是在 Factory 中,分別對應的是 renderEach yieldContent 等。
所以檔案引入等指令的實現方式就是:在主視圖輸出的時候,通過注入的 $__env 來重複調用 Factory 中的 make 方法來輸出引入的檔案。
至於 lang 等關鍵字,替換後就是使用 app() 函數來調用 Laravel 的其他組件。此外 Blade 還提供了 inject 關鍵字來調用任何你想使用的組件。
除了以上這些,你還可以通過 directive 方法來增加一些自訂指令。
compileStatements 方法中最後進行正則替換的Regex看起來比較複雜:
/\B@(\w+)([ \t]*)(\( ( (?>[^()]+) | (?3) )* \))?/x
這是因為正則後面的一部分實現了遞迴模式來匹配語句塊中括弧的數量。
3. 後話
通過以上的分析可以看出來 Laravel 的視圖組件還是十分簡潔的,同時也不失靈活性和可擴充性。如果有興趣的話,也可以實現一個自己的模板解析引擎。
如果你想在其他項目中使用 Blade 引擎,通過 Composer 安裝下來之後會發現還有 Container、Events 等部分,這和 Laravel 本身有關。
為了能夠在任何地方使用 Blade,我把它核心的部分提取了出來,去掉了其他組件的依賴,也不再依賴副檔名來選擇引擎:
項目地址:https://github.com/XiaoLer/blade
此外也通過這個提取之後的版本做了一個 yii2 能夠使用的版本:https://github.com/XiaoLer/yii2-blade。在之前嘗試的版本中直接使用 Laravel 的 View 組件並不靈活,現在感覺好多了。
個人部落格原文:http://0x1.im/blog/laravel/laravel-blade-engine.html