PHP 模板引擎的原理及实现

目录
[隐藏]

从 Struts2 的标签库,到 ThinkPHP 自带的模板引擎,再到 Smarty,一直都只是学习怎么去使用,虽然也清楚模板引擎的来历和作用,但也确实没有仔细思考过。正好《PHP 核心技术与最佳实践》一书中谈到了模板引擎,也就认真的学习啦。  

1、简述模板引擎

无论是PHP 还是 Java,在Web开发上都经历过前后端混编的痛苦,为了让前后端更好的协作,也作为视图层和模型层分离的一种解决方案,就有了模板引擎。毫无疑问,模板引擎的存在,确实让前端页面看上去舒爽了太多,但也增加了学习成本。对于一个模板引擎你得去掌握各种标签的使用,才能在前端页面灵活的展示输出逻辑。对我而言,还是能接受使用模板引擎所需要花费的一个学习成本,当然还是更喜欢轻量级的模板引擎。

2、PHP 模板引擎的原理和简单实现

对于一个模板引擎,最基本的无疑是接收变量、解释标签。书中的实现是将模板引擎分解成两大类:一是模板引擎基础类(负责配置引擎、注入变量、展示模板等功能),另一个是模板引擎编译类(专门用于编译模板,实际上就是实现将标签解释为 PHP 代码。)

配置引擎参数就不多说了,无非就是指定定界符、模板文件后缀、模板文件目录等等。这个实现还是比较简单。

2.1 模板引擎的编译

模板引擎的编译实际上就是一个“翻译”过程,即把模板文件转换成PHP文件。因为模板文件中有一些变量和简单的逻辑,这些变量来自逻辑层,而最终需要展示在浏览器中,因此,需要把这些值注入模板,并最终转换成可以被执行的PHP代码。

这个“翻译”过程,就是用正则表达式去模板中匹配,然后替换。比如要匹配模板变量{$data},就可以设计一个正则 pattern=“#\{\\$([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\}#";

对于要实现一些逻辑,如循环、判断也是用正则进行匹配然后转换成相应 PHP 代码。

因为有许多标签需要匹配,所以可以设计两个数组:T_P 和 T_R,T_P 存放正则表达式,T_R 存放要替换成的 PHP 代码
整个“翻译”的过程简单说来就是:

  1. 获取模板文件的内容
  2. 对内容中的标签进行替换
  3. 将替换后内容写入编译后文件
$this->content = file_get_contents($template);
$this->content = preg_replace($this->T_P, $this->T_R,
   $this->content);
file_put_contents($this->comfile, $this->content);

假设有一模板文件内容如下:

{$data},{$person}
<ul>
{loop $b}<li>{V}</li>{/loop}
</ul>
{if $data == 'abc'}
我是abc
{elseif $data == 'def'}
我是def
{else}
我就是我,{$data}
{/if}

进过编译后就会形成如下样子:

<?php echo $this->value['data']; ?>,
<?php echo $this->value['person']; ?>
<ul>
<?php foreach ((array)$this->value['b'] as $K => $V){ ?>
   <li>
   <?php echo $V; ?>
   </li>
<?php }?>
</ul>
<?php if( $data == 'abc'){ ?>
我是abc
<?php }else if($data == 'def'){ ?>
我是def
<?php }else{ ?>
我就是我,<?php echo $this->value['data']; ?>
<?php }?>

编译后怎么访问这个编译后的文件?简单方法是,直接 include 将文件包含进来呗。。。

2.2 注入变量

注入变量用起来还是很简单的,一般就是 $tpl->assign("name","value"); 但后面的实现其实还是有些内涵的。模板引擎基础类中肯定要有一个存放注入变量的值栈(一个数组),所有通过 assign() 注入的变量都会以键值对形式存放在这个数组中。

public function assign($key,$value){
   $this->value[$key] = $value;
}

看 2.1 中最后编译的结果,可以发现页面使用变量有两种情况,一种是带定界符,一种没有过带定界符。就像:{$data} 和 {if $data == 'abc'} 这两种。这两种情况通常被编译后也会是不一样的,像:$this->value['data']  和 $data。

显然,我们注入变量只讲数据存到了值栈 value 中,并没有这些 $data 变量存在,运行会报错“Notice:Undefined variable:data”,怎么解决?

