基于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 相关文章推荐
js 事件小结 表格区别
Aug 13 Javascript
javascript qq右下角滑出窗口 sheyMsg
Mar 21 Javascript
日历查询的算法 如何计算某一天是星期几
Dec 12 Javascript
JS 跳转页面延迟2种方法
Mar 29 Javascript
Jquery日期选择datepicker插件用法实例分析
Jun 08 Javascript
jQuery Real Person验证码插件防止表单自动提交
Nov 06 Javascript
多种jQuery绑定事件的实现方式
Jun 13 Javascript
JavaScript算法教程之sku(库存量单位)详解
Jun 29 Javascript
jquery对table做排序操作的实例演示
Aug 10 jQuery
js实现简单掷骰子小游戏
Oct 24 Javascript
vue-cli3 热更新配置操作
Sep 18 Javascript
js实现淘宝浏览商品放大镜功能
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
无线电波是什么?它是怎样传输的?
2021/03/01 无线电
颠覆常识!无色透明的咖啡诞生了(中日双语)
2021/03/03 咖啡文化
在SAE上搭建最新wordpress的方法
2014/12/21 PHP
php生成毫秒时间戳的实例讲解
2017/09/22 PHP
php微信公众号开发之欢迎老朋友
2018/10/20 PHP
Jquery.TreeView结合ASP.Net和数据库生成菜单导航条
2010/08/27 Javascript
jquery 清空file域示例(兼容个浏览器)
2013/10/11 Javascript
js中function()使用方法
2013/12/24 Javascript
js 触发select onchange事件代码
2014/03/20 Javascript
js判断手机端(Android手机还是iPhone手机)
2015/07/22 Javascript
jQuery获取父元素及父节点的方法小结
2016/04/14 Javascript
vue子父组件通信的实现代码
2017/07/09 Javascript
深入理解React中何时使用箭头函数
2017/08/23 Javascript
layui 上传插件 带预览 非自动上传功能的实例(非常实用)
2019/09/23 Javascript
python BeautifulSoup使用方法详解
2013/11/21 Python
python使用Queue在多个子进程间交换数据的方法
2015/04/18 Python
用Python实现斐波那契(Fibonacci)函数
2016/03/25 Python
asyncio 的 coroutine对象 与 Future对象使用指南
2016/09/11 Python
python re模块的高级用法详解
2018/06/06 Python
python处理csv中的空值方法
2018/06/22 Python
Python并发:多线程与多进程的详解
2019/01/24 Python
如何使用pyinstaller打包32位的exe程序
2019/05/26 Python
Python 使用PyQt5 完成选择文件或目录的对话框方法
2019/06/27 Python
python进行参数传递的方法
2020/05/12 Python
Python DataFrame使用drop_duplicates()函数去重(保留重复值,取重复值)
2020/07/20 Python
Python grequests模块使用场景及代码实例
2020/08/10 Python
Python测试框架:pytest学习笔记
2020/10/20 Python
python批量合成bilibili的m4s缓存文件为MP4格式 ver2.5
2020/12/01 Python
爱尔兰电脑、家电和家具购物网站:Buy It Direct
2019/07/09 全球购物
巴西备受欢迎的服装和生活方式品牌:FARM Rio
2020/02/04 全球购物
中学教师实习自我鉴定
2013/09/28 职场文书
党建目标管理责任书
2014/07/25 职场文书
电子商务专业求职信范文
2015/03/19 职场文书
4S店销售内勤岗位职责
2015/04/13 职场文书
《你在为谁工作》心得体会(共8篇)
2016/01/20 职场文书
mysql 数据插入优化方法之concurrent_insert
2021/07/01 MySQL