ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
[TOC] * * * * * ## 1 视图模板 ### 视图模板概览 ![](https://box.kancloud.cn/2016-03-18_56ebc970a2e86.jpg) ### 模板内容解析 ![](https://box.kancloud.cn/2016-03-21_56efbf923bc3f.jpg) * * * * * ### 视图模板意义 >[info] 视图模板是控制器操作的输出组织 >主要包含视图模板操作与模板文件解析两部分 >最终以web页面的形式组织控制器操作结果数据 **操作部分:视图模板配置,模板变量准备** **解析部分:模板变量,模板内容,模板标签解析** * * * * * ### 视图模板操作层次 **控制器操作:** 处理具体业务逻辑,准备输出数据 **视图层操作:** 输出数据赋值模板变量,模板主题,模板文件定位 **模板层解析:** 模板文件解析, **标签层解析:** 模板文件标签解析 **模板缓存层读写:** 读写模板编译结果 * * * * * ## 2 视图模板配置 ### 1 全局配置 thinkphp\convention.php >[info] 定义View配置参数, ~~~ $view->config return [ 'theme_on' => false, 'default_theme' => 'default', 'view_path' => '', 'view_suffix' => '.html', 'view_depr' => DS, 'view_layer' => VIEW_LAYER, 'parse_str' => [], 'namespace' => '\\think\\view\\driver\\', 'template' => [], ]; ~~~ >[info] 定义Template配置参数 ~~~ $Template->config return [ 'view_path' => '', 'view_suffix' => '.html', 'view_depr' => DS, 'cache_suffix' => '.php', 'tpl_deny_func_list' => 'echo,exit', 'tpl_deny_php' => false, 'tpl_begin' => '{', 'tpl_end' => '}', 'strip_space' => false, 'tpl_cache' => true, 'compile_type' => 'file', 'cache_prefix' => '', 'cache_time' => 0, 'layout_on' => false, 'layout_name' => 'layout', 'layout_item' => '{__CONTENT__}', 'taglib_begin' => '{', 'taglib_end' => '}', 'taglib_load' => true, 'taglib_build_in' => 'cx', 'taglib_pre_load' => '', 'display_cache' => false, 'cache_id' => '', 'tpl_replace_string' => [], 'tpl_var_identify' => 'array', 'namespace' => '\\think\\template\\driver\\', ]; ~~~ >[info] tp5.0Rc2中 > 视图与模板引擎初始化时不再自动获取配置参数 > 需要在初始化时手动获取配置参数 ### 2 应用配置 application\config.php 配置参数同全局配置参数设置 >[info] tp5.0Rc2中 > 视图与模板引擎初始化时不再自动获取配置参数 > 需要在初始化时手动获取配置参数 ### 3 初始化配置 配置参数同全局配置参数设置 ~~~ $view->new think\View( think\Config::get()) $controller->engine( think\Config::get()) ~~~ ## 4 控制器层操作 think\Controller.php ### 控制器层模板变量赋值 assign() ### 控制器层模板引擎初始化 engine() `public function engine($engine, $config = [])` > $engine:模板引擎名称 > $config:模板引擎配置 `$this->view->engine($engine, $config);` 调用think\View.php视图对象的engine()初始化模板引擎 ## 5 控制器层解析 think\Controller.php ### 控制器层模板解析 fetch() >[info] 模板解析 fetch() `public function fetch($template = '', $vars = [], $cache_id = '')` ~~~ $template :指定模板文件名,默认为**控制器/操作.模板后缀** $vars :模板变量数组 $cache_id :模板缓存id ~~~ ` return $this->view->fetch($template, $vars, $cache_id);` 调用think\View的fetch() 解析模板文件内容 ### 控制器层内容渲染 show() >[info] 渲染内容输出 show() `public function show($content, $vars = [])` > $content:待解析内容 > $vars: 模板变量 `return $this->view->show($content, $vars);` 调用think\View的show() 渲染内容输出 ## 6 视图层操作 think\View.php ### 视图初始化 instance(), __construct(),config(),engine() >[info] 视图实例初始化 instance() `public static function instance(array $config = [])` > $config:视图配置参数 **单例模式对象实例化** ~~~ if (is_null(self::$instance)) { self::$instance = new self($config); } ~~~ 如果没有实例对象,初始化视图对象实例 `return self::$instance;` 如果有实例对象,返回实例对象 >[info] 视图构造函数 __construct() `public function __construct(array $config = [])` > $config: View相关配置参数 ~~~ $this->config($config); ~~~ 调用think\View->config()设置视图配置参数 ~~~ if (!isset($this->config['template']['type'])) { $this->config['template']['type'] = 'think'; } ~~~ 检查模板引擎类型配置,默认为think >[info] 视图参数配置 config() `public function config($config = '', $value = null)` > $config:视图配置参数名,或视图配置参数数组 > $value:视图配置参数名对应值 **读写四功能函数:** 数组写,键值读,键值写,数组读 ~~~ if (is_array($config)) { foreach ($this->config as $key => $val) { if (isset($config[$key])) { $this->config[$key] = $config[$key]; } } } ~~~ **1 $config为数组,数组写 覆盖到think\view->config[]对应参数** ~~~ elseif (is_null($value)) { return $this->config[$config]; } ~~~ **2 $config为字符串,$value为空时,键值读 读取$config键名对应配置值** ~~~ else { $this->config[$config] = $value; } ~~~ **3 $config,$value都为字符串时,键值写 设置$config对应值为$value** `return $this;` **4 $config,$value都为空时,数组读 读取整个配置** >[info] 视图模板引擎初始化 engine() `public function engine($engine, array $config = [])` > $engine:模板引擎名称 > $config:模板引擎参数 ~~~ if ('php' == $engine) { $this->engine = 'php'; } ~~~ php模板引擎 `$class = $this->config['namespace'] . ucfirst($engine);` 非php模板引擎类名 ~~~ if (empty($this->config['view_path']) && defined('VIEW_PATH')) { $this->config['view_path'] = VIEW_PATH; } ~~~ 视图文件路径 默认为base.php的VIEW_PATH ~~~ $config = array_merge($config, [ 'view_path' => $this->config['view_path'], 'view_suffix' => $this->config['view_suffix'], 'view_depr' => $this->config['view_depr'], ]); ~~~ 参数覆盖,默认配置覆盖传入的配置参数? `$this->engine = new $class($config);` 创建模板引擎, 默认为 think\viewdriver\Think类的对象 `return $this;` 返回视图对象,**实现链式操作**。 ### 视图变量操作 __set() __get() __isset() ~~~ public function __set($name, $value) public function __get($name) public function __isset($name) ~~~ > $name:模板变量名称 > $value:模板变量值 ### 开启,关闭,设置模板主题 theme() **读写三功能函数:** 开启,关闭,设置 ~~~ if (true === $theme) { $this->config['theme_on'] = true; } elseif (false === $theme) { $this->config['theme_on'] = false; } ~~~ 开启与关闭主题设置 ~~~ else { $this->config['theme_on'] = true; $this->theme = $theme; } ~~~ 开启模板主题,设置模板主题 `return $this;` 返回视图对象,**实现链式操作** 设置,获取,检查模板变量 ## 7 视图层解析 think\View.php ### 模板文件解析 fetch() `public function fetch($template = '', $vars = [], $config = [], $renderContent = false)` > $template:待解析模板文件与模板内容 > $vars:模板变量 > $config:模板解析引擎配置参数 > $renderContent:是否渲染内容控制 true:返回模板文件名,false:解析后输出 `$vars = array_merge($this->data, $vars);` 合并传入模板参数$vars到$this->data `if (!$renderContent) {}` **不需要解析时** 返回模板文件名 `$template = $this->parseTemplate($template);` 获取模板文件名,并返回。 ~~~ if (!is_file($template) || (APP_DEBUG && IS_WIN && realpath($template) != $template)) { throw new Exception('template file not exists:' . $template, 10700); } ~~~ WIN环境模板不存在,抛出异常 `APP_DEBUG && Log::record('[ VIEW ] ' . $template . ' [ ' . var_export($vars, true) . ' ]', 'info');` 开启调试,记录日志信息 ~~~ if (is_null($this->engine)) { $this->engine($this->config['template']['type'], $this->config['template']); } ~~~ **需要解析时**,首先检查模板引擎是否初始化。 ~~~ ob_start(); ob_implicit_flush(0); ~~~ 开启输出控制,关闭绝对刷送 有关输出控制 见 php的输出控制 ~~~ if ('php' == $this->engine || empty($this->engine)) { extract($vars, EXTR_OVERWRITE); is_file($template) ? include $template : eval('?>' . $template); } ~~~ **原生php引擎处理** 导入模板变量到符号表, 加载或者执行模板文件内容。 ~~~ else { $this->engine->fetch($template, $vars, $config); } ~~~ **非原生php引擎处理** 调用$this->engine->fetch()对模板文件进行解析。 默认使用think\view\driver\Think->fetch() `$content = ob_get_clean();` 获取缓存内容并清空 `APP_HOOK && Hook::listen('view_filter', $content);` 标签过滤回调处理 ~~~ if (!empty($this->config['parse_str'])) { $replace = $this->config['parse_str']; $content = str_replace(array_keys($replace), array_values($replace), $content); } ~~~ 用户自定义的字符串替换 ~~~ if (!Config::get('response_auto_output')) { return Response::send($content, Response::type()); } ~~~ 关闭自动响应输出?? 直接输出模板内容字符串到客户端。 `return $content;` 返回解析后的内容 ### 模板内容解析 show() `public function show($content, $vars = [])` > $content:模板内容 > $vars:模板变量 `return $this->fetch($content, $vars, '', true);` 直接调用think\View->fetch(); 解析模板内容 ### 文件层解析 parseTemplate(),getTemplateTheme() >[info] 模板文件定位 parseTemplate() `private function parseTemplate($template)` $template:模板文件参数 ~~~ if (is_file($template)) { return realpath($template); } ~~~ 如果是文件名,直接返回文件路径名 **获取模板目录** ~~~ if (empty($this->config['view_path']) && defined('VIEW_PATH')) { $this->config['view_path'] = VIEW_PATH; } ~~~ 配置的模板文件目录获取。 ~~~ $theme = $this->getTemplateTheme(); $this->config['view_path'] .= $theme; ~~~ 获取模板文件主题目录。 ~~~ $depr = $this->config['view_depr']; $template = str_replace(['/', ':'], $depr, $template); ~~~ 替换模板文件路径中的分隔符 ~~~ if (strpos($template, '@')) { list($module, $template) = explode('@', $template); $path = APP_PATH . (APP_MULTI_MODULE ? $module . DS : '') . $this->config['view_layer'] . DS; } ~~~ 模板文件路径包含"@"符号时,定位当前模块模板目录 ~~~ else { $path = $this->config['view_path']; } ~~~ 没有包含"@"时,定位默认配置模板目录 **获取模板文件名** ~~~ if (defined('CONTROLLER_NAME')) { if ('' == $template) { $template = str_replace('.', DS, CONTROLLER_NAME) . $depr . ACTION_NAME; } elseif (false === strpos($template, $depr)) { $template = str_replace('.', DS, CONTROLLER_NAME) . $depr . $template; } } ~~~ 添加控制器名称到模板文件名 `return realpath($path) . DS . $template . $this->config['view_suffix'];` 返回模板目录,模板文件名,模板后缀合并的字符串作为模板文件名。 >[info]获取模板主题名称 getTemplateTheme() ~~~ if ($this->config['theme_on']) { if ($this->theme) { $theme = $this->theme; } else { $theme = $this->config['default_theme']; } } ~~~ 开启模板主题时,获取模板主题或者默认主题 `return isset($theme) ? $theme . DS : '';` 返回模板主题或者空字符串 ## 8 模板引擎操作 think\view\driver\Think.php,think\Template.php ### 模板引擎初始化 think\View->engine() >[info] 初始化入口 think\View.php 上面模板引擎初始化过程think\View->engine()中 `this->engine = new $class($config);` 这里默认创建think\view\driver\Think类 ### 视图引擎构造函数 $Think_engine->__construct() `public function __construct($config = [])` > $config:配置参数 ~~~ { $this->template = new Template($config); } ~~~ 创建think\Template.php类的对象实例作为模板引擎。 ### 视图引擎解析入口 $Think_engine->fetch() `public function fetch($template, $data = [], $config = [])` > $template:模板文件名 > $data:模板变量 > $config:模板配置参数 ~~~ if (is_file($template)) { $this->template->display($template, $data, $config); } ~~~ $template为文件名时, 调用think\Template->display()解析文件 ~~~ else { $this->template->fetch($template, $data); } ~~~ $template为字符串内容时 调用think\Template->fetch()渲染内容 ### 视图引擎其他操作 $Think_engine->__call() `public function __call($method, $params)` > $method 调用方法 > $params 调用参数 ~~~ { return call_user_func_array([$this->template, $method], $params); } ~~~ 使用魔术方法__call() 调用$this->template->$method($params); ### 模板引擎构造函数$Template->__construct() `public function __construct(array $config = [])` > $config:解析引擎配置参数 `$this->config['cache_path'] = RUNTIME_PATH . 'temp' . DS;` 缓存目录配置 `$this->config = array_merge($this->config, $config);` 配置参数合并 ~~~ $this->config['taglib_begin'] = $this->stripPreg($this->config['taglib_begin']); $this->config['taglib_end'] = $this->stripPreg($this->config['taglib_end']); $this->config['tpl_begin'] = $this->stripPreg($this->config['tpl_begin']); $this->config['tpl_end'] = $this->stripPreg($this->config['tpl_end']); ~~~ 标签库与普通标签开始结束标记配置 ~~~ $type = $this->config['compile_type'] ? $this->config['compile_type'] : 'File'; $class = $this->config['namespace'] . ucwords($type); ~~~ 模板编译存储驱动类型配置 `$this->storage = new $class();` 创建模板编译缓存驱动对象 ### 模板擎配置读写$Template->config() `public function config($config)` > $config:解析引擎配置参数 **读写功能函数:** 数组写,键值对读 ~~~ if (is_array($config)) { $this->config = array_merge($this->config, $config); } elseif (isset($this->config[$config])) { return $this->config[$config]; } ~~~ $config为数组,合并到配置参数 $config为字符串,读取配置键名对应值 ### 模板引擎配置赋值$Template->__set() `public function __set($name, $value)` > $name:配置键名 > $value:配置键值 `$this->config[$name] = $value;` 设置配置键值对关系 ### 模板引擎变量赋值$Template->assign() `public function assign($name, $value = '')` > $name:变量名 > $value:变量值 数组字符串两写功能:数组写,键值对 ~~~ if (is_array($name)) { $this->data = array_merge($this->data, $name); } else { $this->data[$name] = $value; } ~~~ 数组,合并到模板变量 键值对,键值对赋值 ### 模板引擎变量获取$Template->get() `public function get($name = '')` > $name:待获取变量名 ~~~ if ('' == $name) { return $this->data; } ~~~ 空变量名,读取所有模板变量 ~~~ $data = $this->data; foreach (explode('.', $name) as $key => $val) { if (isset($data[$val])) { $data = $data[$val]; } else { $data = null; break; } } ~~~ 变量二级读取。$test.va1格式变量 `return $data;` 返回读取的对应变量 ### 模板内容布局操作 $Template->layout() `public function layout($name) ` > $name:布局控制 > true:开启,false:关闭,字符串:设置layout_name ~~~ if (false === $name) { $this->config['layout_on'] = false; } ~~~ $name为false,关闭布局 ~~~ else { $this->config['layout_on'] = true; if (is_string($name)) { $this->config['layout_name'] = $name; } } ~~~ $name非false,开启布局,如果是字符串设置布局名称 `return $this;` 返回对象,实现链式操作 ## 9 模板内容层解析 think\Template.php ### 1 模板文件编译入口 display() `public function display($template, $vars = [], $config = [])` > $tempalte:模板文件名称 > $vars:模板变量数组 > $config:模板引擎配置参数 ~~~ if ($vars) { $this->data = $vars; } ~~~ 模板变量设置 ~~~ if ($config) { $this->config($config); } ~~~ 模板引擎配置参数设置 其中的cache_id对应每个渲染模板文件 `if (!empty($this->config['cache_id']) && $this->config['display_cache']) {}` 模板渲染缓存检测, **这里的缓存使用Cache缓存php运行后生成的web页面形式缓存** 不同于使用$Template->storage->write()缓存的编译模板。 后者包含php代码,前者不包含php代码。 前者是一种更高效的缓存技术。可以实现静态页面生成。 ` $cacheContent = Cache::get($this->config['cache_id']);` 调用Cache::get()获取渲染缓存内容 这里使用了tp框架内置缓存机制, 为了目标引擎插件化可以解耦这里的Cache 将其设置为配置参数。 ~~~ if ($cacheContent !== false) { echo $cacheContent; return; } ~~~ 渲染缓存存在的情况下,输出渲染缓存。 这里返回到View->fetch(),即控制器中的$view->fetch()。 如果没有开启渲染缓存,则查找编译模板。 `$template = $this->parseTemplateFile($template);` 调用template->parseTemplateFile()获取对应模板文件名 这里的parseTemplateFile()与View.php的parseTemplate()相似 **后者是在View.php的fetch()的参数$renderContent为false,** **不进行模板编译时,查找目标文件所用** **进行模板编译时,查找目标文件时所用。** `if ($template) {}` 获取模板文件名成功时,继续解析 `$cacheFile = $this->config['cache_path'] . $this->config['cache_prefix'] . md5($template) . $this->config['cache_suffix'];` 合成缓存文件名称 ` if (!$this->checkCache($cacheFile)) {}` 缓存文件检查,如果缓存失效,进行重新编译 ~~~ $content = file_get_contents($template); $this->compiler($content, $cacheFile); ~~~ 缓存失效时,读取模板文件内容,并编译 缓存可用时,获取模板编译缓存 ob_start(); ob_implicit_flush(0); `$this->storage->read($cacheFile, $this->data);` 读取模板文件编译存储 `$content = ob_get_clean();` 读取模板文件编译内容 ~~~ if (!empty($this->config['cache_id']) && $this->config['display_cache']) { Cache::set($this->config['cache_id'], $content, $this->config['cache_time']); } ~~~ 检查是否需要保存编译生成的web模板文件 调用Cache:set()保存编译生成的web模板文件 `echo $content;` 输出解析结果 ### 2 模板内容编译入口 fetch() `public function fetch($content, $vars = [])` > $contens:待编译模板内容 > $vars:模板变量 ~~~ if ($vars) { $this->data = $vars; } ~~~ 保存模板变量 `$cacheFile = $this->config['cache_path'] . $this->config['cache_prefix'] . md5($content) . $this->config['cache_suffix'];` 合成模板编译缓存文件名 ~~~ if (!$this->checkCache($cacheFile)) { $this->compiler($content, $cacheFile); } ~~~ 模板编译缓存无效,重新编译 `$this->storage->read($cacheFile, $this->data);` 读取存储的编译模板缓存 ### 3 模板文件查找 parseTemplateFile() `private function parseTemplateFile($template)` > $template:待查找文件名 `if (false === strpos($template, '.')) {}` 模板文件名中包含符号".",模板文件的后缀 ~~~ if (strpos($template, '@')) { $template = str_replace(['/', ':'], $this->config['view_depr'], $template); $template = APP_PATH . str_replace('@', '/' . basename($this->config['view_path']) . '/', $template) . $this->config['view_suffix']; } ~~~ 跨模块模板查找解析。 首先替换"/" ":"为配置的模板分隔符 然后替换"@"为"/",并添加应用路径/模板路径和模板后缀到文件名 ~~~ else { if (strpos($template, ':')) { list($theme, $template) = explode(':', $template, 2); $path = dirname($this->config['view_path']) . DS . $theme . DS; } else { $path = $this->config['view_path']; } $template = str_replace(['/', ':'], $this->config['view_depr'], $template); $template = $path . $template . $this->config['view_suffix']; } ~~~ 当前模块模板操作 首先解析模板文件名模板主题符号":", 然后替换掉模板文件分割符为配置的分隔符 最后合并模板路径,模板文件名,模板文件后缀, ~~~ if (is_file($template)) { $this->includeFile[$template] = filemtime($template); return $template; } ~~~ 对应模板文件存在时,将对应模板文件加载时间保存到$this->includeFile[] 这里用作模板编译时间标识。 并返回模板文件名 ~~~ else { throw new Exception('template not exist:' . $template, 10700); } ~~~ 文件不存在抛出异常 ### 4 包含文件名解析 parseTemplateName() `private function parseTemplateName($templateName)` $templateName:需要包含的文件名 `$array = explode(',', $templateName);` 多个模板文件 `foreach ($array as $templateName) {}` 遍历加载文件 ~~~ if (empty($templateName)) { continue; } ~~~ 检查数组 if (0 === strpos($templateName, '$')) { $templateName = $this->get(substr($templateName, 1)); } 获取变量文件名 `$template = $this->parseTemplateFile($templateName);` 查找需要加载的文件 ~~~ if ($template) { $parseStr .= file_get_contents($template); } ~~~ 文件存在则进行读取 `return $parseStr;` 返回读取的文件内容 ### 5 模板编译缓存检查 checkCache() `private function checkCache($cacheFile)` > $cacheFile:缓存文件名 ~~~ if (!$this->config['tpl_cache']) { return false; } ~~~ 未开启缓存,返回false ~~~ if (!is_file($cacheFile)) { return false; } ~~~ 文件不存在,返回false ~~~ if (!$handle = @fopen($cacheFile, "r")) { return false; } ~~~ 读取文件失败,返回false `preg_match('/\/\*(.+?)\*\//', fgets($handle), $matches);` 读取文件第一行 ~~~ if (!isset($matches[1])) { return false; } ~~~ 读取内容为空,返回false `$includeFile = unserialize($matches[1]);` 对第一行解序列化 ~~~ if (!is_array($includeFile)) { return false; } ~~~ 检查是否存在包含文件$Template->$inculdeFile[]中 不存在,返回false ~~~ foreach ($includeFile as $path => $time) { if (is_file($path) && filemtime($path) > $time) { return false; } } ~~~ 对比模板文件更新时间与上次编译时间, 如果模板文件有更新,则返回false ~~~ return $this->storage->check($cacheFile, $this->config['cache_time']); ~~~ 最后调用$this->storage->check()检查缓存有效期,并返回检查结果 ### 6 模板内容编译过程 compiler() `private function compiler(&$content, $cacheFile)` > $content:待编译内容引用,避免复制传参带来的内存消耗 > $cacheFile:缓存文件名 `if ($this->config['layout_on']) {}` 模板布局启动检查 ~~~ if (false !== strpos($content, '{__NOLAYOUT__}')) { $content = str_replace('{__NOLAYOUT__}', '', $content); } ~~~ 将模板内容中设置为不使用布局的部分去除掉。 else { $layoutFile = $this->parseTemplateFile($this->config['layout_name']); if ($layoutFile) { $content = str_replace($this->config['layout_item'], $content, file_get_contents($layoutFile)); } } 读取布局模板文件 如:index/view/controller_layout.html 读取布局模板成功时 将布局模板index/view/controller_layout.html中的{__CONTENT__} 替换为模板内容,并获取替换结果。 分析这里可知道 >[info] 布局模板是模板内容的上层组织 > 通常使用布局模板定义页面的总体结构,如页顶,页脚,页边框 > 布局模板中可替换内容对应功能页面组织, 如内容主体 `$this->parse($content);` 对模板内容进行解析编译。 >[info] 前面的模板解析在文件层检查其渲染缓存,编译缓存,布局文件 > 这里是模板内容解析的总入口 > 下面的模板解析在内容层开始解析模板标签等内容 ### 7 模板内容解析总入口 parse() `public function parse(&$content)` > $content:待解析模板文件内容 ~~~ if (empty($content)) { return; } ~~~ 进行参数有效检查如果待解析内容为空,直接返回 ~~~ $this->parseLiteral($content); $this->parseExtend($content); $this->parseLayout($content); $this->parseInclude($content); $this->parseLiteral($content); ~~~ >[info] 1 解析原生标签literal内容 parseliteral() > 2 解析extend标签内容 parseExtend() > 3 解析layout标签内容 parseLayout() > 4 解析include标签内容 parseInclude() > 5 再次解析包含文件中可能携带的原生标签literal内容 parseliteral() `$this->parsePhp($content);` php的语法检查 `if ($this->config['taglib_load']) {}` 允许加载其他标签库 `$tagLibs = $this->getIncludeTagLib($content);` 对模板内容的标签列表进行加载。 ~~~ if (!empty($tagLibs)) { foreach ($tagLibs as $tagLibName) { $this->parseTagLib($tagLibName, $content); } } ~~~ 对标签列表依次进行加载 ### 8 literal标签内容解析 parseLiteral() `private function parseLiteral(&$content, $restore = false)` > $content:待解析内容引用 > $restore:内容是否需要还原 ~~~ $regex = $this->getRegex($restore ? 'restoreliteral' : 'literal'); ~~~ 获取literal标签内容的正则字符串。 `if (preg_match_all($regex, $content, $matches, PREG_SET_ORDER)) {}` 正则匹配内容中的literal标签,并保存到$matches数组中。 有关preg_match_all()见php的正则表达式 ` if (!$restore) {}` literal原生标签内容替换处理, 将literal原生标签内容保存到$this->literal[]数组 并使用特殊字符串替换掉。 `$count = count($this->literal);` 原生标签内容缓存数组。 对于需要替换的内容进行保存。 这里获取数组的下个数字键值, 对应当前处理的literal原生标签内容 `foreach ($matches as $match) {}` 遍历匹配到的literal标签内容 `$this->literal[] = substr($match[0], strlen($match[1]), -strlen($match[2]));` 缓存需要恢复的literal原生标签内容 `$content = str_replace($match[0], "<!--###literal{$count}###-->", $content);` 替换需要literal原生标签内容为对应$count键标记 ~~~ $count++; ~~~ 数字键自增,保存下个literal原生标签内容 ~~~ else { foreach ($matches as $match) { $content = str_replace($match[0], $this->literal[$match[1]], $content); } $this->literal = []; } ~~~ else对模板内容中的literal标签替换字符串 还原为$this->literal[]数组中保存的原生内容 `unset($matches);` 删除正则内容,减少内存消耗 ` return;` 直接返回调用处, 因为处理的引用字符串,不需要返值 ### 9 extend标签解析 parseExtend() `private function parseExtend(&$content)` > $content:待解析字符串引用 `$regex = $this->getRegex('extend');` 获取extend标签正则表达式字符串 `$array = $blocks = $baseBlocks = [];` `$extend = '';` $blocks block标签数组,$baseBlocks 顶层block标签数组 $extend 解析结果字符串声明 **下面声明了一个闭包函数,进行extend标签内容解析** 关于闭包函数 见 php的闭包函数 `$func = function ($template) use (&$func, &$regex, &$array, &$extend, &$blocks, &$baseBlocks) {}` 闭包函数声明 其中$template作为模板内容变量参数 use 包含父作用域变量参数 `if (preg_match($regex, $template, $matches)) {}` 正则模板内容中的extend标签字符串 `if (!isset($array[$matches['name']])) {}` 检查是否解析过这个标签内容,防止重复解析 extend标签,读取继承模板文件内容 `$array[$matches['name']] = 1;` ?? `$extend = $this->parseTemplateName($matches['name']);` 调用parseTemplateName() 根据文件名读取继承模板文件内容 `$func($extend);` 递归调用匿名函数$func,处理$extend的内容 `$blocks = array_merge($blocks, $this->parseBlock($template));` 调用parseBlock() 解析模板内容中extend标签下的的block标签内容 作为父类模板的的子类实现内容。 `return;` extend标签解析返回 `else {}` `$baseBlocks = $this->parseBlock($template, true);` 调用parseBlock() 解析模板内容的block标签内容 作为继承的父类模板的blcok声明内容 **$baseblocks,$blocks两个block标签内容通过name属性对应,并替换。** ~~~ if (empty($extend)) { $extend = $template; } ~~~ extend外的block标签内容,保存到结果 `$func($content);` 匿名函数处理模板内容 `if (!empty($extend)) {}` 返回解析结果内容不为空 `if ($baseBlocks) {}` 父模板block标签内容数组为不空时 ~~~ foreach ($baseBlocks as $name => $val) {} ~~~ 遍历父模板block表示内容数组 ~~~ $replace = $val['content']; ~~~ 父模板block标签内容缓存 `if (!empty($children[$name])) {}` 防止重复遍历 ~~~ foreach ($children[$name] as $key) { $replace = str_replace($baseBlocks[$key]['begin'] . $baseBlocks[$key]['content'] . $baseBlocks[$key]['end'], $blocks[$key]['content'], $replace); } ~~~ 将父模板block标签替换为子模板对应block标签内容 `if (isset($blocks[$name])) {}` 父模板$name的block标签 在子模板block标签数组$blocks中存在对应内容 需要进行子模板内容替换处理 `$replace = str_replace(['{__BLOCK__}', '{__block__}'], $replace, $blocks[$name]['content']);` 合并子模板的{__Bblock__}标签内容 `if (!empty($val['parent'])) {}` 子模板的顶级block标签 `$parent = $val['parent'];` 获取其父标签 ~~~ if (isset($blocks[$parent])) { $blocks[$parent]['content'] = str_replace($blocks[$name]['begin'] . $blocks[$name]['content'] . $blocks[$name]['end'], $replace, $blocks[$parent]['content']); } ~~~ 对子模板block中的内容进行替换处理 `$blocks[$name]['content'] = $replace;` 子模板内容 ` $children[$parent][] = $name;` 子模板解析结果数组$chilldren 在上面的`if (!empty($children[$name])) {}`中使用 ### 10 block标签解析 parseBlock() `private function parseBlock(&$content, $sort = false)` > $content:待解析内容 > $sort:是否排序 `$regex = $this->getRegex('block');` 获取block标签的正则表达式 `if (preg_match_all($regex, $content, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) {}` 匹配$content的block标签内容到$matches `$right = $keys = [];` 子模板块内容$right,对应父模板键数组$keys? `foreach ($matches as $match) {}` 遍历匹配的结果数组$matches `if (empty($match['name'][0])) {}` 这里是子模板的block内容解析 `if (count($right) > 0) {}` 这里的$right在下的else语句中添加 ~~~ $tag = array_pop($right); $start = $tag['offset'] + strlen($tag['tag']); $length = $match[0][1] - $start; ~~~ 单个标签处理,获取其开始与长度 ~~~ $result[$tag['name']] = [ 'begin' => $tag['tag'], 'content' => substr($content, $start, $length), 'end' => $match[0][0], 'parent' => count($right) ? end($right)['name'] : '', ]; ~~~ 子标签内容的组合结果 $keys`[$tag['name']] = $match[0][1];` 保存标签索引到$keys ~~~ $right[] = [ 'name' => $match[2][0], 'offset' => $match[0][1], 'tag' => $match[0][0], ]; ~~~ 父标block签压栈处理 `unset($right, $matches);` 注销无用内存 ~~~ if ($sort) { array_multisort($keys, $result); } ~~~ 排序处理 `return $result;` 返回block标签内容 ### 11 layout标签解析 parseLayout() `private function parseLayout(&$content)` $content:待解析内容索引 `if (preg_match($this->getRegex('layout'), $content, $matches)) {}` 正则匹配布局字符串内容 `$content = str_replace($matches[0], '', $content);` Layout标签内容替换处理 `$array = $this->parseAttr($matches[0]);` Layout表示属性解析 `if (!$this->config['layout_on'] || $this->config['layout_name'] != $array['name']) {}` 布局检测?? ~~~ $layoutFile = $this->parseTemplateFile($array['name']); ~~~ 加载布局模板文件 `if ($layoutFile) {}` 加载布局模板成功 `$replace = isset($array['replace']) ? $array['replace'] : $this->config['layout_item'];` 获取替换目标 `$content = str_replace($replace, $content, file_get_contents($layoutFile));` 对布局模板内容进行替换 ~~~ else { $content = str_replace('{__NOLAYOUT__}', '', $content); } ~~~ 删除模板内容中的{__NOLAYOUT__} ### 12 include标签解析 parseInclude() `private function parseInclude(&$content)` > $content:待解析内容 `$regex = $this->getRegex('include');` 获取include标签字符串正则表达式 下面声明闭包函数递归处理include标签内容 `$func = function ($template) use (&$func, &$regex, &$content) {}` 闭包函数声明 `if (preg_match_all($regex, $template, $matches, PREG_SET_ORDER)) {}` 正则匹配include标签内容 ~~~ $array = $this->parseAttr($match[0]); $file = $array['file']; unset($array['file']); ~~~ 获取其中的file属性 `$parseStr = $this->parseTemplateName($file);` 加载需要包含的文件 ### 13 php语法检查 parsePhp() `private function parsePhp(&$content)` > $content:待解析模板内容 `$content = preg_replace('/(<\?(?!php|=|$))/i', '<?php echo \'\\1\'; ?>' . "\n", $content);` 对短标签进行替换处理 ~~~ if ($this->config['tpl_deny_php'] && false !== strpos($content, '<?php')) { throw new Exception('not allow php tag', 11600); } ~~~ 禁止php代码的模板检测后抛出异常 ### 14 标签库加载 getIncludeTagLib() `private function getIncludeTagLib(&$content)` > $content:待解析内容 `preg_match($this->getRegex('taglib'), $content, $matches)` 获取Taglib标签内容 ~~~ $content = str_replace($matches[0], '', $content); ~~~ 将匹配的内容去除 `return explode(',', $matches['name']);` 以逗号分割标签库名称 `return null;` 如果匹配失败,返回null。 ### 15 标签库标签解析 parseTagLib() `public function parseTagLib($tagLib, &$content, $hide = false)` > $tagLibName:标签库名称 > $content:待解析内容引用 ~~~ if (strpos($tagLib, '\\')) { $className = $tagLib; $tagLib = substr($tagLib, strrpos($tagLib, '\\') + 1); } ~~~ 根命名空间下的标签库类名$className ~~~ else { $className = '\\think\\template\\taglib\\' . ucwords($tagLib); } ~~~ 标签库命名空间下的类名 `$tLib = new $className($this);` 创建标签库类实例对象 默认为think\template\taglib\Cx.php类 `$tLib->parseTag($content, $hide ? '' : $tagLib);` 调用对应标签库类实例对象对模板内容进行解析 这里进入标签库标签解析 ### 16 标签属性分析 parseAttr() `public function parseAttr($str, $name = null)` > $str:标签字符串 > $name: 属性名称 `$regex = '/\s+(?>(?<name>[\w-]+)\s*)=(?>\s*)([\"\'])(?<value>(?:(?!\\2).)*)\\2/is';` 属性正则表达式 ~~~ if (preg_match_all($regex, $str, $matches, PREG_SET_ORDER)) { foreach ($matches as $match) { $array[$match['name']] = $match['value']; } unset($matches); } ~~~ 对标签字符串$str中的属性进行正则匹配, 以键值对形式保存到$array中。 ~~~ if (!empty($name) && isset($array[$name])) { return $array[$name]; } else { return $array; } ~~~ 从$array中获取$name对应属性值,并返回 ### 17 标签正则字符串生成 getRegex() `private function getRegex($tagName)` > $tagName:标签名 `$regex = '';` 生成结果字符串 `if ('tag' == $tagName) {}` 普通标签生成 ~~~ $begin = $this->config['tpl_begin']; $end = $this->config['tpl_end']; ~~~ 获取普通标签开始与结束标记 ~~~ if (strlen(ltrim($begin, '\\')) == 1 && strlen(ltrim($end, '\\')) == 1) {} ~~~ ?? ~~~ $regex = $begin . '((?:[\$]{1,2}[a-wA-w_]|[\:\~][\$a-wA-w_]|[+]{2}[\$][a-wA-w_]|[-]{2}[\$][a-wA-w_]|\/[\*\/])(?>[^' . $end . ']*))' . $end; ~~~ ??生成的普通标签正则字符串 `$regex = $begin . '((?:[\$]{1,2}[a-wA-w_]|[\:\~][\$a-wA-w_]|[+]{2}[\$][a-wA-w_]|[-]{2}[\$][a-wA-w_]|\/[\*\/])(?>(?:(?!' . $end . ').)*))' . $end;` ??生成的普通标签正则字符串 `else {}` else的内容为标签库标签生成 ~~~ $begin = $this->config['taglib_begin']; $end = $this->config['taglib_end']; ~~~ 标签库标签开始与结束标记 `$single = strlen(ltrim($begin, '\\')) == 1 && strlen(ltrim($end, '\\')) == 1 ? true : false;` ?? `switch ($tagName) {}` 根据标签名参数$tagName,生成不同标签 包括 literal,restoreliteral,include, taglib,layout,extend,block 分析可知这里的标签都是内容块标签。 ~~~ $begin = $this->config['taglib_begin']; $end = $this->config['taglib_end']; ~~~ 标签库标签的开始与结束标记 正则表达式相关见 php的正则表达式 ## 10 模板普通标签解析 Template\Template.php ### 1 普通标签解析 parseTag() `private function parseTag(&$content)` > $content:待解析模板内容 ### 2 模板变量解析 parseVar() `public function parseVar(&$varStr)` > $varStr:变量的内容 > 格式为{$varname|function1|function2=arg1,arg2} ### 3 模板变量过滤函数解析 parseVarFunction() `public function parseVarFunction(&$varStr)` > $varStr:变量的内容 > 格式为{$varname|function1|function2=arg1,arg2} ###4 框架模板变量解析 parseThinkVar() `public function parseThinkVar(&$vars)` > $vars: 变量内容 > 格式 以 $Think. 打头的变量属于特殊模板变量 ## 11 模板标签库操作 Template\TagLib.php ### 标签库初始化 `public function __construct($template)` ### 获取标签列表 `public function getTags()` ### 标签添加?? 增加标签格式 增加标签解析 修改标签解析 删除标签解析 ## 12 模板标签库解析 template\TagLib.php,\taglib\Cx.php >[info] TagLib.php ### 模板内容中标签库标签解析 parseTag() `public function parseTag(&$content, $lib = '')` > $cotent:待解析内容 > $lib:标签库类 标签分闭合与非闭合两种解析 ### 标签库标签的属性解析 parseAttr() `public function parseAttr($str, $name, $alias = '')` > $str: 标签属性字符串 > $name:属性名 > $alias:属性别名 ### 标签中条件表达式解析 parseCondition() `public function parseCondition($condition)` `public function autoBuildVar(&$name)` `private function getRegex($tags, $close)` >[info] template\taglib\Cx.php ### php标签解析 _php() `public function _php($tag, $content)` ### volist标签解析 _volist() `public function _volist($tag, $content)` ### foreach标签解析 _foreach() `public function _foreach($tag, $content)` ### if标签解析 _if() `public function _if($tag, $content)` ### elseif标签解析 _elseif() `public function _elseif($tag, $content)` ### else标签解析 _else() `public function _else($tag)` ### switc标签解析 _switc() `public function _switch($tag, $content)` ### case标签解析 _case() `public function _case($tag, $content)` ### defaul标签解析 _defaul() `public function _default($tag)` ### compare标签解析 _compare() `public function _compare($tag, $content)` ### range标签解析 _range() `public function _range($tag, $content)` ### present标签解析 _present() `public function _present($tag, $content)` ### notpresent标签解析 _notpresent() `public function _notpresent($tag, $content)` ### empty标签解析 _empty() `public function _empty($tag, $content)` ### notempty标签解析 _notempty() `public function _notempty($tag, $content)` ### defined标签解析 _defined() `public function _defined($tag, $content)` ### notdefined标签解析 _notdefined() `public function _notdefined($tag, $content)` ### import标签解析 _import() `public function _import($tag, $content, $isFile = false)` ### load标签解析 _load() `public function _load($tag, $content)` ### assign标签解析 _assign() `public function _assign($tag, $content)` ### define标签解析 _define() `public function _define($tag, $content)` ### for标签解析 _for() `public function _for($tag, $content)` ### url标签解析 _url() `public function _url($tag, $content)` ### functio标签解析 _functio() `public function _function($tag, $content)` ## 13 模板缓存读写 template\driver\File.php `public function write($cacheFile, $content)` `public function read($cacheFile, $vars = [])` `public function check($cacheFile, $cacheTime)`