ThinkPHP源码解析之控制器

星图妙赏 2021-01-11 11:57:27

本文会对控制器的执行顺序还有实现过程、源码解析给出解析

前言

在上文中对路由进行了特别的详解,也从应用初始化开始解析一直到路由调度返回给路由检测这一环节。

路由检测获取到的值如下图,也就是路由调度最终返回的值。

使用的路由规则为Route::get('hello/:name', 'index/index/:name');

从上图可以看出重要数据都是在dispatc中存放的,接下来就会对控制器进行详解。

最先说明的就是的当路由检测完毕之后执行的实例化控制器操作。

一、实例化控制器

先来看一下是怎么执行到实例化控制器吧!

毫无疑问代码肯定是先从入口文件开始执行的,这里使用容器返回一个App的实例,然后去调用App类中的run方法。

下来就会来到执行应用程序,在这个方法中也是在上文刚刚解析的路由。

所以检测路由执行完就会去执行实例化控制器。

在路由检测执行完之后返回的是think\route\dispatch\Module Object这个类,并且这个类赋值给了变量$dispatch

接着看一下本方法的这块代码,这里使用的是中间件,在这快代码中还是用了闭包,对闭包的概念不清晰的就需要回头啃基础了。

在上图中咔咔圈出来的一个地方就是$dispatch->run()这块代码,接下来就要对这块代码进行解析了。

在检测路由最终的返回值可以知道其实这个方法是在think\route\dispatch\Module这个类中。

接着就需要对这个类中的run方法进行解析了,这个方法也就是执行路由调度。

在这个方法中不管是获取路由参数还是检测路由、数据自动验证都不会执行(是按照咔咔上文给的路由地址为案例)。

所以根据上图代码就会执行到$data = $this->exec();这里。

跟踪这个方法会到下图地方存在一个抽象类,这里需要知道的是抽象类。

对抽象类做出解释

抽象类不能被实例化

有抽象方法的类一定是抽象类;类必须要abstract修饰

抽象方法不能有函数体;即abstract function fun();

抽象类中的非抽象方法,可以被子类调用

非抽象子类继承抽象类,子类必须实现父类的所有抽象方法

抽象子类继承抽象类,无需继承父类的抽象方法

根据上图的原则可以看到Dispatch这个类是抽象类。

所以就会有俩种情况, 一种是抽象类继承抽象类,无需继承父类的抽象方法。

另一种是非抽象子类继承抽象类,子类必须实现父类的所有抽象方法。

怎么去找谁继承了Dispatch

这个时候是不是有一个疑问就是怎么去找Dispatch的子类。

在这个图中可以看到本类Dispatch,但是还有一个dispatch这个目录。

根据路由检测返回的数据可以轻而易举的就知道是thinkphp/library/think/route/dispatch/Module.php这个类。

来到thinkphp/library/think/route/dispatch/Module.php查看exec方法。

那么接下来的任务就是对这个方法进行深入的解读了。

先看第一行代码$this->app['hook']->listen('module_init');,在这里使用了容器ArrayAccess用数组的形式访问对象,然后执行的魔术方法__get,当访问不存在的属性时会去执行make方法。

使用编辑器追踪这个app会到thinkphp/library/think/route/Dispatch.php这个类里边,在这个类的构造函数中可以看到对于app这个属性是赋值了一个App实例。

接着来到App类可以看到继承的是Container类。

在容器这块已经不止一次的说过这块的知识点了,访问不存在的属性回去执行容器的__get魔术方法。

所以说这块的参数会传入hook,并且会返回hook的实例,关于这个实例是怎么返回的在容器那一节中说的很是详细,可以去看一下哈!

接下来就会去执行hook的listen方法,监听标签的行为。

此时可以来到应用行为扩展定义文件,可以看到这个参数为模块初始化,但是因为这个值是空的。

所以在上图不会去执行,那么就把应用初始化的值给放到这个参数里边进行简单的测试。

这个类就是执行的钩子,对门面类的优化操作。

那么代码就会执行到$results[$key] = $this->execTag($name, $tag, $params);这里来。

参数说明

$name = string(22) "behavior\LoadBehavior"

$tag = module_init

接着通过正则对传过来的参数进行处理,最终返回moduleInit

然后通过$obj = Container::get($class);返回behavior\LoadBehavior的实例

