探究Thinkphp URL路由流程

thinkphp源码浅析-3.视图层解析流程

1、建立控制器方法

  • 路径 thinkphp_5.0.7_core/application/index/controller/test.php
  • 代码 建立一个测试方法
    1
    2
    3
    4
    5
    6
    7
    public function test($name = "")
    {
    $this->assign('name',$name);
    $res = view('test');
    return $res;
    //$this->display();
    }

2、建立视图模板文件

  • 路径 thinkphp_5.0.7_core/application/index/view/test/test.html
  • 代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Title</title>
    </head>
    <body>
    <hr />
    项目更目录:__ROOT__
    <h1>{$name}</h1>
    </body>
    </html>

3、目标 :

  • 同过test控制器test方法:接收参数name,并将变量$name 赋值给视图模板层输出,探究其中运行即解析流程。

4、运行流程

  • 4.1 入口文件index.php

    1
    2
    3
    4
    // 定义应用目录
    define('APP_PATH', __DIR__ . '/../application/');
    // 加载框架引导文件
    require __DIR__ . '/../thinkphp/start.php';
  • 4.2 start.php

    1
    2
    3
    require __DIR__ . '/base.php';
    // 执行应用
    App::run()->send();
  • 4.2.1 App::run() 应用主体运行

  • 4.2.1.1 实例化Request

    1
    is_null($request) && $request = Request::instance();
  • 4.2.1.2 初始化公共配置

    1
    $config = self::initCommon();
  • 4.2.1.3 默认路由绑定

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    if (defined('BIND_MODULE')) {
    // 模块/控制器绑定
    BIND_MODULE && Route::bind(BIND_MODULE);
    } elseif ($config['auto_bind_module']) {
    // 入口自动绑定
    $name = pathinfo($request->baseFile(), PATHINFO_FILENAME);
    if ($name && 'index' != $name && is_dir(APP_PATH . $name)) {
    Route::bind($name);
    }
    }
  • 4.2.1.4 请求过滤

    1
    $request->filter($config['default_filter']);
  • 4.2.1.5 多语言设置与加载

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    if ($config['lang_switch_on']) {
    // 开启多语言机制 检测当前语言
    Lang::detect();
    } else {
    // 读取默认语言
    Lang::range($config['default_lang']);
    }
    $request->langset(Lang::range());
    // 加载系统语言包
    Lang::load([
    THINK_PATH . 'lang' . DS . $request->langset() . EXT,
    APP_PATH . 'lang' . DS . $request->langset() . EXT,
    ]);
  • 4.2.1.6 应用调度路由解析

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    $dispatch = self::$dispatch;
    if (empty($dispatch)) {
    // +----------------------------------------------------------------------
    // | self::routeCheck($request, $config) 开始路由
    // +----------------------------------------------------------------------
    // 进行URL路由检测
    $dispatch = self::routeCheck($request, $config);
    }
    // 记录当前调度信息
    $request->dispatch($dispatch);
    // 记录路由和请求信息
    if (self::$debug) {
    Log::record('[ ROUTE ] ' . var_export($dispatch, true), 'info');
    Log::record('[ HEADER ] ' . var_export($request->header(), true), 'info');
    Log::record('[ PARAM ] ' . var_export($request->param(), true), 'info');
    }
    • self::routeCheck($request, $config) 开始路由 thinkphp具体路由实现步骤存这里开始 源码浅析一、路由
  • 4.2.1.7 监听app_begin

    1
    Hook::listen('app_begin', $dispatch);
  • 4.2.1.8 请求缓存检查

    1
    $request->cache($config['request_cache'], $config['request_cache_expire'], $config['request_cache_except']);
  • 4.2.1.9 根据调度类型执行方法 跳转页面、控制器/方法加载等

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    switch ($dispatch['type']) {
    case 'redirect':
    // 执行重定向跳转
    $data = Response::create($dispatch['url'], 'redirect')->code($dispatch['status']);
    break;
    case 'module':
    // 模块/控制器/操作
    $data = self::module($dispatch['module'], $config, isset($dispatch['convert']) ? $dispatch['convert'] : null);

    break;
    case 'controller':
    // 执行控制器操作
    $vars = array_merge(Request::instance()->param(), $dispatch['var']);
    $data = Loader::action($dispatch['controller'], $vars, $config['url_controller_layer'], $config['controller_suffix']);
    break;
    case 'method':
    // 执行回调方法
    $vars = array_merge(Request::instance()->param(), $dispatch['var']);
    $data = self::invokeMethod($dispatch['method'], $vars);
    break;
    case 'function':
    // 执行闭包
    $data = self::invokeFunction($dispatch['function']);
    break;
    case 'response':
    $data = $dispatch['response'];
    break;
    default:
    throw new \InvalidArgumentException('dispatch type not support');
    }
    } catch (HttpResponseException $exception) {
    $data = $exception->getResponse();
    }
    • 这里类型为 module 执行对应映射控制器、方法返回方法执行内容
    • 映射时 会初始化一个视图instance => 初始化构造方法->初始化模板引擎$this->engine((array) $engine);
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    //path thinkphp_5.0.7_core/thinkphp/library/think/View.php

    /**
    * 初始化视图
    * @access public
    * @param array $engine 模板引擎参数
    * @param array $replace 字符串替换参数
    * @return object
    */
    public static function instance($engine = [], $replace = [])
    {
    if (is_null(self::$instance)) {
    self::$instance = new self($engine, $replace);
    }
    return self::$instance;
    }
  • 执行时 $res = view('test'); 创建一个Response对象
    代码 路径thinkphp_5.0.7_core/thinkphp/helper.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (!function_exists('view')) {
/**
* 渲染模板输出
* @param string $template 模板文件
* @param array $vars 模板变量
* @param array $replace 模板替换
* @param integer $code 状态码
* @return \think\response\View
*/
function view($template = '', $vars = [], $replace = [], $code = 200)
{
return Response::create($template, 'view', $code)->replace($replace)->assign($vars);
}
}
  • 4.2.1.10 清空类的实例化

    1
    Loader::clearInstance();
  • 4.2.1.11 输出数据到客户端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    if ($data instanceof Response) {
    $response = $data;
    } elseif (!is_null($data)) {
    // 默认自动识别响应输出类型
    $isAjax = $request->isAjax();
    $type = $isAjax ? Config::get('default_ajax_return') : Config::get('default_return_type');
    $response = Response::create($data, $type);
    } else {
    $response = Response::create();
    }
  • 4.2.1.12 监听app_end

    1
    Hook::listen('app_end', $response);
  • 4.2.1.13 返回

    1
    return $response;
  • 4.2.2 App::run()->send(); 发送数据到客户端

  • 都数据返回给客户端了?那么内容标签解析呢?哪里执行的?这里说明下如果控制器使用的是printecho等直接输出打印的,那么映射控制器方法的时候是会直接接收到内容,其它返回还需接收处理(模板标签替换也在这块执行)。接下来流程:接收处理数据->编辑header(状态码、信息头等) -> 输出内容->用户端接收响应。

  • 4.2.2.1 输出数据处理

    1
    $data = $this->getContent();  // 返回html页面内容
  • getContent()代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    /**
    * 获取输出数据
    * @return mixed
    */
    public function getContent()
    {
    if (null == $this->content) {
    // +----------------------------------------------------------------------
    // | $this->data
    // | 'test' //这里数据就是在控制器方法中的view方法中创建response 时初始化赋值的数据
    // +----------------------------------------------------------------------

    $content = $this->output($this->data);
    // +----------------------------------------------------------------------
    // | $content 返回html
    // +----------------------------------------------------------------------
    if (null !== $content && !is_string($content) && !is_numeric($content) && !is_callable([
    $content,
    '__toString',
    ])
    ) {
    throw new \InvalidArgumentException(sprintf('variable type error: %s', gettype($content)));
    }

    $this->content = (string) $content;
    }
    return $this->content;
    }
  • 接着output()

    1
    2
    //path thinkphp_5.0.7_core/thinkphp/library/think/Response.php
    $content = $this->output($this->data);
  • 这里需注意,虽然think\Response 当前自身类中有output()方法,但是却不是执行自身的output()方法,而是由于在run()方法,映射类的时候test方法view时已经创建了实例,如果控制器中未实例化view实例在是在通过 Response::create()创建了think\response\View实例,后续操作的也是view实例,所以这里的$this->output($this->data) 自然操作的是think\response\View类的putout()方法^_^

  • 4.2.2.2 初始化视图 ViewTemplate::instance

    1
    2
    //path think\View   instance()
    ViewTemplate::instance(Config::get('template'), Config::get('view_replace_str'));// ->fetch($data, $this->vars, $this->replace);
  • 4.2.2.3 渲染模板文件 fetch

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    //path think\View   fetch()
    /**
    * 解析和获取模板内容 用于输出
    * @param string $template 模板文件名或者内容
    * @param array $vars 模板输出变量
    * @param array $replace 替换内容
    * @param array $config 模板参数
    * @param bool $renderContent 是否渲染内容
    * @return string
    * @throws Exception
    */
    public function fetch($template = '', $vars = [], $replace = [], $config = [], $renderContent = false)
    {
    // 模板变量
    $vars = array_merge(self::$var, $this->data, $vars);
    // +----------------------------------------------------------------------
    // array (size=1)
    // 'name' => string 'hello' (length=5)
    // +----------------------------------------------------------------------


    // 页面缓存
    ob_start();
    ob_implicit_flush(0);

    // 渲染输出
    $method = $renderContent ? 'display' : 'fetch';
    // +----------------------------------------------------------------------
    // | fetch
    // +----------------------------------------------------------------------
    //var_dump($method); //fetch
    $res = $this->engine->$method($template, $vars, $config);
    // var_dump($this->engine); //thinkphp_5.0.7_core/thinkphp/library/think/View.php
    // 获取并清空缓存
    $content = ob_get_clean();
    // 内容过滤标签
    Hook::listen('view_filter', $content);
    // 允许用户自定义模板的字符串替换

    $replace = array_merge($this->replace, $replace);
    // +----------------------------------------------------------------------
    // $this->replace
    // array (size=5)
    // '__ROOT__' => string '/thinkphp_5.0.7_core/public' (length=27)
    // '__URL__' => string '/thinkphp_5.0.7_core/public/index.php/index/test' (length=48)
    // '__STATIC__' => string '/thinkphp_5.0.7_core/public/static' (length=34)
    // '__CSS__' => string '/thinkphp_5.0.7_core/public/static/css' (length=38)
    // '__JS__' => string '/thinkphp_5.0.7_core/public/static/js' (length=37)
    // +----------------------------------------------------------------------
    if (!empty($replace)) {
    // +----------------------------------------------------------------------
    // | // 系统标签 或 自定义配置标签在此处替换
    // +----------------------------------------------------------------------
    $content = strtr($content, $replace);
    }
    return $content;
    }
  • 模板处理

  • $res = $this->engine->$method($template, $vars, $config); 这里$method为fetch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
 path think\Template fetch()
