基于Node的React图片上传组件实现
写在前面
红旗不倒,誓把javascript进行到底!今天介绍我的开源项目 royal 里的图片上传的前后端实现原理(react + node),花了一些时间,希望对你有所帮助。
前端实现
遵循react 组件化的思想,我把图片上传做成了一个独立的组件(没有其他依赖),直接import即可。
import react, { component } from 'react' import upload from '../../components/formcontrols/upload/' //...... render() { return (
uri 参数是必须传的,是图片上传的后端接口地址,接口怎么写下面会讲到。
渲染页面
组件render部分需要体现三个功能:
图片选取(dialog窗口)可拖拽功能(拖拽容器)可预览(预览列表)上传按钮 (button)上传完成图片地址和链接 (信息列表)渲染图片预览列表
_renderpreview() { if (this.state.files) { return this.state.files.map((item, idx) => { return (
{this.state.progress[idx]}
) }) } else { return null } }渲染图片上传信息列表
_renderuploadinfos() { if (this.state.uploadhistory) { return this.state.uploadhistory.map((item, idx) => { return (); }) } else { return null; } }
文件上传
前端要实现图片上传的原理就是通过构建formdata对象,把文件对象append()到该对象,然后挂载在xmlhttprequest对象上 send() 到服务端。
获取文件对象
获取文件对象需要借助 input 输入框的 change 事件来获取 句柄参数 e
onchange={(e)=>this.handlechange(e)}
然后做以下处理:
e.preventdefault() let target = event.target let files = target.files let count = this.state.multiple ? files.length : 1 for (let i = 0; i < count; i++) { files[i].thumb = url.createobjecturl(files[i]) } // 转换为真正的数组 files = array.prototype.slice.call(files, 0) // 过滤非图片类型的文件 files = files.filter(function (file) { return /image/i.test(file.type) })
这时 files 就是 我们需要的文件对象组成的数组,把它 concat 到原有的 files里。
this.setstate({files: this.state.files.concat(files)})
如此,接下来的操作 就可以 通过 this.state.files 取到当前已选中的 图片文件。
利用promise处理异步上传
文件上传对于来说是异步的,为了处理 接下来的多图上传,这里引入了 promise来处理异步操作:
upload(file, idx) { return new promise((resolve, reject) => { let xhr = new xmlhttprequest() if (xhr.upload) { // 上传中 xhr.upload.addeventlistener("progress", (e) => { // 处理上传进度 this.handleprogress(file, e.loaded, e.total, idx); }, false) // 文件上传成功或是失败 xhr.onreadystatechange = (e) => { if (xhr.readystate === 4) { if (xhr.status === 200) { // 上传成功操作 this.handlesuccess(file, xhr.responsetext) // 把该文件从上传队列中删除 this.handledeletefile(file) resolve(xhr.responsetext); } else { // 上传出错处理 this.handlefailure(file, xhr.responsetext) reject(xhr.responsetext); } } } // 开始上传 xhr.open("post", this.state.uri, true) let form = new formdata() form.append("filedata", file) xhr.send(form) }) }
上传进度计算
利用xmlhttprequest对象发异步请求的好处是可以 计算请求处理的进度,这是fetch所不具备的。
我们可以为 xhr.upload 对象的 progress 事件添加事件监听:
xhr.upload.addeventlistener("progress", (e) => { // 处理上传进度 this.handleprogress(file, e.loaded, e.total, i); }, false)
说明:idx参数是纪录多图上传队列的索引
handleprogress(file, loaded, total, idx) { let percent = (loaded / total * 100).tofixed(2) + '%'; let _progress = this.state.progress; _progress[idx] = percent; this.setstate({ progress: _progress }) // 反馈到dom里显示 }
拖拽上传
拖拽文件对于html5来说其实非常简单,因为它自带的几个事件监听机制可以直接做这类处理。主要用到的是下面三个:
ondragover={(e)=>this.handledraghover(e)} ondragleave={(e)=>this.handledraghover(e)} ondrop={(e)=>this.handledrop(e)}
取消拖拽时的浏览器行为:
handledraghover(e) { e.stoppropagation() e.preventdefault() }
处理拖拽进来的文件:
handledrop(e) { this.setstate({progress:[]}) this.handledraghover(e) // 获取文件列表对象 let files = e.target.files || e.datatransfer.files let count = this.state.multiple ? files.length : 1 for (let i = 0; i < count; i++) { files[i].thumb = url.createobjecturl(files[i]) } // 转换为真正的数组 files = array.prototype.slice.call(files, 0) // 过滤非图片类型的文件 files = files.filter(function (file) { return /image/i.test(file.type) }) this.setstate({files: this.state.files.concat(files)}) }
多图同时上传
支持多图上传我们需要在组件调用处设置属性:
multiple = { true } // 开启多图上传 size = { 50 } // 一次最大上传数量(虽没有上限,为保证服务端正常,建议50以下)
然后我们可以使用 promise.all() 处理异步操作队列:
handleupload() { let _promises = this.state.files.map((file, idx) => this.upload(file, idx)) promise.all(_promises).then( (res) => { // 全部上传完成 this.handlecomplete() }).catch( (err) => { console.log(err) }) }
好了,前端工作已经完成,接下来就是node的工作了。
后端实现
为了方便,后端采用的是express框架来快速搭建http服务和路由。具体项目见我的github node-image-upload。逻辑虽然简单,但还是有几个可圈可点的地方:
跨域调用
本项目后端采用的是express,我们可以通过 res.header() 设置 请求的 “允许源” 来允许跨域调用:
res.header('access-control-allow-origin', '*');
设置为 * 说明允许任何 访问源,不太安全。建议设置成 你需要的 二级域名,如 jafeney.com。
除了 “允许源” ,其他还有 “允许头” 、”允许域”、 “允许方法”、”文本类型” 等。常用的设置如下:
function allowcross(res) { res.header('access-control-allow-origin', '*'); res.header('access-control-allow-headers', 'content-type, content-length, authorization, accept, x-requested-with , yourheaderfeild'); res.header('access-control-allow-methods', 'put, post, get, delete, options'); res.header("x-powered-by",' 3.2.1') res.header("content-type", "application/json;charset=utf-8"); }
es6下的ajax请求
es6风格下的ajax请求和es5不太一样,在正式的请求发出之前都会先发一个 类型为 options的请求 作为试探,只有当该请求通过以后,正式的请求才能发向服务端。
所以服务端路由 我们还需要 处理这样一个 请求:
router.options('*', function (req, res, next) { res.header('access-control-allow-origin', '*'); res.header('access-control-allow-headers', 'content-type, content-length, authorization, accept, x-requested-with , yourheaderfeild'); res.header('access-control-allow-methods', 'put, post, get, delete, options'); res.header("x-powered-by",' 3.2.1') res.header("content-type", "application/json;charset=utf-8"); next(); });
注意:该请求同样需要设置跨域。
处理上传
处理上传的图片引人了multiparty模块,用法很简单:
/*使用multiparty处理上传的图片*/ router.post('/upload', function(req, res, next) { // 生成multiparty对象,并配置上传目标路径 var form = new multiparty.form({uploaddir: './public/file/'}); // 上传完成后处理 form.parse(req, function(err, fields, files) { var filestmp = json.stringify(files, null, 2); var relpath = ''; if (err) { // 保存失败 console.log('parse error: ' + err); } else { // 图片保存成功! console.log('parse files: ' + filestmp); // 图片处理 processimg(files); } }); });
图片处理
node处理图片需要引入 gm 模块,它需要用 npm 来安装:
npm install gm --save
bug说明
注意:node的图形操作gm模块前使用必须 先安装 imagemagick 和 graphicsmagick,linux (ubuntu)上使用apt-get 安装:
sudo apt-get install imagemagick sudo apt-get install graphicsmagick --with-webp // 支持webp格式的图片
macos上可以用 homebrew 直接安装:
brew install imagemagick brew install graphicsmagick --with-webp // 支持webp格式的图片
预设尺寸
有些时候除了原图,我们可能需要把原图等比例缩小作为预览图或者缩略图。这个异步操作还是用promise来实现:
function resizeimage(paths, dstpath, size) { return new promise(function(resolve, reject) { gm(dstpath) .noprofile() .resizeexact(size) .write('.' + paths[1] + '@' + size + '00.' + paths[2], function (err) { if (!err) { console.log('resize as ' + size + ' ok!') resolve() } else { reject(err) }; }); }); }
重命名图片
为了方便排序和管理图片,我们按照 “年月日 + 时间戳 + 尺寸” 来命名图片:
var _datesymbol = new date().tolocaledatestring().split('-').join(''); var _timesymbol = new date().gettime().tostring();
至于图片尺寸 使用 gm的 size() 方法来获取,处理方式如下:
gm(uploadedpath).size(function(err, size) { var dstpath = './public/file/' + _datesymbol + _timesymbol + '_' + size.width + 'x' + size.height + '.' + _img.originalfilename.split('.')[1]; var _port = process.env.port || '9999'; relpath = 'https://' + req.hostname + ( _port!==80 ? ':' + _port : '' ) + '/file/' + _datesymbol + _timesymbol + '_' + size.width + 'x' + size.height + '.' + _img.originalfilename.split('.')[1]; // 重命名 fs.rename(uploadedpath, dstpath, function(err) { if (err) { reject(err) } else { console.log('rename ok!'); } }); });
总结
对于大前端的工作,理解图片上传的前后端原理仅仅是浅层的。我们的口号是 “把javascript进行到底!”,现在无论是 reactnative的移动端开发,还是nodejs的后端开发,前端工程师可以做的工作早已不仅仅是局限于web页面,它已经渗透到了互联网应用层面的方方面面,或许,叫 全栈工程师 更为贴切吧。
当然,全栈 两个字的分量很重,不积跬步,无以至千里,功力低下的我还需要不断修炼和实践!
下一篇: Aspjpeg添加水印完整方法