简单解读Xiuno BBS之流程

XiunoBBS

  • Xiuno BBS 4.0 开发手册:https://www.kancloud.cn/xiuno/bbs4
  • XiunoPHP 开发手册:http://www.kancloud.cn/xiuno/xiunophp
  • Xiuno BBS 4.0 二次开发文档:https://bbs.xiuno.com/thread-13108.htm

由来

最近因为特殊原因想建立一个社区(论坛)有某些作用,机缘巧合下找到xiuno bbs,看过上面的开发手册后,感觉很有利于二次开发,所以好久没看PHP的我重新捡起来预习下,记录下整个流程加载。

手册中很多内容写的很详细,不做重新复述。

结构

XiunoBBS目录结构

v4.0

admin/                             -- 后台管理目录
    route                          -- 后台路由
    view                           -- 后台模板
    index.php                  -- 后台入口
    index.inc.php             -- 入口包含的代码段
    admin.func.php         -- 后台依赖的函数
    menu.conf.php         -- 后台配置文件
conf/                              -- 全站配置文件
    conf.php                   -- 配置文件
    conf.default.php       -- 默认配置文件
    attach.conf.php        -- 附件配置文件
    smtp.conf.php          -- 发送邮件的配置文件
install/                            -- 安装目录
lang/                              -- 语言包
log/                                -- 日志目录,按照天存放日志
model/                           -- 数据处理的函数文件目录
plugin/                           -- 插件目录,一个插件一个目录
route/                             -- 路由目录,业务逻辑处理
tmp/                               -- 临时文件存放目录,插件和代码合并后的文件存放于此
upload/                          -- 上传目录
view/                              -- 前端模板目录
robots.txt                    -- 屏蔽蜘蛛的配置文件
.htaccess                      -- Apache URL-Rewrite 文件
index.inc.php               --  前端入口包含代码段
model.inc.php             -- Model 包含目录
index.php                    -- 前台程序入口

hook位置

// hook xiunophp_include_before.php     xiunophp包含前
// hook xiunophp_include_after.php      xiunophp包含完成后
// hook index_inc_start.php            index配置开始
// hook index_inc_end.php              index配置结束
// hook index_inc_route_before.php      路由开始前
……

hook的位置太多了。。

override位置