/**
* 渲染模板文件
* @access public
* @param string $template 模板文件
* @param array $vars 模板变量
* @param array $config 模板参数
* @return void
*/
public function fetch($template, $vars = [], $config = [])
{
//var_dump($template,$vars,$config);
// +----------------------------------------------------------------------
// $template
// thinkphp_5.0.7_core/public/../application/index/view/test/test.html
// $vars
// array (size=1)
// 'name' => string 'hello' (length=5)
// $config
// array (size=0)
// empty
// +----------------------------------------------------------------------
if ($vars) {
$this->data = $vars;
}
if ($config) {
$this->config($config);
}

if (!empty($this->config['cache_id']) && $this->config['display_cache']) {
// 读取渲染缓存
$cacheContent = Cache::get($this->config['cache_id']);
if (false !== $cacheContent) {
echo $cacheContent;
return;
}
}
$template = $this->parseTemplateFile($template);
// +----------------------------------------------------------------------
// | thinkphp_5.0.7_core/public/../application/index/view/test/test.html
// +----------------------------------------------------------------------

if ($template) {
$cacheFile = $this->config['cache_path'] . $this->config['cache_prefix'] . md5($template) . '.' . ltrim($this->config['cache_suffix'], '.');
// +----------------------------------------------------------------------
// $cacheFile
// thinkphp_5.0.7_core/runtime/temp/9495b6f57eb3fe65e44d90b56e260532.php
// +----------------------------------------------------------------------
if (!$this->checkCache($cacheFile)) {
// 缓存无效 重新模板编译
// +----------------------------------------------------------------------
// | 读取末班内容
// +----------------------------------------------------------------------
$content = file_get_contents($template);
// +----------------------------------------------------------------------
// | 编译视图文件 //变量替换标签变量替换php代码 系统定义标签替换字符路径输出
// +----------------------------------------------------------------------
$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']);
}
echo $content;
}
}
  • 编译视图文件 $this->compiler($content, $cacheFile);

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    $this->compiler($content, $cacheFile);
    path think\Template compiler()
    /**
    * 编译模板文件内容
    * @access private
    * @param string $content 模板内容
    * @param string $cacheFile 缓存文件名
    * @return void
    */
    private function compiler(&$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));
    }
    }
    } else {
    $content = str_replace('{__NOLAYOUT__}', '', $content);
    }

    //var_dump($content); 解析前
    // +----------------------------------------------------------------------
    // <!DOCTYPE html>
    // <html lang="en">
    // <head>
    // <meta charset="UTF-8">
    // <title>Title</title>
    // </head>
    // <body>
    // <hr />
    // __ROOT__
    // <h1>{$name}</h1>
    // </body>
    // </html>
    // +----------------------------------------------------------------------
    // 模板解析
    $this->parse($content);

    /**
    //var_dump($content); 解析后
    // +----------------------------------------------------------------------
    // <!DOCTYPE html>
    // <html lang="en">
    // <head>
    // <meta charset="UTF-8">
    // <title>Title</title>
    // </head>
    // <body>
    // <hr />
    // __ROOT__
    // <h1><?php echo $name; ?></h1>
    // </body>
    // </html>
    // +----------------------------------------------------------------------
    */


    if ($this->config['strip_space']) {
    /* 去除html空格与换行 */
    $find = ['~>\s+<~', '~>(\s+\n|\r)~'];
    $replace = ['><', '>'];
    $content = preg_replace($find, $replace, $content);
    }
    // 优化生成的php代码
    $content = preg_replace('/\?>\s*<\?php\s(?!echo\b)/s', '', $content);
    // 模板过滤输出
    $replace = $this->config['tpl_replace_string'];
    $content = str_replace(array_keys($replace), array_values($replace), $content);
    // 添加安全代码及模板引用记录
    $content = '<?php if (!defined(\'THINK_PATH\')) exit(); /*' . serialize($this->includeFile) . '*/ ?>' . "\n" . $content;
    // 编译存储
    $this->storage->write($cacheFile, $content);
    $this->includeFile = [];
    return;
    }
  • 模板解析入口parse

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    path think\Template parse()
    /**
    * 模板解析入口
    * 支持普通标签和TagLib解析 支持自定义标签库
    * @access public
    * @param string $content 要解析的模板内容
    * @return void
    */
    public function parse(&$content)
    {
    // 内容为空不解析
    if (empty($content)) {
    return;
    }

    // 替换literal标签内容
    // +----------------------------------------------------------------------
    // literal 原样输出 防止标签被解析
    // {literal} Hello,{$name}!{/literal}
    // +----------------------------------------------------------------------
    $this->parseLiteral($content);

    // 解析继承
    // +----------------------------------------------------------------------
    // | {extend name="base" /}
    // +----------------------------------------------------------------------
    $this->parseExtend($content);


    // 解析布局
    // +----------------------------------------------------------------------
    // | {layout name="layout" /} 使用参考地址 http://www.kancloud.cn/manual/thinkphp5/125013
    // +----------------------------------------------------------------------
    $this->parseLayout($content);

    // 检查include语法
    // +----------------------------------------------------------------------
    // | {include file='模版文件1,模版文件2,...' /}
    // +----------------------------------------------------------------------
    $this->parseInclude($content);

    // 替换包含文件中literal标签内容
    $this->parseLiteral($content);

    // 检查PHP语法
    $this->parsePhp($content);

    // 获取需要引入的标签库列表
    // 标签库只需要定义一次,允许引入多个一次
    // 一般放在文件的最前面
    // 格式:<taglib name="html,mytag..." />
    // 当TAGLIB_LOAD配置为true时才会进行检测
    if ($this->config['taglib_load']) {
    $tagLibs = $this->getIncludeTagLib($content);
    if (!empty($tagLibs)) {
    // 对导入的TagLib进行解析
    foreach ($tagLibs as $tagLibName) {
    $this->parseTagLib($tagLibName, $content);
    }
    }
    }
    // 预先加载的标签库 无需在每个模板中使用taglib标签加载 但必须使用标签库XML前缀
    if ($this->config['taglib_pre_load']) {
    $tagLibs = explode(',', $this->config['taglib_pre_load']);
    foreach ($tagLibs as $tag) {
    $this->parseTagLib($tag, $content);
    }
    }
    // 内置标签库 无需使用taglib标签导入就可以使用 并且不需使用标签库XML前缀
    $tagLibs = explode(',', $this->config['taglib_build_in']);
    foreach ($tagLibs as $tag) {
    $this->parseTagLib($tag, $content, true);
    }
    // 解析普通模板标签 {$tagName}
    $this->parseTag($content);

    // 还原被替换的Literal标签
    $this->parseLiteral($content, true);
    return;
    }

  • 至此内容模板标签解析完成,接下来返回APP,继续执行send剩余代码,编辑header状态码、头信息->输出内容->执行响应->清空本次请求,整个响应用户操作结束。