Skip to content

Nest JS 接收 navigator.sendBeacon 发送请求传参

615字约2分钟

NestJSJavaScript

2024-12-26

前端使用 navigator.sendBeacon 发送请求,后端接收参数时需要特殊处理

问题复现

在前端页面关闭或者刷新时,我们可能会需要发送一些数据到后端,比如用户关闭页面时,我们需要记录一下用户关闭页面的时间,以便后续分析用户的行为。

在前端,我们可以使用 navigator.sendBeacon 方法来发送请求,这个方法会异步发送一个 HTTP 请求,并且不会阻塞页面的关闭或者刷新。

具体实现代码:

// 前端代码
const data = {
  userId: '123',
  closeTime: new Date().toISOString(),
};

navigator.sendBeacon('/api/close', JSON.stringify(data));

后端代码:

// 后端代码
@Controller('api')
export class ApiController {
  @Post('close')
  async close(@Body() body: any) {
    console.log(body);
  } 
}

如上面所示,我们使用 navigator.sendBeacon 方法发送了一个 POST 请求,请求的 URL 是 /api/close,请求的 body 是一个 JSON 字符串。

但是,当我们打印 body 时,发现 body 是一个空对象 {},而不是我们期望的 JSON 对象。

问题分析

这是因为 @Body() 装饰器默认只能解析 application/json 格式的请求体,而 navigator.sendBeacon 发送的请求体是 text/plain 格式的。

我们可以通过打印 req.headers['content-type'] 来验证这一点:

// 后端代码
@Controller('api')
export class ApiController {
  @Post('close')
  async close(@Body() body: any, @Req() req: Request) {
    console.log(req.headers['content-type']);
    console.log(body);
  } 
}

解决方案

我们可以通过自定义中间件来解析 text/plain 格式的请求体,然后将解析后的数据传递给控制器。

具体实现代码:

// text-plain.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class TextPlainMiddleware implements NestMiddleware {
    use(req: Request, res: Response, next: NextFunction) {
        if (req.is('text/plain')) {
            let body = '';
            req.on('data', chunk => {
                body += chunk.toString(); // 将数据块拼接成字符串
            });
            req.on('end', () => {
                req.body = JSON.parse(body); // 将字符串解析成 JSON 对象
                next();
            });
        } else {
            next();
        }
    }
}
// app.module.ts 也可以 添加到 对应的实体module中
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { TextPlainMiddleware } from './text-plain.middleware';

@Module({
  //...
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    // 将中间件应用到对应的路由上
    consumer.apply(TextPlainMiddleware).forRoutes('api/close');
  }
}

设置好中间件之后,我们就可以在控制器中正常解析请求体了。

// api.controller.ts
import { Controller, Post, Body, Req } from '@nestjs/common';

@Controller('api')
export class ApiController {
    @Post('close')
    async close(@Body() body: any, @Req() req: Request) {
        console.log(body);
    } 
}

通过以上代码,我们就可以成功解析 text/plain 格式的请求体,并将解析后的数据传递给控制器。