index.inc.php
view/htm/*.htm
route/*.php
model/*.php
admin/view/htm/*.htm
admin/route/*.php
admin/index.inc.php
admin/menu.conf.php
lang/*.php

这些都可以被override

加载

在 Xiuno BBS 4.0 当中,采用的单入口设计,全部从 index.php 进。
所有的 xxx-xxx.htm 都通过 Web Server 转发到了 index.php?route-action.htm。
由 route 目录下对应的 php 文件进行处理(Controller 层)。
model 则为数据处理目录(Model 层)。
view 为 js css font 等负责显示的文件 目录(View 层)。

简单解读Xiuno BBS之流程_第1张图片
img

入口

文档中所说是从index.php唯一入口进入,

window.location="install/"');

// 兼容 4.0.3 的配置文件   
!isset($conf['user_create_on']) AND $conf['user_create_on'] = 1;
!isset($conf['logo_mobile_url']) AND $conf['logo_mobile_url'] = 'view/img/logo.png';
!isset($conf['logo_pc_url']) AND $conf['logo_pc_url'] = 'view/img/logo.png';
!isset($conf['logo_water_url']) AND $conf['logo_water_url'] = 'view/img/water-small.png';
$conf['version'] = '4.0.4';     // 定义版本号!避免手工修改 conf/conf.php

// 转换为绝对路径,防止被包含时出错。
substr($conf['log_path'], 0, 2) == './' AND $conf['log_path'] = APP_PATH.$conf['log_path']; 
substr($conf['tmp_path'], 0, 2) == './' AND $conf['tmp_path'] = APP_PATH.$conf['tmp_path']; 
substr($conf['upload_path'], 0, 2) == './' AND $conf['upload_path'] = APP_PATH.$conf['upload_path']; 

$_SERVER['conf'] = $conf;

if(DEBUG > 1) {//根据debug模式 判断加载什么xiunophp。
    include XIUNOPHP_PATH.'xiunophp.php';
} else {
    include XIUNOPHP_PATH.'xiunophp.min.php';
}


// 加载插件
include APP_PATH.'model/plugin.func.php';
include _include(APP_PATH.'model.inc.php');
include _include(APP_PATH.'index.inc.php');

//file_put_contents((ini_get('xhprof.output_dir') ? : '/tmp') . '/' . uniqid() . '.xhprof.xhprof', serialize(xhprof_disable()));

?>
  1. 定义了APP_PATH,ADMIN_PATH,XIUNOPHP_PATH的目录
  2. 包含APP_PATH应用目录下的配置目录下的配置文件conf/conf.php,这里就代表引入了配置
  3. 将log、upload、tmp目录转换成绝对路径
  4. 把配置conf赋值给server数组
  5. 然后加载xiunophp,待会再看里面有哪些东西
  6. 包含plugin.func.php,加载插件某些函数
  7. 包含model.inc.php
  8. 包含index.inc.php

最后这几个包含都挺有内容的,挨个来查看

xiunophp

引入了缓存类、数据库支持类、还有一些自定义封装的函数,然后初始化某些内容,具体可以看代码中如何书写。

最后往server超全局变量中存储了这些内容

// 保存到超级全局变量,防止冲突被覆盖。
$_SERVER['starttime'] = $starttime;
$_SERVER['time'] = $time;
$_SERVER['ip'] = $ip;
$_SERVER['longip'] = $longip;
$_SERVER['useragent'] = $useragent;
$_SERVER['conf'] = $conf;
$_SERVER['lang'] = $lang;
$_SERVER['errno'] = $errno;
$_SERVER['errstr'] = $errstr;
$_SERVER['method'] = $method;
$_SERVER['ajax'] = $ajax;
$_SERVER['get_magic_quotes_gpc'] = $get_magic_quotes_gpc;
$_SERVER['db'] = $db;
$_SERVER['cache'] = $cache;

有些杂项misc函数下面可能经常用到这里稍微列举些

# xiunophp/misc.func.php

// 无 Notice 方式的获取超级全局变量中的 key
function _GET($k, $def = NULL) { return isset($_GET[$k]) ? $_GET[$k] : $def; }
function _POST($k, $def = NULL) { return isset($_POST[$k]) ? $_POST[$k] : $def; }
function _COOKIE($k, $def = NULL) { return isset($_COOKIE[$k]) ? $_COOKIE[$k] : $def; }
function _REQUEST($k, $def = NULL) { return isset($_REQUEST[$k]) ? $_REQUEST[$k] : $def; }
function _ENV($k, $def = NULL) { return isset($_ENV[$k]) ? $_ENV[$k] : $def; }
function _SERVER($k, $def = NULL) { return isset($_SERVER[$k]) ? $_SERVER[$k] : $def; }
function GLOBALS($k, $def = NULL) { return isset($GLOBALS[$k]) ? $GLOBALS[$k] : $def; }
function G($k, $def = NULL) { return isset($GLOBALS[$k]) ? $GLOBALS[$k] : $def; }
function _SESSION($k, $def = NULL) {
    global $g_session; 
    return isset($_SESSION[$k]) ? $_SESSION[$k] : (isset($g_session[$k]) ? $g_session[$k] : $def); 
}

plugin.func.php

plugin.func.php这个文件中定义了有关插件的各种函数,在这里不一一列举。是为后面加载插件做准备。

其中有部分函数经常会调用

function _include($srcfile) {
    global $conf;
    // 合并插件,存入 tmp_path
    $len = strlen(APP_PATH);
    $tmpfile = $conf['tmp_path'].substr(str_replace('/', '_', $srcfile), $len);
    if(!is_file($tmpfile) || DEBUG > 1) {
        // 开始编译
        $s = plugin_compile_srcfile($srcfile);
        
        // 支持 #is', '_include_callback_1', $s);
            if(strpos($s, '#', '// hook \\1', $s);
            $s = preg_replace_callback('#//\s*hook\s+(\S+)#is', 'plugin_compile_srcfile_callback', $s);
        } else {
            break;
        }
    }
    return $s;
}

// 只返回一个权重最高的文件名
function plugin_find_overwrite($srcfile) {
    //$plugin_paths = glob(APP_PATH.'plugin/*', GLOB_ONLYDIR);
    
    $plugin_paths = plugin_paths_enabled();
    
    $len = strlen(APP_PATH);
    /*
    // 如果发现插件目录,则尝试去掉插件目录前缀,避免新建的 overwrite 目录过深。
    if(strpos($srcfile, '/plugin/') !== FALSE) {
        preg_match('#'.preg_quote(APP_PATH).'plugin/\w+/#i', $srcfile, $m);
        if(!empty($m[0])) {
            $len = strlen($m[0]);
        }
    }*/
    
    $returnfile = $srcfile;
    $maxrank = 0;
    foreach($plugin_paths as $path=>$pconf) {
        
        // 文件路径后半部分
        $dir = file_name($path);
        $filepath_half = substr($srcfile, $len);
        $overwrite_file = APP_PATH."plugin/$dir/overwrite/$filepath_half";
        if(is_file($overwrite_file)) {
            $rank = isset($pconf['overwrites_rank'][$filepath_half]) ? $pconf['overwrites_rank'][$filepath_half] : 0;
            if($rank >= $maxrank) {
                $returnfile = $overwrite_file;
                $maxrank = $rank;
            }
        }
    }
    return $returnfile;
}

