简单解读Xiuno BBS之流程
xiunoa 2020-7-12

XiunoBBS 由来

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

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

结构 XiunoBBS目录结构

v4.0


// 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 层)。

img
入口

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

<?php// 0: 线上模式; 1: 调试模式; 2: 插件开发模式;!defined('DEBUG') AND define('DEBUG', 0);define('APP_PATH', dirname(__FILE__).'/'); // __DIR__!defined('ADMIN_PATH') AND define('ADMIN_PATH', APP_PATH.'admin/');!defined('XIUNOPHP_PATH') AND define('XIUNOPHP_PATH', APP_PATH.'xiunophp/');$conf = (@include APP_PATH.'conf/conf.php') OR exit('<script>window.location="install/"</script>');// 兼容 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 方式的获取超级全局变量中的 keyfunction _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);        
        // 支持 <template> <slot>
        $g_include_slot_kv = array();        for($i = 0; $i < 10; $i++) {
            $s = preg_replace_callback('#<template\sinclude="(.*?)">(.*?)</template>#is', '_include_callback_1', $s);            if(strpos($s, '<template') === FALSE) break;
        }
        file_put_contents_try($tmpfile, $s);
        
        $s = plugin_compile_srcfile($tmpfile);
        file_put_contents_try($tmpfile, $s);
        
    }    return $tmpfile;
}// 编译源文件,把插件合并到该文件,不需要递归,执行的过程中 include _include() 自动会递归。function plugin_compile_srcfile($srcfile) {    global $conf;    // 判断是否开启插件
    if(!empty($conf['disabled_plugin'])) {
        $s = file_get_contents($srcfile);        return $s;
    }    
    // 如果有 overwrite,则用 overwrite 替换掉
    $srcfile = plugin_find_overwrite($srcfile);
    $s = file_get_contents($srcfile);    
    // 最多支持 10 层
    for($i = 0; $i < 10; $i++) {        if(strpos($s, '<!--{hook') !== FALSE || strpos($s, '// hook') !== FALSE) {
            $s = preg_replace('#<!--{hook\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) == '<?php' && substr($t, -2, 2) == '?>') {
                    $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中判别的
返回的plugin_paths

返回的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, '<?php');
            $t = rtrim($t, '?>');
            $s .= "<?php\r\n".$t."\r\n?>";

        }
        $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的一个了,之前那些都是初始化和引入文件。都是为了后面做准备,这个文件中应该会具体涉及到具体逻辑和路由转发功能了,否则怎么访问到页面对吧?

<?php!defined('DEBUG') AND exit('Access Denied.');// hook index_inc_start.php$sid = sess_start();// 语言 / Language$_SERVER['lang'] = $lang = include _include(APP_PATH."lang/$conf[lang]/bbs.php");// 用户组 / Group$grouplist = group_list_cache();// 支持 Token 接口(token 与 session 双重登陆机制,方便 REST 接口设计,也方便 $_SESSION 使用)// Support Token interface (token and session dual match, to facilitate the design of the REST interface, but also to facilitate the use of $_SESSION)$uid = intval(_SESSION('uid'));empty($uid) AND $uid = user_token_get() AND $_SESSION['uid'] = $uid;$user = user_read($uid);$gid = empty($user) ? 0 : intval($user['gid']); // 或者当前用户所在组id$group = isset($grouplist[$gid]) ? $grouplist[$gid] : $grouplist[0]; // 获取当前用户所在组// 版块 / Forum$fid = 0;$forumlist = forum_list_cache();$forumlist_show = forum_list_access_filter($forumlist, $gid);   // 有权限查看的板块 / filter no permission forum$forumarr = arrlist_key_values($forumlist_show, 'fid', 'name');// 头部 header.inc.htm $header = array(
    'title'=>$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 accesscheck_runlevel();// 全站的设置数据,站点名称,描述,关键词// $setting = kv_get('setting');$route = param(0, 'index');// hook index_inc_route_before.phpif(!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干了什么

<?php/*
* Copyright (C) 2015 xiuno.com
*/!defined('DEBUG') AND exit('Access Denied.');// hook index_start.php$page = param(1, 1);$order = $conf['order_default'];$order != 'tid' AND $order = 'lastpid';$pagesize = $conf['pagesize'];$active = 'default';// 从默认的地方读取主题列表$thread_list_from_default = 1;// hook index_thread_list_before.phpif($thread_list_from_default) {
    $fids = arrlist_values($forumlist_show, 'fid');
    $threads = arrlist_sum($forumlist_show, 'threads');
    $pagination = pagination(url("$route-{page}"), $threads, $page, $pagesize);
    
    // hook thread_find_by_fids_before.php
    $threadlist = thread_find_by_fids($fids, $page, $pagesize, $order, $threads);}// 查找置顶帖if($order == $conf['order_default'] && $page == 1) {
    $toplist3 = thread_top_find(0);
    $threadlist = $toplist3 + $threadlist;}// 过滤没有权限访问的主题 / filter no permission threadthread_list_access_filter($threadlist, $gid);// SEO$header['title'] = $conf['sitename'];               // site title$header['keywords'] = '';                   // site keyword$header['description'] = $conf['sitebrief'];            // site description$_SESSION['fid'] = 0;// hook index_end.phpinclude _include(APP_PATH.'view/htm/index.htm');?>

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

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

