内容简介:目前市面上有很多PHP的模版引擎,如smarty、blade等。其中大部分都是基于正则表达式将其中的模版语法转换成PHP代码,并进行缓存。模版代码所经历的过程如下:使用正则替换或者直接使用PHP原生有什么问题呢?以下我们以blade为例来看一些具体例子:如上,我们面临的第一个问题是html和blade语法混杂在一起。在阅读逻辑上,我们需要来回的在blade和html之间做转化。 当然,当你熟悉了blade的语法并熟练掌握这个能力的时候,这种转化并不会对你的阅读构成障碍。
目前市面上有很多 PHP 的模版引擎,如smarty、blade等。其中大部分都是基于正则表达式将其中的模版语法转换成PHP代码,并进行缓存。模版代码所经历的过程如下:
template -> php -> html 复制代码
使用正则替换或者直接使用PHP原生有什么问题呢?以下我们以blade为例来看一些具体例子:
<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的语法并熟练掌握这个能力的时候,这种转化并不会对你的阅读构成障碍。
但是,对于编辑器来说,如果不使用合适的插件,无论是代码高亮还是自动格式化都会产生意想不到结果
问题二: html中渲染class等属性
其实以上还不是最令人眼花缭乱的,在我有限的工作经历中,使用PHP渲染html中的class或者其他属性时,经常会看到如下令人恐怖的代码
<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> 复制代码
以上还不是最恐怖的,当有的人既不使用 <?= ?>
又不使用三元运算时...简直不可想象。
问题三: 公共模版中代码代码的不完整
对于大部分网页的头部和尾部,我们单独抽离出来以供复用。对于blade这种支持类似插槽的模版引擎,情况并不算太糟,但对于不支持类似特性的模版引擎,如下的代码也是非常常见
#./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> 复制代码
1. DOM基本知识
- D: Document 代表里文档
- O: Object 代表了对象
- M: Model 代表了模型
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树结构
示例来源于知乎
2. PHP中DomDocument的使用
PHP中原生提供了xml文档解析的拓展,它使用起来非常简单。网上资料大多介绍基于此拓展的封装包,因此这里稍微详细介绍下。
(1). DOM中的基类节点: The DOMNode class
前面介绍dom树的时候说过,文档是由不同类型的节点构成的集合,所以DomDocument中绝大多数的类都继承于此。
它的类属性除了描述了自身名称( $nodeName
)、值( $nodeValue
)、类型( $nodeType
)等,还描述了其父节点( $parentNode
)、子节点( $childNodes
)、同级节点( $previousSibling
、 $nextSibling
)等。
它的类方法除了包括对子节点的插入( appendChild()
)、替换( replaceChild()
)、 移除( removeChild()
)之外,还有诸多用于判断自身属性的函数。
作为任何类型的节点基类我们需要重点关注它的每一个属性和方法,参考官方文档。
(2). 整个文档: DOMDocument extends DOMNode
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(); } 复制代码
(3). 元素节点 DOMElement extends DOMNode
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"); } } 复制代码
(4). 属性节点 DOMAttr extends DOMNode
DOMAttr继承自DOMNode,它代表了标签 class="one"
之类的属性,如上面所讲对元素节点调用 getAttributeNode()
即可获取此元素的属性节点。属性节点的nodeType是XML_ATTRIBUTE_NODE=2
(5). 文本节点 DOMText extends DOMCharacterData
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; } 复制代码
(6). 节点遍历
以上就是最常用的几种节点类型了,我们下面讲一讲如何进行节点遍历.我们需要基于遍历去实现树中节点判断,然后进行树操作
我们在上面介绍了如何加载一个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); } } } 复制代码
3. mustache语法实现
{{ 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; } // ... 复制代码
4. if语法实现
<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); } 复制代码
5. eles语法实现
<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); } } } 复制代码
6. for语法实现
<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); } } } } 复制代码
注意: 和删除节点一样,我们在遍历的过程中也不能插入新节点,他会导致获取的子节点永远为空。所以也和删除一样单纯记录最后统一插入即可
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。