CodeIgniter 源码解读之URL路由

CodeIgniter 3.1.11 源码解读 专栏收录该内容
9 篇文章 0 订阅

URL路由

路由功能也是一个很重要的功能点,需要和大家一起学习一下,其实在之前的项目中,我很少会用到它,因为,通过常规的访问方式就已经很方便了,再其次,CI的框架目录已经非常简单了,所以默认的路由用起来完全可以了。但这里,我希望和大家一起看下CI的路由实现原理,首先,我们先写个例子,让程序 run 起来,然后再看源代码。

首先,我在 application/config/routes.php 定义了一些路由,并且新建了对应的控制器及方法:

/**
 * 自定义的路由
 */
// 使用通配符
$route['product/(:num)'] = 'catalog/product_lookup/$1';

// restful 风格
$route['rest/(\d+)']['get'] = 'api/get/$1';

// restful 风格
$route['rest/(\d+)']['delete'] = 'api/delete/$1';

// 回调方式
$route['product/([a-zA-Z]+)/edit/(\d+)'] = function ($product_type, $id)
{
    return 'catalog/product_edit/' . strtolower($product_type) . '/' . $id;
};

// 登录成功后,回到原先界面
$route['login/(.+)'] = 'auth/login/$1';

第一个路由控制器及方式代码示例:

class Catalog extends CI_Controller {

	/**
	 * Catalog Page for this controller.
	 */
	public function product_lookup($id)
	{
		print_r($id);
	}

	public function product_edit($product_type, $id)
	{
		print_r($product_type);
	}
}

当我们访问 http://localhost:8100/product/31 时,URL路由会触发 第一条路由,返回 product_id:
路由返回结果
好啦,代码已经成功 run 起来了,我们现在去揭开 URL路由 的面纱,去搜寻他的本质:

/*
 1. ------------------------------------------------------
 2.  Instantiate the URI class
 3. ------------------------------------------------------
 */
 # 在 CodeIgniter.php 文件304行,CI 加载了 URL 类
	$URI =& load_class('URI', 'core');
/*
 4. ------------------------------------------------------
 5.  Instantiate the routing class and set the routing
 6. ------------------------------------------------------
 */
 # 加载路由类
	$RTR =& load_class('Router', 'core', isset($routing) ? $routing : NULL);

好,我们接下来进入到 URI 类中,看看他到底做了哪些事:

# URI 的构造函数
public function __construct()
{
	# 加载配置核心类
	$this->config =& load_class('Config', 'core');

	// If query strings are enabled, we don't need to parse any segments.
	// However, they don't make sense under CLI.
	# is_cli() 是检测 CI 的运行环境(fast-cgi[浏览器访问] 和 cli[控制台])
	# enable_query_strings 在 application/config.php 默认为 FALSE
	if (is_cli() OR $this->config->item('enable_query_strings') !== TRUE)
	{
		# 被允许的 url 字符
		# 在 application/config.php 中定义(a-z 0-9~%.:_\-)
		$this->_permitted_uri_chars = $this->config->item('permitted_uri_chars');

		// If it's a CLI request, ignore the configuration
		# 如果是 cli 环境
		# 有很多人会疑问,到底 cli 环境怎么去访问 web
		# 我在文章结尾处会给大家演示
		# 心急的可以直接去看看,不急的可以先看完这些源码 ^_^
		if (is_cli())
		{
			# 调用 _parse_argv,赶紧去看看(如下)
			# 这里得到的 $uri 类似等于 welcome/index 形式
			$uri = $this->_parse_argv();
		}
		else
		{
			# 非 cli 环境
			# 协议,application/config.php 文件中定义了 REQUEST_URI
			$protocol = $this->config->item('uri_protocol');
			# 初始化
			empty($protocol) && $protocol = 'REQUEST_URI';

			switch ($protocol)
			{
				case 'AUTO': // For BC purposes only
				case 'REQUEST_URI':
					# go go go !!! go to _parse_request_uri()
					$uri = $this->_parse_request_uri();
					break;
				case 'QUERY_STRING':
					$uri = $this->_parse_query_string();
					break;
				case 'PATH_INFO':
				default:
					$uri = isset($_SERVER[$protocol])
						? $_SERVER[$protocol]
						: $this->_parse_request_uri();
					break;
			}
		}
		# go to _set_uri_string($uri)
		$this->_set_uri_string($uri);
	}

	log_message('info', 'URI Class Initialized');
}

