### 2019 年 1 月 6 日 發布
## 模板引擎由來
早期做PHP開發WEB應用都是把PHP代碼和HTML模板混在一起,模板引擎的誕生主要就是為了解決后端與前端的完全分離(現在來看其實是屬于不完全分離)的問題,讓開發與美工可以分工合作(雖然實際上最終模板工作大多仍然是由后端開發人員完成),從而提高開發效率和便于維護。
伴隨著PHP的快速成長,模板引擎也越來越多,但大致分為解釋型和編譯型兩種,目前主流的模板引擎大多數是編譯型的,也就是會先把模板編譯成PHP文件執行,只要模板文件本身不變化,就不需要重新編譯,例如老牌的[`Smarty`](https://www.smarty.net/)。解釋型的模板引擎每次執行的時候都會進行模板解析流程,例如小強([`tinybutstrong`](https://www.tinybutstrong.com))。
`ThinkPHP`從一開始就內置了一個基于`XML`標簽庫技術的編譯型模板引擎,早期參考自`Struts`,并且不斷在汲取新的思想不斷進化。
## 如何選擇模板引擎
目前主流框架都帶有模板引擎組件或者封裝了模板引擎的實現,因此選擇內置的解決方案是最佳之選,功能和穩定性都有保證。目前最流行的模板引擎當屬`Laravel`自帶的`Blade`模板引擎,以及`Symfony`自帶的`Twig`模板引擎。
通過安裝模板引擎擴展,你可以在ThinkPHP中輕松使用包括`Angular`、`Twig`和`Blade`在內的模板引擎,甚至完全不使用模板引擎而是直接用PHP文件作為模板。
因為近幾年三大前端框架(`React`/`Vue`/`Angular`)的流行,前后端分離開發逐漸成為主流,因此從ThinkPHP`5.0`開始定位為`API`開發而設計,導致模板引擎的概念已經被弱化了。ThinkPHP`5.1`版本的模板引擎進行過一次內部的重構,使得模板標簽更加易用和接近PHP語法。
至少對于大部分新的應用來說,應該選擇更主流的前后端分離設計,盡量減輕服務端的壓力,也更方便前后端單獨測試。你會在市面上不經意的看到采用`Vue`和`ThinkPHP`的產品(之前幾期的ThinkPHP開發者周刊曾經報道過幾個)。如果是維護一些老項目尤其是內容管理產品的時候,仍然可能會用到模板引擎。
鑒于這種情況,下一個版本的ThinkPHP框架將不會內置模板引擎,但有需要使用模板引擎的開發者仍然可以使用官方獨立出來的`think-template`類庫,具體使用可以參考[這篇文章](https://blog.thinkphp.cn/795679)。
后面的篇幅,我們主要來總結下ThinkPHP內置的模板引擎的使用和技巧。
## 模板執行流程
系統內部的模板引擎調用關系如下:
> ### 視圖(View) <=> 模板驅動(Driver) <=> 模板引擎(Template)
視圖和模板引擎之間增加了一個驅動層,所以可以很方便的替換其它的模板引擎。通常我們在控制器中調用的`assign`/`fetch`等方法其實都是調用的`think\View`類的方法。當然,如果有必要,你也完全可以直接在控制器中操作模板引擎類,只是不方便切換其它模板引擎。
以`fetch`方法為例,我們看下最終的調用過程:
```
think\Controller->fetch();
think\View->fetch();
think\view\driver\Think->fetch();
think\Template->fetch();
```
如果你調用`fetch`方法的時候沒有傳入要渲染的完整模板文件名,則會在第三步的時候自動識別要渲染的模板文件。
很顯然,最關鍵是最后一步,模板編譯和執行的流程則全部由
```
think\Template->fetch();
```
方法完成,這個環節大體又可以分成幾個流程。
### 1、判斷和讀取頁面渲染緩存
如果當前模板設置了頁面輸出緩存并且已經渲染輸出過,如果是則會讀取緩存中的輸出內容直接輸出。
```
if (!empty($this->config['cache_id']) && $this->config['display_cache']) {
// 讀取渲染緩存
$cacheContent = $cache->get($this->config['cache_id']);
if (false !== $cacheContent) {
echo $cacheContent;
return;
}
}
```
### 2、定位模板文件
定位實際的模板文件操作由模板引擎類的`parseTemplateFile`方法實現,這個方法的邏輯其實和視圖驅動類的`parseTemplate`方法是類似的,如果最終的模板文件不存在則會拋出一個模板文件不存在的異常。
```
$template = $this->parseTemplateFile($template);
```
### 3、判斷編譯緩存
如果當前的模板文件已經編譯過,會判斷緩存是否還有效,有效的話就不用重復解析直接讀取緩存的解析內容。由`checkCache`方法負責完成。
```
if (!$this->checkCache($cacheFile)) {
// 緩存無效 重新模板編譯
$content = file_get_contents($template);
$this->compiler($content, $cacheFile);
}
```
### 4、模板編譯并緩存
這一步驟是模板引擎最核心的環節,也是功能最復雜的地方,由`compiler`方法負責完成,主要是解析當前模板文件中的模板標簽語法為PHP可執行代碼,然后生成一個模板解析緩存文件,也就是所謂的模板“編譯”,其中使用了大量的正則表達式替換技術,雖然正則解析有一定的性能開銷,但得益于一次解析多次調用的緩存原理,基本上模板解析的性能開銷不會影響實際使用的性能。
模板編譯方法的關鍵代碼是`parse`方法,`parse`方法負責對模板文件中的標簽進行解析,然后寫入編譯緩存文件,編譯緩存默認使用的是文件緩存,支持擴展。
### 5、讀取編譯緩存
模板編譯的過程只是生成了模板編譯緩存文件,并沒有真正載入模板,這一步驟就是載入模板編譯緩存,然后導入模板變量。實現方法可以參考`think\template\driver\File`類的`read`方法。
```
public function read($cacheFile, $vars = [])
{
$this->cacheFile = $cacheFile;
if (!empty($vars) && is_array($vars)) {
// 模板陣列變量分解成為獨立變量
extract($vars, EXTR_OVERWRITE);
}
//載入模版緩存文件
include $this->cacheFile;
}
```
### 6、緩存頁面輸出
如果當前模板渲染的時候開啟了頁面輸出緩存,就會這一步生成頁面渲染后的輸出緩存。
## 模板編譯原理
我們來了解下ThinkPHP的模板引擎的實現原理。前面提到過,ThinkPHP的模板引擎最早源于`Struts`的設計理念,基于XML和標簽庫的技術實現。在設計模板語言的時候使用系統固定的標簽來實現普通的變量輸出功能(所以稱之為普通標簽),而利用XML標簽庫技術實現的動態標簽用于變量的控制或者條件判斷輸出。
普通標簽的解析是由`think\Template`類的`parseTag`方法完成的,主要實現了下面幾個模板功能:
* 變量輸出(包括系統變量);
* 函數過濾;
* 變量運算;
* 三元運算;
* 執行函數以及輸出結果;
* 模板注釋;
標簽庫采用的是動態擴展的設計方案,采用了類似XML的閉合/開放定義方式(這個其實也是目前模板引擎的一個局限所在),例如下面的這個:
```
// 閉合類型標簽
<tagLib:tagName name="value" >
...
</tagLib:tagName>
// 開放類型標簽
<tagLib:tagName name="value" />
```
`tagLib`就代表了一個標簽庫(類),后面的`tagName`標簽就表示該標簽庫下面的某個標簽(通常對應了標簽庫類的某個方法),后面的屬性就是該標簽支持的屬性定義。具體該標簽的屬性和功能則完全由標簽庫類的這個方法來決定。
可以在模板開頭明確指出,當前模板使用了哪些標簽庫
```
{taglib name="html,article" /}
```
所以要擴展模板引擎的功能只需要通過擴展一個標簽庫類就可以了。大多數的內容管理系統都會定義一套自己的模板二次開發標簽,利用標簽庫功能就可以很方便的定義一套屬于自己的標簽功能。
系統內置了一套標簽庫`Cx`,主要用于文件包含、條件控制、循環輸出等功能。內置標簽庫在使用的時候無需引入,而且在使用的時候可以省略標簽庫前綴,例如:
```
{foreach $list as $key=>$vo }
{$vo.id}:{$vo.name}
{/foreach}
```
這個模板語法相信PHP開發的很容易上手,上面的標簽解析由`think\template\taglib\Cx`類的`tagForeach`方法完成,該方法的返回值是一個字符串,其實就是最終會解析成的一段包含變量的PHP可執行代碼。
到這里,模板引擎的執行過程和原理現在基本就明白了,剩下的就是模板標簽的解析細節,考驗的就是正則表達式的掌握程度了。本文就不做深入了,有興趣的朋友可以去看一些正則表達式的相關資料(例如這本《[正則指引](https://amzn.to/2Dd3Px4)》,開發者周刊第14期也提供了一些在線的正則工具)。
## 遵循的原則
使用模板引擎,要盡量遵循幾個重要的原則。
### 不要在模板文件中添加任何的業務邏輯
模板的作用主要是進行模板變量的控制和輸出,不要在模板文件中添加業務邏輯代碼。
### 明確指定渲染模板
養成明確指定渲染模板的好習慣,避免當方法名發生變化,或者被其它方法調用的時候發生錯誤。也不易受模板命名規范的影響。
### 變量統一賦值
使用`assign`方法或者在`view`助手函數的時候,統一一次傳入模板變量。不要多次賦值,以免混亂。
### 系統變量無需賦值到模板
對于系統變量(包括請求變量、`$_SESSION`和`$_SERVER`等系統變量)無需進行模板變量賦值,可以直接在模板中輸出。
## 常見問題
這里總結一下經常會遇到的一些常見問題。
### 修改定界符
可以通過模板配置文件修改模板標簽的定界符。
例如,修改普通標簽定界符
```
'tpl_begin' => '{{', // 模板引擎普通標簽開始標記
'tpl_end' => '}}', // 模板引擎普通標簽結束標記
```
標簽庫標簽定界符
```
'taglib_begin' => '<{', // 標簽庫標簽開始標記
'taglib_end' => '}>', // 標簽庫標簽結束標記
```
### 保持原樣輸出
如果擔心模板標簽和JS代碼產生混淆,可以使用`literal`標簽
```
{literal} Hello,{$name}! {/literal}
```
頁面最終會直接輸出`Hello,{$name}!`
### 避免輸出轉義
5.1版本為了避免XSS攻擊,默認對模板變量的輸出使用了安全轉義,默認的轉義函數是`htmlentities`,你可以通過更改`default_filter`配置改變默認的轉義函數。
如果你不需要對某個模板變量輸出進行轉義(例如包含了HTML代碼),可以使用:
```
{$data.content|raw}
```
分頁輸出就是一個需要輸出HTML的典型例子,因此必須增加`|raw`。
### 關于模板主題
新版取消了原來的模板主題功能,因為模板主題對模板引擎來說,其實無非是一個模板目錄,完全可以根據自己的需求控制。
例如
```
$theme = 'blue';
$this->fetch('/' . $theme. '/user/index');
```
或者動態設置模板引擎的`view_path`參數
```
$this->view->config('view_path', \think\facade\App::getModulePath(). 'view/'. $theme . '/');
```
### 如何關閉模板緩存
由于是編譯型模板引擎,模板標簽不能被直接執行,必須編譯成PHP語法后才能執行,因此不能關閉模板編譯緩存,模板引擎每次執行渲染的時候會檢測模板文件是否有變化,當模板文件的修改時間超過模板編譯緩存的修改時間后,模板引擎會自動更新編譯緩存。
但你可以強制模板引擎每次都重新編譯,只需要在配置文件中設置
```
'tpl_cache' => false, // 關閉模板緩存
```
### 使用PHP作為模板引擎
如果不希望使用內置的模板引擎,直接使用PHP作為模板引擎,可以配置
~~~
'type' => 'php',
~~~
配置使用PHP作為模板引擎的話,是不會生成模板編譯緩存的。
### 如何使用第三方模板引擎
系統支持擴展其它的第三方模板引擎,你只需要開發一個模板引擎驅動,目前已經支持的第三方模板引擎包括`Smarty`、`Twig`和`Blade`。
### 如何跨模塊輸出模板
要渲染一個跨模塊的模板文件,你需要使用
```
// 渲染user模塊的模板文件
$this->fetch('User@order/index');
```
### 是否支持變量運算
可以直接在模板文件中進行變量運算而不需要在控制器中進行運算后再賦值都模板變量輸出。
```
{$score1+$score2}
{$count++}
```
### 文件包含是否支持變量
include標簽可以支持傳入變量,但只能使用
```
{include file="$file" /}
```
而不能使用
```
{include file="file_$name" /}
```
### 可以支持模板輸出替換么
支持兩個方式對模板進行輸出替換,如果需要對**模板文件的內容**進行替換,可以配置:
```
'tpl_replace_string' => [
'__STATIC__'=>'/static',
'__JS__' => '/static/javascript',
]
```
如果是對**模板渲染輸出的內容**進行替換,可以在控制器中使用視圖過濾功能:
~~~
public function index()
{
// 使用視圖輸出過濾
return $this->filter(function($content){
return str_replace("\r\n",'<br/>',$content);
})->fetch();
}
~~~
### 模板繼承的`block`是否支持嵌套
目前模板繼承的`block`無法支持嵌套功能,你應該使用其它方式解決。
- 值得升級到5.1的18個理由
- 5.1.7版本新特性
- JSON字段類型在ORM中的使用
- 文件下載響應對象
- 教你使用5.1的數組對象查詢
- 模型三大利器之一:搜索器
- 在ThinkPHP中使用Yaconf
- 掌握命令行的表格輸出
- 5.1.25查詢參數綁定的改進
- ThinkPHP安全規范指引
- 巧用數據集的排序功能實現統計排序
- think-orm ——基于5.1的獨立ORM庫
- think-template——基于ThinkPHP的獨立模板引擎
- ThinkPHP5.1.26版本發布——修正版本,包含安全更新
- ThinkPHP5.0和3.2再發安全更新
- 官宣:ThinkPHP發布首個LTS版本
- 你真的了解Db類和模型的正確使用姿勢么?
- 如何更有效的記錄和管理日志
- 模型三大利器之二:修改器
- ThinkPHP5.1.28版本發布——修正上一版本問題,改進關聯查詢
- 模型三大利器之三:獲取器
- API版本控制的幾種思路
- ThinkPHP5.2第一個Beta版本發布測試
- 讓你少犯錯的數據查詢基本原則
- ThinkPHP發布5.1.29版本——常規更新
- 這15個好習慣讓你更容易升級到5.2
- 如何有效提高ThinkPHP的應用性能
- 讓你提高開發效率的查詢技巧
- 模型關聯查詢不完全指南
- 5.2發布Beta2版本——統一和精簡大量用法
- ThinkPHP發布5.1.30版本——支持微秒時間字段寫入
- ThinkPHP的數據緩存使用
- ThinkPHP5.2安裝及入口文件
- ThinkPHP榮獲2018 年度最受歡迎中國開源開發框架第1名
- 5.1路由使用心得技巧
- ThinkPHP5.*版本發布安全更新
- ThinkPHP項目及代碼規范指北
- 5.2版本的設計規范指導
- ThinkPHP5.1.32版本發布——圣誕快樂
- 利用Trait特性給模型增加樂觀鎖功能
- 5.2數據庫和模型的變化(摘要)
- ThinkPHP模板引擎實現和常見問題
- ThinkPHP5.0.24版本發布——安全更新
- 不忘初心,方得始終——ThinkPHP十三周年報告
- ThinkPHP5+相關資源匯總
- 異步社區ThinkPHP周年慶專享優惠活動
- 5.2路由的調整和改進
- ThinkPHP發布5.1.33版本——包含安全更新
- ThinkPHP擴展開發指南
- ThinkPHP發布5.2Beta3版本
- ThinkPHP發布5.1.34版本——喜迎新年
- ThinkPHP發布5.2RC1版本
- ThinkPHP發布5.1.35版本——常規更新
- 5.2配置類的調整
- 5.2時間查詢的改進和優化
- 5.2RC版本升級不完全指導(僅供學習參考)
- ThinkPHP5.2版本正式變更為6.0版本
- ThinkPHP百度云云虛擬主機專享免費活動
- 事件系統以及查詢事件、模型事件的使用
- ThinkPHP6.0RC2版本發布——架構升級、精簡核心
- ThinkPHP5.1.36LTS版本發布——常規更新
- 新版Session和Cookie設計變化
- ThinkPHP5.1.37版本發布——常規更新
- ThinkPHP6.0RC3版本發布——細節完善,體驗優化
- 6.0中間件使用詳解
- Composer各大廠商鏡像地址
- ThinkPHP6.0發布計劃公告
- 「ThinkPHP開發者周刊」招募志愿者
- ThinkPHP6.0日志變化
- ThinkPHP5.1.38版本發布——常規更新
- ThinkPHP6.0RC4版本發布——ORM獨立,日志多通道支持
- ThinkORM2.0開發指南上線
- ThinkPHP6.0RC5版本發布——多應用模式獨立,中間件機制調整
- ThinkPHP6.0版本發布——程序員節福利
- ThinkPHP5.1.39LTS版本發布——常規更新
- ThinkPHP6.0.1版本發布——圣誕快樂!
- 回顧2019,展望2020!
- ThinkPHPV6.0.2版本發布——2020新春快樂!
- 周年福利系列:Swoole合作優惠
- 億速云成為ThinkPHPV6.0獨家贊助發布商??
- 新冠疫情工具和限免資源專題(保持更新中)
- 周年福利系列:創宇信用認證合作優惠
- 周年福利系列:碼云企業版限時10%優惠
- 周年福利系列:想天短說抵現優惠
- think-swoole直播:從零開始掌握swoole開發
- 周年福利系列:B2C開源電商ShopXO授權8折優惠
- 周年福利系列:LayuiAdmin 永久授權限時優惠
- ThinkPHP資源導航站上線——構建生態 服務未來
- ThinkPHP官方技術支持服務和應用服務市場上線公測
- ThinkPHP市場精選——推廣基本要素
- ThinkPHP市場精選——客服聊天專題
- ThinkPHPV6.0.3版本發布——端午安康
- ThinkPHP開發者扶持計劃
- 6.0.3版本關鍵更新及升級事項
- 「ThinkPHP開發者周刊」改版重啟
- ThinkPHP市場精選——企業建站專題
- ThinkPHP 提供統一API接口服務
- ThinkPHP市場精選——直播電商專題
- ThinkAPI服務SDK發布
- 官方服務市場啟用獨立子域名
- ThinkPHP市場精選——刷臉支付專題
- ThinkAPI推出會員服務計劃
- ThinkPHPV6.0.4版本發布——中秋國慶雙節快樂
- ThinkPHPV5.1.40版本發布——常規更新
- 1024程序員節福利走一波
- ThinkPHP V6.0.5版本發布——兼容Composer2.0
- 知識圖譜應用場景——源論技術沙龍
- ThinkPHP5.*版本改進Composer2.0的兼容
- 官方市場雙十一精選推薦
- 技術人做產品有機會么(文末送課程)
- 本周秒殺——古德云售后獲客營銷系統
- ThinkAPI服務更新——支持接口分組和PHP版本依賴調整
- PHP8新特性盤點
- PHP8新特性系列:構造器屬性提升使用及注意事項
- ThinkPHP2021新年寄語
- ThinkPHP V6.0.6&V5.1.41版本發布——兼容PHP8.0
- PHP如何更優雅地調用API接口
- ThinkPHP V6.0.7發布——修正版本
- ThinkAPI服務更新——IP白名單
- 最新版ThinkORM對于時間字段的調整
- ThinkAPI短信接口正式上線
- ThinkPHP V6.0.8版本發布——多環境變量配置支持
- 頂想云寫作服務開啟第一次公測
- ThinkSSL上線——官方SSL/TLS證書服務
- MDBootstrap國內用戶福利——ThinkPHP官方市場首發
- ThinkPHP V6.0.9版本發布——常規更新
- ThinkORM功能盤點——虛擬模型
- 全面支持主流GIT版本庫——云寫作服務第二次公測
- 云寫作服務私有化部署方案之:版本庫私有化
- 看云雙十一活動
- ThinkPHP V6.0.10LTS發布——兼容PHP8.1
- ThinkPHP V6.0.12發布——命令行兼容8.1
- 頂想云知識管理上線公測——構建企業文檔中心和知識庫
- 頂想云上線——助力生態數字化建設
- 618活動進行中——官方市場迎來一波更新
- 頂想云知識管理正式上線——看云文檔啟動遷移服務
- ThinkPHP V6.0.13發布——常規更新
- 頂想云網站助理服務上線——構建產品支持服務
- ThinkPHP發布6.1.0&6.0.14版本——安全更新
- ThinkPHP新版社區上線試運營
- ThinkAPI上架人臉核身接口——助力網站實名認證
- 辭舊迎新——舊版社區停止注冊及發帖
- ThinkPHP6.1.2版本發布——兼容PHP8.2