基于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 相关文章推荐
jQuery表单验证插件formValidator(改进版)
Feb 03 Javascript
jquery 新建的元素事件绑定问题解决方案
Jun 12 Javascript
node.js中的fs.writeFileSync方法使用说明
Dec 14 Javascript
javascript实现简单的html5视频播放器
May 06 Javascript
JavaScript对HTML DOM使用EventListener进行操作
Oct 21 Javascript
JavaScript的Ext JS框架中的GridPanel组件使用指南
May 21 Javascript
Javascript 闭包详解及实例代码
Nov 30 Javascript
详谈js原型继承的一些问题
Sep 06 Javascript
AngularJS的$location使用方法详解
Oct 19 Javascript
jQuery选择器之层次选择器用法实例分析
Feb 19 jQuery
jquery实现动态创建form并提交的方法示例
May 27 jQuery
vue h5移动端禁止缩放代码
Oct 28 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
让PHP COOKIE立即生效,不用刷新就可以使用
2011/03/09 PHP
destoon实现首页显示供应、企业、资讯条数的方法
2014/07/15 PHP
php中return的用法实例分析
2015/02/28 PHP
使用PHPExcel操作Excel用法实例分析
2015/03/26 PHP
PHP面向对象自动加载机制原理与用法分析
2016/10/14 PHP
PHP实现负载均衡session共享redis缓存操作示例
2018/08/22 PHP
Laravel5.5+ 使用API Resources快速输出自定义JSON方法详解
2020/04/06 PHP
JS 统计时间
2021/03/09 Javascript
Javascript中eval函数的使用方法与示例
2007/04/09 Javascript
autoIMG 基于jquery的图片自适应插件代码
2011/03/12 Javascript
40个有创意的jQuery图片、内容滑动及弹出插件收藏集之一
2011/12/31 Javascript
打豆豆小游戏 用javascript编写的[打豆豆]小游戏
2013/01/08 Javascript
jQuery.position()方法获取不到值的安全替换方法
2015/03/13 Javascript
javascript css红色经典选项卡效果实现代码
2016/05/17 Javascript
jQuery判断元素是否显示 是否隐藏的简单实现代码
2016/05/19 Javascript
Node.js Streams文件读写操作详解
2016/07/04 Javascript
jQuery网页定位导航特效实现方法
2016/12/19 Javascript
原生js封装添加class,删除class的实例
2017/11/06 Javascript
浅谈Vuex@2.3.0 中的 state 支持函数申明
2017/11/22 Javascript
说说如何在Vue.js中实现数字输入组件的方法
2019/01/08 Javascript
vue接入腾讯防水墙代码
2019/05/07 Javascript
详解微信小程序回到顶部的两种方式
2019/05/09 Javascript
vue实现学生信息管理系统
2020/05/30 Javascript
pandas object格式转float64格式的方法
2018/04/10 Python
python递归实现快速排序
2018/08/18 Python
Python面向对象之类的定义与继承用法示例
2019/01/14 Python
python 对类的成员函数开启线程的方法
2019/01/22 Python
python2.7的flask框架之引用js&amp;css等静态文件的实现方法
2019/08/22 Python
Python坐标线性插值应用实现
2019/11/13 Python
利用pytorch实现对CIFAR-10数据集的分类
2020/01/14 Python
销售自荐信
2013/10/22 职场文书
2014年三万活动总结
2014/04/26 职场文书
2014年志愿者工作总结
2014/11/20 职场文书
感恩老师主题班会
2015/08/12 职场文书
MySql 缓存查询原理与缓存监控和索引监控介绍
2021/07/02 MySQL
python 学习GCN图卷积神经网络
2022/05/11 Python