egg.js 通过 form 和 ajax 两种方式上传文件并自定义目录和文件名
一、需求
egg.js 的文件上传个人觉得很一般,内置的 multipart 插件并不怎么好用。
egg-multipart 也是基于 co-busboy 实现的。
egg 官方给的文件上传的示例地址:
二、CSRF 校验
egg 文件的上传需要进行 csrf 校验,而且这个校验默认只支持放在 ctx.query._csrf 字段中,很不方便,每次文件上传都会携带。
习惯 laravel 或者某些 PHP 框架的人都知道,直接放在 <input type="hidden" name="_csrf" > 的字段中方便的多。(不过我默认试过放在 input 字段中,是不起作用的,其他方法我暂时没有尝试)
至于生成 csrf 的方式,可以在 cookie 中获取,cookie 中存放了 csrfToken=token,因此是可以通过前端获取的,这为通过 ajax 实现文件上传也提供了帮助。
如果在模板文件中使用的话,可以通过 {{ctx.csrf | safe}} 生成 token。
因为 token 必须放在 query._csrf 中,所以 form 的 html 如下:
<form action="/post/add?_csrf={{ctx.csrf | safe}}" method="POST" enctype="multipart/form-data">
</form>

三、通过 form 表单上传文件
表单 html 不过多描述,默认使用上面的 url 进行表单提交,路由也不具体说明。
文件上传的时候使用了额外的4个依赖,依赖如下:
| 依赖 | 说明 |
|---|---|
| stream-wormhole | 将 stream 流消耗掉 |
| await-stream-ready | 文件读写流 ready 库,能够使用 await |
await-stream-ready 主要是方便的使用 await 进行文件上传,而 stream-wormhole 是因为如果上传失败出现异常,那会导致浏览器响应崩溃,因此需要将 stream 消耗掉。
重点说下 controller 中的代码:
app/controller/post.js:
// 添加文章
async add(){
await this.ctx.render('post/add');
}
// 添加文章操作
async addAction(){
const {ctx} = this;
// 获取 steam
const stream = await ctx.getFileStream();
// 生成文件名
const filename = Date.now() + '' + Number.parseInt(Math.random() * 10000) + path.extname(stream.filename);
// 写入路径
const target = path.join(this.config.baseDir, 'app/public/upload/', filename);
const writeStream = fs.createWriteStream(target);
try {
// 写入文件
await awaitStreamReady(stream.pipe(writeStream));
} catch (err) {
// 必须将上传的文件流消费掉,要不然浏览器响应会卡死
await sendToWormhole(stream);
throw err;
}
ctx.body = stream.fields;
}
上面代码中,通过 await ctx.getFileStream() 获取到流,stream 是一个 FileStream 对象,有许多有用的属性,比如下面这些属性,其中 stream.filename 能获取文件的原始名称,上面 controller 代码中,便是使用 filename 获取到了文件的后缀。
当表单的 enctype 设置成 multipart/form-data 之后,便不能使用 ctx.request.body 获取其他字段,现在是个空对象。
要获取其他字段的名字,需要使用 stream.fields 来获取其他字段的值。
FileStream {
fieldname: 'photo', // 字段名
filename: '1.jpg', // 文件名
encoding: '7bit', // 编码
transferEncoding: '7bit',
mime: 'image/jpeg', // 类型
mimeType: 'image/jpeg',
fields: { title: '', description: '', author: '', content: '' } }
上传结果:
{"title":"","description":"","author":"","content":""}
四、通过 ajax 上传文件
ajax 文件上传后端代码基本不用变动,只是前端代码这边构建一个 formData 即可。
下面的代码是官方示例的给的,因为用 formData 进行 ajax 文件上传很多示例,不再重复,即使使用其他插件也是无所谓的,本质上 HTTP 请求的 Content-type 还是 multipart/form-data。
关键点在于 _csrf 的获取,方法 getCsrf() 是通过 cookie 获取 token 的方式,当然如果页面还是在模板中,那我觉得通过一个隐藏的值存放 csrf token,然后再去获取更加方便。
后端代码没什么变动的,本质是一样的。
$('form').submit(function(e) {
e.preventDefault();
var formData = new FormData();
formData.append('name', $('input[type=text]').val());
// Attach file
formData.append('image', $('input[type=file]')[0].files[0]);
// console.log(formData);
$.ajax({
url: '/ajax?_csrf=' + getCsrf(),
data: formData,
method: 'POST',
contentType: false, // NEEDED, DON'T OMIT THIS (requires jQuery 1.6+)
processData: false, // NEEDED, DON'T OMIT THIS
success: function(result) {
console.log(result);
},
error: function(responseStr) {
alert("error", responseStr);
}
});
// 通过 cookie 获取 csrf token
function getCsrf() {
var keyValue = document.cookie.match('(^|;) ?csrfToken=([^;]*)(;|$)');
return keyValue ? keyValue[2] : null;
}
});
五、自定义文件上传目录
按照我自己的习惯,我在上传文件的时候,自然是希望文件上传到 app/public/upload/20180707/xxxx.jpg,但是由于 writeStream 的限制,20180707 必然是需要确定文件目录存在的。
因此如果使用这种方式上传,则需要增加几行代码用来判断并且生成文件夹。
为了方便生成日期如 20180707 的目录,我用了 dayjs 库来格式化时间。
为了省事我直接用了
mkdirSync()
// 上传基础目录
const uplaodBasePath = 'app/public/upload/';
// 生成文件名
const filename = Date.now() + '' + Number.parseInt(Math.random() * 10000) + path.extname(stream.filename);
// 生成文件夹
const dirName = dayjs(Date.now()).format('YYYYMMDD');
// 判断文件夹是否存在,不存在则直接创建文件夹
if(! fs.existsSync()) fs.mkdirSync(path.join(this.config.baseDir,uplaodBasePath,dirName));
// 生成写入路径
const target = path.join(this.config.baseDir, uplaodBasePath, dirName, filename);
// 写入流
const writeStream = fs.createWriteStream(target);
最终结果:

