Discuz! Q 源码浅析

前言

简单分析了一下Discuz! Q 的源码,纯属个人学习,不对的地方望指正哈。

用到的laravel组件

在vendor/discuz/core/composer.json中可以看到

  • “illuminate/container”: ioc服务容器
  • “illuminate/database”: 数据库
  • “illuminate/bus”: 事件总线
  • “illuminate/events”: 事件
  • “illuminate/config”: 配置服务
  • “illuminate/view”: 视图
  • “illuminate/session”: session
  • 路由没有用laravel,而是”nikic/fast-route”
  • 中间件没有用laravel,而是”laminas/laminas-stratigility”,遵循 PSR-7 HTTP 规范
  • 处理请求用的”laminas/laminas-httphandlerrunner”,遵循 PSR-15 HTTP 处理器规范

入口文件分析

在public/index.php中

  1. $app = new Discuz\Foundation\Application(dirname(__DIR__)); //实例化一个容器,并绑定了事件服务和别名绑定
  2. $app->make(Discuz\Http\Server::class)->listen(); //实例化Server,并处理请求(重心在这里)

Discuz\Http\Server::class 分析

public function listen()
{
    // 这里绑定了核心的服务类,包括ApiServiceProvider、WebServiceProvider,这两个Provider会加载routes/下的路由配置和注册中间件
    $this->siteBoot();

    $pipe = new MiddlewarePipe();
    //new RequestHandler里用到了ApiServiceProvider、WebServiceProvider注册的中间件
    $pipe->pipe(new RequestHandler([
        '/api' => 'discuz.api.middleware',
        '/' => 'discuz.web.middleware'
    ], $this->app));

    $psr17Factory = new Psr17Factory();
    $request = (new ServerRequestCreator($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory))->fromGlobals();

    $this->app->instance('request', $request);
    $this->app->alias('request', ServerRequestInterface::class);

    // 上面的实例化的中间件对象和Request对象都注入到RequestHandlerRunner
    $runner = new RequestHandlerRunner(
        $pipe,
        new SapiEmitter,
        function () use ($request) {
            return $request;
        },
        function (Throwable $e) {
            $generator = new ErrorResponseGenerator;
            return $generator($e, new ServerRequest, new Response);
        }
    );

    // run方法有两个重要步骤:1. 处理请求$this->handler->handle($request);2. 收尾工作$this->emitter->emit($response);
    $runner->run();
}

$this->handler->handle($request) 在 Laminas\Stratigility\MiddlewarePipe

  • 主要是中间件的处理流程,利用SplQueue的先进先出队列来实现,在ApiServiceProvider、WebServiceProvider中注册了中间件,enqueue进SplQueue队列,然后一个一个的dequeue出队列并处理请求。
  • 最后一个中间件是DispatchRoute::class,它则是用来根据请求的url找到对应的控制器开始业务处理。
  • PS:中间件的处理不再像laravel那样用到array_reduce的特性,我写了一个简单的demo模拟了下:
class Next
{
    private $fallbackHandler;
    private $queue;

    public function __construct($queue, $fallbackHandler)
    {
        $this->queue = clone $queue;
        $this->fallbackHandler = $fallbackHandler;
    }

    public function handle($request)
    {
        if ($this->queue->isEmpty()) {
            $this->queue = null;
            $fallbackHandler = $this->fallbackHandler;
            return $fallbackHandler($request);
        }
        $middleware = $this->queue->dequeue();
        $next = clone $this;
        $this->queue = null;

        return $middleware($request, $next);
    }
}

class SplQueuePipeline
{
    private $pipeline;

    public function __construct()
    {
        $this->pipeline = new SplQueue();
    }

    public function __clone()
    {
        $this->pipeline = clone $this->pipeline;
    }

    public function handle($request)
    {
        return $this->process($request, function ($r) {
            return $r;
        });
    }

    public function process($request, $handler)
    {
        return (new Next($this->pipeline, $handler))->handle($request);
    }

    public function pipe($middleware)
    {
        $this->pipeline->enqueue($middleware);
    }
}

$pipe = new SplQueuePipeline();
$pipe->pipe(function ($request, $next) {
    var_dump("pipe 1");
    return $next->handle($request);
});
$pipe->pipe(function ($request, $next) {
    var_dump("pipe 2");
    return $next->handle($request);
});
$rs = $pipe->handle("request");
var_dump($rs);

疑问:app\Api\Middleware目录下的中间件是如何生效的

  1. App\Listeners\AddApiMiddleware 将 app\Api\Middleware 目录的中间件进行注册。
  2. App\Providers\EventServiceProvider 注册了 Discuz\Api\Events\ConfigMiddleware 事件的监听,listener 就是 App\Listeners\AddApiMiddleware。
  3. App\Providers\EventServiceProvider 定义在配置文件的 providers 数组内。而这些provider 的注册在上面提到的$this->siteBoot();中完成。
  4. 那 ConfigMiddleware 事件又是怎么产生的呢?在上面提到的 ApiServiceProvider 中,注册完系统中间件后会触发 ConfigMiddleware 事件。这个流程我模拟了一个简单的demo如下:
class EventDispatcher
{
    private $bindings = [];

    public function listen($eventClass, $listenerClass)
    {
        if (empty($this->bindings[$eventClass])) {
            $this->bindings[$eventClass] = [];
        }
        $this->bindings[$eventClass][] = $listenerClass;
    }

    public function dispatch($obj)
    {
        $eventClass = get_class($obj);
        if (!empty($this->bindings[$eventClass])) {
            foreach ($this->bindings[$eventClass] as $listenerClass) {
                $reflector = new ReflectionClass($listenerClass);
                $instance = $reflector->newInstance();
                $method = $reflector->getMethod('handle');
                $method->invoke($instance, $obj);
            }
        }
    }
}

class AEvent
{
    public $pipe;

    public function __construct($obj)
    {
        $this->pipe = $obj;
    }
}

class AListener
{
    public function handle(AEvent $obj)
    {
        $obj->pipe->arr[] = "pipe 2";
    }
}

$obj = new stdClass();
$obj->arr = ['pipe 1'];
$eventDispatcher = new EventDispatcher();
$eventDispatcher->listen(AEvent::class, AListener::class);
$eventDispatcher->dispatch(new AEvent($obj));
print_r($obj->arr);
本作品采用《CC 协议》,转载必须注明作者和本文链接
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
讨论数量: 15

Discuz Q 里用到了很多腾讯云的 api,基本都是收费的,可以理解为腾讯云深度定制。

没找到前端未打包的源码,不好二开

3年前 评论

@showcj 前端有开源的代码的

3年前 评论
showcj 3年前

还是没用伪静态呀

3年前 评论
dongzhiyu 3年前
Cooper
3年前 评论

@Cooper @showcj
那是旧版了,后台管理还在用,新的前台用的 uni-app
discuz.chat/pages/topic/index?id=4...
dl.discuz.chat/uniapp_latest.zip

3年前 评论

请问大佬,后台接口得路由时怎么配置得?

3年前 评论
lddtime 3年前
likui (作者) 3年前
lddtime 3年前

感谢大佬。刚入职就是要二开,看了你的文章帮助很大,再次感谢!!!

2年前 评论

现在还有哪些类似discuzQ这样的论坛门户比较好用来快速开发

3个月前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!