Typecho源码分析1 - 开篇、整体流程分析
学习编程到现在,虽然写过很多代码,但是总感觉自己依旧是业余选手。究其原因,可能是写过的正儿八经地追求高质量代码的项目做的太少。就Web开发而言,可能大家刚开始都是使用过几门语言、用过几个框架写一些东西,但是总感觉缺了点什么。所以最近准备分析一些评价和关注度都不错的开源项目的源码,边学习边记录。
关于Typecho
Typecho貌似基于magike修改演化而来(未考证)。项目维护有好几年了,所以必须考虑到历史问题,故在分析的过程中取其精华、对比学习即可。官网首页评价很不错哈哈。
仅仅7张数据表,加上不足 400KB 的代码,就实现了完整的插件与模板机制。 ... 原生支持 Markdown 排版语法,易读更易写。 ... 精心打磨过的操作界面,依然是你熟悉的面孔,更多了一份成熟与贴心。...Typecho 浑身都透着她简洁的性格,就像一个苗条的美女那样吸引着你。...
入口文件、配置文件分析
Typecho 入口文件清晰明了,就是根目录的 index.php
。入口文件首先载入配置文件,配置文件里面定义了一堆路径的常量,然后载入了很多东西,在 Typecho 里面以 Widget 表示,这里称为组件好了,好几个组件在 config.inc.php
里面先载入,以备后面使用(当然其实需要使用组件时也会自动载入,这里载入多了有点冗余,提了PR),思路可以说很常规。这些载入这么放着很容易让人先进去挨个分析,我刚开始看也是这么干的,回头再看感觉刚开始分析不宜挨个去分析,应当先把握大体流程。配置文件里面的数据库的设置就不说了,关注以下两个地方即可:
/** 设置包含路径 */
@set_include_path(get_include_path() . PATH_SEPARATOR .
__TYPECHO_ROOT_DIR__ . '/var' . PATH_SEPARATOR .
__TYPECHO_ROOT_DIR__ . __TYPECHO_PLUGIN_DIR__);
⇧这里设置了 include_path
,作用是之后如果代码里面进行文件包含操作例如 include('example.php') 的时候, php 解释器会先按参数给出的路径寻找,如果没有给出目录(只有文件名)时则按照 include_path
也就是设置的目录(可以为多个)去寻找。如果在 include_path
下还没找到该文件则 include 最后才在调用 php 脚本文件所在的目录和当前工作目录下面去寻找。而显然,这里设置了多个 include_path
,以 PATH_SEPARATOR 分割(linux 下就是 :
),而这里的多个目录就是 Typecho 目录的 var
和 usr/plugins
,插件目录先不关注,var 目录构成按照官方说法:
在Typecho中,所有的程序文件以包(package)形式组织。我们亦集成了一些优秀的第三方开源包,向Typecho贡献包文件是最常见和容易的实现方式。我们对包文件的集成方式采用如下两种方案:
1.对于可以直接独立使用的包,我们会直接放在/var目录下,供程序直接使用。比如IXR包。
2.对于需要整合才能使用的包,我们会集成到/var/Typecho目录下,作为Typecho包的一个子集。比如Feed和I18n包,它们都是被我们扩展或修改后使用的。
不论是否会被我们修改,我们都将遵循第三方包的发行协议,并保留作者版权(如果有的话),我们将在文件头部声明出处。并尽可能得保留程序注释与代码风格。对于经过修改的包,我们会标注我们修改的地方。
配置文件中需要关注的第二个点代码如下:
/** 程序初始化 */
Typecho_Common::init();
⇧这里执行了 Typecho_Common 组件的 init 操作,组件名和操作名一看,显然是执行了一些对整个程序跑起来所必须的初始化操作,跟进去一看便知,初始化操作代码如下:
/**
* 程序初始化方法
*
* @access public
* @return void
*/
public static function init()
{
/** 设置自动载入函数 */
spl_autoload_register(array('Typecho_Common', '__autoLoad'));
/** 兼容php6 */
if (function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc()) {
$_GET = self::stripslashesDeep($_GET);
$_POST = self::stripslashesDeep($_POST);
$_COOKIE = self::stripslashesDeep($_COOKIE);
reset($_GET);
reset($_POST);
reset($_COOKIE);
}
/** 设置异常截获函数 */
set_exception_handler(array('Typecho_Common', 'exceptionHandle'));
}
可见,在 init 操作中注册了全局自动载入函数,然后对 $_GET、$_POST、$_COOKIE
三个超全局数组进行了去转义化操作,注释写的是兼容 php6 ,然而... php 并没有版本6,这里可以认为是兼容自动开启 HTTP 转义的环境。最后设置了全局 Exception Handler
,在没有 try ... catch ...
并且没有忽略异常的上下文里产生的任何 Exception 都将由其捕获并进行处理,在 php7 中,将捕获实现了 Throwable Interface
的异常,也就是 Error、Exception
都会由其处理。具体的自动载入过程如下:
public static function __autoLoad($className)
{
@include_once str_replace(array('\\', '_'), '/', $className) . '.php';
}
那么结合之前设置的包含路径,Typecho_Common
的路径实际上就是 var/Typecho/Common.php
,而若在 var
目录下找不到则去 usr/plugins
目录下去尝试加载插件。
整体流程
看到这里暂且打住,现在知道自动载入有了,基本的错误处理有了,数据库连接配置完毕,剩下的就是载入需要的组件,接受 HTTP
信息,进行处理并返回结果了。继续看 index.php 的内容:
/** 初始化组件 */
Typecho_Widget::widget('Widget_Init');
/** 注册一个初始化插件 */
Typecho_Plugin::factory('index.php')->begin();
/** 开始路由分发 */
Typecho_Router::dispatch();
/** 注册一个结束插件 */
Typecho_Plugin::factory('index.php')->end();
可见这里调用了 Typecho_Widget 组件的 widget 静态方法,套用一下所谓设计模式的说法,这是一个工厂函数,实例化了 Widget_Init
类并进行其他初始化操作,然后是一个插件挂载点 index.php:begain
这里先不用在意,然后进行路由分发,最后是整体结束的插件挂载点 index.phhp:end
。那么很显然,先关注初始化操作和路由分发即可。跟进去工厂函数可见其代码如下:
/**
* 工厂方法,将类静态化放置到列表中
*
* @access public
* @param string $alias 组件别名
* @param mixed $params 传递的参数
* @param mixed $request 前端参数
* @param boolean $enableResponse 是否允许http回执
* @return Typecho_Widget
* @throws Typecho_Exception
*/
public static function widget($alias, $params = NULL, $request = NULL, $enableResponse = true)
{
$parts = explode('@', $alias);
$className = $parts[0];
$alias = empty($parts[1]) ? $className : $parts[1];
if (isset(self::$_widgetAlias[$className])) {
$className = self::$_widgetAlias[$className];
}
if (!isset(self::$_widgetPool[$alias])) {
/** 如果类不存在 */
if (!class_exists($className)) {
throw new Typecho_Widget_Exception($className);
}
/** 初始化request */
if (!empty($request)) {
$requestObject = new Typecho_Request();
$requestObject->setParams($request);
} else {
$requestObject = Typecho_Request::getInstance();
}
/** 初始化response */
$responseObject = $enableResponse ? Typecho_Response::getInstance()
: Typecho_Widget_Helper_Empty::getInstance();
/** 初始化组件 */
$widget = new $className($requestObject, $responseObject, $params);
$widget->execute();
self::$_widgetPool[$alias] = $widget;
}
return self::$_widgetPool[$alias];
}
可见其专门用于实例化各组件类,并且有个组件池做的单例模式。需要注意 1、每个类实例化的时候都会送入 Typecho_Request
和 Typecho_Response
的单例形成数据流,每个组件都可以按需获取需要的请求信息及输出信息; 2、每个类实例化之后都会调用其 execute()
方法。而此时则是执行 Widget_Init 的 execute 方法,其主要初始化了设置信息,Cookie封装,语言、字符集,时区设置,session管理等,这里先不用关注。下一步是路由分发,需要先看数据库里面的配置信息有个印象(已经过处理方便阅读):
[index] => Array
(
[url] => /
[widget] => Widget_Archive
[action] => render
[regx] => |^[/]?$|
[format] => /
[params] => Array
(
)
)
[archive] => 。。。。。。
。。。。。。
[post] => Array
(
[url] => /archives/[cid:digital]/
[widget] => Widget_Archive
[action] => render
[regx] => |^/archives/([0-9]+)[/]?$|
[format] => /archives/%s/
[params] => Array
(
[0] => cid
)
)
。。。。。。
而跟进去路由的 dispatch 操作:
/**
* 路由分发函数
*
* @return void
* @throws Exception
*/
public static function dispatch()
{
/** 获取PATHINFO */
$pathInfo = self::getPathInfo();
foreach (self::$_routingTable as $key => $route) {
if (preg_match($route['regx'], $pathInfo, $matches)) {
self::$current = $key;
try {
/** 载入参数 */
$params = NULL;
if (!empty($route['params'])) {
unset($matches[0]);
$params = array_combine($route['params'], $matches);
}
$widget = Typecho_Widget::widget($route['widget'], NULL, $params);
if (isset($route['action'])) {
$widget->{$route['action']}();
}
Typecho_Response::callback();
return;
} catch (Exception $e) {
if (404 == $e->getCode()) {
Typecho_Widget::destory($route['widget']);
continue;
}
throw $e;
}
}
}
/** 载入路由异常支持 */
throw new Typecho_Router_Exception("Path '{$pathInfo}' not found", 404);
}
其实就是依次根据路由配置信息里面的 regx 正则信息去匹配当前的 url_pathinfo ,一旦匹配上则实例化对应的组件,并调用其与路由配置信息的 action 项所对应的方法。而有些 url 里面带有参数信息,如上面的 post 项,则通过正则的捕获获取,参数名是由路由表配置的 param 项确定,最终通过 $params = array_combine($route['params'], $matches);
构造并作为信息流的一部分传递给当前 url 对应的组件。接下来以 index 项为例,通过参考上述路由信息的 action 项可知接下来会进入其 render 方法,代码如下:
/**
* 输出视图
*
* @access public
* @return void
*/
public function render()
{
/** 处理静态链接跳转 */
$this->checkPermalink();
/** 添加Pingback */
if (2 == $this->options->allowXmlRpc) {
$this->response->setHeader('X-Pingback', $this->options->xmlRpcUrl);
}
$validated = false;
//~ 自定义模板
if (!empty($this->_themeFile)) {
if (file_exists($this->_themeDir . $this->_themeFile)) {
$validated = true;
}
}
if (!$validated && !empty($this->_archiveType)) {
//~ 首先找具体路径, 比如 category/default.php
if (!$validated && !empty($this->_archiveSlug)) {
$themeFile = $this->_archiveType . '/' . $this->_archiveSlug . '.php';
if (file_exists($this->_themeDir . $themeFile)) {
$this->_themeFile = $themeFile;
$validated = true;
}
}
//~ 然后找归档类型路径, 比如 category.php
if (!$validated) {
$themeFile = $this->_archiveType . '.php';
if (file_exists($this->_themeDir . $themeFile)) {
$this->_themeFile = $themeFile;
$validated = true;
}
}
//针对attachment的hook
if (!$validated && 'attachment' == $this->_archiveType) {
if (file_exists($this->_themeDir . 'page.php')) {
$this->_themeFile = 'page.php';
$validated = true;
} else if (file_exists($this->_themeDir . 'post.php')) {
$this->_themeFile = 'post.php';
$validated = true;
}
}
//~ 最后找归档路径, 比如 archive.php 或者 single.php
if (!$validated && 'index' != $this->_archiveType && 'front' != $this->_archiveType) {
$themeFile = $this->_archiveSingle ? 'single.php' : 'archive.php';
if (file_exists($this->_themeDir . $themeFile)) {
$this->_themeFile = $themeFile;
$validated = true;
}
}
if (!$validated) {
$themeFile = 'index.php';
if (file_exists($this->_themeDir . $themeFile)) {
$this->_themeFile = $themeFile;
$validated = true;
}
}
}
/** 文件不存在 */
if (!$validated) {
Typecho_Common::error(500);
}
/** 挂接插件 */
$this->pluginHandle()->beforeRender($this);
/** 输出模板 */
require_once $this->_themeDir . $this->_themeFile;
/** 挂接插件 */
$this->pluginHandle()->afterRender($this);
}
到这里就是取用需要的信息,确定需要包含的主题文件,并且 include 进来。那么很显然,主题里面经常使用的 $this 实际上就是 Widget_Archive
的实例。例如官网文档提到的神奇的 is 语法 $this->is('single')
,实际上对应于 Widget_Archive 的 is 方法,如下:
/**
* 判断归档类型和名称
*
* @access public
* @param string $archiveType 归档类型
* @param string $archiveSlug 归档名称
* @return boolean
*/
public function is($archiveType, $archiveSlug = NULL)
{
return ($archiveType == $this->_archiveType ||
(($this->_archiveSingle ? 'single' : 'archive') == $archiveType && 'index' != $this->_archiveType) ||
('index' == $archiveType && $this->_makeSinglePageAsFrontPage))
&& (empty($archiveSlug) ? true : $archiveSlug == $this->_archiveSlug);
}
总结
分析下来感觉 Typecho 的整体设计还是很小巧的。调用时自动载入需要的类,工厂方式提供需要的组件并且形成信息流,通过配置的路由信息结合当前 URL 调用对应的组件与方法,在方法中加载主题文件顺便暴露有用的对象信息给主题,也让主题有较高的可定制性。整个流程简单来看就是这样,当然还有很多细节未提到,后面再记录分享。
分类: 编程
标签: Typecho 源码分析
看来你的typecho的源码分析,请问typecho是怎么做到伪静态的,https://segmentfault.com/q/1010000000580190/a-1020000000580959这有一篇wordpress的分析,但是typecho环境下,nginx并没有类似的配置
在 Typecho 后台设置里面,“永久链接”设置作对应配置就行。在 nginx 的 rewrite 配置没有问题的情况下,配置“永久链接”后面保留有 .html 后缀,应该就实现这里所指的伪静态了。我的博客的关于页面应该就是你想要的效果。nginx 不需要有别的特别的配置。
我一开始也是想着研究代码,但是一直没有研究,思路也是一行一行的看,看着你说的之后,有了新的选择,那就是看你的博客就可以了
哈哈,老哥过奖了,希望能有所帮助节省分析时间