写在前面

红旗不倒,誓把JavaScript进行到底!今天介绍我的开源项目 Royal 里的图片上传组件的前后端实现原理(React + Node),花了一些时间,希望对你有所帮助。
前端实现
遵循React 组件化的思想,我把图片上传做成了一个独立的组件(没有其他依赖),直接import即可。
import React, { Component } from 'react'
import Upload from '../../components/FormControls/Upload/'
//......
render() {
return (
<div><Upload uri={'http://jafeney.com:9999/upload'} /></div>
)
}
uri 参数是必须传的,是图片上传的后端接口地址,接口怎么写下面会讲到。
渲染页面
组件render部分需要体现三个功能:
主render函数
render() {
return (
<form action={this.state.uri} method="post" encType="multipart/form-data">
<div className="ry-upload-box">
<div className="upload-main">
<div className="upload-choose">
<input
onChange={(v)=>this.handleChange(v)}
type="file"
size={this.state.size}
name="fileSelect"
accept="image/*"
multiple={this.state.multiple} />
<span ref="dragBox"
onDragOver={(e)=>this.handleDragHover(e)}
onDragLeave={(e)=>this.handleDragHover(e)}
onDrop={(e)=>this.handleDrop(e)}
className="upload-drag-area">
或者将图片拖到此处
</span>
</div>
<div className={this.state.files.length?
"upload-preview":"upload-preview ry-hidden"}>
{ this._renderPreview(); // 渲染图片预览列表 }
</div>
</div>
<div className={this.state.files.length?
"upload-submit":"upload-submit ry-hidden"}>
<button type="button"
onClick={()=>this.handleUpload()}
class="upload-submit-btn">
确认上传图片
</button>
</div>
<div className="upload-info">
{ this._renderUploadInfos(); // 渲染图片上传信息 }
</div>
</div>
</form>
)
}
渲染图片预览列表
_renderPreview() {
if (this.state.files) {
return this.state.files.map((item, idx) => {
return (
<div className="upload-append-list">
<p>
<strong>{item.name}</strong>
<a href="javascript:void(0)" rel="external nofollow"
className="upload-delete"
title="删除" index={idx}></a>
<br/>
<img src={item.thumb} className="upload-image" />
</p>
<span className={this.state.progress[idx]?
"upload-progress":
"upload-progress ry-hidden"}>
{this.state.progress[idx]}
</span>
</div>
)
})
} else {
return null
}
}
渲染图片上传信息列表
_renderUploadInfos() {
if (this.state.uploadHistory) {
return this.state.uploadHistory.map((item, idx) => {
return (
<p>
<span>上传成功,图片地址是:</span>
<input type="text" class="upload-url" value={item.relPath}/>
<a href={item.relPath} target="_blank">查看</a>
</p>
);
})
} 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 = 'http://' + 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页面,它已经渗透到了互联网应用层面的方方面面,或许,叫 全栈工程师 更为贴切吧。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
# react
# 图片上传组件
# 上传组件
# 上传图片
# ReactNative实现图片上传功能的示例代码
# react native实现往服务器上传网络图片的实例
# React+ajax+java实现上传图片并预览功能
# React中上传图片到七牛的示例代码
# React实现阿里云OSS上传文件的示例
# React Native使用fetch实现图片上传的示例代码
# react quill中图片上传由默认转成base64改成上传到服务器的方法
# React获取Java后台文件流并下载Excel文件流程解析
# react-native-fs实现文件下载、文本存储的示例代码
# react使用axios实现上传下载功能
# 上传
# 图片上传
# 后端
# 多图
# 的是
# 拖拽
# 服务端
# 几个
# 不太
# 文件上传
# 我们可以
# 转换为
# 进行到底
# 重命名
# 这是
# 图片处理
# 互联网
# 成了
# 好了
# 是有
相关文章:
如何在企业微信快速生成手机电脑官网?
大连网站制作公司哪家好一点,大连买房网站哪个好?
制作网站的过程怎么写,用凡科建站如何制作自己的网站?
网站制作企业,网站的banner和导航栏是指什么?
宿州网站制作公司兴策,安徽省低保查询网站?
Android自定义listview布局实现上拉加载下拉刷新功能
制作网站哪家好,cc、.co、.cm哪个域名更适合做网站?
如何安全更换建站之星模板并保留数据?
建站之星代理如何获取技术支持?
官网自助建站系统:SEO优化+多语言支持,快速搭建专业网站
建站之星安装后如何自定义网站颜色与字体?
如何在IIS7中新建站点?详细步骤解析
如何通过虚拟机搭建网站?详细步骤解析
详解ASP.NET 生成二维码实例(采用ThoughtWorks.QRCode和QrCode.Net两种方式)
简易网站制作视频教程,使用记事本编写一个简单的网页html文件?
香港服务器WordPress建站指南:SEO优化与高效部署策略
制作网站公司那家好,网络公司是做什么的?
已有域名和空间如何快速搭建网站?
怎么制作网站设计模板图片,有电商商品详情页面的免费模板素材网站推荐吗?
官网自助建站平台指南:在线制作、快速建站与模板选择全解析
广州美橙建站如何快速搭建多端合一网站?
建站之星安装需要哪些步骤及注意事项?
建站主机是否属于云主机类型?
SQL查询语句优化的实用方法总结
如何在VPS电脑上快速搭建网站?
广州建站公司哪家好?十大优质服务商推荐
网站制作大概要多少钱一个,做一个平台网站大概多少钱?
C++如何编写函数模板?(泛型编程入门)
微信网站制作公司有哪些,民生银行办理公司开户怎么在微信网页上查询进度?
,网页ppt怎么弄成自己的ppt?
如何彻底卸载建站之星软件?
如何在阿里云通过域名搭建网站?
网站网页制作专业公司,怎样制作自己的网页?
魔方云NAT建站如何实现端口转发?
c++怎么实现高并发下的无锁队列_c++ std::atomic原子变量与CAS操作【详解】
常州自助建站:操作简便模板丰富,企业个人快速搭建网站
义乌企业网站制作公司,请问义乌比较好的批发小商品的网站是什么?
非常酷的网站设计制作软件,酷培ai教育官方网站?
网站制作公司,橙子建站是合法的吗?
网站海报制作教学视频教程,有什么免费的高清可商用图片网站,用于海报设计?
如何在腾讯云服务器快速搭建个人网站?
网站网页制作电话怎么打,怎样安装和使用钉钉软件免费打电话?
网站制作公司排行榜,四大门户网站排名?
如何打造高效商业网站?建站目的决定转化率
零基础网站服务器架设实战:轻量应用与域名解析配置指南
如何优化Golang Web性能_Golang HTTP服务器性能提升方法
php json中文编码为null的解决办法
网站专业制作公司,网站编辑是做什么的?好做吗?工作前景如何?
详解jQuery停止动画——stop()方法的使用
如何在Golang中实现微服务服务拆分_Golang微服务拆分与接口管理方法
*请认真填写需求信息,我们会在24小时内与您取得联系。