视图view

接着这个示例来看index.htm

<?php include _include(APP_PATH.'view/htm/header.inc.htm');?><!--{hook index_start.htm}--><div class="row">
    <div class="col-lg-9 main">
        <!--{hook index_main_start.htm}-->
        <div class="card card-threadlist">
            <div class="card-header">
                <ul class="nav nav-tabs card-header-tabs">
                    <li class="nav-item">
                        <a class="nav-link <?php echo $active == 'default' ? 'active' : '';?>" href="./<?php echo url("$route");?>"><?php echo lang('new_thread');?></a>
                    </li>
                    <!--{hook index_thread_list_nav_item_after.htm}-->
                </ul>
            </div>
            <div class="card-body">
                <ul class="list-unstyled threadlist mb-0">
                    <!--{hook index_threadlist_before.htm}-->
                    <?php include _include(APP_PATH.'view/htm/thread_list.inc.htm');?>
                    <!--{hook index_threadlist_after.htm}-->
                </ul>
            </div>
        </div>
        
        <?php include _include(APP_PATH.'view/htm/thread_list_mod.inc.htm');?>
        
        <!--{hook index_page_before.htm}-->
        <nav class="my-3"><ul class="pagination justify-content-center flex-wrap"><?php echo $pagination; ?></ul></nav>
        <!--{hook index_page_end.htm}-->
    </div>
    <div class="col-lg-3 d-none d-lg-block aside">
        <a role="button" class="btn btn-primary btn-block mb-3" href="<?php echo url('thread-create-'.$fid);?>"><?php echo lang('thread_create_new');?></a>
        <!--{hook index_site_brief_before.htm}-->
        <div class="card card-site-info">
            <!--{hook index_site_brief_start.htm}-->
            <div class="m-3">
                <h5 class="text-center"><?php echo $conf['sitename'];?></h5>
                <div class="small line-height-3"><?php echo $conf['sitebrief'];?></div>
            </div>
            <div class="card-footer p-2">
                <table class="w-100 small">
                    <tr align="center">
                        <td>
                            <span class="text-muted"><?php echo lang('threads');?></span><br>
                            <b><?php echo $runtime['threads'];?></b>
                        </td>
                        <td>
                            <span class="text-muted"><?php echo lang('posts');?></span><br>
                            <b><?php echo $runtime['posts'];?></b>
                        </td>
                        <td>
                            <span class="text-muted"><?php echo lang('users');?></span><br>
                            <b><?php echo $runtime['users'];?></b>
                        </td>
                        <?php if($runtime['onlines'] > 0) { ?>
                        <td>
                            <span class="text-muted"><?php echo lang('online');?></span><br>
                            <b><?php echo $runtime['onlines'];?></b>
                        </td>
                        <?php } ?>
                    </tr>
                </table>
            </div>
            <!--{hook index_site_brief_end.htm}-->
        </div>
        <!--{hook index_site_brief_after.htm}-->
    </div></div><!--{hook index_end.htm}--><?php include _include(APP_PATH.'view/htm/footer.inc.htm');?><script>$('li[data-active="fid-0"]').addClass('active');</script><!--{hook index_js.htm}-->

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

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

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

收获

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



作者:二歪求知iSk2y
链接:https://www.jianshu.com/p/b515e902e83f
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


最新回复 (2)
全部楼主
  • test
    2021-1-15 2
    0
    看不懂
  • xiaoheizi
    7月前 3
    0
    你就是我心中的那首忐忑,总是让我惊心动魄。 
返回