_parse_argv() 方法:

/**
 7. Parse CLI arguments
 8.  9. Take each command line argument and assume it is a URI segment.
 10.  11. @return	string
 */
protected function _parse_argv()
{
	# 首先先解释下,通过 cli 方法访问,$_SERVER 中存在 $_SERVER['argv'] 的数组
	# $_SERVER['argv'] 是 array('index.php', 'controller', 'method', 'params') 的数组
	# 这里是保存 $_SERVER['argv'] 下标为1及后面的数据,并赋值给 $argv
	$args = array_slice($_SERVER['argv'], 1);
	# 返回一个使用 / 连接的数组成 string
	# 如:Welcome/index
	return $args ? implode('/', $args) : '';
}

_parse_request_uri() 方法:

protected function _parse_request_uri()
{
	# 空值判断
	if ( ! isset($_SERVER['REQUEST_URI'], $_SERVER['SCRIPT_NAME']))
	{
		return '';
	}

	// parse_url() returns false if no host is present, but the path or query string
	// contains a colon followed by a number
	# 格式化请求地址,得到一个关联数组,因为我使用 http://localhost:8100/welcome/index 访问
	# 这里的 $uri = array('scheme' => 'http', 'host' => 'dummy', 'path' => '/welcome/index');
	$uri = parse_url('http://dummy'.$_SERVER['REQUEST_URI']);
	# 初始化 $query(通过 get 方式 提交的 参数)
	$query = isset($uri['query']) ? $uri['query'] : '';
	# 得到 $path
	$uri = isset($uri['path']) ? $uri['path'] : '';
	# /
	if (isset($_SERVER['SCRIPT_NAME'][0]))
	{
		# 去掉 /index.php
		if (strpos($uri, $_SERVER['SCRIPT_NAME']) === 0)
		{
			$uri = (string) substr($uri, strlen($_SERVER['SCRIPT_NAME']));
		}
		elseif (strpos($uri, dirname($_SERVER['SCRIPT_NAME'])) === 0)
		{
			$uri = (string) substr($uri, strlen(dirname($_SERVER['SCRIPT_NAME'])));
		}
	}

	// This section ensures that even on servers that require the URI to be in the query string (Nginx) a correct
	// URI is found, and also fixes the QUERY_STRING server var and $_GET array.
	# 处理 get 提交的参数
	if (trim($uri, '/') === '' && strncmp($query, '/', 1) === 0)
	{
		$query = explode('?', $query, 2);
		$uri = $query[0];
		$_SERVER['QUERY_STRING'] = isset($query[1]) ? $query[1] : '';
	}
	else
	{
		$_SERVER['QUERY_STRING'] = $query;
	}
	
	# 将 get 提交的参数值:type=1&name=niceman
	# 转化为键值形式的并保存在 $_GET 中
	parse_str($_SERVER['QUERY_STRING'], $_GET);

	if ($uri === '/' OR $uri === '')
	{
		return '/';
	}

	// Do some final cleaning of the URI and return it
	# 清除 $uri 中可能存在的相对目录:../
	return $this->_remove_relative_directory($uri);
}

_set_uri_string($uri) 方法:

protected function _set_uri_string($str)
{
	// Filter out control characters and trim slashes
	# 去除不可见字符,去除收尾 / 符号
	$this->uri_string = trim(remove_invisible_characters($str, FALSE), '/');

	if ($this->uri_string !== '')
	{
		// Remove the URL suffix, if present
		# 如果定义了 url 后缀,则要加上
		if (($suffix = (string) $this->config->item('url_suffix')) !== '')
		{
			$slen = strlen($suffix);

			if (substr($this->uri_string, -$slen) === $suffix)
			{
				$this->uri_string = substr($this->uri_string, 0, -$slen);
			}
		}
		# 将 segments 下标为 0 的 值 定义为 NULL
		# 因为 要让 segments 值从下标 从 1 开始
		$this->segments[0] = NULL;
		// Populate the segments array
		# 切割 url 成一个数组
		foreach (explode('/', trim($this->uri_string, '/')) as $val)
		{
			# 去除字符串两端空格
			$val = trim($val);
			// Filter segments for security
			# 可靠字符串校验
			# 是一个正则匹配
			$this->filter_uri($val);

			if ($val !== '')
			{
				# 保存被切割的 控制器、方法
				# 此时的下标从 1 开始哦
				$this->segments[] = $val;
			}
		}
		
		unset($this->segments[0]);
	}
}

