文件上传。
-
前端中与后台的交互无外乎表单的交互,如今的前端和后台交互主要采用的是Ajax发送JSON的形式来实现的,一些组件也封装了一些获取表单内容的方式。但是对于文件上传我之前一直使用的是 组件封装的Upload。所以之前对于文件上传就没有太关心,直到。。。。,先说用他人组件上传的方式把。
-
之前自己做的一个小项目需要上传头像,就使用到了文件上传,当时直接使用了
antd
的Upload组件,当时时间比较紧,做完以后没有进行后续的学习,所以对于文件上传一直很模糊,先看下当时使用的文件上传。(这里是独立出来的文件上传) -
对于antd也是简简单单的使用,当时也没有深入了解他的原理,所以就导致了使用仅仅是使用,一旦遇到了变通的需求就懵了。下面是基于antd的上传代码:
const UploadExample: React.FC = () => {
const uploadParam = {
name: 'testFile',
action: 'http://127.0.0.1:7001/uploadFile',
onChange: fileChange,
showUploadList:false
}
return (
<>
<h3>文件上传实例</h3>
<Upload
{...uploadParam}
>
<Button>上传文件</Button>
</Upload>
</>
)
}
- 实现的功能也比较简单,就是上传一个文件到指定的服务器,这里后续的上传回调可给定onChange事件中处理。这里没有给出代码。后台接受也比较简单,基于Egg官方文档里也给出了详细的实现。有兴趣可以去文档中查看。文档地址
- 基于文档中的实现方式,我本地也做了个小deme来接受前端传递的文件,具体的实现代码如下:
@POST('/uploadFile')
public async testUpload() {
const {ctx} = this;
const fileList:string[] = [];
for(const file of ctx.request.files) {
const filename = (new Date()).getTime() + Math.random().toString(36).substr(2) + path.basename(file.filename);
const fileValue = fs.readFileSync(file.filepath);
fs.writeFileSync(path.resolve(__dirname, `../public/img/${filename}`), fileValue);
await fs.unlink(file.filepath,() =>{});
fileList.push(filename);
}
this.returnSuccess({fileList: fileList.join(',')})
}
- 这里可以自己对文件进行处理,我自己是把他保存到了本地,返回文件名字。也可以上传到OSS,这里不再深入研究(主要咱也没有OSS)
- 然后本以为文件上传实现了,谁知事隔一年文件上传它又来了,这次来让我措手不及。
- 具体的需求是有一个表单,其中有表单内容和上传的文件需要一起传给后端,一听好像有点懵,之前使用组件的上传方式好像不行了,况且这次是jquery, 所以决定自己来实现表单的上传。具体的表单内容如下所示,不仅有表单数据,还有多个文件的上传。
- 经过查找,发现古老的form表单上传。。。记得之前写jsp的时候使用过给form设置action 然后点击按钮就直接上传了。就像下面的方式一样:
<form action="http://127.0.0.1:7001/formUpload" method = "post">
<div>
<input type="text" name="input" id="input">
</div>
<h4>select选择内容</h4>
<div>
<select name="select" id="select">
<option value="first">第一个</option>
<option value="second">第二个</option>
<option value="three">第三个</option>
</select>
</div>
<h4>文件区域</h4>
<div>
<input type="file" name="file1" id="file1">
<input type="file" name=file2" id="file2">
</div>
<input type="submit" value = "提交">
</form>
- 上面是比较传统的方式,只要点击提交浏览器会自动与action指定的服务器建立联系,这里使用的是post的方式,所以建立联系以后浏览器,浏览器就会按分段传输的方法将数据发送给服务器看起来是可行的,但是提交会自动跳转页面,这对于如今前后端分离的方式来说是十分不友好的,那么可以对其进行一些改进,自己对上传进行控制,通过Ajax的方式来发送数据。
- 通过ajax发送的话需要把form表单的默认提交事件给阻止掉,然后自己使用Ajax的方式上传,参数使用formData的方式,直接获取到表单中的数据,然后使用Ajax的方式发送。这样就自己实现了表单的默认提交方式。同时还可以自己实现对表单提交的校验。下面是实现的代码:
$("#formEle").submit(function(e) {
e.preventDefault();
const param = new FormData(this);
$.ajax({
url: 'http://127.0.0.1:7001/shuuy',
type: 'POST',
data: param,
dataType: 'JSON',
cache: false,
processData: false,
contentType: false,
success: function (data) {
console.log(data);
}
});
})
然后后台的处理就跟上面的方式一样了,只需要分开接受string类型的参数和文件参数。代码如下:
@POST('/shuuy')
public async buSHuuy() {
const { ctx } = this;
const fileList:string[] = [];
for (const file of ctx.request.files) {
const filename = (new Date()).getTime() + Math.random().toString(36).substr(2) + path.basename(file.filename);
let fileValue = fs.readFileSync(file.filepath)
fs.writeFileSync(path.resolve(__dirname, `../public/img/${filename}`), fileValue)
await fs.unlink(file.filepath,() =>{});
fileList.push(filename);
}
this.returnSuccess({ ...this.ctx.request.body, fileName: fileList.join(',') })
}
- 可以查看响应体可以看到响应的所有信息:
- 到这里基本上就实现了,然后到那边一看,好像跟自己想的不太一样,这里是使用form提交的方式,可是那边给出的页面直接是div 布局法,代码如下:
<div>
<input type="text" name="input" id="input">
</div>
<h4>select选择内容</h4>
<div>
<select name="select" id="select">
<option value="first">第一个</option>
<option value="second">第二个</option>
<option value="three">第三个</option>
</select>
</div>
<h4>文件区域</h4>
<div>
<input type="file" name="file1" id="file1">
<input type="file" name=file2" id="file2">
</div>
<button>表单提交</button>
- 同样需要通过ajax 把所有的内容发送到后台,基于这种情况就可以针对上边表单提交的方式进行改进:使用FormData的方式注入参数,然后通过Ajax发送请求。FormData对象可以添加键值对,和form键值对是一样的。同时也可以自己对其进行校验。
const formSubmit = () => {
const formData = new FormData();
const validator = [
{ dom: 'input', required: true, message: 'input是必须的' },
{ dom: 'select', required: true, message: 'select是必须的' },
{ dom: 'file1', required: true, message: 'file1是必须的', type: "file" },
{ dom: 'file2', required: true, message: 'file2是必须的',type: 'file'}
]
let flag = false;
validator.forEach(item => {
if (item.hasOwnProperty('dom') && item.hasOwnProperty('required')) {
if (item.required && !$(`#${item.dom}`).val()) {
!flag && item.hasOwnProperty('message') && alert(item.message);
flag = true;
}
}
})
if (flag) {
return;
}
validator.forEach(item => {
item.hasOwnProperty('type') && item.type === 'file' ? formData.append(item.dom, $(`#${item.dom}`)[0].files[0]) : formData.append(item.dom, $(`#${item.dom}`).val())
})
$.ajax({
url: 'http://127.0.0.1:7001/shuuy',
type: 'POST',
data: formData,
dataType: 'JSON',
cache: false,
processData: false,
contentType: false,
success: function (data) {
console.log(data);
}
});
}
- 至此需求大概算是实现了,然而问题又来了,因为后端使用某平台开发,所以导致了使用
multipart/form-data
提交的请求直接返回403(这里平台的上传是需要一个登录以后获得到的id的,因为没有所以就被拦截了),此路不通,好像没有办法可以走了,于是就回家了,第二天继续突然想到了能不能使用base64的方式使用JSON的方式提交。于是只能试一试了。我需要把文件转换为base64然后拼成键值对的方式提交。于是先要把文件转换为base64,之前没有接触过,找了个方法
const blodTo64 = (blod) => {
const reader = new FileReader();
reader.onload = function (e) {
console.log(e.target.result);
}
reader.readAsDataURL(blob);
}
- 由于该方法是异步的,又需要对多个文件转换,那么就封装个promise把
const blobToBase = (blob, key) => new Promise(resolve => {
const reader = new FileReader();
reader.readAsDataURL(blob);
reader.onload = function (e) {
resolve({ [key]: e.target.result })
}
})
const formSubmit = () => {
const input = $("#input").val();
const select = $("#select").val();
Promise.all([blobToBase($("#file1")[0].files[0], $("#file1")[0].files[0].name), blobToBase($("#file2")[0].files[0], $("#file2")[0].files[0].name)]).then(res => {
const value = res.reduce((prev, curr) => Object.assign(prev, curr), {});
const param = Object.assign({input,select},value);
$.ajax({
url: 'http://127.0.0.1:7001/formBaseUplaod',
type: 'POST',
data: JSON.stringify(param),
contentType:'application/json;charset=utf-8',
dataType: 'JSON',
success: function (data) {
console.log(data);
}
});
})
}
- 需求已经完成,交给了后台,但是这里也有自己的思考如果自己接收到这样一个这么大的base64数据,后台怎么处理呢? 自己后台写个小demo试试。
- 首先是跟传统的接口请求一样使用JSON的方式接收到数据,然后把base64转换为文件存储起来,最后返回前台路径。这里我只是简单的保存了一下,所以直接返回存储的文件名字。
- 后台代码,这里通过判断传递的内容是否是
data:
开头,如果是的话就可以直接拿到key当文件名字,然后写入文件。代码如下:
@POST('/formBaseUplaod')
public async formBaseUpload() {
const { ctx } = this;
const reqBody = ctx.request.body;
const fileList:string[] = [];
const formValue =<{index: string}> {}
for (const key in reqBody) {
if (typeof reqBody[key] === 'string') {
// 如果是 base64文件上传,就进行处理
if (reqBody[key].startsWith('data:')) {
const base64Data = reqBody[key].replace(/^data:image\/\w+;base64,/, "");
const dataBuffer = new Buffer(base64Data, 'base64');
const filename = (new Date()).getTime() + Math.random().toString(36).substr(2) + '.' + key;
fileList.push(filename);
await fs.writeFile(path.resolve(__dirname, `../public/img/${filename}`), dataBuffer,() => {});
}else {
formValue[key] = reqBody[key];
}
}
}
this.returnSuccess({...formValue,fileList: fileList.join(',')});
}