完善hyperf
This commit is contained in:
parent
3500b5eec0
commit
a39a272dee
@ -7,14 +7,11 @@ export default [
|
|||||||
text: 'Hyperf',
|
text: 'Hyperf',
|
||||||
collapsed: true,
|
collapsed: true,
|
||||||
items: [
|
items: [
|
||||||
{ text: '阅读版本', link: hyperf + '阅读版本' },
|
|
||||||
{ text: '入口文件', link: hyperf + '入口文件' },
|
{ text: '入口文件', link: hyperf + '入口文件' },
|
||||||
{ text: '初始化依赖注入 (DI) 容器', link: hyperf + 'DI' },
|
{ text: '初始化依赖注入 (DI) 容器', link: hyperf + 'DI' },
|
||||||
{ text: '初始化容器类', link: hyperf + 'container' },
|
{ text: '初始化容器类', link: hyperf + 'container' },
|
||||||
{ text: '启动服务', link: hyperf + '启动服务' },
|
{ text: '启动服务', link: hyperf + '启动服务' },
|
||||||
{ text: '接收请求', link: hyperf + '接收请求' },
|
{ text: '接收请求', link: hyperf + '接收请求' },
|
||||||
{ text: '路由寻址', link: hyperf + '路由寻址' },
|
|
||||||
{ text: '中间件', link: hyperf + '中间件' },
|
|
||||||
{ text: '响应', link: hyperf + '响应' },
|
{ text: '响应', link: hyperf + '响应' },
|
||||||
{ text: '附录1 container的get方法', link: hyperf + '附录1 container的get方法' },
|
{ text: '附录1 container的get方法', link: hyperf + '附录1 container的get方法' },
|
||||||
{ text: '附录2 注解命令获取', link: hyperf + '附录2 注解命令获取' },
|
{ text: '附录2 注解命令获取', link: hyperf + '附录2 注解命令获取' },
|
||||||
|
@ -1 +1,34 @@
|
|||||||
hhhh
|
---
|
||||||
|
title: 版本记录
|
||||||
|
---
|
||||||
|
|
||||||
|
> 记录阅读代码版本
|
||||||
|
|
||||||
|
### docker镜像
|
||||||
|
::: tip
|
||||||
|
基于`hyperf/hyperf:8.1-alpine-v3.18-swoole`镜像环境
|
||||||
|
:::
|
||||||
|
### php版本
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
`php`版本要求`>=8.1.0`
|
||||||
|
:::
|
||||||
|
|
||||||
|
### hyperf版本
|
||||||
|
|
||||||
|
::: tip
|
||||||
|
`hyperf`版本`~3.1.0`
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 启动命令
|
||||||
|
在命令行中输入以下命令启动项目
|
||||||
|
```bash
|
||||||
|
docker run --name hyperf \
|
||||||
|
-v $PWD/skeleton:/data/project \
|
||||||
|
-w /data/project \
|
||||||
|
-p 9501:9501 -it \
|
||||||
|
--privileged -u root \
|
||||||
|
--entrypoint /bin/sh \
|
||||||
|
hyperf/hyperf:8.1-alpine-v3.18-swoole
|
||||||
|
# 其中 $PWD/skeleton可替换为自己的目录
|
||||||
|
```
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
---
|
|
||||||
title: 中间件
|
|
||||||
---
|
|
||||||
|
|
||||||
#### getAttribute方法
|
|
||||||
上面将匹配的路由信息放到`attribute`属性中,现在通过`getAttribute`方法获取。
|
|
||||||
```php
|
|
||||||
$dispatched = $psr7Request->getAttribute(Dispatched::class);
|
|
||||||
// 获取中间件
|
|
||||||
$middlewares = $this->middlewares;
|
|
||||||
$registeredMiddlewares = [];
|
|
||||||
// 如果匹配到路由,则获取路由定义的中间件
|
|
||||||
if ($dispatched->isFound()) {
|
|
||||||
$registeredMiddlewares = MiddlewareManager::get($this->serverName, $dispatched->handler->route, $psr7Request->getMethod());
|
|
||||||
$middlewares = array_merge($middlewares, $registeredMiddlewares);
|
|
||||||
}
|
|
||||||
// 对路由进行排序
|
|
||||||
if ($this->option?->isMustSortMiddlewares() || $registeredMiddlewares) {
|
|
||||||
$middlewares = MiddlewareManager::sortMiddlewares($middlewares);
|
|
||||||
}
|
|
||||||
```
|
|
327
src/hyperf/响应.md
327
src/hyperf/响应.md
@ -2,49 +2,140 @@
|
|||||||
title: 响应
|
title: 响应
|
||||||
---
|
---
|
||||||
|
|
||||||
|
# 响应
|
||||||
|
|
||||||
|
最后一步,获取响应。
|
||||||
|
|
||||||
```php
|
```php
|
||||||
// $this->dispatcher 是实例化时传入的HttpDispatcher对象
|
public function onRequest($request, $response): void
|
||||||
$psr7Response = $this->dispatcher->dispatch($psr7Request, $middlewares, $this->coreMiddleware);
|
{
|
||||||
|
try {
|
||||||
|
CoordinatorManager::until(Constants::WORKER_START)->yield();
|
||||||
|
[$psr7Request, $psr7Response] = $this->initRequestAndResponse($request, $response);
|
||||||
|
$psr7Request = $this->coreMiddleware->dispatch($psr7Request);
|
||||||
|
|
||||||
|
$this->option?->isEnableRequestLifecycle() && $this->event?->dispatch(new RequestReceived(
|
||||||
|
request: $psr7Request,
|
||||||
|
response: $psr7Response,
|
||||||
|
server: $this->serverName
|
||||||
|
));
|
||||||
|
|
||||||
|
/** @var Dispatched $dispatched */
|
||||||
|
$dispatched = $psr7Request->getAttribute(Dispatched::class);
|
||||||
|
$middlewares = $this->middlewares;
|
||||||
|
|
||||||
|
$registeredMiddlewares = [];
|
||||||
|
if ($dispatched->isFound()) {
|
||||||
|
$registeredMiddlewares = MiddlewareManager::get($this->serverName, $dispatched->handler->route, $psr7Request->getMethod());
|
||||||
|
$middlewares = array_merge($middlewares, $registeredMiddlewares);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->option?->isMustSortMiddlewares() || $registeredMiddlewares) {
|
||||||
|
$middlewares = MiddlewareManager::sortMiddlewares($middlewares);
|
||||||
|
}
|
||||||
|
|
||||||
|
$psr7Response = $this->dispatcher->dispatch($psr7Request, $middlewares, $this->coreMiddleware); // [!code focus]
|
||||||
|
} catch (Throwable $throwable) {
|
||||||
|
// Delegate the exception to exception handler.
|
||||||
|
$psr7Response = $this->container->get(SafeCaller::class)->call(function () use ($throwable) {
|
||||||
|
return $this->exceptionHandlerDispatcher->dispatch($throwable, $this->exceptionHandlers);
|
||||||
|
}, static function () {
|
||||||
|
return (new Psr7Response())->withStatus(400);
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
if (isset($psr7Request) && $this->option?->isEnableRequestLifecycle()) {
|
||||||
|
defer(fn () => $this->event?->dispatch(new RequestTerminated(
|
||||||
|
request: $psr7Request,
|
||||||
|
response: $psr7Response ?? null,
|
||||||
|
exception: $throwable ?? null,
|
||||||
|
server: $this->serverName
|
||||||
|
)));
|
||||||
|
|
||||||
|
$this->event?->dispatch(new RequestHandled(
|
||||||
|
request: $psr7Request,
|
||||||
|
response: $psr7Response ?? null,
|
||||||
|
exception: $throwable ?? null,
|
||||||
|
server: $this->serverName
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the Response to client.
|
||||||
|
if (! isset($psr7Response) || ! $psr7Response instanceof ResponseInterface) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isset($psr7Request) && $psr7Request->getMethod() === 'HEAD') {
|
||||||
|
$this->responseEmitter->emit($psr7Response, $response, false);
|
||||||
|
} else {
|
||||||
|
$this->responseEmitter->emit($psr7Response, $response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`$this->dispatcher`调度器是在注册回调事件的时候实例化赋值得到的。
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function __construct(
|
||||||
|
protected ContainerInterface $container,
|
||||||
|
protected HttpDispatcher $dispatcher, // [!code focus]
|
||||||
|
protected ExceptionHandlerDispatcher $exceptionHandlerDispatcher,
|
||||||
|
protected ResponseEmitter $responseEmitter
|
||||||
|
) {
|
||||||
|
if ($this->container->has(EventDispatcherInterface::class)) {
|
||||||
|
$this->event = $this->container->get(EventDispatcherInterface::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
`$this->coreMiddleware`传入的是一个`CoreMiddleware`对象,
|
`$this->coreMiddleware`传入的是一个`CoreMiddleware`对象,
|
||||||
`$middlewares`传入的是配置的中间件数组。
|
`$middlewares`传入的是配置的中间件数组。
|
||||||
|
|
||||||
> 文件位置: /vendor/hyperf/dispatcher/src/HttpDispatcher.php
|
> 文件位置: /vendor/hyperf/dispatcher/src/HttpDispatcher.php
|
||||||
|
|
||||||
```php
|
```php
|
||||||
public function dispatch(...$params): ResponseInterface
|
public function dispatch(...$params): ResponseInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
* 从参数中解析对应的三个变量(请求对象,中间件数组,核心
|
||||||
* @param RequestInterface $request
|
* @param RequestInterface $request
|
||||||
* @param array $middlewares
|
* @param array $middlewares
|
||||||
* @param MiddlewareInterface $coreHandler
|
* @param MiddlewareInterface $coreHandler
|
||||||
*/
|
*/
|
||||||
[$request, $middlewares, $coreHandler] = $params;
|
[$request, $middlewares, $coreHandler] = $params;
|
||||||
// 实例化
|
// 实例化HttpRequestHandler处理器
|
||||||
$requestHandler = new HttpRequestHandler($middlewares, $coreHandler, $this->container);
|
$requestHandler = new HttpRequestHandler($middlewares, $coreHandler, $this->container);
|
||||||
return $requestHandler->handle($request);
|
return $requestHandler->handle($request);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`dispatch`方法实例化`HttpRequestHandler`对象,并调用`handle`方法。
|
`dispatch`方法实例化`HttpRequestHandler`对象,并调用`handle`方法。
|
||||||
|
|
||||||
### HttpRequestHandler类
|
### 处理请求
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$requestHandler = new HttpRequestHandler($middlewares, $coreHandler, $this->container);
|
$requestHandler = new HttpRequestHandler($middlewares, $coreHandler, $this->container);
|
||||||
```
|
```
|
||||||
|
|
||||||
该类没有构造方法,在父类中定义了`__construct`方法,接收三个参数。
|
该类没有构造方法,在父类中定义了`__construct`方法,接收三个参数。
|
||||||
|
|
||||||
```php
|
```php
|
||||||
public function __construct(protected array $middlewares, protected $coreHandler, protected ContainerInterface $container)
|
public function __construct(protected array $middlewares, protected $coreHandler, protected ContainerInterface $container)
|
||||||
{ // 将中间件数组的value取出
|
{ // 将中间件数组的value取出
|
||||||
$this->middlewares = array_values($this->middlewares);
|
$this->middlewares = array_values($this->middlewares);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
实例化后调用`handle`方法。
|
实例化后调用`handle`方法。
|
||||||
|
|
||||||
```php
|
```php
|
||||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||||
{
|
{
|
||||||
return $this->handleRequest($request);
|
return $this->handleRequest($request);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`handleRequest`方法,这里执行所有的中间件,全部执行完成之后,会执行`CoreMiddleware`对象中的`process`方法。
|
`handleRequest`方法,这里执行所有的中间件,全部执行完成之后,会执行`CoreMiddleware`对象中的`process`方法。
|
||||||
|
|
||||||
```php
|
```php
|
||||||
protected function handleRequest($request)
|
protected function handleRequest($request)
|
||||||
{
|
{
|
||||||
@ -71,8 +162,42 @@ protected function next(): self
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### CoreMiddleware的process方法
|
当执行完一个中间件后,最后会返回`return $handler->handle($request);`,而`$handler`参数依旧是`HttpRequestHandler`对象。所以会将中间件以此调用对应的`process`方法,直到配置的中间件全部执行完成,然后调用`CoresMiddleware`中间件的`process`方法。
|
||||||
|
::: details 参考中间件
|
||||||
|
|
||||||
|
```php
|
||||||
|
class CorsMiddleware implements MiddlewareInterface
|
||||||
|
{
|
||||||
|
public function __construct(protected ContainerInterface $container)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||||
|
{
|
||||||
|
$response = Context::get(ResponseInterface::class);
|
||||||
|
$response = $response->withHeader('Access-Control-Allow-Origin', '*')
|
||||||
|
->withHeader('Access-Control-Allow-Credentials', 'true')
|
||||||
|
// Headers 可以根据实际情况进行改写。
|
||||||
|
->withHeader('Access-Control-Allow-Headers', 'DNT,Keep-Alive,User-Agent,Cache-Control,Content-Type,Authorization');
|
||||||
|
|
||||||
|
Context::set(ResponseInterface::class, $response);
|
||||||
|
|
||||||
|
if ($request->getMethod() == 'OPTIONS') {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
return $handler->handle($request); // [!code focus]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
#### 核心中间件的process方法
|
||||||
|
>
|
||||||
> 文件位置: /vendor/hyperf/http-server/src/CoreMiddleware.php
|
> 文件位置: /vendor/hyperf/http-server/src/CoreMiddleware.php
|
||||||
|
|
||||||
|
该方法根据路由信息,获取响应内容,
|
||||||
|
|
||||||
```php
|
```php
|
||||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||||
{
|
{
|
||||||
@ -102,9 +227,36 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
当匹配到路由时,执行`handleFound`方法。
|
##### 路由不存在
|
||||||
|
|
||||||
|
抛出`NotFoundException`异常。
|
||||||
|
|
||||||
|
```php
|
||||||
|
protected function handleNotFound(ServerRequestInterface $request): mixed
|
||||||
|
{
|
||||||
|
throw new NotFoundHttpException();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### 方法不允许
|
||||||
|
|
||||||
|
抛出`MethodNotAllowedHttpException`异常。
|
||||||
|
|
||||||
|
```php
|
||||||
|
protected function handleMethodNotAllowed(array $methods, ServerRequestInterface $request): mixed
|
||||||
|
{
|
||||||
|
throw new MethodNotAllowedHttpException('Allow: ' . implode(', ', $methods));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### 匹配成功
|
||||||
|
|
||||||
`dispatched`对象结构如下,
|
`dispatched`对象结构如下,
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
如果匹配成功,则会执行`handleFound`方法,去执行路由中定义的方法或者闭包。
|
||||||
|
|
||||||
```php
|
```php
|
||||||
protected function handleFound(Dispatched $dispatched, ServerRequestInterface $request): mixed
|
protected function handleFound(Dispatched $dispatched, ServerRequestInterface $request): mixed
|
||||||
{
|
{
|
||||||
@ -130,8 +282,18 @@ protected function handleFound(Dispatched $dispatched, ServerRequestInterface $r
|
|||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
##### 控制器类型分析
|
|
||||||
`prepareHandler`方法解析出控制器类和控制器方法。
|
###### 控制器类型分析
|
||||||
|
|
||||||
|
`prepareHandler`方法解析出控制器类和控制器方法。根据解析方式,可以使用四种定义方式,
|
||||||
|
|
||||||
|
```php
|
||||||
|
Router::get('/array', [LoginController::class, 'chunk']);
|
||||||
|
Router::get('/str', 'App\Controller\StrController@index');
|
||||||
|
Router::get('/str2', 'App\Controller\StrController::index');
|
||||||
|
Router::get('/invoke', 'App\Controller\StrController');
|
||||||
|
```
|
||||||
|
|
||||||
```php
|
```php
|
||||||
protected function prepareHandler(string|array $handler): array
|
protected function prepareHandler(string|array $handler): array
|
||||||
{
|
{
|
||||||
@ -150,22 +312,32 @@ protected function prepareHandler(string|array $handler): array
|
|||||||
throw new RuntimeException('Handler not exist.');
|
throw new RuntimeException('Handler not exist.');
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
解析出控制器实例。
|
|
||||||
|
解析出控制器实例,执行控制器方法。
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$controllerInstance = $this->container->get($controller);
|
$controllerInstance = $this->container->get($controller);
|
||||||
|
$parameters = $this->parseMethodParameters($controller, $action, $dispatched->params);
|
||||||
|
// 调用控制器方法
|
||||||
|
$response = $controllerInstance->{$action}(...$parameters)
|
||||||
```
|
```
|
||||||
|
|
||||||
##### 闭包类型分析
|
###### 闭包类型分析
|
||||||
|
|
||||||
解析闭包参数,调用闭包,返回响应。
|
解析闭包参数,调用闭包,返回响应。
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$parameters = $this->parseClosureParameters($dispatched->handler->callback, $dispatched->params);
|
$parameters = $this->parseClosureParameters($dispatched->handler->callback, $dispatched->params);
|
||||||
$callback = $dispatched->handler->callback;
|
$callback = $dispatched->handler->callback;
|
||||||
$response = $callback(...$parameters);
|
$response = $callback(...$parameters);
|
||||||
```
|
```
|
||||||
|
|
||||||
`handleFound`方法走完,获取到响应对象。
|
`handleFound`方法走完,获取到响应信息。
|
||||||
|
|
||||||
##### 转换响应对象
|
##### 转换响应对象
|
||||||
|
|
||||||
如果响应对象不是`ResponsePlusInterface`接口的实现类,需要进行转换。
|
如果响应对象不是`ResponsePlusInterface`接口的实现类,需要进行转换。
|
||||||
|
|
||||||
```php
|
```php
|
||||||
if (! $response instanceof ResponsePlusInterface) {
|
if (! $response instanceof ResponsePlusInterface) {
|
||||||
$response = $this->transferToResponse($response, $request);
|
$response = $this->transferToResponse($response, $request);
|
||||||
@ -210,7 +382,9 @@ protected function transferToResponse($response, ServerRequestInterface $request
|
|||||||
|
|
||||||
返回响应对象。
|
返回响应对象。
|
||||||
|
|
||||||
### 发送响应到客户端
|
## 发送响应到客户端
|
||||||
|
|
||||||
|
前面已经完成对请求的处理工作,接下来需要将响应信息返回给客户端。
|
||||||
|
|
||||||
```php
|
```php
|
||||||
if (isset($psr7Request) && $psr7Request->getMethod() === 'HEAD') {
|
if (isset($psr7Request) && $psr7Request->getMethod() === 'HEAD') {
|
||||||
@ -219,8 +393,11 @@ if (isset($psr7Request) && $psr7Request->getMethod() === 'HEAD') {
|
|||||||
$this->responseEmitter->emit($psr7Response, $response);
|
$this->responseEmitter->emit($psr7Response, $response);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
#### emit方法
|
|
||||||
|
### emit方法
|
||||||
|
|
||||||
发送响应到客户端。
|
发送响应到客户端。
|
||||||
|
|
||||||
```php
|
```php
|
||||||
public function emit(ResponseInterface $response, mixed $connection, bool $withContent = true): void
|
public function emit(ResponseInterface $response, mixed $connection, bool $withContent = true): void
|
||||||
{
|
{
|
||||||
@ -248,4 +425,128 @@ public function emit(ResponseInterface $response, mixed $connection, bool $withC
|
|||||||
$this->logger?->critical((string) $exception);
|
$this->logger?->critical((string) $exception);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### 将响应转换为swoole的响应对象
|
||||||
|
|
||||||
|
```php
|
||||||
|
protected function buildSwooleResponse(Response $swooleResponse, ResponseInterface $response): void
|
||||||
|
{
|
||||||
|
// Headers
|
||||||
|
foreach ($response->getHeaders() as $key => $value) {
|
||||||
|
$swooleResponse->header($key, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cookies
|
||||||
|
// This part maybe only supports of hyperf/http-message component.
|
||||||
|
if (method_exists($response, 'getCookies')) {
|
||||||
|
foreach ((array) $response->getCookies() as $domain => $paths) {
|
||||||
|
foreach ($paths ?? [] as $path => $item) {
|
||||||
|
foreach ($item ?? [] as $name => $cookie) {
|
||||||
|
if ($this->isMethodsExists($cookie, [
|
||||||
|
'isRaw', 'getValue', 'getName', 'getExpiresTime', 'getPath', 'getDomain', 'isSecure', 'isHttpOnly', 'getSameSite',
|
||||||
|
])) {
|
||||||
|
$value = $cookie->isRaw() ? $cookie->getValue() : rawurlencode($cookie->getValue());
|
||||||
|
$swooleResponse->rawcookie($cookie->getName(), $value, $cookie->getExpiresTime(), $cookie->getPath(), $cookie->getDomain(), $cookie->isSecure(), $cookie->isHttpOnly(), (string) $cookie->getSameSite());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trailers
|
||||||
|
if (method_exists($response, 'getTrailers') && method_exists($swooleResponse, 'trailer')) {
|
||||||
|
foreach ($response->getTrailers() ?? [] as $key => $value) {
|
||||||
|
$swooleResponse->trailer($key, $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status code
|
||||||
|
$swooleResponse->status($response->getStatusCode(), $response->getReasonPhrase());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 异常捕获
|
||||||
|
|
||||||
|
当在代码中出现异常时,会被`catch`住,然后通过`SafeCaller`类来调用配置的异常处理器。
|
||||||
|
:::code-group
|
||||||
|
```php
|
||||||
|
try {
|
||||||
|
CoordinatorManager::until(Constants::WORKER_START)->yield();
|
||||||
|
[$psr7Request, $psr7Response] = $this->initRequestAndResponse($request, $response);
|
||||||
|
$psr7Request = $this->coreMiddleware->dispatch($psr7Request);
|
||||||
|
|
||||||
|
$this->option?->isEnableRequestLifecycle() && $this->event?->dispatch(new RequestReceived(
|
||||||
|
request: $psr7Request,
|
||||||
|
response: $psr7Response,
|
||||||
|
server: $this->serverName
|
||||||
|
));
|
||||||
|
|
||||||
|
/** @var Dispatched $dispatched */
|
||||||
|
$dispatched = $psr7Request->getAttribute(Dispatched::class);
|
||||||
|
$middlewares = $this->middlewares;
|
||||||
|
|
||||||
|
$registeredMiddlewares = [];
|
||||||
|
if ($dispatched->isFound()) {
|
||||||
|
$registeredMiddlewares = MiddlewareManager::get($this->serverName, $dispatched->handler->route, $psr7Request->getMethod());
|
||||||
|
$middlewares = array_merge($middlewares, $registeredMiddlewares);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->option?->isMustSortMiddlewares() || $registeredMiddlewares) {
|
||||||
|
$middlewares = MiddlewareManager::sortMiddlewares($middlewares);
|
||||||
|
}
|
||||||
|
|
||||||
|
$psr7Response = $this->dispatcher->dispatch($psr7Request, $middlewares, $this->coreMiddleware);
|
||||||
|
} catch (Throwable $throwable) { // [!code focus:7]
|
||||||
|
// Delegate the exception to exception handler.
|
||||||
|
$psr7Response = $this->container->get(SafeCaller::class)->call(function () use ($throwable) {
|
||||||
|
return $this->exceptionHandlerDispatcher->dispatch($throwable, $this->exceptionHandlers);
|
||||||
|
}, static function () {
|
||||||
|
return (new Psr7Response())->withStatus(400);
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
if (isset($psr7Request) && $this->option?->isEnableRequestLifecycle()) {
|
||||||
|
defer(fn () => $this->event?->dispatch(new RequestTerminated(
|
||||||
|
request: $psr7Request,
|
||||||
|
response: $psr7Response ?? null,
|
||||||
|
exception: $throwable ?? null,
|
||||||
|
server: $this->serverName
|
||||||
|
)));
|
||||||
|
|
||||||
|
$this->event?->dispatch(new RequestHandled(
|
||||||
|
request: $psr7Request,
|
||||||
|
response: $psr7Response ?? null,
|
||||||
|
exception: $throwable ?? null,
|
||||||
|
server: $this->serverName
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the Response to client.
|
||||||
|
if (! isset($psr7Response) || ! $psr7Response instanceof ResponseInterface) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isset($psr7Request) && $psr7Request->getMethod() === 'HEAD') {
|
||||||
|
$this->responseEmitter->emit($psr7Response, $response, false);
|
||||||
|
} else {
|
||||||
|
$this->responseEmitter->emit($psr7Response, $response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
:::
|
||||||
|
|
||||||
|
`call`方法会执行闭包里的内容,如果执行闭包发生错误,则捕获异常并输出日志,返回默认400状态码。
|
||||||
|
```php
|
||||||
|
|
||||||
|
public function call(Closure $closure, ?Closure $default = null, string $level = LogLevel::CRITICAL): mixed
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return $closure();
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
if ($this->container->has(StdoutLoggerInterface::class) && $logger = $this->container->get(StdoutLoggerInterface::class)) {
|
||||||
|
$logger->log($level, (string) $exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value($default);
|
||||||
|
}
|
||||||
```
|
```
|
@ -1,6 +1,10 @@
|
|||||||
---
|
---
|
||||||
title: 接收请求
|
title: 接收请求
|
||||||
---
|
---
|
||||||
|
::: tip
|
||||||
|
请参考[附录5](./附录5%20初始化中间件){target="_blank"}之后,再来看这篇。
|
||||||
|
:::
|
||||||
|
|
||||||
# 接收请求
|
# 接收请求
|
||||||
|
|
||||||
由前一节可知,事件的注册在`Hyerf\Server\Server`类的`registerSwooleEvents`方法中完成。
|
由前一节可知,事件的注册在`Hyerf\Server\Server`类的`registerSwooleEvents`方法中完成。
|
||||||
@ -20,6 +24,7 @@ title: 接收请求
|
|||||||
::: tip
|
::: tip
|
||||||
要理解上面事件,请参考`Swoole`的运行流程。
|
要理解上面事件,请参考`Swoole`的运行流程。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
### Swoole流程
|
### Swoole流程
|
||||||
|
|
||||||
首先了解`Swoole`的运行流程,(图源网络)
|
首先了解`Swoole`的运行流程,(图源网络)
|
||||||
@ -29,16 +34,20 @@ title: 接收请求
|
|||||||
::: tip
|
::: tip
|
||||||
当请求过来时,会触发`Request`事件,基于之前准备阶段注册的`swoole`事件可知,会调用`Hyperf\HttpServer\Server`类的`onRequest`函数。
|
当请求过来时,会触发`Request`事件,基于之前准备阶段注册的`swoole`事件可知,会调用`Hyperf\HttpServer\Server`类的`onRequest`函数。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
### OnRequest方法
|
### OnRequest方法
|
||||||
|
|
||||||
```php
|
```php
|
||||||
public function onRequest($request, $response): void
|
public function onRequest($request, $response): void
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
// 等待worker进程启动
|
||||||
CoordinatorManager::until(Constants::WORKER_START)->yield();
|
CoordinatorManager::until(Constants::WORKER_START)->yield();
|
||||||
|
// 请求和响应对象转换为符合 PSR-7标准的对象($request, $response为swoole返回的对象)
|
||||||
[$psr7Request, $psr7Response] = $this->initRequestAndResponse($request, $response);
|
[$psr7Request, $psr7Response] = $this->initRequestAndResponse($request, $response);
|
||||||
|
// 路由匹配
|
||||||
$psr7Request = $this->coreMiddleware->dispatch($psr7Request);
|
$psr7Request = $this->coreMiddleware->dispatch($psr7Request);
|
||||||
|
|
||||||
$this->option?->isEnableRequestLifecycle() && $this->event?->dispatch(new RequestReceived(
|
$this->option?->isEnableRequestLifecycle() && $this->event?->dispatch(new RequestReceived(
|
||||||
request: $psr7Request,
|
request: $psr7Request,
|
||||||
response: $psr7Response,
|
response: $psr7Response,
|
||||||
@ -46,18 +55,24 @@ public function onRequest($request, $response): void
|
|||||||
));
|
));
|
||||||
|
|
||||||
/** @var Dispatched $dispatched */
|
/** @var Dispatched $dispatched */
|
||||||
|
// 获取匹配的路由信息
|
||||||
$dispatched = $psr7Request->getAttribute(Dispatched::class);
|
$dispatched = $psr7Request->getAttribute(Dispatched::class);
|
||||||
|
// 获取配置文件中定义的路由
|
||||||
$middlewares = $this->middlewares;
|
$middlewares = $this->middlewares;
|
||||||
$registeredMiddlewares = [];
|
$registeredMiddlewares = [];
|
||||||
|
// 路由存在的情况
|
||||||
if ($dispatched->isFound()) {
|
if ($dispatched->isFound()) {
|
||||||
|
// 获取路由注册的中间件
|
||||||
$registeredMiddlewares = MiddlewareManager::get($this->serverName, $dispatched->handler->route, $psr7Request->getMethod());
|
$registeredMiddlewares = MiddlewareManager::get($this->serverName, $dispatched->handler->route, $psr7Request->getMethod());
|
||||||
|
// 跟配置文件合并
|
||||||
$middlewares = array_merge($middlewares, $registeredMiddlewares);
|
$middlewares = array_merge($middlewares, $registeredMiddlewares);
|
||||||
}
|
}
|
||||||
|
// 如果mustSortMiddlewares参数为true或者registeredMiddlewares中间件存在,则进行排序操作
|
||||||
if ($this->option?->isMustSortMiddlewares() || $registeredMiddlewares) {
|
if ($this->option?->isMustSortMiddlewares() || $registeredMiddlewares) {
|
||||||
$middlewares = MiddlewareManager::sortMiddlewares($middlewares);
|
$middlewares = MiddlewareManager::sortMiddlewares($middlewares);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取响应信息
|
||||||
$psr7Response = $this->dispatcher->dispatch($psr7Request, $middlewares, $this->coreMiddleware);
|
$psr7Response = $this->dispatcher->dispatch($psr7Request, $middlewares, $this->coreMiddleware);
|
||||||
} catch (Throwable $throwable) {
|
} catch (Throwable $throwable) {
|
||||||
// Delegate the exception to exception handler.
|
// Delegate the exception to exception handler.
|
||||||
@ -98,21 +113,260 @@ public function onRequest($request, $response): void
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### CoordinatorManager类
|
|
||||||
`CoordinatorManager::until`用于创建一个协程等待器,指示协程等待某个事件的完成,
|
`CoordinatorManager::until`用于创建一个协程等待器,指示协程等待某个事件的完成,
|
||||||
这里是等待`worker`进程启动完成。
|
这里是等待`worker`进程启动完成。
|
||||||
|
|
||||||
```php
|
```php
|
||||||
CoordinatorManager::until(Constants::WORKER_START)->yield();
|
CoordinatorManager::until(Constants::WORKER_START)->yield();
|
||||||
```
|
```
|
||||||
|
|
||||||
确保在处理请求之前,所有的`Worker`进程都已经启动完成,以保证后续的操作能够顺利执行。
|
确保在处理请求之前,所有的`Worker`进程都已经启动完成,以保证后续的操作能够顺利执行。
|
||||||
|
|
||||||
#### initRequestAndResponse方法
|
:::code-group
|
||||||
将请求和响应对象转换为符合 `PSR-7`标准的对象。
|
|
||||||
```php
|
```php [转换为标准对象]
|
||||||
[$psr7Request, $psr7Response] = $this->initRequestAndResponse($request, $response);
|
[$psr7Request, $psr7Response] = $this->initRequestAndResponse($request, $response);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```php [转换实现]
|
||||||
|
protected function initRequestAndResponse($request, $response): array
|
||||||
|
{
|
||||||
|
// 设置到`ResponseContext`中,
|
||||||
|
ResponseContext::set($psr7Response = new Psr7Response());
|
||||||
|
|
||||||
|
$psr7Response->setConnection(new WritableConnection($response));
|
||||||
|
|
||||||
|
if ($request instanceof ServerRequestInterface) {
|
||||||
|
$psr7Request = $request;
|
||||||
|
} else {
|
||||||
|
$psr7Request = Psr7Request::loadFromSwooleRequest($request);
|
||||||
|
}
|
||||||
|
// 设置请求到RequestContext中
|
||||||
|
RequestContext::set($psr7Request);
|
||||||
|
|
||||||
|
return [$psr7Request, $psr7Response];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
:::
|
||||||
|
创建一个新的 `Psr7Response` 实例,并将其设置到 `ResponseContext` 上下文中。`ResponseContext`和`RequestContext` 是静态类,用于在全局范围内存储响应对象。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### 路由寻址
|
||||||
|
|
||||||
|
请求和响应对象转换完成之后,开始匹配路由,
|
||||||
|
::: code-group
|
||||||
|
```php [寻址]
|
||||||
|
$psr7Request = $this->coreMiddleware->dispatch($psr7Request);
|
||||||
|
```
|
||||||
|
```php [路由查找]
|
||||||
|
public function dispatch(ServerRequestInterface $request): ServerRequestInterface
|
||||||
|
{
|
||||||
|
// 进行查找路由操作
|
||||||
|
$routes = $this->dispatcher->dispatch($request->getMethod(), $request->getUri()->getPath());
|
||||||
|
|
||||||
|
$dispatched = new Dispatched($routes, $this->serverName);
|
||||||
|
|
||||||
|
return RequestContext::set($request)->setAttribute(Dispatched::class, $dispatched);
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
```php [匹配实现]
|
||||||
|
public function dispatch($httpMethod, $uri)
|
||||||
|
{
|
||||||
|
if (isset($this->staticRouteMap[$httpMethod][$uri])) {
|
||||||
|
$handler = $this->staticRouteMap[$httpMethod][$uri];
|
||||||
|
return [self::FOUND, $handler, []];
|
||||||
|
}
|
||||||
|
|
||||||
|
$varRouteData = $this->variableRouteData;
|
||||||
|
if (isset($varRouteData[$httpMethod])) {
|
||||||
|
$result = $this->dispatchVariableRoute($varRouteData[$httpMethod], $uri);
|
||||||
|
if ($result[0] === self::FOUND) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For HEAD requests, attempt fallback to GET
|
||||||
|
if ($httpMethod === 'HEAD') {
|
||||||
|
if (isset($this->staticRouteMap['GET'][$uri])) {
|
||||||
|
$handler = $this->staticRouteMap['GET'][$uri];
|
||||||
|
return [self::FOUND, $handler, []];
|
||||||
|
}
|
||||||
|
if (isset($varRouteData['GET'])) {
|
||||||
|
$result = $this->dispatchVariableRoute($varRouteData['GET'], $uri);
|
||||||
|
if ($result[0] === self::FOUND) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If nothing else matches, try fallback routes
|
||||||
|
if (isset($this->staticRouteMap['*'][$uri])) {
|
||||||
|
$handler = $this->staticRouteMap['*'][$uri];
|
||||||
|
return [self::FOUND, $handler, []];
|
||||||
|
}
|
||||||
|
if (isset($varRouteData['*'])) {
|
||||||
|
$result = $this->dispatchVariableRoute($varRouteData['*'], $uri);
|
||||||
|
if ($result[0] === self::FOUND) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find allowed methods for this URI by matching against all other HTTP methods as well
|
||||||
|
$allowedMethods = [];
|
||||||
|
|
||||||
|
foreach ($this->staticRouteMap as $method => $uriMap) {
|
||||||
|
if ($method !== $httpMethod && isset($uriMap[$uri])) {
|
||||||
|
$allowedMethods[] = $method;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($varRouteData as $method => $routeData) {
|
||||||
|
if ($method === $httpMethod) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->dispatchVariableRoute($routeData, $uri);
|
||||||
|
if ($result[0] === self::FOUND) {
|
||||||
|
$allowedMethods[] = $method;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are no allowed methods the route simply does not exist
|
||||||
|
if ($allowedMethods) {
|
||||||
|
return [self::METHOD_NOT_ALLOWED, $allowedMethods];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [self::NOT_FOUND];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
当匹配到时,返回参数如下所示,数组第一个参数为标识是否匹配到路由的状态码。
|
||||||
|
|
||||||
|
```php
|
||||||
|
Array
|
||||||
|
(
|
||||||
|
[0] => 1
|
||||||
|
[1] => Hyperf\HttpServer\Router\Handler Object
|
||||||
|
(
|
||||||
|
[callback] => Array
|
||||||
|
(
|
||||||
|
[0] => App\Controller\IndexController
|
||||||
|
[1] => index
|
||||||
|
)
|
||||||
|
|
||||||
|
[route] => /index/index
|
||||||
|
[options] => Array
|
||||||
|
(
|
||||||
|
[middleware] => Array
|
||||||
|
(
|
||||||
|
)
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
[2] => Array
|
||||||
|
(
|
||||||
|
)
|
||||||
|
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
```php
|
||||||
|
$dispatched = new Dispatched($routes, $this->serverName);
|
||||||
|
```
|
||||||
|
|
||||||
|
使用获取到的路由信息数组和当前`Server`实例化一个`Dispatched`类来保存匹配信息。
|
||||||
|
|
||||||
|
```php
|
||||||
|
namespace Hyperf\HttpServer\Router;
|
||||||
|
|
||||||
|
use FastRoute\Dispatcher;
|
||||||
|
|
||||||
|
class Dispatched
|
||||||
|
{
|
||||||
|
public int $status;
|
||||||
|
|
||||||
|
public ?Handler $handler = null;
|
||||||
|
|
||||||
|
public array $params = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据匹配信息,设置实例参数
|
||||||
|
*
|
||||||
|
* @param array $array with one of the following formats:
|
||||||
|
*
|
||||||
|
* [Dispatcher::NOT_FOUND]
|
||||||
|
* [Dispatcher::METHOD_NOT_ALLOWED, ['GET', 'OTHER_ALLOWED_METHODS']]
|
||||||
|
* [Dispatcher::FOUND, $handler, ['varName' => 'value', ...]]
|
||||||
|
*/
|
||||||
|
public function __construct(array $array, public ?string $serverName = null)
|
||||||
|
{
|
||||||
|
$this->status = $array[0];
|
||||||
|
switch ($this->status) {
|
||||||
|
case Dispatcher::METHOD_NOT_ALLOWED:
|
||||||
|
$this->params = $array[1];
|
||||||
|
break;
|
||||||
|
case Dispatcher::FOUND:
|
||||||
|
$this->handler = $array[1];
|
||||||
|
$this->params = $array[2];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isFound(): bool
|
||||||
|
{
|
||||||
|
return $this->status === Dispatcher::FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isNotFound(): bool
|
||||||
|
{
|
||||||
|
return $this->status === Dispatcher::NOT_FOUND;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
最后设置请求上下文,并返回修改后的请求对象。
|
||||||
|
|
||||||
|
```php
|
||||||
|
return RequestContext::set($request)->setAttribute(Dispatched::class, $dispatched);
|
||||||
|
```
|
||||||
|
|
||||||
|
```php
|
||||||
|
$this->option?->isEnableRequestLifecycle() && $this->event?->dispatch(new RequestReceived(
|
||||||
|
request: $psr7Request,
|
||||||
|
response: $psr7Response,
|
||||||
|
server: $this->serverName
|
||||||
|
));
|
||||||
|
```
|
||||||
|
|
||||||
|
如果`option`参数设置了`enable_request_lifecycle`为`true`,则触发`RequestReceived`事件。
|
||||||
|
|
||||||
|
```php
|
||||||
|
$dispatched = $psr7Request->getAttribute(Dispatched::class);
|
||||||
|
```
|
||||||
|
|
||||||
|
从请求中取出刚才设置的`Dispatched`实例。
|
||||||
|
|
||||||
|
#### 处理中间件
|
||||||
|
|
||||||
|
首先获取配置文件中的路由,
|
||||||
|
如果路由匹配成功,则获取路由上的中间件配置,然后将中间件进行合并。
|
||||||
|
```php
|
||||||
|
$middlewares = $this->middlewares;
|
||||||
|
$registeredMiddlewares = [];
|
||||||
|
if ($dispatched->isFound()) {
|
||||||
|
$registeredMiddlewares = MiddlewareManager::get($this->serverName, $dispatched->handler->route, $psr7Request->getMethod());
|
||||||
|
$middlewares = array_merge($middlewares, $registeredMiddlewares);
|
||||||
|
}
|
||||||
|
// 如果mustSortMiddlewares参数为true或者registeredMiddlewares中间件存在,则进行排序操作
|
||||||
|
if ($this->option?->isMustSortMiddlewares() || $registeredMiddlewares) {
|
||||||
|
$middlewares = MiddlewareManager::sortMiddlewares($middlewares);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
@ -1,26 +0,0 @@
|
|||||||
---
|
|
||||||
title: 路由寻址
|
|
||||||
---
|
|
||||||
|
|
||||||
对请求进行匹配,放到请求的`attribute`属性中,`key`为`Dispatched::class`。
|
|
||||||
```php
|
|
||||||
$psr7Request = $this->coreMiddleware->dispatch($psr7Request);
|
|
||||||
```
|
|
||||||
> 文件位置:/vendor/hyperf/http-server/src/CoreMiddleware.php
|
|
||||||
```php
|
|
||||||
public function dispatch(ServerRequestInterface $request): ServerRequestInterface
|
|
||||||
{
|
|
||||||
// 获取路由信息
|
|
||||||
$routes = $this->dispatcher->dispatch($request->getMethod(), $request->getUri()->getPath());
|
|
||||||
$dispatched = new Dispatched($routes, $this->serverName);
|
|
||||||
return RequestContext::set($request)->setAttribute(Dispatched::class, $dispatched);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
该行默认不触发事件,如果需要触发事件,要设置`enable_request_lifecycle`为`true`。
|
|
||||||
```php
|
|
||||||
$this->option?->isEnableRequestLifecycle() && $this->event?->dispatch(new RequestReceived(
|
|
||||||
request: $psr7Request,
|
|
||||||
response: $psr7Response,
|
|
||||||
server: $this->serverName
|
|
||||||
));
|
|
||||||
```
|
|
@ -1,34 +0,0 @@
|
|||||||
---
|
|
||||||
title: 版本记录
|
|
||||||
---
|
|
||||||
|
|
||||||
> 记录阅读代码版本
|
|
||||||
|
|
||||||
### docker镜像
|
|
||||||
::: tip
|
|
||||||
基于`hyperf/hyperf:8.1-alpine-v3.18-swoole`镜像环境
|
|
||||||
:::
|
|
||||||
### php版本
|
|
||||||
|
|
||||||
::: tip
|
|
||||||
`php`版本要求`>=8.1.0`
|
|
||||||
:::
|
|
||||||
|
|
||||||
### hyperf版本
|
|
||||||
|
|
||||||
::: tip
|
|
||||||
`hyperf`版本`~3.1.0`
|
|
||||||
:::
|
|
||||||
|
|
||||||
### 启动命令
|
|
||||||
在命令行中输入以下命令启动项目
|
|
||||||
```bash
|
|
||||||
docker run --name hyperf \
|
|
||||||
-v $PWD/skeleton:/data/project \
|
|
||||||
-w /data/project \
|
|
||||||
-p 9501:9501 -it \
|
|
||||||
--privileged -u root \
|
|
||||||
--entrypoint /bin/sh \
|
|
||||||
hyperf/hyperf:8.1-alpine-v3.18-swoole
|
|
||||||
# 其中 $PWD/skeleton可替换为自己的目录
|
|
||||||
```
|
|
Loading…
Reference in New Issue
Block a user