02-理解五种http请求对方式和使用场景

准备工作

新建一个nestjs工程

可以使用全局安装的nestjs进行初始化一个新的工程

npm install -g @nestjs/cli
nest new 项目名 

注意这种方式要经常手动更新一下自己本地的nestjs版本

npm update -g @nestjs/cli

也可以直接使用npx, 就是后面要用nest的一些命令去生成模块等会比较麻烦。

npx @nestjs/cli new 项目名 

前端部分: 引入了axios的index.html

我们只需要用让nestjs可以返回public下面的html页面,然后在html页面引入axios lib, 就可以发起http请求了。

修改main.ts

// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { NestExpressApplication } from '@nestjs/platform-express';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  app.useStaticAssets('public');
  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

然后在public目录下新建一个Index.html,添加如下内容。接下来我们不同的请求方式就会在这五个不同的html文件里面写。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <script src="https://unpkg.com/axios@0.24.0/dist/axios.min.js"></script>
  <script src="https://unpkg.com/qs@6.10.2/dist/qs.js"></script>
  <title>Document</title>
</head>

<body>
  <h1>HTTP 传参方式示例</h1>
  <ul>
    <li><a href="/path-param.html">1. Path Params (路径参数)</a></li>
    <li><a href="/query-param.html">2. Query Strings (查询字符串)</a></li>
    <li><a href="/body-json.html">3. Request Body (JSON 格式)</a></li>
    <li><a href="/form-urlencoded.html">4. Form (URL Encoded - 原生提交)</a></li>
    <li><a href="/form-multipart.html">5. Form (Multipart - 文件上传)</a></li>
  </ul>
</body>

</html>

启动工程,访问http://localhost:3000/index.html,可以访问到这个html页面

npm run start:dev

Image

后端部分

新建一个模块,用于提供后端服务。这样前端就可以在html去访问这个person模块提供的接口。

nest generate resource person

Http请求方式

路径参数: Path Param

这种请求模式就是直接把查询参数放在url上,通常用来代表路径有比较明确的层级关系,比如orders/orderId。而且后面的查询参数是必填的,不像url query string的?aaa=bbb这种形式,不带查询参数也可以正确匹配到路由。

前端请求就是直接在url后面带上参数,后端要先在方法上定义好参数的次序,然后就可以用@Param(参数路径名)去取出来。

path-param.html: 路径里面包含参数,需要手动对参数做一次url encode,不然可能会因为参数里面包含特殊字符,可能会导致路径的解析错误。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="https://unpkg.com/axios@0.24.0/dist/axios.min.js"></script>
  <script src="https://unpkg.com/qs@6.10.2/dist/qs.js"></script>
</head>

<body>
  <h2>query by path parameter</h2>
  <script>
    // 如果不encode就匹配不到正确的路径了。因为参数的/会导致匹配到下一层路径
    const queryString = encodeURIComponent('李/光');
    axios.get(`/api/person/123/${queryString}`)
      .then(response => {
        console.log(response.data);
      })
      .catch(error => {
        console.error(error);
      });
  </script>
</body>

</html>

person.controller.ts: 通过@Param取出参数

import { Controller, Get, Param, Query } from '@nestjs/common';
import { PersonService } from './person.service';

@Controller('api/person')
export class PersonController {
  constructor(private readonly personService: PersonService) {}

  @Get(':id/:name')
  getPersonById(@Param('id') id: string, @Param('name') name: string) {
    return `received: id=${id}, name=${name}`;
  }
}

Query String

这种方式前端请求过来就是在url后面拼接?key=value的形式,而后端就是用@Query key取出每个参数。

@Controller('api/person')
export class PersonController {
  constructor(private readonly personService: PersonService) {}

  @Get('find')
  findPerson(@Query('name') name: string) {
    return `received: name=${name}`;
  }
}

前端推荐使用params的写法,因为axios会自动帮我们做encode。

axios.get(`/api/person/find`, { params: { name: 'C++', age: 30 } })
       .then(response => {
         console.log(response.data);
       })
       .catch(error => {
         console.error(error);
       });

