写了一个ThinkPHP的模板引擎, 仿angular的, 简单版

前段时间学习angularjs, 里面的模板思想和实现方法很酷, 就心血来潮, 想实现一个php版的, 今天试着写了一下, 发现貌似可以, 具体看源码.

./ThinkPHP/Library/Think/Template/Driver/Angular.class.php

  1. <?php
  2. namespace Think\Template\Driver;
  3. use Think\Storage;
  4. /**
  5. * Angular模板引擎驱动
  6. */
  7. class Angular {
  8. private $config = array();
  9. private $tpl_var = array();
  10. /**
  11. * 架构函数
  12. */
  13. public function __construct() {
  14. $this->config['cache_path'] = C('CACHE_PATH');
  15. $this->config['tpl_dir'] = THEME_PATH;
  16. $this->config['cache_path'] = C('CACHE_PATH');
  17. $this->config['template_suffix'] = C('TMPL_TEMPLATE_SUFFIX');
  18. $this->config['cache_suffix'] = C('TMPL_CACHFILE_SUFFIX');
  19. $this->config['tmpl_cache'] = C('TMPL_CACHE_ON');
  20. $this->config['cache_time'] = C('TMPL_CACHE_TIME');
  21. $this->config['attr'] = 'tp-';
  22. }
  23. /**
  24. * 编译模板
  25. * @param type $tpl_file 模板文件
  26. * @param type $tpl_var 模板变量
  27. */
  28. public function fetch($tpl_file, $tpl_var) {
  29. $this->tpl_var = $tpl_var;
  30. $tpl_file = $this->load_template($tpl_file);
  31. Storage::load($tpl_file, $tpl_var, null, 'tpl');
  32. }
  33. /**
  34. * 加载主模板并缓存
  35. * @param string $tpl_file 模板文件名
  36. * @return string 缓存的模板文件名
  37. */
  38. public function load_template($tpl_file) {
  39. if (is_file($tpl_file)) {
  40. // 读取模板文件内容
  41. $tpl_content = file_get_contents($tpl_file);
  42. } else {
  43. $tpl_content = $tpl_file;
  44. }
  45. // 根据模版文件名定位缓存文件
  46. $tpl_cache_file = $this->config['cache_path'] . md5($tpl_file) . $this->config['cache_suffix'];
  47. if (Storage::has($tpl_cache_file) && !APP_DEBUG && $this->config['tmpl_cache']) {
  48. return $tpl_cache_file;
  49. }
  50. // 编译模板内容
  51. $tpl_content = $this->compiler($tpl_content);
  52. Storage::put($tpl_cache_file, trim($tpl_content), 'tpl');
  53. return $tpl_cache_file;
  54. }
  55. /**
  56. * 编译模板内容
  57. * @param string $tpl_content 模板内容
  58. * @return string 编译后端php混编代码
  59. */
  60. protected function compiler($tpl_content) {
  61. //模板解析
  62. $tpl_content = $this->parse($tpl_content);
  63. // 添加安全代码
  64. $tpl_content = '<?php if (!defined(\'THINK_PATH\')) exit();?>' . $tpl_content;
  65. // 优化生成的php代码
  66. $tpl_content = str_replace('?><?php', '', $tpl_content);
  67. return strip_whitespace($tpl_content);
  68. }
  69. /**
  70. * 解析模板标签属性
  71. * @param string $content 要模板代码
  72. * @return string 解析后的模板代码
  73. */
  74. public function parse($content) {
  75. while (true) {
  76. $sub = $this->match($content);
  77. if ($sub) {
  78. $method = 'parse_' . $sub['attr'];
  79. if (method_exists($this, $method)) {
  80. $content = $this->$method($content, $sub);
  81. } else {
  82. E("模板属性" . $this->config['attr'] . $sub['attr'] . '没有对应的解析规则');
  83. break;
  84. }
  85. } else {
  86. break;
  87. }
  88. }
  89. $content = $this->parse_value($content);
  90. return $content;
  91. }
  92. /**
  93. * 解析include属性
  94. * @param string $content 源模板内容
  95. * @param array $match 一个正则匹配结果集, 包含 html, value, attr
  96. * @return string 解析后的模板内容
  97. */
  98. private function parse_include($content, $match) {
  99. $tpl_name = $match['value'];
  100. if (substr($tpl_name, 0, 1) == '$') {
  101. //支持加载变量文件名
  102. $tpl_name = $this->get(substr($tpl_name, 1));
  103. }
  104. $array = explode(',', $tpl_name);
  105. $parse_str = '';
  106. foreach ($array as $tpl) {
  107. if (empty($tpl))
  108. continue;
  109. if (false === strpos($tpl, $this->config['template_suffix'])) {
  110. // 解析规则为 模块@主题/控制器/操作
  111. $tpl = T($tpl);
  112. }
  113. // 获取模板文件内容
  114. $parse_str .= file_get_contents($tpl);
  115. }
  116. return str_replace($match['html'], $parse_str, $content);
  117. }
  118. /**
  119. * 解析if属性
  120. * @param string $content 源模板内容
  121. * @param array $match 一个正则匹配结果集, 包含 html, value, attr
  122. * @return string 解析后的模板内容
  123. */
  124. private function parse_if($content, $match) {
  125. $new = "<?php if ({$match['value']}) { ?>";
  126. $new .= str_replace($match['exp'], '', $match['html']);
  127. $new .= '<?php } ?>';
  128. return str_replace($match['html'], $new, $content);
  129. }
  130. /**
  131. * 解析repeat属性
  132. * @param string $content 源模板内容
  133. * @param array $match 一个正则匹配结果集, 包含 html, value, attr
  134. * @return string 解析后的模板内容
  135. */
  136. private function parse_repeat($content, $match) {
  137. $new = "<?php foreach ({$match['value']}) { ?>";
  138. $new .= str_replace($match['exp'], '', $match['html']);
  139. $new .= '<?php } ?>';
  140. return str_replace($match['html'], $new, $content);
  141. }
  142. /**
  143. * 解析show属性
  144. * @param string $content 源模板内容
  145. * @param array $match 一个正则匹配结果集, 包含 html, value, attr
  146. * @return string 解析后的模板内容
  147. */
  148. private function parse_show($content, $match) {
  149. $new = "<?php if ({$match['value']}) { ?>";
  150. $new .= str_replace($match['exp'], '', $match['html']);
  151. $new .= '<?php } ?>';
  152. return str_replace($match['html'], $new, $content);
  153. }
  154. /**
  155. * 解析hide属性
  156. * @param string $content 源模板内容
  157. * @param array $match 一个正则匹配结果集, 包含 html, value, attr
  158. * @return string 解析后的模板内容
  159. */
  160. private function parse_hide($content, $match) {
  161. $new = "<?php if (!({$match['value']})) { ?>";
  162. $new .= str_replace($match['exp'], '', $match['html']);
  163. $new .= '<?php } ?>';
  164. return str_replace($match['html'], $new, $content);
  165. }
  166. /**
  167. * 解析普通变量和函数{$title}{:function_name}
  168. * @param string $content 源模板内容
  169. * @return string 解析后的模板内容
  170. */
  171. private function parse_value($content) {
  172. $content = preg_replace('/\{(\$.*?)\}/', '<?php echo \1 ?>', $content);
  173. $content = preg_replace('/\{\:(.*?)\}/', '<?php echo \1 ?>', $content);
  174. return $content;
  175. }
  176. /**
  177. * 获取第一个表达式
  178. * @param string $content 要解析的模板内容
  179. * @return array 一个匹配的标签数组
  180. */
  181. private function match($content) {
  182. $reg = '#<(?<tag>[\w]+)[^>]*?\s(?<exp>' . preg_quote($this->config['attr']) . '(?<attr>[\w]+)=([\'"])(?<value>[^\4]*?)\4)[^>]*>#s';
  183. $match = null;
  184. if (!preg_match($reg, $content, $match)) {
  185. return null;
  186. }
  187. $sub = $match[0];
  188. $tag = $match['tag'];
  189. /* 如果是但标签, 就直接返回 */
  190. if (substr($sub, -2) == '/>') {
  191. $match['html'] = $match[0];
  192. return $match;
  193. }
  194. /* 查找完整标签 */
  195. $start_tag_len = strlen($tag) + 1; // <div
  196. $end_tag_len = strlen($tag) + 3; // </div>
  197. $start_tag_count = 0;
  198. $content_len = strlen($content);
  199. $pos = strpos($content, $sub);
  200. $start_pos = $pos + strlen($sub);
  201. while ($start_pos < $content_len) {
  202. $is_start_tag = substr($content, $start_pos, $start_tag_len) == '<' . $tag;
  203. $is_end_tag = substr($content, $start_pos, $end_tag_len) == "</$tag>";
  204. if ($is_start_tag) {
  205. $start_tag_count++;
  206. }
  207. if ($is_end_tag) {
  208. $start_tag_count--;
  209. }
  210. if ($start_tag_count < 0) {
  211. $match['html'] = substr($content, $pos, $start_pos - $pos + $end_tag_len);
  212. return $match;
  213. }
  214. $start_pos++;
  215. }
  216. return null;
  217. }
  218. }

