首发于 前端日记
Fetch API与POST请求参数格式那些事

Fetch API与POST请求参数格式那些事

简述

相信不少前端开发童鞋与后端联调接口时,都会碰到前端明明已经传了参数,后端童鞋却说没有收到,尤其是 post 请求,遇到的非常多。本文以 node.js 作为服务端语言,借用 express 框架,简要分析客户端发送 post 请求的四种方式以及服务端如何接收。本文客户端请求没有借助第三方 ajax 库,采用的是 Fetch API ,虽然浏览器兼容性有点问题,但是用法简洁灵活,以后可能会是一个趋势。在说 post 请求之前,先简要概述下 Fetch API

Fetch API

Fetch API 提供了一个获取资源的接口(包括跨域请求),提供了更强大和灵活的功能集。未来可能是 XMLHttpRequest 的一种替代方案。去年 GitHub 代码去 jQuery 重构时,就使用 Fetch API 替代 jQuery ajax ,毕竟目前 JavaScript 很多原生语法都进行了大量精简,比如 DOM 操作 API http 请求 fetch es6+ 等。今天的 axios 可能就是明日的 jQuery

简单的实例

Fetch API 主要暴露了三个接口一个方法。

  • 三个接口
  • Request (资源请求)
  • Response (请求的响应)
  • Headers ( Request/Response 头部信息)
  • 一个方法
  • fetch() (获取资源调用的方法)