// 可用的插件 必须enable和installed 这个在conf.json中有配置
function plugin_paths_enabled() {
    static $return_paths;
    if(empty($return_paths)) {
        $return_paths = array();
        $plugin_paths = glob(APP_PATH.'plugin/*', GLOB_ONLYDIR);
        if(empty($plugin_paths)) return array();
        foreach($plugin_paths as $path) {
            $conffile = $path."/conf.json";
            if(!is_file($conffile)) continue;
            $pconf = xn_json_decode(file_get_contents($conffile));
            if(empty($pconf)) continue;
            if(empty($pconf['enable']) || empty($pconf['installed'])) continue;
            $return_paths[$path] = $pconf;
        }
    }
    return $return_paths;
}

function plugin_compile_srcfile_callback($m) {
    static $hooks;
    if(empty($hooks)) {
        $hooks = array();
        $plugin_paths = plugin_paths_enabled();
        
        //$plugin_paths = glob(APP_PATH.'plugin/*', GLOB_ONLYDIR);
        foreach($plugin_paths as $path=>$pconf) {
            $dir = file_name($path);
            $hookpaths = glob(APP_PATH."plugin/$dir/hook/*.*"); // path
            if(is_array($hookpaths)) {
                foreach($hookpaths as $hookpath) {
                    $hookname = file_name($hookpath);
                    $rank = isset($pconf['hooks_rank']["$hookname"]) ? $pconf['hooks_rank']["$hookname"] : 0;
                    $hooks[$hookname][] = array('hookpath'=>$hookpath, 'rank'=>$rank);
                }
            }
        }
        foreach ($hooks as $hookname=>$arrlist) {
            $arrlist = arrlist_multisort($arrlist, 'rank', FALSE);
            $hooks[$hookname] = arrlist_values($arrlist, 'hookpath');
        }
        
    }
    
    $s = '';
    $hookname = $m[1];
    if(!empty($hooks[$hookname])) {
        $fileext = file_ext($hookname);
        foreach($hooks[$hookname] as $path) {
            $t = file_get_contents($path);
            if($fileext == 'php' && preg_match('#^\s*<\?php\s+exit;#is', $t)) {
                // 正则表达式去除兼容性比较好。
                $t = preg_replace('#^\s*<\?php\s*exit;(.*?)(?:\?>)?\s*$#is', '\\1', $t);
                
                /* 去掉首尾标签
                if(substr($t, 0, 5) == '') {
                    $t = substr($t, 5, -2);     
                }
                // 去掉 exit;
                $t = preg_replace('#\s*exit;\s*#', "\r\n", $t);
                */
            }
            $s .= $t;
        }
    }
    return $s;
}
  • _include:将srcfile文件编译等操作后存入tmp_path中,
  • plugin_compile_srcfile:编译源文件,寻找overwrite和hook的位置并合成最终文件
  • plugin_find_overwrite:寻找overwrite,只返回一个权重最高的文件名
  • plugin_compile_srcfile_callback:hook是在这个回调函数中替换的
    • glob — 寻找与模式匹配的文件路径
    • array_multisort — 对多个数组或多维数组进行排序
    • array_values — 返回数组中所有的值
  • plugin_paths_enabled:可用的插件,是根据插件下的conf.json中判别的
简单解读Xiuno BBS之流程_第2张图片
返回的plugin_paths

简单解读Xiuno BBS之流程_第3张图片
返回的hooks数组

model.inc.php

model.inc.php这个文件中定义了待会要载入的model等许多

if(DEBUG) {
    foreach ($include_model_files as $model_files) {
        include _include($model_files);
    }
} else {
    
    $model_min_file = $conf['tmp_path'].'model.min.php';
    $isfile = is_file($model_min_file);
    if(!$isfile) {
        $s = '';
        foreach($include_model_files as $model_files) {
            
            // 压缩后不利于调试,有时候碰到未结束的 php 标签,会暴 500 错误
            //$s .= php_strip_whitespace(_include($model_files));

            $t = file_get_contents(_include($model_files));
            $t = trim($t);
            $t = ltrim($t, '');
            $s .= "";

        }
        $r = file_put_contents($model_min_file, $s);
        unset($s);
    }
    include $model_min_file;
}