到此 URI类的初始化工作已完成了,我们先总结下初始化都完成哪些工作:
1、分别从 fast-cgi 和 cli 两种环境中获取 请求的 url
2、处理请求的 query 参数
3、对 uri 进行可靠字符校验及去除不可见字符
4、将请求 uri 分割到 segments 数组

咱们继续看 Router 类到底做了哪些事?有木有偷懒:

# 按照罐栗(惯例),先上 构造 函数
public function __construct($routing = NULL)
{
	# 加载 uri 及 config 类实例
	$this->config =& load_class('Config', 'core');
	$this->uri =& load_class('URI', 'core');
	
	# FALSE
	$this->enable_query_strings = ( ! is_cli() && $this->config->item('enable_query_strings') === TRUE);

	// If a directory override is configured, it has to be set before any dynamic routing logic
	# 咦~ 介里 $routing['directory'] 从哪冒出来的,为什么有介个?
	# 其实在 index.php 文件中有详细的说明,好奇的小朋友可以去看看(Line 140)
	# 我意淫这个 $routing['directory'] 的作用类似 thinkPHP 的 模块
	is_array($routing) && isset($routing['directory']) && $this->set_directory($routing['directory']);
	# 去看看本类的 _set_routing 方法
	$this->_set_routing();

	// Set any routing overrides that may exist in the main index file
	# 如果你在 index.php 文件中定义了 $routing 则会使用 routing 覆盖原有值
	if (is_array($routing))
	{
		empty($routing['controller']) OR $this->set_class($routing['controller']);
		empty($routing['function'])   OR $this->set_method($routing['function']);
	}

	log_message('info', 'Router Class Initialized');
}

_set_routing() 方法:

protected function _set_routing()
{
	// Load the routes.php file. It would be great if we could
	// skip this for enable_query_strings = TRUE, but then
	// default_controller would be empty ...
	# 加入路由配置文件
	if (file_exists(APPPATH.'config/routes.php'))
	{
		include(APPPATH.'config/routes.php');
	}
	# 加入路由配置文件(环境)
	if (file_exists(APPPATH.'config/'.ENVIRONMENT.'/routes.php'))
	{
		include(APPPATH.'config/'.ENVIRONMENT.'/routes.php');
	}

	// Validate & get reserved routes
	# 常规赋值
	if (isset($route) && is_array($route))
	{
		isset($route['default_controller']) && $this->default_controller = $route['default_controller'];
		isset($route['translate_uri_dashes']) && $this->translate_uri_dashes = $route['translate_uri_dashes'];
		unset($route['default_controller'], $route['translate_uri_dashes']);
		$this->routes = $route;
	}

	// Are query strings enabled in the config file? Normally CI doesn't utilize query strings
	// since URI segments are more search-engine friendly, but they can optionally be used.
	// If this feature is enabled, we will gather the directory/class/method a little differently
	# 是否开启 enable_query_strings
	# 开启后使用 ?d=home&c=welcome&m=index 方式访问
	if ($this->enable_query_strings)
	{
		// If the directory is set at this time, it means an override exists, so skip the checks
		if ( ! isset($this->directory))
		{
			$_d = $this->config->item('directory_trigger');
			$_d = isset($_GET[$_d]) ? trim($_GET[$_d], " \t\n\r\0\x0B/") : '';

			if ($_d !== '')
			{
				$this->uri->filter_uri($_d);
				$this->set_directory($_d);
			}
		}

		$_c = trim($this->config->item('controller_trigger'));
		if ( ! empty($_GET[$_c]))
		{
			# 通过 get 提交的 控制器及方法处理
			$this->uri->filter_uri($_GET[$_c]);
			$this->set_class($_GET[$_c]);

			$_f = trim($this->config->item('function_trigger'));
			if ( ! empty($_GET[$_f]))
			{
				$this->uri->filter_uri($_GET[$_f]);
				$this->set_method($_GET[$_f]);
			}
			# 将处理得到的 控制器及方法 赋值给 rsegments
			$this->uri->rsegments = array(
				1 => $this->class,
				2 => $this->method
			);
		}
		else
		{
			$this->_set_default_controller();
		}

		// Routing rules don't apply to query strings and we don't need to detect
		// directories, so we're done here
		return;
	}

	// Is there anything to parse?
	# 解析路由规则,跳去 _parse_routes() 方法,一探究竟
	if ($this->uri->uri_string !== '')
	{
		$this->_parse_routes();
	}
	else
	{
		# 设置默认控制器
		$this->_set_default_controller();
	}
}

