基于Node的React图片上传组件实现实例代码


Posted in Javascript onMay 10, 2017

写在前面

红旗不倒,誓把JavaScript进行到底!今天介绍我的开源项目 Royal 里的图片上传组件的前后端实现原理(React + Node),花了一些时间,希望对你有所帮助。

基于Node的React图片上传组件实现实例代码

前端实现

遵循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部分需要体现三个功能:

  1. 图片选取(dialog窗口)
  2. 可拖拽功能(拖拽容器)
  3. 可预览(预览列表)
  4. 上传按钮 (button)
  5. 上传完成图片地址和链接 (信息列表)

主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页面,它已经渗透到了互联网应用层面的方方面面,或许,叫 全栈工程师 更为贴切吧。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
javascript中的对象创建 实例附注释
Feb 08 Javascript
33个优秀的jQuery 教程分享(幻灯片、动画菜单)
Jul 08 Javascript
JS获取下拉列表所选中的TEXT和Value的实现代码
Jan 11 Javascript
JavaScript是如何实现继承的(六种方式)
Mar 31 Javascript
简单实现JS倒计时效果
Dec 23 Javascript
JS Testing Properties 判断属性是否在对象里的方法
Oct 01 Javascript
使用webpack打包koa2 框架app
Feb 02 Javascript
JavaScript数据结构与算法之检索算法实例分析【顺序查找、最大最小值、自组织查询】
Feb 22 Javascript
javascript操作元素的常见方法小结
Nov 13 Javascript
vue实现瀑布流组件滑动加载更多
Mar 10 Javascript
解决父组件将子组件作为弹窗调用只执行一次created的问题
Jul 24 Javascript
VUE子组件向父组件传值详解(含传多值及添加额外参数场景)
Sep 01 Javascript
JavaScript使用ZeroClipboard操作剪切板
May 10 #Javascript
VUE 更好的 ajax 上传处理 axios.js实现代码
May 10 #Javascript
vuejs2.0子组件改变父组件的数据实例
May 10 #Javascript
详解vue.js2.0父组件点击触发子组件方法
May 10 #Javascript
详解vue-router 2.0 常用基础知识点之router-link
May 10 #Javascript
JavaScript使用readAsDataURL读取图像文件
May 10 #Javascript
angular.js指令中的controller、compile与link函数的不同之处
May 10 #Javascript
You might like
全国FM电台频率大全 - 5 内蒙古自治区
2020/03/11 无线电
PHP Memcached + APC + 文件缓存封装实现代码
2010/03/11 PHP
php+mysql 实现身份验证代码
2010/03/24 PHP
php使用gettimeofday函数返回当前时间并存放在关联数组里
2015/03/19 PHP
原型方法的不同写法居然会影响调试的解决方法
2007/03/08 Javascript
ASP.NET jQuery 实例17 通过使用jQuery validation插件校验ListBox
2012/02/03 Javascript
JavaScript的各种常见函数定义方法
2014/09/16 Javascript
JS实现可调整倒计时间代码分享
2015/08/18 Javascript
总结jQuery插件开发中的一些要点
2016/05/16 Javascript
AngularJS应用开发思维之依赖注入3
2016/08/19 Javascript
基于JavaScript实现图片剪切效果
2017/03/07 Javascript
vue生成随机验证码的示例代码
2017/09/29 Javascript
Vue-Router2.X多种路由实现方式总结
2018/02/09 Javascript
使用canvas实现一个vue弹幕组件功能
2018/11/30 Javascript
JS实现的A*寻路算法详解
2018/12/14 Javascript
Vue快速实现通用表单验证功能
2019/12/05 Javascript
VueJS实现用户管理系统
2020/05/29 Javascript
Ant Design Vue table中列超长显示...并加提示语的实例
2020/10/31 Javascript
[02:09]2018DOTA2亚洲邀请赛TNC赛前采访
2018/04/04 DOTA
django1.8使用表单上传文件的实现方法
2016/11/04 Python
Python查找数组中数值和下标相等的元素示例【二分查找】
2019/02/13 Python
在tensorflow中实现去除不足一个batch的数据
2020/01/20 Python
使用Python和百度语音识别生成视频字幕的实现
2020/04/09 Python
iPython pylab模式启动方式
2020/04/24 Python
Python DataFrame使用drop_duplicates()函数去重(保留重复值,取重复值)
2020/07/20 Python
python为什么要安装到c盘
2020/07/20 Python
HTML5离线缓存在tomcat下部署可实现图片flash等离线浏览
2012/12/13 HTML / CSS
法国面料和小百货在线商店:Mondial Tissus
2019/03/23 全球购物
Hotels.com韩国:海外国内旅行所需的酒店和住宿预订网站
2020/05/08 全球购物
个人委托书范本
2014/09/13 职场文书
2014小学二年级班主任工作总结
2014/12/05 职场文书
党校学习党性分析材料
2014/12/19 职场文书
大学生自荐材料范文
2014/12/30 职场文书
pytorch显存一直变大的解决方案
2021/04/08 Python
JavaScript模拟实现网易云轮播效果
2022/04/04 Javascript
原生JS实现分页
2022/04/19 Javascript