前后端数据交互的渠道没几种,常用的有 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