欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2023-01-29 11:38:41
写在前面 红旗不倒,誓把javascript进行到底!今天介绍我的开源项目 royal 里的图片上传组件的前后端实现原理(react + node),花了一些时间,希望对...

写在前面

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

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