蚂蚁论坛

 找回密码
 立即注册

QQ登录

只需一步,快速开始

扫一扫,访问微社区

查看: 44|回复: 0

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

[复制链接]

1923

主题

1

好友

1591

积分

管理员

Rank: 64Rank: 64Rank: 64Rank: 64

  • TA的每日心情
    奋斗
    2016-1-22 16:23
  • 签到天数: 4 天

    [LV.2]偶尔看看I

    荣誉管理 论坛元老 热心会员 活跃会员 最佳新人 灌水之王

    发表于 7 天前 |显示全部楼层
    基于Node的React图片上传组件实现实例代码
    写在前面
    红旗不倒,誓把JavaScript进行到底!今天介绍我的开源项目 Royal 里的图片上传组件的前后端实现原理(React + Node),花了一些时间,希望对你有所帮助。

    前端实现
    遵循React 组件化的思想,我把图片上传做成了一个独立的组件(没有其他依赖),直接import即可。


    import React, { **ponent } from 'react'
    import Upload from '//ponents/FormControls/Upload/'

    //

    render() {
      return (
        lt;divgt;lt;Upload uri={'jafeney:9999/upload'} /gt;lt;/divgt;
      )
    }


    uri 参数是必须传的,是图片上传的后端接口地址,接口怎么写下面会讲到。

    渲染页面
    组件render部分需要体现三个功能:

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

    主render函数


    render() {
      return (
        lt;form action={thisstateuri} method="post" encType="multipart/form-data"gt;
          lt;div className="ry-upload-box"gt;
            lt;div className="upload-main"gt;
              lt;div className="upload-choose"gt;
                lt;input
                  onChange={(v)=gt;thishandleChange(v)}
                  type="file"
                  size={thisstatesize}
                  name="fileSelect"
                  accept="image/*"
                  multiple={thisstatemultiple} /gt;
                lt;span ref="dragBox"
                  onDragOver={(e)=gt;thishandleDragHover(e)}
                  onDragLeave={(e)=gt;thishandleDragHover(e)}
                  onDrop={(e)=gt;thishandleDrop(e)}
                  className="upload-drag-area"gt;
                  或者将图片拖到此处
                lt;/spangt;
              lt;/divgt;
              lt;div className={thisstatefileslength63;
                 "upload-preview":"upload-preview ry-hidden"}gt;
                { this_renderPreview();  // 渲染图片预览列表 }
              lt;/divgt;
            lt;/divgt;
            lt;div className={thisstatefileslength63;
               "upload-submit":"upload-submit ry-hidden"}gt;
               lt;button type="button"
                 onClick={()=gt;thishandleUpload()}
                 class="upload-submit-btn"gt;
                 确认上传图片
               lt;/buttongt;
            lt;/divgt;
            lt;div className="upload-info"gt;
              { this_renderUploadInfos();  // 渲染图片上传信息 }
            lt;/divgt;
          lt;/divgt;
        lt;/formgt;
      )
    }



    渲染图片预览列表


    _renderPreview() {
      if (thisstatefiles) {
        return thisstatefilesmap((item, idx) =gt; {
          return (
            lt;div className="upload-append-list"gt;
              lt;pgt;
                lt;stronggt;{itemname}lt;/stronggt;
                lt;a href="javascript:void(0)" rel="external nofollow"
                  className="upload-delete"
                  title="删除" index={idx}gt;lt;/agt;
                lt;br/gt;
                lt;img src={itemthumb} className="upload-image" /gt;
               lt;/pgt;
               lt;span className={thisstateprogress[idx]63;
                 "upload-progress":
                 "upload-progress ry-hidden"}gt;
                 {thisstateprogress[idx]}
               lt;/spangt;
             lt;/divgt;
          )
        })
      } else {
        return null
      }
    }



    渲染图片上传信息列表


    _renderUploadInfos() {
      if (thisstateuploadHistory) {
        return thisstateuploadHistorymap((item, idx) =gt; {
          return (
            lt;pgt;
              lt;spangt;上传成功,图片地址是:lt;/spangt;
              lt;input type="text" class="upload-url" value={itemrelPath}/gt;
              lt;a href={itemrelPath} target="_blank"gt;查看lt;/agt;
             lt;/pgt;
          );
        })
      } else {
        return null;
      }
    }

    文件上传
    前端要实现图片上传的原理就是通过构建FormData对象,把文件对象append()到该对象,然后挂载在XMLHttpRequest对象上 send() 到服务端。
    获取文件对象
    获取文件对象需要借助 input 输入框的 change 事件来获取 句柄参数 e


    onChange={(e)=gt;thishandleChange(e)}

    然后做以下处理:


    epreventDefault()
    let target = eventtarget
    let files = targetfiles
    let count = thisstatemultiple 63; fileslength : 1
    for (let i = 0; i lt; count; i++) {
      files[i]thumb = URLcreateObjectURL(files[i])
    }
    // 转换为真正的数组
    files = Arrayprototypeslicecall(files, 0)
    // 过滤非图片类型的文件
    files = filesfilter(function (file) {
      return /image/itest(filetype)
    })

    这时 files 就是 我们需要的文件对象组成的数组,把它 concat 到原有的 files里。


    thissetState({files: thisstatefilesconcat(files)})

    如此,接下来的操作 就可以 通过 thisstatefiles 取到当前已选中的 图片文件。
    利用Promise处理异步上传
    文件上传对于浏览器来说是异步的,为了处理 接下来的多图上传,这里引入了 Promise来处理异步操作:


    upload(file, idx) {
      return new Promise((resolve, reject) =gt; {
        let xhr = new XMLHttpRequest()
        if (xhrupload) {
          // 上传中
          xhruploadaddEventListener("progress", (e) =gt; {
            // 处理上传进度
            thishandleProgress(file, eloaded, etotal, idx);
          }, false)
          // 文件上传成功或是失败
          xhronreadystatechange = (e) =gt; {
            if (xhrreadyState === 4) {
              if (xhrstatus === 200) {
              // 上传成功操作
              thishandleSuccess(file, xhrresponseText)
              // 把该文件从上传队列中删除
              thishandleDeleteFile(file)
              resolve(xhrresponseText);
             } else {
              // 上传出错处理
              thishandleFailure(file, xhrresponseText)
              reject(xhrresponseText);
             }
          }
        }
        // 开始上传
        xhropen("POST", thisstateuri, true)
        let form = new FormData()
        formappend("filedata", file)
        xhrsend(form)
      })
    }



    上传进度计算
    利用XMLHttpRequest对象发异步请求的好处是可以 计算请求处理的进度,这是fetch所不具备的。

    我们可以为 xhrupload 对象的 progress 事件添加事件监听:


    xhruploadaddEventListener("progress", (e) =gt; {
      // 处理上传进度
      thishandleProgress(file, eloaded, etotal, i);
    }, false)

    说明:idx参数是纪录多图上传队列的索引


    handleProgress(file, loaded, total, idx) {
      let percent = (loaded / total * 100)toFixed(2) + '%';
      let _progress = thisstateprogress;
      _progress[idx] = percent;
      thissetState({ progress: _progress }) // 反馈到DOM里显示
    }

    拖拽上传
    拖拽文件对于HTML5来说其实非常简单,因为它自带的几个事件监听机制可以直接做这类处理。主要用到的是下面三个:


    onDragOver={(e)=gt;thishandleDragHover(e)}
    onDragLeave={(e)=gt;thishandleDragHover(e)}
    onDrop={(e)=gt;thishandleDrop(e)}

    取消拖拽时的浏览器行为:


    handleDragHover(e) {
      estopPropagation()
      epreventDefault()
    }

    处理拖拽进来的文件:


    handleDrop(e) {
      thissetState({progress:[]})
      thishandleDragHover(e)
      // 获取文件列表对象
      let files = etargetfiles || edataTransferfiles
      let count = thisstatemultiple 63; fileslength : 1
      for (let i = 0; i lt; count; i++) {
        files[i]thumb = URLcreateObjectURL(files[i])
      }
      // 转换为真正的数组
      files = Arrayprototypeslicecall(files, 0)
      // 过滤非图片类型的文件
      files = filesfilter(function (file) {
        return /image/itest(filetype)
      })
      thissetState({files: thisstatefilesconcat(files)})
    }



    多图同时上传
    支持多图上传我们需要在组件调用处设置属性:


    multiple = { true } // 开启多图上传
    size = { 50 }    // 一次最大上传数量(虽没有上限,为保证服务端正常,建议50以下)

    然后我们可以使用 Promiseall() 处理异步操作队列:


    handleUpload() {
      let _promises = thisstatefilesmap((file, idx) =gt; thisupload(file, idx))
      Promiseall(_promises)then( (res) =gt; {
        // 全部上传完成
        thishandle**plete()
      })catch( (err) =gt; { consolelog(err) })
    }

    好了,前端工作已经完成,接下来就是Node的工作了。
    后端实现
    为了方便,后端采用的是express框架来快速搭建Http服务和路由。具体项目见我的github node-image-upload。逻辑虽然简单,但还是有几个可圈可点的地方:
    跨域调用
    本项目后端采用的是express,我们可以通过 resheader() 设置 请求的 “允许源” 来允许跨域调用:


    resheader('Access-Control-Allow-Origin', '*');

    设置为 * 说明允许任何 访问源,不太安全。建议设置成 你需要的 二级域名,如 jafeney。
    除了 “允许源” ,其他还有 “允许头” 、”允许域”、 “允许方法”、”文本类型” 等。常用的设置如下:


    function allowCross(res) {
      resheader('Access-Control-Allow-Origin', '*');  
      resheader('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');
      resheader('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
      resheader("X-Powered-By",' 321')
      resheader("Content-Type", "application/json;charset=utf-8");
    }

    ES6下的Ajax请求
    ES6风格下的Ajax请求和ES5不太一样,在正式的请求发出之前都会先发一个 类型为 OPTIONS的请求 作为试探,只有当该请求通过以后,正式的请求才能发向服务端。
    所以服务端路由 我们还需要 处理这样一个 请求:


    routeroptions('*', function (req, res, next) {
      resheader('Access-Control-Allow-Origin', '*');
      resheader('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');
      resheader('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
      resheader("X-Powered-By",' 321')
      resheader("Content-Type", "application/json;charset=utf-8");
      next();
    });

    注意:该请求同样需要设置跨域。
    处理上传
    处理上传的图片引人了multiparty模块,用法很简单:


    /*使用multiparty处理上传的图片*/
    routerpost('/upload', function(req, res, next) {
      // 生成multiparty对象,并配置上传目标路径
      var form = new multipartyForm({uploadDir: '/public/file/'});
      // 上传完成后处理
      formparse(req, function(err, fields, files) {
        var filesTmp = JSONstringify(files, null, 2);
        var relPath = '';
        if (err) {
          // 保存失败
          consolelog('Parse error: ' + err);
        } else {
          // 图片保存成功!
          consolelog('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) {
            consolelog('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
        + '_' + sizewidth + 'x' + sizeheight + ''
        + _imgoriginalFilenamesplit('')[1];
      var _port = processenvPORT || '9999';
        relPath = '' + reqhostname + ( _port!==80 63; ':' + _port : '' )
        + '/file/' + _dateSymbol + _timeSymbol + '_' + sizewidth + 'x'
        + sizeheight + '' + _imgoriginalFilenamesplit('')[1];
      // 重命名
      fsrename(uploadedPath, dstPath, function(err) {
        if (err) {
          reject(err)
        } else {
          consolelog('rename ok!');
        }
      });
    });

    总结
    对于大前端的工作,理解图片上传的前后端原理仅仅是浅层的。我们的口号是 “把JavaScript进行到底!”,现在无论是 ReactNative的移动端开发,还是NodeJS的后端开发,前端工程师可以做的工作早已不仅仅是局限于web页面,它已经渗透到了互联网应用层面的方方面面,或许,叫 全栈工程师 更为贴切吧。

    您需要登录后才可以回帖 登录 | 立即注册

    关闭

    蚂蚁站长论坛推荐上一条 /1 下一条

    QQ|Archiver|手机版|广告服务|蚂蚁站长-站长之家 ( 黔ICP备16008478号-1  

    GMT+8, 2017-10-19 07:41

    Powered by 站长论坛

    © 2007-2013 Jzant.com Inc.

    回顶部