Form & AJAX & Fetch

1,012 阅读5分钟
原文链接: zhuanlan.zhihu.com

前后端数据交互的渠道没几种,常用的有 Form 表单、AJAX、Fetch,三种方式大同小异,这篇文章的目的是把这件小事说清楚。

Form 表单传输普通数据

Form 表单不存在跨域的问题,地址栏可直接打开,例如我的地址是 file:///C:/Users/Kang/Desktop/form-ajax-fetch/01_form/index.html ,所有以表单提交数据的代码也都是以这种方式打开的。

form 表单有个 enctype 属性,当值为 application/x-www-form-urlencoded 时(默认也是这个),传输的数据格式是(注意数据格式很关键,因为后端要根据此进行解析):

username=weixian&age=18

后端(这里的后端一律指 Node)怎么解析接收到的上面的数据呢?前端 method 为 GET 时,通过解析 req.url 就可以得到数据。当 method 为 POST 时,就需要监听 req 的 data 事件去接收了,具体代码如下:

const http = require('http');
const url = require('url');
const querystring = require('querystring');

let server = http.createServer((req, res) => {
    // req.url => /login?username=weixian&password=aaa
    let { pathname, query } = url.parse(req.url, true);
    // 在 Node 中取到的方法名永远是大写的
    let method = req.method.toLowerCase();
    if (pathname === '/login') {
        if (method === 'get') {
            res.end(JSON.stringify(query));
        } else {
            let buffers = [];
            req.on('data', chunk => {
                buffers.push(chunk);
            });
            req.on('end', () => {
                let str = Buffer.concat(buffers).toString();

                let result = JSON.stringify(querystring.parse(str));

                res.end(result);
            });
        }
    }
});

server.listen(4000);

点击提交按钮后,会发现页面跳转至 http://localhost:4000/login ,并在页面上显示了返回的数据。

上面代码见:01_form 文件夹

Form 表单传输文件

文件在数据传输中是一个特殊的存在,需要设置 enctype="multipart/form-data" 对其进行编码,当然也必须是以 POST 的方式进行传输(GET 方式的传输编码只能是 application/x-www-form-urlencoded,写其他的也没用),传输的数据格式是:

------WebKitFormBoundaryaAuaxgzJrbdHZAij
Content-Disposition: form-data; name="username"

weixian
------WebKitFormBoundaryaAuaxgzJrbdHZAij
Content-Disposition: form-data; name="password"

18
------WebKitFormBoundaryaAuaxgzJrbdHZAij
Content-Disposition: form-data; name="avatar"; filename=""
Content-Type: application/octet-stream

------WebKitFormBoundaryaAuaxgzJrbdHZAij--

那么问题来了,后端是怎么解析上面的奇怪数据呢?直接写的话比较麻烦,有各种各样的包已经帮我们解决了这个问题,下面是使用 formidable 进行解析的后端代码:

const http = require('http');
const url = require('url');
const formidable = require('formidable');
const path = require('path');

let server = http.createServer((req, res) => {
    let { pathname, query } = url.parse(req.url, true);
    // 在node中取到的方法名永远是大写的
    let method = req.method.toLowerCase();
    if (pathname === '/form') {
        if (method === 'get') {
            res.end(JSON.stringify(query));
        } else {
            let form = new formidable.IncomingForm();
            // 不写路径就是临时文件夹:require('os').tmpDir()
            form.uploadDir = path.join(__dirname, "upload");

            form.parse(req, (err, fields, files) => {
                if (err) {
                    console.log(err);
                } else {
                    res.writeHead(200, { 'content-type': 'application/json' });
                    res.end(JSON.stringify({ fields: fields, files: files }));
                }
            });
        }
    }
});

server.listen(4000);

上面代码见:02_form 文件夹

Form 表单的前后端应用就说完了,其实关键东西就那么多,当前原理说起来又是一堆,这里先不表。

AJAX GET 传输数据

Form 表单提交数据有个小小的问题,就是页面会跳转(其实也可以解决,参见说清楚文件上传),这往往不是我们需要的,于是 AJAX 出现了。AJAX 则存在跨域问题(Form 不存在),所以我们最好搭个简单的服务以运行运行后面的代码。

