Template.php 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. <?php
  2. declare (strict_types = 1);
  3. namespace app\common\library;
  4. use Exception;
  5. use think\facade\Config;
  6. /**
  7. * 重写ThinkPHP分模板引擎
  8. */
  9. class Template extends \think\Template
  10. {
  11. private $includeFile = [];
  12. public function fetch(string $template, array $vars = []): void
  13. {
  14. if(strpos($template,'@')!==false){
  15. $modelname= substr($template,0,strpos($template,'@'));
  16. $this->config['view_path'] = root_path().'app'.DS.$modelname.DS.'view'.DS;
  17. $this->config['cache_path'] = root_path().'runtime'.DS.'temp'.DS;
  18. $template= substr($template,strpos($template,'@')+1);
  19. if(str_starts_with($template,'/')){
  20. $template= substr($template,1);
  21. }
  22. }else{
  23. $modelname= app('http')->getName();
  24. $this->config['view_path'] = root_path().'app'.DS.$modelname.DS.'view'.DS;
  25. $this->config['cache_path'] = root_path().'runtime'.DS.$modelname.DS.'temp'.DS;
  26. }
  27. if ($vars) {
  28. $this->data = array_merge($this->data, $vars);
  29. }
  30. if ($this->isCache($this->config['cache_id'])) {
  31. // 读取渲染缓存
  32. echo $this->cache->get($this->config['cache_id']);
  33. return;
  34. }
  35. $template = $this->parseTemplateFile($template);
  36. if ($template) {
  37. $cacheFile = $this->config['cache_path'] . $this->config['cache_prefix'] . md5($this->config['layout_on'] . $this->config['layout_name'] . $template) . '.' . ltrim($this->config['cache_suffix'], '.');
  38. if (!$this->checkCache($cacheFile)) {
  39. // 缓存无效 重新模板编译
  40. $content = file_get_contents($template);
  41. //处理vue包含标签
  42. $preg='/\{include\s+vue=[\'"](.*)[\'"]\s+\/\}/';
  43. $vuetemp='';
  44. preg_replace_callback($preg,function ($match) use (&$vuetemp){
  45. $vuetemp=$match[1];
  46. },$content);
  47. if($vuetemp){
  48. $this->fetch($vuetemp,$vars);
  49. return;
  50. }else{
  51. $this->compiler($content, $cacheFile);
  52. }
  53. }
  54. // 页面缓存
  55. ob_start();
  56. ob_implicit_flush(false);
  57. // 读取编译存储
  58. $this->storage->read($cacheFile, $this->data);
  59. // 获取并清空缓存
  60. $content = ob_get_clean();
  61. if (!empty($this->config['cache_id']) && $this->config['display_cache'] && null !== $this->cache) {
  62. // 缓存页面输出
  63. $this->cache->set($this->config['cache_id'], $content, $this->config['cache_time']);
  64. }
  65. echo $content;
  66. }
  67. }
  68. /**
  69. * 编译模板文件内容
  70. * @access private
  71. * @param string $content 模板内容
  72. * @param string $cacheFile 缓存文件名
  73. * @return void
  74. */
  75. private function compiler(string &$content, string $cacheFile): void
  76. {
  77. // 判断是否启用布局
  78. if ($this->config['layout_on']) {
  79. if (str_contains($content, '{__NOLAYOUT__}')) {
  80. // 可以单独定义不使用布局
  81. $content = str_replace('{__NOLAYOUT__}', '', $content);
  82. } else {
  83. // 读取布局模板
  84. $layoutFile = $this->parseTemplateFile($this->config['layout_name']);
  85. if ($layoutFile) {
  86. $layoutContent=file_get_contents($layoutFile);
  87. if($this->config['layout_name']=='layout'.DS.'vue'){
  88. [$content, $jsfile, $cssfile] = $this->parseVue($content,$cacheFile);
  89. $layoutContent = str_replace('{__CSS__}', $cssfile, $layoutContent);
  90. $layoutContent = str_replace('{__JS__}', $jsfile, $layoutContent);
  91. }
  92. // 替换布局的主体内容
  93. $content = str_replace($this->config['layout_item'], $content,$layoutContent);
  94. }
  95. }
  96. } else {
  97. $content = str_replace('{__NOLAYOUT__}', '', $content);
  98. }
  99. $this->compilerFileToPhp($content, $cacheFile);
  100. $this->includeFile = [];
  101. }
  102. private function parseVue(string $content,string $cacheFile): array
  103. {
  104. //删除注释
  105. $content=preg_replace('/<!--(.*)-->/Uis','',$content);
  106. $content=trim($content);
  107. if(!str_starts_with($content,'<template>')){
  108. throw new Exception(__('模板文件格式不正确'));
  109. }
  110. $preg='/<template>(.*)<\/template>/s';
  111. preg_match($preg,$content,$match);
  112. $template=$match[1];
  113. $content=trim(substr($content,strlen($match[0])));
  114. if(!str_starts_with($content,'<script>')){
  115. throw new Exception(__('模板文件格式不正确'));
  116. }
  117. $preg='/<script>(.*)<\/script>/s';
  118. preg_match($preg,$content,$match);
  119. $script=$match[1];
  120. $jsfile=$this->parseJS($script,$cacheFile);
  121. $content=trim(substr($content,strlen($match[0])));
  122. if(!str_starts_with($content,'<style>')){
  123. throw new Exception(__('模板文件格式不正确'));
  124. }
  125. $preg='/<style>(.*)<\/style>/s';
  126. preg_match($preg,$content,$match);
  127. $style=$match[1];
  128. $style=$this->parseCSS($style);
  129. return [$template,$jsfile,$style];
  130. }
  131. private function parseCSS(string $style):string
  132. {
  133. $preg='/@import\s+url\([\'"](.*)[\'"]\);/';
  134. $css=[];
  135. $style=preg_replace_callback($preg,function ($match) use (&$css){
  136. $url=$match[1];
  137. $css[]=$url;
  138. return '';
  139. },$style);
  140. $str='';
  141. foreach ($css as $item){
  142. $str.=<<<EOF
  143. <link rel="stylesheet" href="{$item}"/>
  144. EOF;
  145. }
  146. $style=trim($style);
  147. if($style) {
  148. $str .= <<<EOF
  149. <style>
  150. {$style}
  151. </style>
  152. EOF;
  153. }
  154. return $str;
  155. }
  156. //生成js文件
  157. private function parseJS(string $script,string $cacheFile):string
  158. {
  159. $domain=request()->domain();
  160. $preg='/import\s+(.*)\s+from\s+["\'](.*)["\'];/';
  161. $script=preg_replace_callback($preg,function ($match) use ($domain){
  162. $import=$match[1];
  163. $path=$match[2];
  164. $path=str_replace('@',$domain.'/assets/js/',$path);
  165. return 'import '.$import.' from "'.$path.'";';
  166. },$script);
  167. $cacheFile=str_replace('.php','-js.php',$cacheFile);
  168. $this->compilerFileToPhp($script,$cacheFile);
  169. //获取$cacheFile文件名
  170. $cacheFile=substr($cacheFile,strrpos($cacheFile,DS)+1);
  171. $cacheFile=str_replace('-js.php','',$cacheFile);
  172. return $cacheFile;
  173. }
  174. private function compilerFileToPhp(string $content,$cacheFile)
  175. {
  176. $this->parse($content);
  177. if ($this->config['strip_space']) {
  178. /* 去除html空格与换行 */
  179. $find = ['~>\s+<~', '~>(\s+\n|\r)~'];
  180. $replace = ['><', '>'];
  181. $content = preg_replace($find, $replace, $content);
  182. }
  183. // 优化生成的php代码
  184. $content = preg_replace('/\?>\s*<\?php\s(?!echo\b|\bend)/s', '', $content);
  185. // 模板过滤输出
  186. $replace = $this->config['tpl_replace_string'];
  187. $content = str_replace(array_keys($replace), array_values($replace), $content);
  188. // 添加安全代码及模板引用记录
  189. $content = '<?php /*' . serialize($this->includeFile) . '*/ ?>' . "\n" . $content;
  190. // 编译存储
  191. $this->storage->write($cacheFile, $content);
  192. return $cacheFile;
  193. }
  194. /**
  195. * 解析模板文件名
  196. * @access private
  197. * @param string $template 文件名
  198. * @return string
  199. */
  200. private function parseTemplateFile(string $template): string
  201. {
  202. if ('' == pathinfo($template, PATHINFO_EXTENSION)) {
  203. $template = str_replace(['/', ':'], $this->config['view_depr'], $template);
  204. $template = $this->config['view_path'] . $template . '.' . ltrim($this->config['view_suffix'], '.');
  205. }
  206. if (is_file($template)) {
  207. // 记录模板文件的更新时间
  208. $this->includeFile[$template] = filemtime($template);
  209. return $template;
  210. }
  211. throw new Exception('template not exists:' . $template);
  212. }
  213. /**
  214. * 检查编译缓存是否有效,如果无效则需要重新编译
  215. * @access private
  216. * @param string $cacheFile 缓存文件名
  217. * @return bool
  218. */
  219. private function checkCache(string $cacheFile): bool
  220. {
  221. if (!$this->config['tpl_cache'] || !is_file($cacheFile) || !$handle = @fopen($cacheFile, "r")) {
  222. return false;
  223. }
  224. // 读取第一行
  225. $line = fgets($handle);
  226. if (false === $line) {
  227. return false;
  228. }
  229. preg_match('/\/\*(.+?)\*\//', $line, $matches);
  230. if (!isset($matches[1])) {
  231. return false;
  232. }
  233. $includeFile = unserialize($matches[1]);
  234. if (!is_array($includeFile)) {
  235. return false;
  236. }
  237. // 检查模板文件是否有更新
  238. foreach ($includeFile as $path => $time) {
  239. if (is_file($path) && filemtime($path) > $time) {
  240. // 模板文件如果有更新则缓存需要更新
  241. return false;
  242. }
  243. }
  244. // 检查编译存储是否有效
  245. return $this->storage->check($cacheFile, $this->config['cache_time']);
  246. }
  247. }