根据debug来判断是否要用压缩版的model.min.php,一般线上模式都是用min版,响应更快

这边主要是遍历$include_model_files这个数组,将各个model去_include编译放置到临时目录下,然后trim下。然后把所有的都编译到model.min.php中

这里注意下,我开始一直有个疑问,那如果加入安装插件,按照这个代码逻辑如果临时目录下存在该已经编译过的文件了,那怎么插件会安装完就能用呢?

这个流程我反复看了好久,后来想到,肯定是安装的时候动手脚了把,肯定显式执行删除了。。果然翻看后看到了删除编译文件的操作

// 清空插件的临时目录
function plugin_clear_tmp_dir() {
    global $conf;
    rmdir_recusive($conf['tmp_path'], TRUE);
    xn_unlink($conf['tmp_path'].'model.min.php');
}

至于如果是自己开发插件,把debug改成2就可以了,这样每次都会自动去查找发现overwrite和hook。

index.inc.php

这个是index.php中最后include的一个了,之前那些都是初始化和引入文件。都是为了后面做准备,这个文件中应该会具体涉及到具体逻辑和路由转发功能了,否则怎么访问到页面对吧?

$conf['sitename'],
    'mobile_title'=>'',
    'mobile_link'=>'./',
    'keywords'=>'', // 搜索引擎自行分析 keywords, 自己指定没用 / Search engine automatic analysis of key words, so keep it empty.
    'description'=>strip_tags($conf['sitebrief']),
    'navs'=>array(),
);

// 运行时数据,存放于 cache_set() / runtime data
$runtime = runtime_init();

// 检测站点运行级别 / restricted access
check_runlevel();

// 全站的设置数据,站点名称,描述,关键词
// $setting = kv_get('setting');

$route = param(0, 'index');

// hook index_inc_route_before.php

if(!defined('SKIP_ROUTE')) {
    
    // 按照使用的频次排序,增加命中率,提高效率
    // According to the frequency of the use of sorting, increase the hit rate, improve efficiency
    switch ($route) {
        // hook index_route_case_start.php
        case 'index':   include _include(APP_PATH.'route/index.php');   break;
        case 'thread':  include _include(APP_PATH.'route/thread.php');  break;
        case 'forum':   include _include(APP_PATH.'route/forum.php');   break;
        case 'user':    include _include(APP_PATH.'route/user.php');    break;
        case 'my':  include _include(APP_PATH.'route/my.php');  break;
        case 'attach':  include _include(APP_PATH.'route/attach.php');  break;
        case 'post':    include _include(APP_PATH.'route/post.php');    break;
        case 'mod':     include _include(APP_PATH.'route/mod.php');     break;
        case 'browser': include _include(APP_PATH.'route/browser.php'); break;
        // hook index_route_case_end.php
        default: 
            // hook index_route_case_default.php
            include _include(APP_PATH.'route/index.php');   break;
            //http_404();
            /*
            !is_word($route) AND http_404();
            $routefile = _include(APP_PATH."route/$route.php");
            !is_file($routefile) AND http_404();
            include $routefile;
            */
    }
}

// hook index_inc_end.php

?>

很多操作代码中的注释写的很清楚了,大赞作者!!

路由、Controller 层

index.inc.php最后的就是路由控制了,默认是route/index.php了,$route是由param这个xiunophp中的封装函数解析后得来的。

然后要注意的是这里也是执行_include的,

include _include(APP_PATH.'route/index.php');   break;

会先判断是否在tmp目录下存在这个文件,如果不存在就重新编译一份。

在编译的时候会找到所有的overwrite和hook并按照rank排列好,overwrite是按照权重找最高的,hook按照rank,然后最终合成为一个文件放在tmp这个临时文件中去了,

然后我们回到正题,这个route的index.php干了什么


注释仍然写得很清楚,再大赞作者

最后包含了_include(APP_PATH.'view/htm/index.htm')这个,这里就到了视图最后显示给用户的阶段了

视图view

接着这个示例来看index.htm





在上个部分路由控制层最后包含这个文件又执行了_include这个函数,所以这其中的hook一样到最后都是会被插件中对应的hook内容增加上去。

而且这个文件中还包含了header.inc.htm和footer.inc.htm内容,所以_include会嵌套包含最终组合编译成一个文件放在tmp的临时目录中,等到下次访问就直接会访问那个目录下的文件,速度就会提高很多了!!

然后将内容返回给前端浏览器渲染给我们了。

收获

暂时先简单记录下这个流程,没有给出太多的细节,因为看代码更直观。

你可能感兴趣的:(简单解读Xiuno BBS之流程)