AJAX GET 传输的数据是拼接在接口后面的一串字符串而已,前端代码如下:

let $ = document.querySelector.bind(document);

$('#btn').addEventListener('click', () => {
    let username = $('#username').value;
    let password = $('#password').value;

    let xhr = new XMLHttpRequest;
    xhr.open('GET', `/login?username=${username}&password=${password}`, true);
    
    xhr.onreadystatechange = function () {
        if (xhr.readyState === 4) {
            if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
                console.log(xhr.responseText);
            }
        }
    };
    xhr.send();
});

后端接收时也很简单,和 Form 表单 GET 形式的接收原理一样:

let express = require('express');
let app = express();

app.use(express.static(__dirname));

app.get('/login', (req, res) => {
    res.send(req.query);
});

app.listen(4000);

上面代码见:03_AJAX 文件夹

AJAX POST 传输普通数据

AJAX POST 数据传输时,是把数据放进 xhr.send 函数中,但放进去的数据需要长什么样(什么样的数据格式),是需要 xhr.setRequestHeader 这个属性进行设置的。

xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); 对应的数据格式就是 username=weixian&age=19

xhr.setRequestHeader('Content-Type', 'application/json'); 很明显对应的数据格式就是一个对象字符串,例如:

let $ = document.querySelector.bind(document);

$('#btn').addEventListener('click', () => {
    let username = $('#username').value;
    let password = $('#password').value;

    let xhr = new XMLHttpRequest();
    xhr.open('POST', '/login', true);

    // 注意这里的 Content-Type 而不是 ContentType,被坑了半个钟头
    xhr.setRequestHeader('Content-Type', 'application/json');

    xhr.onreadystatechange = function () {
        if (xhr.readyState === 4) {
            if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
                console.log(xhr.responseText);
            }
        }
    };
    // 这里的数据格式是由 xhr.setRequestHeader 决定的
    xhr.send(JSON.stringify({
        username,
        password
    }));
});

后端解析 POST 类型的数据,我们还是可以像 Form 表单时那样通过 req.on('data'),但这样确实有点麻烦,同样有各种各样的包(中间件)已经帮我们解决了这个问题。这里我们用 body-parser 中间件去解析 POST 类型的数据,代码如下:

let express = require('express');
let app = express();

app.use(express.static(__dirname));

// 不再用 req.on('data') 接收 POST 数据了,太麻烦
let bodyParser = require('body-parser');

// 这里中间件的使用方式要和前端对应上
// 例如前端是 application/x-www-form-urlencoded,这里就是 bodyParser.urlencoded({...})
// 前端是 application/json,这里就是 bodyParser.json({...})
// 前端啥都不设置,这里可以 bodyParser.text({...})
app.use(bodyParser.json({
    extended: true
}));

app.get('/login', (req, res) => {
    res.send(req.query);
});

app.post('/login', (req, res) => {
    // 通过 req.body 取到最终解析后的结果
    res.send(req.body);
});

app.listen(4000);

上面代码见:04_ajax 文件夹

AJAX POST 传输文件

涉及到文件传输时,我们用到了 FormData,它也是属于 xhr.send 可接受的参数之一,这里有一点需要注意:涉及到 FormData 时,不要设置 xhr.setRequestHeader('Content-Type', 'xxx'); 它有自己的一套规范,会在请求时自动加上,前端代码如下:

let $ = document.querySelector.bind(document);

let oFile = $('#file');
let con;
oFile.onchange = function (e) {
    con = e.target.files[0]
};

$('#btn').addEventListener('click', () => {
    let username = $('#username').value;
    let password = $('#password').value;

    let fd = new FormData();
    fd.append('username', username);
    fd.append('password', password);
    fd.append('file', con);

    let xhr = new XMLHttpRequest;
    xhr.open('POST', `/login`, true);

    // 注意涉及到 FormData 时,不要设置 xhr.setRequestHeader('Content-Type', 'xxx'); 它有自己的一套规范,会在请求时自动加上

    xhr.onreadystatechange = function () {
        if (xhr.readyState === 4) {
            if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
                console.log(xhr.responseText);
            }
        }
    };

    xhr.send(fd);
});