最终通过is_callable这个函数进行验证,检测类里边的方法是否可以被调用,方法数组格式,这个方法后期咔咔单独写一篇文章作为对象来解析,这里只需要知道会返回false即可。

然后会把本类的$portal这个值赋值给$method,这个值就是run。

最后通过$result = $this->app->invoke($call, [$params]);这行代码,这行代码的底部执行就是通过反射机制实现的。

最后这段代码会返回NULL。

实例化控制器

接下来就是进行实例化控制器,调用的方法是$this->app->controller()

这里需要注意的是list这个函数,这个函数的后边会返回一个数组,然后list中的俩个变量会分别为索引0和1。

判断也会去执行第一个,同样会执行到容器类的make方法,这个方法会直接返回app\index\controller\Index这个类的实例。

二、关于ArrayAccess和直接执行魔术访问返回实例的区别

有一部分小伙伴都已经学会了ArrayAccess和魔术方法__get的使用了。

估计也有一部分在这俩个地方处于模糊地段,咔咔将这俩个放在一起在解析一次。

先聊ArrayAccess的使用

这个案例在之前也给大家演示过,主要就是实现ArrayAccess的这个类。

然后在来到控制器进行使用,先进行实例化,之前实现的案例如下。

但是这次需要实现的案例并不是下图所实现的。

接下来使用下图的方式进行访问,直接使用数组访问对象属性。

在上图中可以看到设置了一个属性title为kaka,在这个案例中直接用数组形式直接获取。

看到返回结果为kaka,也就是说直接使用数组形式访问对象的属性。

框架实战案例

在上一期文章中解析的路由中存在以下代码,接下来进行简单的解析一下。

先来看一下这个app的值打印出来就是think\App Object对象。

当think\App Object这个对象去访问request时,因为app属性就没有这个request,又因为app类是继承着container类,所以会去容器类执行下图方法。

然后就会去执行__get方法,执行make方法返回对应的实例。

此时你要是还有疑问就是,怎么就咔咔说会执行就会执行呢!

接下来咔咔带着大家做一个简单的测试就知道了。

在这个位置中随机打印一个数值。

然后来到容器类的ArrayAccess的offsetGet方法中打印一下传过来的值。

看一下打印结果,就很明确了。

关于ArrayAccess的使用就到这里就结束了,这也是在之前的基础上详细的进行了一次说明,接下来对容器中的get方法进行详解,看在什么情况会执行get方法。

**__get方法使用详解**

这个案例请看下图中的这个$this->hook。

同样的道理先来调试一下这个$this是什么值。

打印这个值都没什么必要,因为就是在本类中。

在类中属性的访问应该都会,就是直接使用$this-> 即可。

所以说当系统访问$this->hook这个的时候,由于App类是不存在hook这个属性的,所以就会去执行容器类的魔术方法。

然后在去执行make方法,创建类的实例。

三、执行控制器中的方法

本文的请求地址为配置的域名。

通过上文可以知道$instance的值就是app\index\controller\Index的实例。

这块也是存在中间件的概念,依然如此中间件会在后文中单独提到,这里不做解释。

在这里$this->app['middleware']->controller这段代码的使用,还能记得是使用的ArrayAccess还是直接为__get吗?

这里是在使用访问数组的形式访问对象,所以使用的是ArrayAccess的形式,这俩种概念一定要区分清楚。

接下来就会执行获取方法名,至于这个方法名怎么获取的是在本类的init方法执行的,这里只需要知道返回的是index即可。

在这里需要注意的就是这行代码$this->rule->getConfig('action_suffix'),这里获取的是操作方法后缀。

假如现在给这个操作方法后缀设置一个值会变成什么样子呢!

给添加一个kaka的值,进行访问一下看会是什么结果。

这个时候进行访问会提示indexkaka的这个方法不存在,是不是清晰可见了,说明这个参数是在为所有的方法名追加一个kaka。