// 实例化一个Request实例
// 第一个参数一般指资源路径
// 第二个参数可以理解为请求的配置项,包含头部信息和http请求一些关键配置(请求类型、参数...)
let requestInstance = new Request('/hello', {
    method: 'post',
    headers: {
        'Content-Type': 'application/json;charset=utf-8'
    body: '{"hello": "world"}'
// fetch方法参数同Request实例
// 第一个参数为url或者Request实例
// 第二个参数为请求配置项
fetch(requestInstance).then(response => {
    // 返回的是一个Response的实例
    // 调用Response实例的序列化方法,序列化成json,返回值是一个promise
    // 序列化方法有 json,text,formData,blob,arrayBuffer,redirct
    let result = response.json()
    result.then(res => {
        consolee.log(res)

有意思的特性

Fetch API 添加了一个实验性的功能,支持客户端手动取消 http 请求了,这个比较有意思,因为之前的 ajax 貌似都不支持手动取消。借助 AbortSignal 接口,可以通过 AbortController 实例化一个控制器,将实例的 siginal 当做请求的配置项,传递到服务端,客户端可以通过 AbortController 实例的 abort 方法,来终止当前的 http 请求,示例代码如下:

<template>
    <button id='btn'>中止请求</button>
</template>
<script>
    // 实例化controller
    var controller = new AbortController()
    // 获取实例的signal接口
    var signal = controller.signal
    let btn = document.getElementById('btn')
    // 点击按钮,中止请求
    btn.addEventListener('click', e => {
        controller.abort()
    // json方式提交数据
    const url = 'http://192.168.43.216:3000'
    // 将signal接口放到请求配置项中
    let testRequest = new Request(url + '/test', {
        method: 'post',
        headers: {
            'Content-Type': 'application/json;charset=utf-8;'
        body: '{"foo":"bar"}',
        signal
    fetch(testRequest).then(response => {
        let result = response.text()
        result.then(res => {
            console.log(res)
</script>

post请求四种传参方式

本文所说的前端传递数据格式相对于主流的 ajax 函数库有一定区别,一般的 ajax 函数库为了方便用户使用,都会对数据进行二次封装。本文主要说 原始的数据格式交互 ,具体 ajax 库的使用,还是以官方文档为准。 请求头( Request Headers )的实体 Content-Type 用于指示资源的 MIME 类型,即客户端传递消息的格式;响应头中 Content-Type 用于指示服务端返回消息的格式。所以在 http 请求中,我们可以从报文中的 Content-Type 属性来判断客户端-服务端消息传递的格式。

JSON提交

JSON 是常用的一种前后端数据接收格式。前端传递的是键值对数据,即对象( Object )。采用 JSON 传递参数,请求头 Content-Type application/json;charset=utf-8 ,其中 charset 为采用的字符集。

注意点:

  1. 既然为JSON提交,就要对参数进行序列化,即 JSON.stringify(params) ,否则传递到服务端的参数可能是 [Object object]
  2. 服务端( node.js )是以流的方式进行接收,接收完是一个 JSON 字符串,调用 JSON.parse(params) 可以对参数进行序列化

示例代码

客户端:

const url = 'http://192.168.43.216:3000'
let testRequest = new Request(url + '/test', {
    method: 'post',
    headers: {
        'Content-Type': 'application/json;charset=utf-8;'
    body: JSON.stringify({a: 1})
fetch(testRequest).then(response => {
    let result = response.text()
    result.then(res => {
        console.log(res)

服务端:

router.post('/test', (req, res, next) => {
    let data = ''
    req.on('data', chunk => {
        data += chunk
    req.on('end', () => {
        // 将JSON字符串解析成对象
        data = JSON.parse(data)
        res.send(data)

请求头提交

在实际开发中,遇到过不少后端开发,喜欢吧请求参数放在请求头,类似于 get 请求,即请求的参数是拼接在请求地址后面。个人觉得这种传参方式并不好,一般浏览器对 URL 长度是有限制的,以 Chrome 为例, URL 最大长度正在 7700 个字符左右,对于 post 请求来说,最好参数还是放在 body 中。

注意点

  1. 客户端请求参数拼接在 url 后,在 ? 后,键值对写法 a=1 ,多个键值对之间通过连接符 & 连接
  2. 服务端能够在 request 对象中,通过 request.query 直接进行接收
  3. 由于参数是拼接在 url 后面,所以请求头 Content-Type 无需设置

示例代码

客户端:

let queryStringRequest = new Request(`${url}/querystring?a=1&b=2`, {
    method: 'post'
fetch(queryStringRequest).then(response => {
    let result = response.json()
    result.then(res => {
        console.log(res)

服务端:

router.post('/querystring', (req, res, next) => {
    res.send(req.query)


普通表单提交

表单提交的方式有两种,一种是普通的表单提交,另外一种是通过 FormData 进行提交(主要应用在文件上传)。单纯的表单提交,与上述两种参数格式上还是存在一定的差别的,主要体现在以下几个方面。

  1. Content-Type 表单提交 Request Headers Content-Type application/x-www-form-urlencoded;charset=utf-8
  2. 参数 表单提交参数是放在 body 中,感觉是 JSON 和请求头提交的合体。参数位置与 JSON 提交相同,参数格式与请求头提交一致

示例代码

客户端:

let formRequest = new Request(url + '/form', {
    method: 'post',
    headers: {
        'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8;'
    body: 'a=1&b=2'
fetch(formRequest).then(response => {
    let result = response.json()
    result.then(res => {
        console.log(res)

服务端:

const fs = require('fs')
router.post('/form', (req, res, next) => {
    let data = ''
    req.on('data', chunk => {
        data += chunk
    req.on('end', () => {
        data = decodeURI(data)
        // 将a=1&b=2解析成{a: 1, b: 2}
        let dataObj = querystring.parse(data)
        res.send(dataObj)

FormData提交 (文件上传)

通常我们在进行文件上传时,都会采用表单提交。参数放在 body 中,只不过格式与普通的有差别,具体如下:

  1. 参数需要放在 FormData 的实例中,通过 append 进行参数的添加
  2. 请求头 Content-Type multipart/formdata

示例代码

客户端:

<template>
    <input type="file" id="uploadFile">
</template>
<script>
let $input = document.getElementById('uploadFile')
// 监听文件上传
$input.addEventListener('change', e => {
    let file = e.target.files[0]
    handleUploadFile(file)
function handleUploadFile (file) {
    let bean = new FormData()
    bean.append('file', file)
    bean.append('hello', 'world')
    let uploadFileRequest = new Request(`${url}/upload`, {
        method: 'post',
        headers: {
            'Content-Type': 'multipart/formdata'
        body: bean
    fetch(uploadFileRequest).then(response => {
        let result = response.text()
        result.then(res => {
            console.log(res)
</script>

服务端:

router.post('/upload', (req, res, next) => {
    let data = []
    let size = 0
    req.on('data', chunk => {
        data.push(chunk)
        size += chunk.length
    let rems = []
    req.on('end', () => {
        let buffer = Buffer.concat(data, size)
        for (let i = 0; i < buffer.length; i++) {
            var v = buffer[i];
            var v2 = buffer[i+1];
            if(v==13 && v2==10){
                rems.push(i);
        // 图片信息
        var picmsg_1 = buffer.slice(rems[0]+2,rems[1]).toString();
        var filename = picmsg_1.match(/filename=".*"/g)[0].split('"')[1];
        // 图片数据
        var nbuf = buffer.slice(rems[3]+2,rems[rems.length-2]);
        var path = './static/'+filename;