后端解析 FormDate 格式的数据时,比较成熟的包有 multiparty、multer,这里还是使用 formidable,代码如下:

const express = require('express');
const app = express();
const formidable = require('formidable');
const path = require('path');

app.use(express.static(__dirname));

app.post('/login', (req, res) => {
    let form = new formidable.IncomingForm();
    // 不写路径就是临时文件夹:require('os').tmpDir()
    form.uploadDir = path.join(__dirname, "upload");

    form.parse(req, (err, fields, files) => {
        if (err) {
            console.log(err);
        } else {
            res.writeHead(200, { 'content-type': 'application/json' });
            res.end(JSON.stringify({ fields: fields, files: files }));
        }
    });
});

app.listen(4000);

以上代码见:05_ajax 文件夹

AJAX 的一些常用配置

  • xhr.setRequestHeader,可以设置请求头的信息,除了 Content-Type,connection,cookie 等一些固定的头,我们还可以自定义一些头信息带上我们想带的数据
  • xhr.getResponseHeader/xhr.getResponseHeaderAll,可以获取响应头信息,注意并不是所有的响应头信息都是可以拿到的,这里有讲究
  • xhr.responseType,用于指定 xhr.response 的类型,例如设置 xhr.responseType='json'; 后,会自动帮我们格式化后端返回的数据,相当于 JSON.parse 了一下,前端对应接收时要通过 xhr.response,而不再是 xhr.responseText 了,其他还可以设置为 blob、arrayBuffer、text 等等,这里和可 send 的类型是相对应的,前端对应的接受方式也会有差异
  • xhr.timeout,可以设置超时的时间,单位是毫秒,过了这个时间还没有数据返回则会触发 xhr.ontimeout 事件,我们可以在里面做一些超时处理
  • xhr.onerror,断网了会触发
  • xhr.onprogress,等等等一堆事件,做进度时需要用到
  • xhr.withCredentials,配置 cookie 等认证的信息,例如默认情况下 AJAX 在发送跨域请求时,是不能携带 cookie 信息的,除非 xhr.withCredentials 设置为 true,服务端也要进行对应的设置 Access-Control-Allow-Credentials:true(即允许携带认证信息),此时服务端也不能 Access-Control-Allow-Origin: *,要设置为对应的域名
  • ...

代码见:06_ajax 文件夹

AJAX 封装 和 AJAX 与 Form 的配合使用

在用 AJAX 提交数据时,我们可以再用 form 表单包裹一次(按理说不用再包裹了),目的是为了更方便的序列化数据,再配合 submit 事件也可以利用 input 的一些校验特性,例如 required 等等(只有 form 表单包着通过 submit 触发时才生效)。

HTML 代码:

<form id="form">
    <!-- 写name方便获取值,但并不是和form形式的提交一样name是必须的 -->
    用户名:
    <input type="text" name="username">
    <br> 密码:
    <input type="text" name="password" required>
    <br>
    <input type="submit" id="btn" value="提交">
</form>

JS 代码

$('#form').addEventListener('submit', (e) => {
    e.preventDefault();

    let data = serialized($('#form'));
    ajax({
        url: '/login',
        method: 'post',
        dataType: 'json',
        contentType: 'application/json',
        data
    }).then(data => {
        console.log(data);
    });
});

完整代码见:07_ajax 文件夹

Fetch

注意一点就行,上传 FormData 数据时,Fetch 和 AJAX 类似,不要在 headers 里面设置 Content-Type,它有自己的一套规范,会在请求时自动加上。其他和 AJAX 原理一样,只是使用方式的差异,详见 API

let $ = document.querySelector.bind(document);

let userName = $('#username'),
    passWord = $('#password'),
    fileBtn = $('#file'),
    btn = $('#btn');

btn.addEventListener('click', () => {
    let fd = new FormData();
    fd.append('userName', userName.value);
    fd.append('passWord', passWord.value);
    fd.append('file', fileBtn.files[0]);

    fetch('http://localhost:4000/login', {
        method: 'POST',
        body: fd,
    }).then(res => {
        if (res.ok) {
            return res.json();
        } else {
            console.log('error')
        }
    }).then(res => {
        console.log('res is', res);
    });
});

完整代码见:08_fetch 文件夹


题图来源:Joshua Earle