对获取当前操作名的代码进行扩展完成之后,紧接着就是if (is_callable([$instance, $action])) {,在这里可以看见我们的老朋友is_callable。

对于这里的is_callable俩个参数通过上文都知道是什么了,第一个参数为app\index\controller\Index的实例,第二个参数为index执行操作方法。

那么is_callable的作用就是检测在app\index\controller\Index类中的index方法名是否可以执行。

很明显这里会返回一个true,因为在index类里边存在index方法的。

这里在做测试之前一定要把刚刚在app的配置文件中配置的方法名后缀那个给取消掉。

通过这个is_callable判断会存在三种情况,接下来咔咔将会从三个方面给大家进行解析。

第一种情况:类里边存在可执行的方法

首先返回一个 ReflectionMethod 类

获取方法名 index

未设置方法名后缀返回空

设置当前的操作名

在这个案例中没有设置参数,所以最终的$vars就是一个空数组。

为了测试带有参数的这一段代码,我们需要对路由地址进行一点改动。

在之前没有使用路由,而是直接使用的默认地址,接下来将使用这个路由地址

使用这个路由地址进行一下数据的打印,可以看到就是我们设置的路由参数。

这段获取请求变量的方法会进入到$this->request->param();这行代码

框架是如何获取参数的

访问地址:http://www.source.com/index.php/hello/hello

在上文知道是通过$this->request->param()来获取参数的,那么在框架是如何获取参数的呢!

根据流程代码会执行到下图,根据获取的请求方式来使用对应的方式来获取参数,在这里需要明确的是我们使用的是get请求。

所以代码会执行到$this->param,当前请求参数和URL地址中的参数合并这里,在这里注意咔咔圈出来的地方。

由于咔咔是使用路由方式进行的请求所以,在这里框架专门为路由封装了一个获取请求参数。

来到这个route方法,看到注释就明白是用来获取路由参数的,但是还是需要在进入一层到input。

在之前路由的那一期文章中在获取到路由参数的时候会把参数合并到request的route属性。

所以说$this->route就是存放的这条路由规则所有的参数,包含路由参数。

这时执行流程会执行到获取变量 支持过滤和默认值,在上文中$this->route穿进来的参数是false,所以说这块会直接返回。

这里返回的结果会返回给上文我们开始解析的地方,也就是说这个$vars就是获取到的路由参数。

第二种情况:类里边不存在可执行的方法

当第一种判断执行is_callable判断类里边的方法不可执行时,就会执行到第二种情况。

先来请求的一个没有设置的路由地址,看会返回什么。

根据代码给的提示,我们来到index控制器建立一个_empty方法,然后在来请求一次,看一下会发生什么。

根据打印结果就可以看出来当访问的方法不存在时就会去执行_empty这个方法。

那么这个方法是怎么执行的呢!这种执行方式就是利用反射机制来实现的,关于反射咔咔之前专门出了一篇文章来讲解的,但是大家还是需要对着文档进行阅读查看。

第三种情况:类里边不存在可执行的方法也不存在_empty方法

这种情况就比较简单了,就是直接返回错误信息,关于这里异常处理咔咔也会在后文说到。

三种情况执行完

三种情况分析完了,最后都会去执行统计的方法。

调用反射执行类的方法 支持参数绑定,也就说这里的闭包执行流程到这里就执行完了。

关于后边的自动请求,在第五节中详细说明。

四、路由地址是怎么进行控制器实例化的

在上一节中我们对路由进行了三四期的讲解,最终讲解的位置就路由调度,那么设置的路由是如何执行呢!

接下来使用这个路由作为案例

还记得在开始进行路由检测时的返回值是什么吗?请看下图

当时没有对接下里的代码进行详解,直接说明了实例化控制器,现在要说的就是记录当前调度信息这行代码。

在这里$this->request是使用的当访问不存在的属性时会去执行容器类的魔术方法,最后通过容器返回一个实例。

所以说代码会执行到下图位置,设置或者获取当前请求的调度信息

通过在控制器实例化这里进行打印会发现在这里的返回的值是index,这个值是在控制器进行设置的,接下来来到控制器进行查看一下。

来到init方法对result做打印查看结果,使用的是路由地址

你知道到为什么这里的值发生改变了吗?

在上文打印出来的值为下图,为什么在这里就是上图的呢!

在路由那一节中最后一步就是发起路由调度,最后调用了一个路由到模块/控制器/操作这个方法。

这个方法dispatchModule最后也是实例化一个类,接下来需要对这个类进行深究

根据代码追踪可以看到其实就是think\route\dispatch\Module这个类

来到Module这个类,又会发现继承着Dispatch类

在thinkphp/library/think/route/Dispatch.php这个类的控制器中,会发现对dispatch这个变量进行了设置。

这个时候回头在看一下路由到模块/控制器/操作这里的方法传入的参数是什么,哈哈

所以说最终的值就是刚刚打印的只是单独的数组形式的。

那么接下来的动作就跟不使用路由访问的流程一样的,就不用在进行解析了。

直到这里关于路由地址是怎么进行控制器实例化的就结束了。

关于给$this->app->controller传入的是index,返回的是整个类名,具体的实现过程就不去解析了,实现的方法是$this->parseModuleAndClass,可以自行进行研究哈!

五、执行autoResponse调度

在第四节中只提到了执行控制器中的方法是从下图的地方进行返回的,但是怎么返回的没有进行详解。

接下里会用一丢丢的时间来说一下是如何执行的。

访问路由地址为下图,可以看到返回的数据就是控制器中需要返回的数据。

打印的值是下图地方,这里就需要明确哈!源码阅读就是这样需要一点点的进行摸索,时间长了就对其中的东西就明白了。

接下来就对$this->autoResponse($data);这个方法进行深入的解析,这个方法按照字面意思就是自动响应。

在这个执行流程的第一行中$data instanceof Response,对这个不了解接下来就没办法阅读了。

不会和不明白的还是需要去解决的,阅读源码就这样,一点点的攻克才能获得胜利。

关于instanceof的使用

instanceof可以判断某个对象是否是某个类的实例,判断一个对象是否实现了某个接口。

接下来咔咔针对这个做一个简单的实例给大家演示一下,就明白这个是怎么回事了。

案例一

首先建立俩个类,案例如下图。

下图就是打印结果,可以看到第一个返回true,第二个返回false。

判断某个对象是否是某个类的实例,也就是说$instance就是类Test的实例,所以会返回true。

案例二

案例二跟案例一是不同的,建立了一个接口,然后类实现接口的案例。

最终返回结果全是true,也就是说如果一个类实现了另一个接口,那么在判断时都会是true。

以上就是针对instanceof给出的俩种演示案例,对其理解就是判断一个实例是否为某个类的实例。

那么就在回到正文,$data instanceof Response这行代码肯定不会成立,因为data传过来的就是控制器返回的值。

所以说代码执行流程会执行到下图位置,使用了is_null函数来做判断,判断肯定为false,所以会执行以下的代码。

在这块代码中前俩个点就不去解析了。

第一个就是默认自动识别响应输出类型,这里就是在判断是否为ajax请求,具体实现方法等咔咔这次把框架源码解析完之后,然后每天会抽一点时间,对框架的一些方法进行一点点的剖析。

第二处位置就是在配置文件获取对应的配置信息,看的是执行的rule类的方法,但是在方法中是执行的获取配置信息的代码。

接下就需要对上文没提到的第三处进行解析了,也就是代码$response = Response::create($data, $type);

来到类thinkphp/library/think/Response.php的方法create中,这个方法就是用来创建Response对象。

这里只需要去关注一下咔咔圈出来的地方即可,在thinkphp/library/think/response这个目录下是不存在html的。

所以代码会直接去实例化本类,然后进行返回。

来到本类的构造函数就主要做一下几件事情。

将返回值赋给本类的data属性

设置页面输出类型

返回状态码

设置app实例对象

头部信息

然后代码会将返回值赋值给autoResponse这个方法的$response这个变量。

最后就是将这个$response给返回出去,并且返回信息如下图打印结果。

然后代码依然会向上层返回,回到最初的闭包函数。

在咔咔圈出来的地方,下一行代码也是关于中间件,只需要知道最终返回结果跟上图打印的结果一样即可。

最终返回结果回到thinkphp/library/think/route/Dispatch.php,咱们也就是从这里开始的解析的。

将返回的结果返回给$data,然后在进行执行return $this->autoResponse($data);

你没看错,这里的代码熟悉吧!

这个时候返回的结果就是Response的实例,所以会直接返回$response。

直到这里关于执行控制器中的方法,并且响应就都解析完了。

不轮是设置的路由规则,还是直接使用模块控制器方法的方式访问最终都会通过上文的方式进行返回响应结果。

六、如何输出数据到终端

当执行完控制器中的方法响应数据给App类的run方法,直到这里就已经执行完了。

是不是有点懵这里的数据最终会返回哪里呢!

之前写过的框架执行流程、路由、控制器实例化都是从这里开始进入的。

所以当run方法执行完成之后,就会把对应的结果给返回到这里。

这一部分的代码Container::get('app')应该都知道了是返回一个App类的实例。

然后通过App类去执行run方法,才会有之前讲过的一切。

下图是咔咔从半中腰做的一个思维导图,前面的没有,后边的所有知识点都会写在这个思维导图里。

执行完run方法就会去执行Container::get('app')->run()->send()send这个方法,有多少人会认为在App类里边执行send方法。

其实不是的,回想一下之前执行控制器方法然后返回的响应结果是什么?

如果你不是很粗略的看都会记得是Response的一个对象实例。

所以说send方法会去response类里边去执行。

先不看其它的,先看这行代码$this->app['hook'],现在知道是执行的那里吗?

这种形式就是通过访问数组形式去访问对象的属性,也就是之前解析的ArrayAccess这个类。当访问的属性不存在时会去执行offsetGet,然后执行魔术方法__get,最终通过make方法返回实例,这一切的操作都是在容器中。

对这行代码具体是监听的什么就不去做解析了。

接着需要看处理输出数据的这行代码$data = $this->getContent();

这个方法做的事情就是将传过来的数据赋值给本类的content属性。

其实在获取输出数据这个方法中,请看咔咔圈出来的第一个地方感觉是很没有必要。

可以看到根本对数据就没有任何的处理,只是简单的返回了,所以说框架有好的地方也有不好的地方,只有你去阅读了才会知道,否则你会对你经常使用的工具一无所知。

在接着就是Trace调试注入,就是通过配置文件配置的,通过调用debug类实现的,这里就不详解了。

然后就是缓存判断,缓存会在后文中单独拎出来讲,所以也是过。

在接下来就对响应头的设置了,检测 HTTP 头是否已经发送,这块的东西就很重要了,也是平时接触不多的知识点了。

headers_sent() : 检测 HTTP 头是否已经发送

http_response_code() :获取/设置响应的 HTTP 状态码

header : 函数向客户端发送原始的 HTTP 报头。

最后一步,来了来了,它来了,它带着echo来了,执行了一个方法$this->sendData($data);

给人一种媳妇熬成娘的感觉,终于来到的终点站,一个echo输出了咔咔几十天的心酸啊!

为了到达这个echo咔咔是经历九九八十一难啊!战斗还未停止,同志仍需努力啊!

那么到这里关于框架执行然后到应用初始化,在到路由检测、控制器的实例化、然后返回response实例,在通过入口文件执行send方法。

最后将数据输出到终端,也就是一个echo的事情。

虽然这里的战斗结束了,但是在下面还有一个非常重要的知识点,咔咔将重新提一节来进行说明。

七、fastcgi_finish_request方法巧用

在上一节中通过Container::get('app')->run()->send();在response类中执行了send方法,输出了数据。

但是在输出数据之后还执行了一个方法fastcgi_finish_request();,给的注释是提高页面响应,接下来好好来扒一扒其中的奥秘。

在PHP官网中看到这样一段话

The script will still occupy a FPM process after fastcgi_finish_request(). So using it excessively for long running tasks may occupy all your FPM threads up to pm.max_children. This will lead to gateway errors on the webserver.

在fastcgi_finish_request()之后,脚本仍将占用FPM进程。 因此,对于长时间运行的任务过度使用它可能会占用您的所有FPM线程,直到pm.max_children。 这将导致Web服务器上的网关错误。

所以说在没有彻底的了解这个方法之前不要轻易的在自己的项目中使用这个方法。

接下来咔咔将使用一个案例来演示这个方法的使用,仅仅只是演示使用,如果需要使用到项目中请仔细阅读文档应该注意的问题。

案例演示

公司有一个业务需要发送通知给用户,但是由于发送时间太久,非常费时间,有可能需要好几十秒的时间,更严重的会直接导致浏览器连接超时。

在一个问题就是用户体验的问题,用户等待时间过程,体验当然不好。

为了解决以上俩个问题,今天谈论的fastcgi_finish_request就派上了用场。

理解

对这个函数的理解其实就是发送响应给浏览器,用户等待时间大大缩短,但是PHP进程还是在运行的。

这样就达到了来个目的,就类似于我们经常说的异步执行。

直观的来说就是发送邮件有可能需要10秒,但是用户是没有感知的,用户点击发送邮件之后直接就返回发送成功,浏览器响应结束,用户做其它事情,后台进程继续执行发送邮件的任务。

案例

具体代码

<?php/** * 设置超时时间,变成不限制 * */set_time_limit(0);/** * 本函数模拟非常耗时的任务,执行完毕需要5秒的时间 */function writeFile(){    $path = 'D:/phpstudy_pro/WWW/kaka.txt';    file_put_contents($path,'程序运行开始' . PHP_EOL,FILE_APPEND);    for($i =0;$i < 5;$i++) {        file_put_contents($path,time() . PHP_EOL,FILE_APPEND);        sleep(1);    }    file_put_contents($path,'程序运行结束' . PHP_EOL,FILE_APPEND);}/** * 输出文字标记,任务开始 */echo('任务开始');/** *  后台执行非常耗时的任务 */register_shutdown_function(writeFile);/** * 立即发送请求 */fastcgi_finish_request();

以上测试全部使用linux系统进行测试哈,否则你看不到直观的效果。

经过上面的演示,响应非常快,浏览器响应结束后,后台程序依然进行执行每秒执行一个时间戳。

以上就是对fastcgi_finish_request方法的简单介绍,如果你也感兴趣可以进行简单的尝试一下,有助于更好的去理解其中的小秘密。

八、trait特性讲解

应该在俩年前咔咔就对这个特性进行过一次解析,trait就是常说的超类。

这个特性是在PHP5.4才加入的,这个特性不是经常使用的接口更不是类。

这个特性是为了解决PHP的一大弱点只能单继承的缺点,但是也不能叫多继承,严谨一点的就是类似多继承的功能而已。

接下来给大家演示一个案例。

创建test文件一,并且返回对应类名。

创建test1文件,并且返回对应类名

创建控制器文件用来输出信息。

然后在控制器中引入对应的超类文件,这里需要注意的是圈住的第一个框,这个框就是直接引入超类test文件。

然后可以直接进行访问,看会返回什么。

通过上图访问结果结果可以看得到返回的是Test超类文件的方法,但是此控制器同样也基础了Controller控制器,这也就是在文章一开头就说的超类就是实现了一种多继承的功能而已。

但是这里会存在一个问题,请看下图报错信息。

上图的报错信息是因为在控制器中使用了俩个超类导致的,也就是下图的使用方式。

那么如何解决这种报错信息呢!接下来跟这咔咔的节奏一起来。

解决报错信息

在解决之前问题之前得先清楚这个问题是由于什么引起的。

出现这个错误的原因是引用的两个trait里面有同名的hello函数,出现了冲突。

但是在日常开发中这种情况都是可以避免的,因为手动改方法名还是很方便的,但是这里咔咔教大家如何解决这种问题。

一是用其中一个trait里的hello方法覆盖另外一个trait的同名方法,因为两个方法内容是一致的,所以我这里直接选择insteadof覆盖;

二是给他们用as起别名,这样就不会有冲突了。as关键词还有另外一个用途,那就是修改方法的访问控制。

经过上图的改动之后,再一次的进行访问,看一下返回结果。

那么这个时候就会有伙伴有疑问了,就是案例打印结果一直是Test类的方法,Test1类的方法一直没有进行打印。

那是如何进行访问的呢!来接着看一下。

从上图可以看到将访问方法改为了别名控制访问,接着来看一下访问结果。

从上图中可以可以看到返回结果就是超类Test1类的返回结果。

那么关于as这个的使用就需要大家在去搜索一下使用方式,有时候注意一下细节就可以学到很多知识点。

总结

直到这里关于控制器的源码解析就到这了,咔咔通过源码给大家分析控制器的如如何进行实例化的。

也再一次的进行了对ArrayAccess和魔术方法的调用关系,一定要有自己的思考去想问题。

在就是对访问控制器后是如何进行响应数据的,等等。

也在源码中学到了关于fastcgi_finish_request方法巧用,但是在使用这个函数一定要注意关于咔咔提到的俩个注意点。

最后就是对超类的一个简单案例描述。

坚持学习、坚持写博、坚持分享是咔咔从业以来一直所秉持的信念。希望在偌大互联网中咔咔的文章能带给你一丝丝帮助。我是咔咔,下期见。

0 阅读:41