响应
响应
基础
所有的路由闭包和 Controller 方法都应该返回一个响应,最基本的情况是直接返回一个字符串,如果返回一个数组,那么 Laravel 会将数组转换成 JSON 并返回。
大多数情况下,一个接口不会只返回一个字符串或一个数组,而是返回一个完整的 HTTP 响应。使用 response
方法可以创建一个 Illuminate\Http\Response
的对象实例:
public function update(Request $request, string $id)
{
return response(null, 200)->header('result', 'ok');
}
附加响应头
使用 header
方法可以按对添加响应头,或者使用 withHeaders
方法批量添加响应头:
public function update(Request $request, string $id)
{
return response(null, 200)->withHeaders([
'a' => 1,
'b' => 2,
]);
}
在中间件中也可以附加请求头和响应头,在一个全局中间件中将给请求头和响应头都添加一个 X-Request-Id
头:
class First
{
public function handle(Request $request, Closure $next): Response
{
$reqId = Str::uuid()->toString();
$request->headers->set('X-Request-Id', $reqId);
$response = $next($request);
$response->headers->set('X-Request-Id', $reqId);
return $response;
}
}
附加 Cookie
使用 cookie
或 withCookie
方法可以添加 Cookie:
public function update(Request $request, string $id)
{
return response(null, 200)->cookie(
'accessToken', 'test', 60 * 60 * 24 * 30, '/', null, false, true
);
}
参数含义:
return response('Hello World')->cookie(
'name', 'value', $minutes, $path, $domain, $secure, $httpOnly
);
如果需要在响应外添加 Cookie,可以使用 Cookie::queue
方法:
public function update(Request $request, string $id)
{
Cookie::queue('newName', 'value', 60, '/', null, false, true);
return response(null, 200);
}
提示
如果在 API 路由中使用 Cookie::queue
,需要将 Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse
这个中间件添加到 api 中间件组中:
->withMiddleware(function (Middleware $middleware) {
$middleware->prependToGroup('api', AddQueuedCookiesToResponse::class);
})
Laravel 还提供了 cookie
辅助函数来创建一个 Symfony\Component\HttpFoundation\Cookie
实例:
public function update(Request $request, string $id)
{
$cookie = cookie('cookieObj', 'value', 60, '/', null, false, true);
return response(null, 200)->cookie($cookie);
}
在响应中使用 withoutCookie
方法可以提前过期一个 Cookie:
public function update(Request $request, string $id)
{
return response(null, 200)->withoutCookie('cookieObj');
}
或者也可以使用 Cookie::expire
方法:
public function update(Request $request, string $id)
{
Cookie::expire('accessToken');
return response(null, 200);
}
提示
同样,Cookie::expire
方法在 API 路由中使用时也要注册 Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse
中间件。
重定向
重定向响应是 Illuminate\Http\RedirectResponse
类实例,使用 redirect
辅助函数可以创建一个重定向响应:
public function index(Request $request)
{
return redirect('https://PPG007.github.io/');
}
重定向到命名路由
当不使用任何参数调用 redirect
辅助函数时,将返回一个 Illuminate\Routing\Redirector
实例,该实例可以调用 route
方法将请求重定向到命名路由:
public function index(Request $request)
{
return redirect()->route('demo.get');
}
如果需要传参:
public function index(Request $request)
{
return redirect()->route('demo.get', ['id' => 'test']);
}
重定向到 Controller
使用 action
方法可以重定向到 Controller 中的方法,第一个参数表明了要重定向到的 Controller 和方法,第二个参数是传给该方法参数:
public function index(Request $request)
{
return redirect()->action([DemoController::class, 'getMember'], ['id' => '123']);
}
重定向到外部域名
public function index(Request $request)
{
return redirect()->away('https://PPG007.github.io/');
}
其他响应类型
JSON 响应
使用 json
方法可以返回 JSON 响应:
public function getMember(string $id = '')
{
return response()->json([
'obj' => [
'value' => false,
]
]);
}
json
方法会自动将 Content-Type
响应头设置为 application/json
,并使用 json_encode
方法将数组转换为 JSON 字符串。
文件下载
download
方法可以强制浏览器在给定路径下载文件:
return response()->download($pathToFile, $name, $headers);
文件响应
file
方法可以直接在浏览器中显示文件而不是启动下载,此方法接收文件的绝对路径作为第一个参数,第二个参数可以指定响应头:
public function getMember(string $id = '')
{
return response()->file('/home/user/playground/scrm/.env.example', ['test' => '123']);
}
流式响应
如果响应比较大,使用流式传输可以减少内存占用并提高性能,当前的各种 AI 对话的场景都是流式响应,不需要 AI 完整的返回结果就能看到最新生成的文本。
流式返回文本
public function getMember(string $id = '')
{
return response()->stream(function () {
$data = ['Hello', 'World'];
foreach ($data as $item) {
echo $item;
ob_flush();
flush();
sleep(3);
}
});
}
提示
需要使用 ob_flush()
和 flush()
来刷新输出缓冲。
可以使用 JavaScript 来读取这种响应:
const main = async () => {
const url = 'http://localhost:8000/v1/members/123';
const resp = await fetch(url);
const reader = resp.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
console.log(
decoder.decode(value, {
stream: true,
})
);
}
};
main();
流式 JSON
public function getMember(string $id = '')
{
$data = [
['name' => 1, 'isBlocked' => false],
['name' => 2, 'isBlocked' => true],
];
return response()->streamJson($data);
}
const main = async () => {
const url = 'http://localhost:8000/v1/members/123';
const resp = await fetch(url);
const reader = resp.body.getReader();
const decoder = new TextDecoder();
const parts = [];
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
parts.push(decoder.decode(value, { stream: true }));
}
const obj = JSON.parse(parts.join(''));
console.log(obj);
};
main();
事件流
eventStream
方法可以用于返回 text/event-stream
类型的响应(SSE)。
关于 Server-Sent Event
可以参考 SSE。
public function getMember(string $id = '')
{
return response()->eventStream(function () {
for ($i = 1; $i <= 3; $i++) {
yield [
'data' => ['count' => $i, 'time' => now()->toDateTimeString()]
];
sleep(1);
}
});
}
<html>
<head>
<title>A</title>
<script>
const source = new EventSource("http://localhost:8000/v1/members/123");
source.addEventListener("update", (event) => {
console.log(event);
if (event.data === '</stream>') {
source.close();
}
});
</script>
</head>
<body></body>
</html>
提示
由于 NodeJs 没有 EventSource
,所以需要在浏览器中请求。
由于浏览器中请求可能会存在跨域问题,所以可能需要配置跨域,首先需要暴露 CORS 配置文件,参考 CORS。通过修改 config/cors.php
可以解决跨域问题。
默认情况下,Laravel 的 EventStream 的消息事件不是 message
而是 update
,所以使用 EventSource
的 onmessage
监听不到内容,同时,Laravel 结束相应的事件仍然是 update
但是会返回 </stream>
结束符,所以需要特殊处理并关闭连接防止不停重连。
如果希望自定义事件名称,可以生成 StreamedEvent
类实例:
public function getMember(string $id = '')
{
return response()->eventStream(function () {
for ($i = 1; $i <= 3; $i++) {
$data = [
'data' => ['count' => $i, 'time' => now()->toDateTimeString()]
];
yield new StreamedEvent(
event: 'message',data: $data,
);
sleep(1);
}
});
}
<html>
<head>
<title>A</title>
<script>
const source = new EventSource("http://localhost:8000/v1/members/123");
source.onmessage = function(event) {
console.log(event);
};
source.addEventListener('update', () => {
source.close();
})
</script>
</head>
<body></body>
</html>
流式下载
TODO:
响应宏
如果想定义一个可重用的响应,可以使用 macro
方法并在 ServiceProvider 的 boot
方法中调用:
public function boot(): void
{
Response::macro('ppg', function ($value) {
if (is_array($value) || is_object($value)) {
return Response::make(json_encode($value));
}
if (is_string($value)) {
return Response::make(Str::upper($value));
}
return Response::make([], 400);
});
}
public function getMember(string $id = '')
{
return response()->ppg('Hello');
}