1. 程式人生 > >Laravel 模板引擎(Blade)原理簡析

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

轉自

http://0x1.im/blog/laravel/laravel-blade-engine.html?utm_source=tuicool&utm_medium=referral

上次提到過,模板引擎一般是要做三件事情:

  1. 變數值的輸出(echo)
  2. 條件判斷和迴圈(if ... else、for、foreach、while)
  3. 引入或繼承其他檔案

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

1. 檢視解析流程

Laravel 的 View 部分是內建了兩套輸出系統:直接輸出和使用 Blade 引擎“編譯”後輸出,預設情況下它們通過檔名字尾來選擇:.blade.php

 字尾的認為是模板檢視檔案,其他的 .php 檔案按照 PHP 本身的方式執行。雖然 Blade 模板檔案中也可以隨意嵌入 PHP 程式碼,但如果並沒有使用,系統還去進行語法解析和替換也是沒有必要的,這樣可以提高效率。

在使用 View 元件輸出時,不管是呼叫 helpers 中提供的 view 函式還是使用 Facades 提供靜態介面View::make(),實際上執行的都是 Illuminate\View\Factory 中的 make 方法。以此為入口,很容易就能知道檢視解析輸出的流程:

  1. 查詢檢視檔案;
  2. 根據檔名字尾從 Container 中取出響應的引擎;
  3. 載入檢視檔案或編譯後加載編譯後的檔案執行,同時將需要解析的資料暴露在檢視檔案環境中。

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 -> 正常輸出 ({{ ... }})

預設情況下經過字元替換之後 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

語句塊部分可以分成三種情況:

  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,我把它核心的部分提取了出來,去掉了其他元件的依賴,也不再依賴副檔名來選擇引擎:

此外也通過這個提取之後的版本做了一個 yii2 能夠使用的版本:https://github.com/XiaoLer/yii2-blade。在之前嘗試的版本中直接使用 Laravel 的 View 元件並不靈活,現在感覺好多了。