最终的 controller 代码如下:
// 添加文章操作
async addAction(){
const {ctx} = this;
// 获取 steam
const stream = await ctx.getFileStream();
// 上传基础目录
const uplaodBasePath = 'app/public/upload/';
// 生成文件名
const filename = Date.now() + '' + Number.parseInt(Math.random() * 10000) + path.extname(stream.filename);
// 生成文件夹
const dirName = dayjs(Date.now()).format('YYYYMMDD');
if(! fs.existsSync()) fs.mkdirSync(path.join(this.config.baseDir,uplaodBasePath,dirName));
// 生成写入路径
const target = path.join(this.config.baseDir, uplaodBasePath, dirName, filename);
// 写入流
const writeStream = fs.createWriteStream(target);
try {
// 写入文件
await awaitStreamReady(stream.pipe(writeStream));
} catch (err) {
// 必须将上传的文件流消费掉,要不然浏览器响应会卡死
await sendToWormhole(stream);
throw err;
}
ctx.body = stream.fields;
}
egg.js 通过 form 和 ajax 两种方式上传文件并自定义目录和文件名的更多相关文章
- 三种方式上传文件-Java
前言:负责,因为该项目他(jetty嵌入式开始SpringMvc)实现文件上传的必要性,并拥有java文件上传这一块还没有被曝光.并 Http 更多晦涩协议.因此,这种渐进的方式来学习和实践上载文件的 ...
- Vue.js项目中使用 Ajax 和 FormDate 对象上传文件
let param = new FormData(); param.append("paths", this.ruleForm.uploadPath); param.append( ...
- 一个ajax实现表单上传文件的神器 formdata
通过传统的form表单提交的方式上传文件: $.ajax({ url : "http://localhost:8080/STS/rest/user", type : "P ...
- 通过Ajax方式上传文件,使用FormData进行Ajax请求
通过传统的form表单提交的方式上传文件: <form id= "uploadForm" action= "http://localhost:8080/cfJAX_ ...
- [转] 通过Ajax方式上传文件,使用FormData进行Ajax请求
通过传统的form表单提交的方式上传文件: <form id= "uploadForm" action= "http://localhost:8080/cfJAX_ ...
- 原生js更改css样式的两种方式
下面我给大家介绍的是原生js更改CSS样式的两种方式: 1通过在javascript代码中的node.style.cssText="css表达式1:css表达式2:css表达式3 &quo ...
- 用js实现九九乘法口诀两种方式
js实现九九乘法口诀两种方式: 第一种是用户输入一个数弹出所对应的乘法口诀: <script type="text/javascript"> function art( ...
- 【javascript】原生js更改css样式的两种方式
下面我给大家介绍的是原生js更改CSS样式的两种方式: 1通过在javascript代码中的node.style.cssText="css表达式1:css表达式2:css表达式3 &quo ...
- 两种方法上传本地文件到github
https://www.jianshu.com/p/c70ca3a02087 自从使用github以来,一直都是在github网站在线上传文件到仓库中,但是有时因为网络或者电脑的原因上传失败.最重要的 ...
随机推荐
- 洛谷 P3979 遥远的国度
题目描述 修改某条路径上的值以及询问子树的最小值都最树剖的基础操作,那么如何实现换根呢? 考虑一下三种情况: 1.rot=询问的子树x,答案就是整棵树的最小值 2.rot在x的子树里,只有rot到x这 ...
- nyar4psg: Cannot find a class or type named "MultiMarker"
Cannot find a class or type named "MultiMarker" 是一种常见错误,产生的原因是Library里面有1个以上的ar库. 以我的电脑为例, ...
- (3)Angular的开发
Angular CLI是一个命令行界面工具 创建项目.添加文件以及执行一大堆开发任务,比如测试.打包和发布 Angular CLI 功能与常用参数 使用Angular CLI 高效构建angular ...
- C++标准库分析总结(九)——<HashFunction、Tuple>
一.HashFunction 当我们在使用hash table以及由它做底层的数据结构时,我们必不可少要讨论hash function,所谓的哈希函数就是产生一个数,这个数越乱越好,以至于达到避免碰撞 ...
- reverse啥时候可以用
在做历史搜索记录的时候,当你想把最新的数据放到前面,可以用到,其实就是一个数组的反转. let array=[ '周小姐','好可爱的' ] var box=array.reverse() conso ...
- BurpSuite intuder里保存所有网页的特定内容:以bugku的cookies欺骗为例题
这题里想读取index.php只能一行一行的读,通过控制line参数的值.如下: 正常的writeup都是写个爬虫,但我觉得burp肯定有自带这种功能,不用重造轮子.经过学习后发现以下步骤可以实现. ...
- git工作总结
一.简单介绍 简介:Git是一个开源的分布式版本控制系统,可以有效.高速地处理项目版本管理. 发展史:CSV -> SVN -> Git 优点:Git速度快.开源.完全分布式管理系统 相关 ...
- 【转】JVM类装载机制的解析,热更新的探讨(二)
同样,一个Class对象必须知道自己的超类.超级接口.因此,Class对象会引用自己的超类和超级接口的Class对象.这种引用一定是实例引用.(实际上,超类.超级接口的引用也存储在常量池中,但为了区分 ...
- Python安装第三方库常用方法
在学习Python过程中,经常要用到很多第三方库,面对各种不同情况,Python为我们提供了多种安装方法: 一.pip安装: pip安装相信大家都不陌生了,在安装第三方库中,pip是最常使用的一种方法 ...
- 在java的Map集合中,怎样更改value的值
版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.csdn.net/chenyao1994/article/de ...