上次提到過,模板引擎通常是要作三件事情:php
變量值的輸出(echo)html
條件判斷和循環(if ... else、for、foreach、while)git
引入或繼承其餘文件github
如今就來看看 Laravel 的模板引擎是如何來處理這三件事情的。我是在 Laravel 5.1 的實現上來寫這篇文章的。正則表達式
Laravel 的 View 部分是內置了兩套輸出系統:直接輸出和使用 Blade 引擎「編譯」後輸出,默認狀況下它們經過文件名後綴來選擇:.blade.php
後綴的認爲是模板視圖文件,其餘的 .php
文件按照 PHP 自己的方式執行。雖然 Blade 模板文件中也能夠隨意嵌入 PHP 代碼,但若是並無使用,系統還去進行語法解析和替換也是沒有必要的,這樣能夠提升效率。express
在使用 View 組件輸出時,不論是調用 helpers 中提供的 view
函數仍是使用 Facades 提供靜態接口 View::make()
,實際上執行的都是 Illuminate\View\Factory
中的 make
方法。以此爲入口,很容易就能知道視圖解析輸出的流程:緩存
查找視圖文件;yii2
根據文件名後綴從 Container 中取出響應的引擎;app
加載視圖文件或編譯後加載編譯後的文件執行,同時將須要解析的數據暴露在視圖文件環境中。less
Factory 中的一些方法完成了以上第一步的過程,文件查找是調用的 FileViewFinder,其中使用了一些 Illuminate\Filesystem\Filesystem
中的方法,這個類中還有一些方法是跟 events
相關的,這裏就忽略不表了。
在以上步驟中,若是中獲取到的視圖文件是須要「編譯」的,引擎會調用 「Blade 編譯器」將原視圖進行「編譯」並保存在 cache 目錄中而後加載輸出。下次調用時若是發現源文件並無被修改過就再也不從新編譯而是直接獲取緩存文件並輸出。
CompilerEngine
調用的編譯器是 CompilerInterface
接口的實現,默認狀況下也就只有 BladeCompiler
(若是不知道解析器是如何注入的,你須要去了解 Laravel 的服務容器,這裏就不細表)。
接下來就是本文的重點: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
方法來添加可擴展。
註釋部分也很簡單,就是將 {{-- ... --}}
替換成 <?php /* ... */ ?php>
。
輸出部分提供了三個方法,分別對應上文提到的三種狀況:
compileRawEchos -> 輸出未經轉義的內容 ({!! ... !!}
)
compileEscapedEchos -> 輸出轉義以後的內容 ({{{ ... }}}
)
compileRegularEchos -> 正常輸出 ({{ ... }}
)
默認狀況下通過字符替換以後 compileEscapedEchos
和 compileRegularEchos
的函數體實際上是徹底同樣的,在輸出的時候都是調用一個 e()
的輔助函數來輸出:
<?php function e($value) { if ($value instanceof Htmlable) { return $value->toHtml(); } return htmlentities($value, ENT_QUOTES, 'UTF-8', false); }
這貌似是 5.0 以後的版本才改的,以前的版本里 compileRegularEchos
執行的是 compileRawEchos
的行爲。不過兩個函數仍是有一個區別:compileRegularEchos
的轉義函數是能夠經過 setEchoFormat
自定義的(只是默認是 e()
),可是 compileEscapedEchos
不容許自定義。
echo
後的內容也是通過正則替換的:
<?php public function compileEchoDefaults($value) { return preg_replace('/^(?=\$)(.+?)(?:\s+or\s+)(.+?)$/s', 'isset($1) ? $1 : $2', $value); }
從正則表達式中能夠看出來輸出提供了一個 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
等關鍵字代替大括號來控制流程的方法。
第二種狀況稍微複雜一點,好比下面的函數:
<?php protected function compileYield($expression) { return "<?php echo \$__env->yieldContent{$expression}; ?>"; }
解析以後的語句是調用了一個名爲 $_env
的實例中的方法。這個實例其實就是 Illuminate\View\Factory
的實例:
Factory 的構造函數:
<?php public function __construct(EngineResolver $engines, ViewFinderInterface $finder, Dispatcher $events) { ... $this->share('__env', $this); }
Illuminate\View\View
中:
<?php protected function getContents() { return $this->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
方法中最後進行正則替換的正則表達式看起來比較複雜:
/\B@(\w+)([ \t]*)(\( ( (?>[^()]+) | (?3) )* \))?/x
這是由於正則後面的一部分實現了遞歸模式來匹配語句塊中括號的數量。
經過以上的分析能夠看出來 Laravel 的視圖組件仍是十分簡潔的,同時也不失靈活性和可擴展性。若是有興趣的話,也能夠實現一個本身的模板解析引擎。
若是你想在其餘項目中使用 Blade 引擎,經過 Composer 安裝下來以後會發現還有 Container、Events 等部分,這和 Laravel 自己有關。
爲了可以在任何地方使用 Blade,我把它核心的部分提取了出來,去掉了其餘組件的依賴,也再也不依賴文件擴展名來選擇引擎:
項目地址:https://github.com/XiaoLer/blade
此外也經過這個提取以後的版本作了一個 yii2 可以使用的版本:https://github.com/XiaoLer/yii2-blade。在以前嘗試的版本中直接使用 Laravel 的 View 組件並不靈活,如今感受好多了。
我的博客地址:http://0x1.im/