Laravel 模板引擎(Blade)原理簡析

上次提到過,模板引擎通常是要作三件事情:php

  1. 變量值的輸出(echo)html

  2. 條件判斷和循環(if ... else、for、foreach、while)git

  3. 引入或繼承其餘文件github

如今就來看看 Laravel 的模板引擎是如何來處理這三件事情的。我是在 Laravel 5.1 的實現上來寫這篇文章的。正則表達式

1. 視圖解析流程

Laravel 的 View 部分是內置了兩套輸出系統:直接輸出和使用 Blade 引擎「編譯」後輸出,默認狀況下它們經過文件名後綴來選擇:.blade.php 後綴的認爲是模板視圖文件,其餘的 .php 文件按照 PHP 自己的方式執行。雖然 Blade 模板文件中也能夠隨意嵌入 PHP 代碼,但若是並無使用,系統還去進行語法解析和替換也是沒有必要的,這樣能夠提升效率。express

在使用 View 組件輸出時,不論是調用 helpers 中提供的 view 函數仍是使用 Facades 提供靜態接口 View::make(),實際上執行的都是 Illuminate\View\Factory 中的 make 方法。以此爲入口,很容易就能知道視圖解析輸出的流程:緩存

  1. 查找視圖文件;yii2

  2. 根據文件名後綴從 Container 中取出響應的引擎;app

  3. 加載視圖文件或編譯後加載編譯後的文件執行,同時將須要解析的數據暴露在視圖文件環境中。less

Factory 中的一些方法完成了以上第一步的過程,文件查找是調用的 FileViewFinder,其中使用了一些 Illuminate\Filesystem\Filesystem 中的方法,這個類中還有一些方法是跟 events 相關的,這裏就忽略不表了。

在以上步驟中,若是中獲取到的視圖文件是須要「編譯」的,引擎會調用 「Blade 編譯器」將原視圖進行「編譯」並保存在 cache 目錄中而後加載輸出。下次調用時若是發現源文件並無被修改過就再也不從新編譯而是直接獲取緩存文件並輸出。

CompilerEngine 調用的編譯器是 CompilerInterface 接口的實現,默認狀況下也就只有 BladeCompiler(若是不知道解析器是如何注入的,你須要去了解 Laravel 的服務容器,這裏就不細表)。

2. Blade 引擎

接下來就是本文的重點:Blade 是如何「編譯」的。我一直給「編譯」兩個字加引號,由於這顯然不是真正意義上的代碼編譯的過程,只是一些正則替換的過程。

咱們知道 Laravel 的模板引擎是很簡潔的,使用時並不須要掌握太多東西,基本上只須要知道如下兩點:

  1. {{}} 之間是要輸出的內容,也有擴展的兩個方法 {{{ ... }}}{!! .. !!} 分別用於轉義輸出和不轉義輸出,5.0 之後的版本中 {{ ... }} 之間的默認狀況下也是轉義輸出的;

  2. @ 符號開頭的都是指令,包括 PHP 自己有的 if else foreach 以及擴展的 include yield stop 等等;

而 Blade 對於解析的處理其實是分了四種狀況:

  1. Extensions -> 擴展部分

  2. Statements -> 語句塊(就是 @ 開頭的指令)

  3. Comments -> 註釋部分({{-- ... --}} 的寫法,解析以後是 PHP 的註釋而不是 HTML的註釋)

  4. Echos -> 輸出

在解析(解析是在 cache 不存在的狀況下)過程當中,Blade 會先使用 token_get_all 函數獲取到視圖文件中的被 PHP 解釋器認爲是 HTML(T_INLINE_HTML)的部分,而後依次進行以上四種狀況的解析。

擴展部分是調用用戶自定義的編譯器解析字符串。BladeCompiler 中提供了的 extend 方法來添加可擴展。

註釋部分也很簡單,就是將 {{-- ... --}} 替換成 <?php /* ... */ ?php>

輸出部分提供了三個方法,分別對應上文提到的三種狀況:

  1. compileRawEchos -> 輸出未經轉義的內容 ({!! ... !!}

  2. compileEscapedEchos -> 輸出轉義以後的內容 ({{{ ... }}}

  3. compileRegularEchos -> 正常輸出 ({{ ... }}

默認狀況下通過字符替換以後 compileEscapedEchoscompileRegularEchos 的函數體實際上是徹底同樣的,在輸出的時候都是調用一個 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

語句塊部分能夠分紅三種狀況:

  1. 和 PHP 自己同樣的 if else foreach 以及擴展的 unless 等流程和循環控制的關鍵字;

  2. include yield 等模板文件引入、內容替換的部分;

  3. 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

這是由於正則後面的一部分實現了遞歸模式來匹配語句塊中括號的數量。

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/

相關文章
相關標籤/搜索