_parse_routes() 方法:

# 介个方法的作用是 将我们写在 routes.php 文件中的路由规则进行匹配
protected function _parse_routes()
{
	// Turn the segment array into a URI string
	# 将 segments 数组 转化为 字符串
	$uri = implode('/', $this->uri->segments);

	// Get HTTP verb
	# 获取 http 请求方式 get? post? put? delete? ... otherwise cli
	$http_verb = isset($_SERVER['REQUEST_METHOD']) ? strtolower($_SERVER['REQUEST_METHOD']) : 'cli';

	// Loop through the route array looking for wildcards
	# 遍历 $routes
	foreach ($this->routes as $key => $val)
	{
		// Check if route format is using HTTP verbs
		# restful 风格 在这里会是个数组
		if (is_array($val))
		{
			# 将 get、post、delete 等 http verb 转化为 小写
			$val = array_change_key_case($val, CASE_LOWER);
			if (isset($val[$http_verb]))
			{
				# 取出满足当前 http 请求方式的 路由规则
				$val = $val[$http_verb];
			}
			else
			{
				continue;
			}
		}

		// Convert wildcards to RegEx
		# 替换当前 $key 存在的 通配符 如:product/(:num)
		# 替换成正常的正则表达式
		$key = str_replace(array(':any', ':num'), array('[^/]+', '[0-9]+'), $key);

		// Does the RegEx match?
		# 拿替换后的 $key 去匹配 $uri 看是否匹配
		# 如:preg_match('#^product/[0-9]+$#', 'product/3', $matches)
		# 明显可以匹配
		if (preg_match('#^'.$key.'$#', $uri, $matches))
		{
			// Are we using callbacks to process back-references?
			# 判断是否为回调函数
			if ( ! is_string($val) && is_callable($val))
			{
				// Remove the original string from the matches array.
				# 去掉匹配数组第一项
				# 只保留正则匹配到值
				array_shift($matches);

				// Execute the callback using the values in matches as its parameters.
				# 调用回调函数
				$val = call_user_func_array($val, $matches);
			}
			// Are we using the default routing method for back-references?
			# 反向引用替换
			# 将 $1 替换成 匹配到的 值
			elseif (strpos($val, '$') !== FALSE && strpos($key, '(') !== FALSE)
			{
				$val = preg_replace('#^'.$key.'$#', $val, $uri);
			}
			# 此时的 $val 可能如: catalog/product/12
			# explode 过后 为 一个数组
			# go to _set_request() 方法吧
			$this->_set_request(explode('/', $val));
			return;
		}
	}

	// If we got this far it means we didn't encounter a
	// matching route so we'll set the site default route
	$this->_set_request(array_values($this->uri->segments));
}

_set_request() 方法:

# 将之前处理得到的信息(控制器、方法)
# 将实现他们
protected function _set_request($segments = array())
{
	# 处理带目录的控制器问题(有兴趣可以自己去看看)
	# 及 - => _
	$segments = $this->_validate_request($segments);
	// If we don't have any segments left - try the default controller;
	// WARNING: Directories get shifted out of the segments array!
	if (empty($segments))
	{
		$this->_set_default_controller();
		return;
	}
	# 如果开启
	# 则需要转化
	if ($this->translate_uri_dashes === TRUE)
	{
		$segments[0] = str_replace('-', '_', $segments[0]);
		if (isset($segments[1]))
		{
			$segments[1] = str_replace('-', '_', $segments[1]);
		}
	}
	# 字符处理及赋值
	$this->set_class($segments[0]);
	# 如果存在 method的话则赋值
	# 不存在则 定义 index 为 默认 方法
	if (isset($segments[1]))
	{
		$this->set_method($segments[1]);
	}
	else
	{
		$segments[1] = 'index';
	}
	# -----start------------
	# 这个过程是保证 $segments 数组下标从 1 开始
	array_unshift($segments, NULL);
	unset($segments[0]);
	# ------end-------------
	$this->uri->rsegments = $segments;
}

哎呀~~ 好累啊,写了一大推,到此,URI 路由的功能就完成了,它也算是 完成使命了。希望大家都看懂了整个流程了。我得赶紧去休息下了。好啦!! 咱们下期见!!

  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 深蓝海洋 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值