上面这个例子,如果前端用拼接字符串的写法,就要手动encode,不然后端由于历史问题,会把+转成空格。

let queryString = `name=${encodeURIComponent('C++')}&age=30`;
    // 或者用库
    let queryString = Qs.stringify({ name: 'C++', age: 30 });
    
    const url = `/api/person/find?${queryString}`)

Post Json

这种是最常用的请求方式,前端用post请求传参数,后端用@body直接取出来整个对象。这种方式请求的数据会被转成Json字符串,放在request body里面,不需要考虑encode的事情,因为json字符串本身已经是一个安全的纯文本了


<body>
  <h2>POST JSON 请求示例</h2>
  <script>
    const data = {
      name: '李光',
      age: 30
    };

    axios.post('/api/person/create', data)
      .then(response => {
        console.log(response.data);
      })
      .catch(error => {
        console.error(error);
      });
  </script>
</body>

  @Post('create')
  createPerson(@Body() createPersonDto: any) {
    return `received: ${JSON.stringify(createPersonDto)}`;
  }

Form-urlencoded

我们在页面上写一个form请求,就会触发这样的http请求。浏览器会自动对请求的参数做encode后放在request body里面。

Content-type: "application/x-www-form-urlencoded"

<body>
  <form action="api/person/submit-form" method="post">
    <label for="name">姓名:</label>
    <input type="text" id="name" name="name"><br><br>

    <label for="age">年龄:</label>
    <input type="number" id="age" name="age"><br><br>

    <input type="submit" value="提交">
  </form>
</body>

Image

所以在nestjs端,也是通过@body取出参数。

@Controller('api/person')
export class PersonController {
  constructor(private readonly personService: PersonService) {}

  @Post('/submit-form')
  submitForm(@Body() formData: any) {
    return `received: ${JSON.stringify(formData)}`;
  }
}

普通AJAX POST适合"前端发请求、拿数据、自己渲染"的场景; 而form-submit适合"我要把数据交给另一个页面/另一个域名,然后让浏览器替我跳过去"的场景。在实际使用中,主要用于以下场景:

支付

前端通过动态生成并提交表单(Form Submit)跳转到支付中心是非常经典的方案。主要有这些优势:

  1. 利用浏览器的原生行为实现接管: 表单提交会触发浏览器的默认跳转行为,相当于直接进入了支付中心的网站,有利于支付环境的纯净和安全。
  2. 避免跨域问题:在浏览器中,第三方商铺请求支付中心的页面,会出现跨域报错,需要支付中心做白名单限制才可以。而使用form请求就没有这个问题。

典型的支付核心交互流程:

  1. 发起下单:前端请求后端
  2. 后端请求支付中心接口并组装数据:后端按支付中心的要求,生成订单号、金额和签名以及一些redirect的targetUrl和formAttribute,把组装好的数据返回给前端。
  3. 前端拿到返回数据库,拼装form进行跳转:前端拿到参数后,在内存中隐式构建出一个 <form> 元素,塞入隐藏的 <input>,调用 form.submit()跳转到支付中心
单点登录

登录的流程如下:

  1. 前端跳转到登录中心:通常是直接通过window.location.href=“https://sso.../?client\_id=xxx”实现跳转,因为第一次跳转的参数不多,也没有什么私密到不能放在url上的信息
  2. 登录后,登录中心跳回来后端: 在SSO认证中心输入了账号密码并且验证成功后,登录中心要带着一些私密信息请求我们的后端,这个过程是通过返回一个html给浏览器, 然后在浏览器中执行form.submit来实现的。

Image

  1. 后端拿到登录中心的认证信息后,再跳转回前端。
文件下载

文件下载是依赖于浏览器的默认行为,就是当收到一个请求的Response数据中包含Content-Disposition: attachment; filename=文件名,浏览器就会把文件保存起来。而要对文件下载的接口发起请求,通常有两种做法:

  1. 通过get请求来做,就是通过window.location.href=xxx或者a标签触发get请求,跳转到文件下载的api接口,收到响应后浏览器会自动下载文件
  2. 通过form post: 当请求的参数过多或者不能放在url上面,就可以通过form post来做。

