基于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 RadioButtonList获取选中值
Apr 09 Javascript
JavaScript调用后台的三种方法实例
Oct 17 Javascript
js data日期初始化的5种方法
Dec 29 Javascript
JS 数字转换为大写金额的简单实例
Aug 04 Javascript
Javascript 调用 ActionScript 的简单方法
Sep 22 Javascript
JS及JQuery对Html内容编码,Html转义
Feb 17 Javascript
vue2.0使用Sortable.js实现的拖拽功能示例
Feb 21 Javascript
js中json对象和字符串的理解及相互转化操作实现方法
Sep 22 Javascript
Vue组件系列开发之模态框
Apr 18 Javascript
Vue-cli3简单使用(图文步骤)
Apr 30 Javascript
浅谈TypeScript的类型保护机制
Feb 23 Javascript
JavaScript中跨域问题的深入理解
Mar 04 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/02/28 无线电
风格模板初级不完全修改教程
2006/10/09 PHP
php preg_match_all结合str_replace替换内容中所有img
2008/10/11 PHP
使用WAMP搭建PHP本地开发环境
2017/05/10 PHP
PHP长网址与短网址的实现方法
2017/10/13 PHP
Laravel框架在本地虚拟机快速安装的方法详解
2018/06/11 PHP
WordPress免插件实现面包屑导航的示例代码
2020/08/20 PHP
用javascript实现给出的盒子的序列是否可连为一矩型
2007/08/30 Javascript
javascript检查日期格式的函数[比较全]
2008/10/17 Javascript
Javascript继承(上)——对象构建介绍
2012/11/08 Javascript
js call方法详细介绍(js 的继承)
2013/11/18 Javascript
Node.js编程中客户端Session的使用详解
2015/06/23 Javascript
javascript九宫格图片随机打乱位置的实现方法
2017/03/15 Javascript
Web开发使用Angular实现用户密码强度判别的方法
2017/09/27 Javascript
微信小程序实现多选功能
2018/11/04 Javascript
vue通过指令(directives)实现点击空白处收起下拉框
2018/12/06 Javascript
layui自定义工具栏的方法
2019/09/19 Javascript
深入学习Vue nextTick的用法及原理
2019/10/08 Javascript
Vue父组件监听子组件生命周期
2020/09/03 Javascript
python logging类库使用例子
2014/11/22 Python
利用Python-iGraph如何绘制贴吧/微博的好友关系图详解
2017/11/02 Python
解决python大批量读写.doc文件的问题
2018/05/08 Python
python 统计数组中元素出现次数并进行排序的实例
2018/07/02 Python
python实现在图片上画特定大小角度矩形框
2018/10/24 Python
在Python中使用MongoEngine操作数据库教程实例
2019/12/03 Python
Python Websocket服务端通信的使用示例
2020/02/25 Python
OpenCV Python实现拼图小游戏
2020/03/23 Python
python爬虫破解字体加密案例详解
2021/03/02 Python
幼教求职信
2014/03/12 职场文书
校庆接待方案
2014/03/18 职场文书
护士演讲稿优秀范文
2014/04/30 职场文书
医院保洁员岗位职责
2015/02/13 职场文书
领导欢送会主持词
2015/07/06 职场文书
团干部培训班心得体会
2016/01/06 职场文书
使用pandas生成/读取csv文件的方法实例
2021/07/09 Python
为什么MySQL8新特性会修改自增主键属性
2022/04/18 MySQL