./Application/Home/Controller/TestController.class.php

  1. <?php
  2. namespace Home\Controller;
  3. use Think\Controller;
  4. class TestController extends Controller {
  5. public function index() {
  6. C('SHOW_PAGE_TRACE', true);
  7. C('TMPL_ENGINE_TYPE', 'Angular');
  8. $data = array();
  9. $data['title'] = '标题';
  10. $data['nav'] = array(
  11. array('title' => '首页', 'url' => '/'),
  12. array('title' => '文章', 'url' => '/article'),
  13. array('title' => '图片', 'url' => '/pic'),
  14. array('title' => '新闻', 'url' => '/news'),
  15. );
  16. $data['count'] = 6;
  17. $data['list'] = array(
  18. array('id' => 1, 'title' => '这是标题1', 'create_time' => strtotime('-5 seconds')),
  19. array('id' => 2, 'title' => '这是标题2', 'create_time' => strtotime('-4 seconds')),
  20. array('id' => 3, 'title' => '这是标题3', 'create_time' => strtotime('-3 seconds')),
  21. array('id' => 4, 'title' => '这是标题4', 'create_time' => strtotime('-2 seconds')),
  22. array('id' => 5, 'title' => '这是标题5', 'create_time' => strtotime('-1 seconds')),
  23. array('id' => 6, 'title' => '这是标题6', 'create_time' => NOW_TIME),
  24. );
  25. $this->assign($data);
  26. $this->display('index');
  27. }
  28. }