那我们为什么不能通过AJAX(Ajax/Axios/Fetch)的方式来下载文件呢?因为跳转 =\> 下载符合浏览器的默认行为规范,而Ajax只会请求后去获取二进制文本流,要实现下载功能,需要手动地把二进制文本流生成一个临时的url,然后模拟点击才能实现,过程很繁琐,而且可能会出现内存溢出,跨域时读取不到文本名的问题。

Image

Code Sample

<body>
  <h1>文件下载示例 (Form POST)</h1>
  <p>填写下方表单,后端会将填写的内容生成一个 .txt 文件并触发浏览器下载。</p>

  <form action="/api/person/download" method="post">
    <label for="name">姓名:</label>
    <input type="text" id="name" name="name" value="张三"><br><br>

    <label for="age">年龄:</label>
    <input type="number" id="age" name="age" value="25"><br><br>

    <button type="submit">提交并下载文件</button>
  </form>

  <hr>
  <p>说明:点击提交后,浏览器会跳转到后端接口。由于后端设置了 <code>Content-Disposition: attachment</code>,浏览器不会显示页面内容,而是直接弹出下载任务。</p>
</body>
@Controller('api/person')
export class PersonController {
  constructor(private readonly personService: PersonService) {}

  @Post('download')
  @Header('Content-Type', 'text/plain')
  @Header('Content-Disposition', 'attachment; filename="hello.txt"')
  downloadFile(@Body() body: any) {
    return `Hello, ${body.name || 'Guest'}! This is a file downloaded via POST. Your age is ${body.age || 'unknown'}.`;
  }
}

这里的Content-Type,如果我们不知道是什么类型,可以写application/octet-stream,代表任意二进制类型。其他的类型可以用到了再去查,比如text/plain是纯文本,text.csv是csv数据文件,application/pdf是pdf.

文件上传form-multipart

对于文件上传,前端需要定义好content-type: 'multipart/form-data'。

下面这个例子,当想要上传多个文件,就是在点击上传的时候,按住command键,选中多个文件,一次性上传多个文件,浏览器会自动在不同文件间添加分隔符。

<!DOCTYPE html>
<html lang="en">
<head>
    <script src="https://unpkg.com/axios@0.24.0/dist/axios.min.js"></script>
</head>
<body>
    <h2>上傳多個文件</h2>
    <input id="fileInput" type="file" multiple/>
    <button id="uploadBtn">開始上傳</button>

    <script>
        const fileInput = document.querySelector('#fileInput');
        const uploadBtn = document.querySelector('#uploadBtn');

        uploadBtn.onclick = async function () {
            // 確保有選擇文件
            if (fileInput.files.length === 0) {
                alert('請先選擇文件');
                return;
            }

            const data = new FormData();
            
            // 【重點】動態讀取所有選中的文件,並以相同的欄位名稱 'files' 加入
            for (let i = 0; i < fileInput.files.length; i++) {
                data.append('files', fileInput.files[i]);
            }

            try {
                const res = await axios.post('/api/person/file', data, {
                    headers: { 'content-type': 'multipart/form-data' }
                });
                console.log('上傳成功:', res.data);
            } catch(e) {
                console.error('上傳失敗:', e);
            }
        }
    </script>
</body>
</html>

Nestjs解析文件内容需要用拦截器AnyFilesInterceptor,对于文件的内容用@UploadedFiles取出来,对于非文件内容仍然用@Body()取出来。另外这里需要引入这个包来定义类型。

npm i -D @types/multer
@Controller('api/person')
export class PersonController {
  constructor(private readonly personService: PersonService) {}

  @Post('file')
  @UseInterceptors(AnyFilesInterceptor())
  body2(@Body() body: any, @UploadedFiles() files: Array<Express.Multer.File>) {
    console.log(files);
    return `received: \n body: ${JSON.stringify(body)} \n files: ${JSON.stringify(files)}`;
  }
}

supportlss
230 声望16 粉丝

引用和评论

0 条评论