目前市面上有不少PHP的模版引擎,如smarty、blade等。其中大部分都是基於正則表達式將其中的模版語法轉換成PHP代碼,並進行緩存。模版代碼所經歷的過程以下:php
template -> php -> html
複製代碼
使用正則替換或者直接使用PHP原生有什麼問題呢?如下咱們以blade爲例來看一些具體例子:html
<html>
<body>
<div>
<div class="items" >
@if (count($records) === 1)
<p>我有一個記錄!</p>
@elseif (count($records) > 1)
<p>我有多個記錄!</p>
@else
<p>我沒有任何記錄!</p>
@endif
</div>
</div>
</body>
</html>
複製代碼
如上,咱們面臨的第一個問題是html和blade語法混雜在一塊兒。在閱讀邏輯上,咱們須要來回的在blade和html之間作轉化。 固然,當你熟悉了blade的語法並熟練掌握這個能力的時候,這種轉化並不會對你的閱讀構成障礙。前端
可是,對於編輯器來講,若是不使用合適的插件,不管是代碼高亮仍是自動格式化都會產生意想不到結果node
其實以上還不是最使人眼花繚亂的,在我有限的工做經歷中,使用PHP渲染html中的class或者其餘屬性時,常常會看到以下使人恐怖的代碼git
<html>
<body>
<div>
<ul class="items" >
<li <?= $cur==1 ? 'class="active"' : ''?>>NO.1</li>
<li <?= $cur==2 ? 'class="active"' : ''?>>NO.2</li>
<li <?= $cur==3 ? 'class="active"' : ''?>>NO.3</li>
<li <?= $cur==4 ? 'class="active"' : ''?>>NO.4</li>
</ul>
</div>
</body>
</html>
複製代碼
以上還不是最恐怖的,當有的人既不使用<?= ?>
又不使用三元運算時...簡直不可想象。github
對於大部分網頁的頭部和尾部,咱們單獨抽離出來以供複用。對於blade這種支持相似插槽的模版引擎,狀況並不算太糟,但對於不支持相似特性的模版引擎,以下的代碼也是很是常見web
#./header.phtml 頭文件
<html>
<body>
<div class="nav">
</div>
<div class="content">
複製代碼
#./bottom.phtml 尾文件
</div>
<div class="bottom">
</div>
</body>
</html>
複製代碼
如上的問題在於什麼呢,每一個部分模版都不是標籤閉合的,每一部分並不完整。在獨立模版存在很是多的狀況下,正確的讓html標籤閉合也成爲開發負擔之一。正則表達式
好了,說完了這麼多問題,咱們來想想是否有解決的辦法。要知道之前前端js代碼合併也是基於正則,可是新的三大框架都是基於dom解析來實現。那若是說,咱們在寫php渲染頁面的時候也能夠和Vue同樣,使用相似以下的語法,是否是就能解決以上的問題呢? 固然本文只是給你們提供一個最基本的思路,和最基礎的實現,僅供娛樂和思路拓展吧。數組
<!-- ./tpl.html -->
<html>
<body>
<div class="title">
<div p-if="is_author">
<p>{{ author }}</p>
</div>
<div p-else>
<p>{{ vistor }}</p>
</div>
</div>
<div p-for="(value, idx) in items">
<p>{{ value }} - {{ idx }}</p>
<p>{{ value }}</p>
</div>
</body>
</html>
複製代碼
$params = [
"is_author" => true,
"author" => "liangwt",
"vistor" => "Welcome",
"items" => [
"A",
"B",
"C",
],
];
csRender("./tpl.html", $params);
複製代碼
<!-- out -->
<html>
<body>
<div class="title">
<div>
<p>liangwt</p>
</div>
<div>
<p>Welcome</p>
</div>
</div>
<div>
<p>A - 0</p>
<p>A</p>
<p>B - 1</p>
<p>B</p>
<p>C - 2</p>
<p>C</p>
</div>
</body>
</html>
複製代碼
DOM把整個文檔表示爲一棵樹,確切的說是一個家譜樹。家譜樹中咱們使用 parent(父)、child(子)、sibling(兄弟)來描述成員之間的關係。 對於一個普通的以下的xml來講緩存
<?xml version="1.0" encoding="utf-8"?>
<bookstore>
<book category="children">
<title lang="en">Harry Potter</title>
<author>J K. Rowling</author>
<year>2005</year>
<price>29.99</price>
</book>
<book category="cooking">
<title lang="en">Everyday Italian</title>
<author>Giada De Laurentiis</author>
<year>2005</year>
<price>30.00</price>
</book>
<book category="web">
<title lang="en">Learning XML</title>
<author>Erik T. Ray</author>
<year>2003</year>
<price>39.95</price>
</book>
<book category="web">
<title lang="en">XQuery Kick Start</title>
<author>James McGovern</author>
<author>Per Bothner</author>
<author>Kurt Cagle</author>
<author>James Linn</author>
<author>Vaidyanathan Nagarajan</author>
<year>2003</year>
<price>49.99</price>
</book>
</bookstore>
複製代碼
咱們能夠生成以下的dom樹結構
示例來源於知乎
PHP中原生提供了xml文檔解析的拓展,它使用起來很是簡單。網上資料大多介紹基於此拓展的封裝包,所以這裏稍微詳細介紹下。
前面介紹dom樹的時候說過,文檔是由不一樣類型的節點構成的集合,因此DomDocument中絕大多數的類都繼承於此。
它的類屬性除了描述了自身名稱($nodeName
)、值($nodeValue
)、類型($nodeType
)等,還描述了其父節點($parentNode
)、子節點($childNodes
)、同級節點($previousSibling
、$nextSibling
)等。
它的類方法除了包括對子節點的插入(appendChild()
)、替換(replaceChild()
)、 移除(removeChild()
)以外,還有諸多用於判斷自身屬性的函數。
做爲任何類型的節點基類咱們須要重點關注它的每個屬性和方法,參考官方文檔。
DOMDocument繼承自DOMNode,它表明了整個文檔,也是整個文檔樹的根結點。其中繼承自基類的屬性$nodeType
是XML_DOCUMENT_NODE(9)
咱們一般使用它的load*()
來建立dom樹,和save*()
系列方法將dom轉換成文本
咱們的代碼也是如此開頭和結束
function csRender(string $tpl, array $params) {
$dom = new DomDocument("1.0", "UTF-8");
$dom->loadHTMLFile($tpl);
// ...
echo $dom->saveHTML();
}
複製代碼
DOMElement繼承自DOMNode,它表明了
之類的標籤,是構成dom結構的基本節點.其中標籤的名字就是節點的屬性tagName
,它的$nodeType
是XML_ELEMENT_NODE = 1
元素能夠包含其餘的元素,元素節點中也包含了其餘類型的節點。
咱們可使用getAttributeNode()
或者getAttribute()
來獲取元素節點的屬性或者屬性名,使用getElementsByTagName(string $name)
獲取元素包含的標籤名$name
爲的節點.以及使用remove*()
和set*()
函數來刪除和修改指定屬性
咱們在實現上面p-if的時候須要進行判斷if條件是否成立,並在以後刪除掉這個屬性
if ($item->nodeType == XML_ELEMENT_NODE
&& $if_value = $item->getAttribute("p-if") {
if ($if_result) {
$item->removeAttribute("p-if");
}
}
複製代碼
DOMAttr繼承自DOMNode,它表明了標籤class="one"
之類的屬性,如上面所講對元素節點調用getAttributeNode()
便可獲取此元素的屬性節點。屬性節點的nodeType是XML_ATTRIBUTE_NODE=2
DOMText繼承自DOMCharacterData,DOMCharacterData也是繼承自DOMNode。在dom中它表明了元素節點包含的文本.其中nodeValue屬性就是文本的內容。文本節點的nodeType 是XML_TEXT_NODE = 3
除此以外須要知道的是,文本節點單老是被包含在元素節點中,文本節點的父節點是元素節點。咱們經過$elementNode->childNodes
便可獲取(若是有文本節點的話),此函數返回的是 DOMNodeList 類型,它表明節點集合,並實現了Traversable接口
咱們在實現mustache語法的時候須要判斷元素的文本節點中是否有{{}}包裹的變量
if ($item->nodeType == XML_TEXT_NODE) {
$str = preg_replace_callback('/\{\{(.*?)\}\}/', function ($matches) use ($params) {
// ...處理邏輯
}, $item->nodeValue);
$item->nodeValue = $str;
}
複製代碼
以上就是最經常使用的幾種節點類型了,咱們下面講一講如何進行節點遍歷.咱們須要基於遍歷去實現樹中節點判斷,而後進行樹操做
咱們在上面介紹瞭如何加載一個html文檔,其中獲取的變量$dom
也是dom樹的根結點
function csRender(string $tpl, array $params) {
$dom = new DomDocument("1.0", "UTF-8");
$dom->loadHTMLFile($tpl);
traversingtDomNode($dom, $params);
echo $dom->saveHTML();
}
複製代碼
擁有一個節點以後如何遍歷它的子節點呢,咱們獲取其$domNode->childNodes子屬性進行遍歷便可
function traversingtDomNode($dom, $params){
foreach ($domNode->childNodes as $item) {
//...
}
}
複製代碼
在遍歷每個節點過程當中,能夠經過判斷nodeType來對不一樣類型節點進行操做。同時若是此節點依舊有子節點,咱們繼續把節點放入此函數進行遞歸調用
function traversingtDomNode($dom, $params){
foreach ($domNode->childNodes as $item) {
if ($item->nodeType == XML_ELEMENT_NODE
&& $if_value = $item->getAttribute("p-if")) {
// ...
}
if ($item->nodeType == XML_ELEMENT_NODE
&& $item->hasAttribute("p-else")) {
// ...
}
if ($item->hasChildNodes()) {
traversingtDomNode($item, $params);
}
}
}
複製代碼
{{ key }}
語法實現很簡單,咱們只要經過正則拿到{{ key }}
中的key值,而後把連着{{ }}一塊兒替換成$params[$key]
便可
// ...
if ($item->nodeType == XML_TEXT_NODE) {
$str = preg_replace_callback('/\{\{(.*?)\}\}/', function ($matches) use ($params) {
return $params[trim($matches[1])];
}, $item->nodeValue);
$item->nodeValue = $str;
}
// ...
複製代碼
<div p-if="is_author">
<p>{{ author }}</p>
</div>
複製代碼
if語法實現也很簡單,咱們經過$if_value =$item->getAttribute("p-if")
獲取屬性值,並經過判斷$params[$if_value
]`的值,若是成立,則刪掉屬性,展現此元素節點。若是不成立則刪掉此節點。
// ...
if ($item->nodeType == XML_ELEMENT_NODE && $if_value = $item->getAttribute("p-if")) {
$if_result = $params[$if_value] ?? false;
if ($if_result) {
$item->removeAttribute("p-if");
} else {
array_push($elementsToRemove, $item);
}
}
// ...
複製代碼
注意這裏面有個小坑: 參考文檔中的一條評論:notes: NO.1 在遍歷中移除節點會致使dom樹重構,遍歷終止。因此咱們採起將要移除的節點單獨記錄到$elementsToRemove
,在循環結束後統一移除
$elementsToRemove = [];
foreach ($domNode->childNodes as $item) {
// ..
}
foreach ($elementsToRemove as $item) {
$item->parentNode->removeChild($item);
}
複製代碼
<div p-if="is_author">
<p>{{ author }}</p>
<div p-if="show_intro">
<p>{{ intro }}</p>
</div>
<div p-else>
<p>{{ vistor }}</p>
</div>
</div>
複製代碼
else 的實現會用到頗有意思的技巧,由於else的真值並不取決於它自身,而是取決於和它配對的if的值。注意!是和它配對的if值,若是你想固然的認爲是else以前的那個if值可就錯咯。咱們看下面這個例子:
<div p-if="is_author">
<p>{{ author }}</p>
<div p-if="show_intro_one">
<p>{{ intro_one }}</p>
</div>
<div p-if="show_comment_one">
<p>{{ comment_one }}</p>
</div>
<div p-else>
<p>{{ comment_two }}</p>
</div>
<div p-else>
<p>{{ intro_two }}</p>
</div>
</div>
複製代碼
其中最後一個else屬性的值取決於第一個if "show_intro_one" 的值,即$params[$if_value]
的值.那如何才能實現if-else正確的匹配呢,答案就是: 棧。在咱們實現括號匹配,if-else匹配得各類匹配問題中,棧是一個很是好的思路。
咱們第一步須要在dom樹同一深度給予不一樣棧,由於if-else的匹配只會發生在同級元素直接,而不會發生在父子元素之間。
第二步天然是每遇到一個if就把值放入對應棧的棧頂。
第三步在遇到else時,從棧頂取出一個值,它的反值即爲else的值
foreach ($domNode->childNodes as $item) {
// 1. 第一步
$if_stack = [];
// ...
if ($item->nodeType == XML_ELEMENT_NODE
&& $if_value = $item->getAttribute("p-if")) {
$if_result = $params[$if_value] ?? false;
// 第二步
array_push($if_stack, $if_result);
// ...
}
if ($item->nodeType == XML_ELEMENT_NODE && $item->hasAttribute("p-else")) {
// 第三步
$if_result = array_pop($if_stack);
if (!$if_result) {
$item->removeAttribute("p-else");
} else {
array_push($elementsToRemove, $item);
}
}
}
複製代碼
<div p-for="(value, idx) in items">
<p>{{ value }} - {{ idx }}</p>
<p>{{ value }}</p>
</div>
複製代碼
for的語法實現思路很簡單,把含有屬性p-for屬性的元素全部子節點按照遍歷的數組循環賦值便可。其中稍有難度的就是$params
中的值傳遞問題,或者說$params
值的做用域問題,若是剛好$params中也有個字段叫value或者idx,但很明顯在for的子節點中,value和idx應該是局部做用域,他們須要在每次循環開始賦予新值,並在整個循環結束後被銷燬.
因此咱們讓一個新值$for_runtime_params
等於外部$params
參數,並在循環中繼續遞歸調用遍歷函數
if ($item->nodeType == XML_ELEMENT_NODE
&& $for_value = $item->getAttribute("p-for")) {
preg_match("/\((.*?), (.*?)\) in (.*)/", $for_value, $matches);
[, $value, $index, $items] = $matches;
foreach ($params[$items] as $k => $v) {
$for_runtime_params = $params;
$for_runtime_params[$value] = $v;
$for_runtime_params[$index] = $k;
foreach ($item->childNodes as $el) {
$e = $el->cloneNode(true);
if ($e->hasChildNodes()) {
traversingtDomNode($e, $for_runtime_params);
}
}
}
}
複製代碼
注意: 和刪除節點同樣,咱們在遍歷的過程當中也不能插入新節點,他會致使獲取的子節點永遠爲空。因此也和刪除同樣單純記錄最後統一插入便可
本文實現確定還有諸多細節未考慮,可是給你們提供一個不錯的思路。對於將來能夠嘗試繼續實現v-class
語法,slot
功能,components
功能,都是至關不錯的
更詳細的實現能夠能夠查看個人github: cs-render
同時也歡迎在個人博客-showthink閱讀更多其餘文章
也能夠關注個人微博@不會涼的涼涼與我交流