./Application/Home/View/Test/index.html

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>Angular 模板测试 - {$title}</title>
  5. <meta charset="UTF-8">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7. <style>
  8. * {
  9. margin: 0px;
  10. padding: 0px;
  11. font-size: 12px;
  12. color: #333;
  13. line-height: 20px;
  14. }
  15. a {
  16. color: #33F;
  17. text-decoration: none;
  18. }
  19. a:hover {
  20. color: #f00;
  21. text-decoration: underline;
  22. }
  23. .center {
  24. text-align: center;
  25. }
  26. h1{
  27. font-size: 30px;
  28. line-height: 50px;
  29. }
  30. .nav {
  31. line-height: 30px;
  32. }
  33. .nav a{
  34. padding: 0px;
  35. margin: 0px 20px;
  36. }
  37. .main table{
  38. width: 500px;
  39. margin: 0px auto;
  40. }
  41. table {
  42. border: 1px solid #666;
  43. }
  44. table td,
  45. table th{
  46. border: 1px solid #666;
  47. line-height: 20px;
  48. padding: 0px 5px;
  49. }
  50. table th{
  51. background: #CCC;
  52. }
  53. #footer p{
  54. text-align: center;
  55. line-height: 30px;
  56. }
  57. </style>
  58. </head>
  59. <body>
  60. <div class="header">
  61. <h1 class="center">Angular 模板测试 - {$title}</h1>
  62. <div class="nav center" tp-if="$nav">
  63. <a tp-repeat="$nav as $vo" href="{$vo['url']}">{$vo['title']}</a>
  64. </div>
  65. </div>
  66. <div class="main">
  67. <table>
  68. <tr>
  69. <th>编号</th>
  70. <th>标题</th>
  71. <th>创建时间</th>
  72. <th>操作</th>
  73. </tr>
  74. <tr tp-if="$list" tp-repeat="$list as $vo">
  75. <td>{$vo['id']}</td>
  76. <td>{$vo['title']}</td>
  77. <td>{:date('Y-m-d H:i:s', $vo['create_time'])}</td>
  78. <td><a href="#del={$vo['id']}">删除</a></td>
  79. </tr>
  80. <tr tp-if="$count">
  81. <td colspan="4" class="center">共 {$count} 条数据</td>
  82. </tr>
  83. <tr tp-hide="$list">
  84. <td colspan="4" class="center">没有数据</td>
  85. </tr>
  86. </table>
  87. </div>
  88. <div tp-include="footer"></div>
  89. </body>
  90. </html>
  1. <footer id="footer">
  2. <div class="foot-warp">
  3. <p>
  4. © 2015 {:C('SITE_TITLE')} zhaishuaigan@qq.com    豫ICP备13012601号
  5. </p>
  6. </div>
  7. </footer>

运行/Test/index, 显示结果

目前只是实现了简单的解析, 还需要进一步完善, 比如配置啊, 扩展更多的标签啊什么的.

Nginx配置ThinkPHP的UrlRewrite和PathInfo模式

打开对应的站点配置文件, 找到下面的代码段:

  1. location ~ .*\.(php|php5)?$
  2. {
  3. fastcgi_pass 127.0.0.1:9000;
  4. fastcgi_index index.php;
  5. include fastcgi.conf;
  6. }

修改为

  1. location ~ .php
  2. {
  3. fastcgi_pass 127.0.0.1:9000;
  4. fastcgi_index index.php;
  5. include fastcgi.conf;
  6. #定义变量 $path_info ,用于存放pathinfo信息
  7. set $path_info "";
  8. #定义变量 $real_script_name,用于存放真实地址
  9. set $real_script_name $fastcgi_script_name;
  10. #如果地址与引号内的正则表达式匹配
  11. if ($fastcgi_script_name ~ "^(.+?\.php)(/.+)$") {
  12. #将文件地址赋值给变量 $real_script_name
  13. set $real_script_name $1;
  14. #将文件地址后的参数赋值给变量 $path_info
  15. set $path_info $2;
  16. }
  17. #配置fastcgi的一些参数
  18. fastcgi_param SCRIPT_FILENAME $document_root$real_script_name;
  19. fastcgi_param SCRIPT_NAME $real_script_name;
  20. fastcgi_param PATH_INFO $path_info;
  21. }
  22. location / {
  23. if (!-e $request_filename) {
  24. rewrite ^(.*)$ /index.php?s=$1 last;
  25. break;
  26. }
  27. }