用 PHP 的 extract() 函数,将值栈中的数据导入当前符号表中

extract($this->value,EXTR_OVERWRITE)

这样,所有值栈中的数据,都会有对应的变量存在了。

2.3 展示模板

展示模板 show() 的核心内容其实比较简单,就是调用模板引擎编译类,然后对模板文件进行编译,产生一个编译后的文件,再包含进来就可以了。当然实际应用中,还需要处理很多问题。

$this->compileTool = new CompileClass(
   $this->path(),
   $compileFile,$this->arrayConfig);
//编译模板文件
$this->compileTool->compile();
//执行编译后文件
include $compileFile;

2.4 模板缓存

在前面的步骤已经实现了模板引擎对自定义标签的解析,但是意义不大。因为模板引擎实现了从模板语法到PHP语法的翻译过程,而PHP本身特点决定了这是一个低效和耗时的过程。而且翻译过程又是通过正则表达式来实现的,所以效率会比较低。要解决这个问题,就需要使用缓存了。
将模板翻译后的 PHP 文件所执行的结果保存为 HTML 静态文件,在下一次请求时,直接返回HTML文件,这样省去了翻译和执行的过程,就可以大大提高效率了。

要减少模板引擎的开销,首先就要减少"编译" 的开销,只有在不存在编译后文件,或者编译后文件的修改时间早于模板文件的修改时间时,才进行再次编译。否则直接记载即可。

再一个就是静态缓存,如果开启了缓存,并且缓存文件没有过期,则直接读取缓存文件,否则生成新缓存。

要实现静态化,需要用到PHP 中的两个函数:ob_start() 和 ob_get_contents()

如果开启了缓存,则在执行编译后文件之前,先调用 ob_start() 打开缓存区,这样PHP 页面的输出的额所有内容就会到了输出缓冲中。然后再调用 ob_get_contents() 获取输出缓冲,然后将获取到的所有信息全部写入缓存文件即可!


一个简单的使用了缓存的模板引擎的展示 show() 方法大致如下:

//模板文件
$this->file = $file;
if(!is_file($this->path())){
   exit('找不到对应的模板');
}
//编译文件
$compileFile = $this->arrayConfig['compileDir'].
   md5($file).'.php';
//缓存静态文件
$cacheFile = $this->arrayConfig['compileDir'].
   md5($file).'.htm';
//判断是否需要重新缓存(包括判断缓存文件是否存在)
if($this->reCache($file) === false){
   //初始化编译类
   $this->compileTool = new CompileClass(
       $this->path(),
       $compileFile,$this->arrayConfig);
   //如果开启缓存,则打开缓存区
   if($this->needCache()){
       ob_start();
   }
   //将值栈数据导入到当前的符号表中!
   extract($this->value,EXTR_OVERWRITE);
   //如果编译文件不存在或者模板已被修改,则重新编译执行
   if(!is_file($compileFile)
       || filemtime($compileFile)
       < filemtime($this->path())){
       //编译模板文件
       $this->compileTool->compile();
       //执行编译后文件
       include $compileFile;
   }else{ //否则,直接执行
       include $compileFile;
   }
   //如果开启缓存,从输出缓冲区获取内容并写入缓存静态文件
   if($this->needCache()){
       $message = ob_get_contents();
       file_put_contents($cacheFile, $message);
   }
}else{  //如果已经缓存了,直接读取缓存文件
   readfile($cacheFile);
}

3. 一些总结

当然,对于一个模板引擎,还需要有一些功能才会比较完善,比如文件绝对路径处理、缓存清理等。这里只是个人简单的模拟一下,也算是对模板引擎的处理走了一个流程。个人觉得模板引擎还是轻量级使用起来更舒服,学习成本也更容易接受。
在《PHP 核心技术与最佳实践》的作者看来,模板引擎有两个发展思路:一个是简介,尽量接近PHP原生语法,提供少而精、易于学习的模板语法;另一个是特化,就是模板引擎的语法和当前应用紧密结合。DedeCms(织梦) 就是特化的一个很好的例子,一个标签可以代表一篇文章或一个栏目的显示,可以更加简洁的进行内容展示。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

To