Maison  >  Article  >  interface Web  >  Partagez un exemple de tutoriel sur le principe d'implémentation du composant de téléchargement d'images (React + Node)

Partagez un exemple de tutoriel sur le principe d'implémentation du composant de téléchargement d'images (React + Node)

零下一度
零下一度original
2017-05-11 13:37:482618parcourir

Cet article présente principalement l'exemple de code d'implémentation du composant Reactpictureupload basé sur Node, qui est d'une grande valeur pratique. Les amis qui en ont besoin peuvent s'y référer. à cela

est écrit devant

Si le drapeau rouge ne tombe pas, je m'engage à exécuter JavaScript jusqu'au bout ! Aujourd'hui, je vais vous présenter les principes d'implémentation front-end et back-end (React + Node) du composant de téléchargement d'images dans mon projet open source Royal. Cela m'a pris du temps et j'espère que cela vous sera utile.

Implémentation front-end

Suite à l'idée de la composantisation React, j'ai fait télécharger l'image d'un composant indépendant (non autre dépendance), importez-le simplement directement.

import React, { Component } from 'react'
import Upload from '../../components/FormControls/Upload/'

//......

render() {
  return (
    <p><Upload uri={&#39;http://jafeney.com:9999/upload&#39;} /></p>
  )
}

Le paramètre uri doit être transmis, qui est l'adresse de l'interface du backend pour le téléchargement de l'image. La façon d'écrire l'interface sera discutée ci-dessous.

Page de rendu

La partie rendu du composant doit refléter trois fonctions :

  1. Sélection d'image ( fenêtre de dialogue)

  2. Fonction pouvant être glissée (conteneur de déplacement)

  3. Prévisualisable (liste d'aperçu)

  4. Télécharger bouton (bouton)

  5. Télécharger l'adresse et le lien de l'image terminée (liste d'informations)

Principal renderFonction

render() {
  return (
    <form action={this.state.uri} method="post" encType="multipart/form-data">
      <p className="ry-upload-box">
        <p className="upload-main">
          <p 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>
          </p>
          <p className={this.state.files.length?
             "upload-preview":"upload-preview ry-hidden"}>
            { this._renderPreview();  // 渲染图片预览列表 }
          </p>
        </p>
        <p className={this.state.files.length?
           "upload-submit":"upload-submit ry-hidden"}>
           <button type="button"
             onClick={()=>this.handleUpload()}
             class="upload-submit-btn">
             确认上传图片
           </button>
        </p>
        <p className="upload-info">
          { this._renderUploadInfos();  // 渲染图片上传信息 }
        </p>
      </p>
    </form>
  )
}

Liste d'aperçu des images rendues

_renderPreview() {
  if (this.state.files) {
    return this.state.files.map((item, idx) => {
      return (
        <p 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>
         </p>
      )
    })
  } else {
    return null
  }
}

Liste d'informations de téléchargement des images rendues

_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;
  }
}

Téléchargement de fichiers

Le principe du téléchargement d'images sur le front-end est de construire l'objet FormData, d'append() l'objet fichier à l'objet, puis de le monter en XMLSend() sur l'objet HttpRequest vers le serveur.

Obtenir l'objet fichier

Pour obtenir l'objet fichier, vous devez utiliser l'événement change de la zone de saisie pour obtenir le handle paramètre e

onChange={(e)=>this.handleChange(e)}

Ensuite, effectuez le traitement suivant :

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)
})

À ce stade, les fichiers sont un tableau d'objets fichiers dont nous avons besoin et le concatérons dans les fichiers d'origine.

this.setState({files: this.state.files.concat(files)})

De cette façon, l'opération suivante peut obtenir le fichier image actuellement sélectionné via this.state.files.

Utilisez Promise pour gérer le téléchargement asynchrone

Le téléchargement de fichiers est asynchrone par rapport au navigateur Afin de gérer le téléchargement multi-images ultérieur, Promise est introduit ici pour gérer l'asynchrone. Fonctionnement :

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)
  })
}

Calcul de la progression du téléchargement

L'avantage d'utiliser l'objet XMLHttpRequest pour envoyer des requêtes asynchrones est qu'il peut calculer la progression du traitement des requêtes, qui récupère n'a pas.

Nous pouvons ajouter une surveillance d'événement pour l'événement de progression de l'objet xhr.upload :

xhr.upload.addEventListener("progress", (e) => {
  // 处理上传进度
  this.handleProgress(file, e.loaded, e.total, i);
}, false)

Explication : Le paramètre idx est le index qui enregistre la file d'attente de téléchargement multi-images

handleProgress(file, loaded, total, idx) {
  let percent = (loaded / total * 100).toFixed(2) + &#39;%&#39;;
  let _progress = this.state.progress;
  _progress[idx] = percent;
  this.setState({ progress: _progress }) // 反馈到DOM里显示
}

Téléchargement par glisser-déposer

Le glisser-déposer de fichiers est en fait très simple pour HTML5 car il vient avec plusieurs écouteurs d'événements Le mécanisme peut effectuer ce type de traitement directement. Les trois suivants sont principalement utilisés :

onDragOver={(e)=>this.handleDragHover(e)}
onDragLeave={(e)=>this.handleDragHover(e)}
onDrop={(e)=>this.handleDrop(e)}

Comportement du navigateur lors de l'annulation du glisser-déposer :

handleDragHover(e) {
  e.stopPropagation()
  e.preventDefault()
}

Traitement des fichiers glissés :

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)})
}

Télécharger plusieurs images en même temps

Prend en charge plusieurs téléchargements d'images. Nous devons définir l'attribut au point d'appel du composant :

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

Ensuite. nous pouvons utiliser Promise.all() pour gérer la file d'attente des opérations asynchrones :

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) })
}

D'accord, le travail frontal est terminé et la prochaine étape est le travail de Node.

Implémentation du backend

Pour plus de commodité, le backend utilise le framework express pour créer rapidement des services et un routage HTTP. Pour des projets spécifiques, voir mon github node-image-upload. Bien que la logique soit simple, il y a quand même plusieurs points remarquables :

Appel cross-domain

Le backend de ce projet utilise express, on peut utiliser res .header( ) définit la "source autorisée" de la requête pour autoriser les appels inter-domaines :

res.header(&#39;Access-Control-Allow-Origin&#39;, &#39;*&#39;);

est défini sur * pour autoriser toute source d'accès, qui est moins sécurisée. Il est recommandé de le définir sur le nom de domaine de deuxième niveau dont vous avez besoin, tel que jafeney.com.

En plus de « Autoriser la source », d'autres incluent « Autoriser l'en-tête », « Autoriser le domaine », « Autoriser la méthode », « Type de texte », etc. Les paramètres couramment utilisés sont les suivants :

function allowCross(res) {
  res.header(&#39;Access-Control-Allow-Origin&#39;, &#39;*&#39;);  
  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");
}

Requête Ajax sous ES6

La requête Ajax sous ES6 est différente de celle d'ES5 et sera réinitialisée avant la demande formelle est émis. Envoyez d'abord une requête de type OPTIONS à titre de test. Ce n'est que lorsque la requête est réussie que la requête formelle peut être envoyée au serveur.

所以服务端路由 我们还需要 处理这样一个 请求:

router.options('*', function (req, res, next) {
  res.header(&#39;Access-Control-Allow-Origin&#39;, &#39;*&#39;);
  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(&#39;/upload&#39;, function(req, res, next) { 
  // 生成multiparty对象,并配置上传目标路径
  var form = new multiparty.Form({uploadDir: &#39;./public/file/&#39;});
  // 上传完成后处理
  form.parse(req, function(err, fields, files) {
    var filesTmp = JSON.stringify(files, null, 2);
    var relPath = &#39;&#39;;
    if (err) {
      // 保存失败 
      console.log(&#39;Parse error: &#39; + err);
    } else {
      // 图片保存成功!
      console.log(&#39;Parse Files: &#39; + 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(&#39;.&#39; + paths[1] + &#39;@&#39; + size + &#39;00.&#39; + paths[2], function (err) {
      if (!err) {
        console.log(&#39;resize as &#39; + size + &#39; ok!&#39;)
        resolve()
      }
      else {
        reject(err)
      };
    });
  });
}

重命名图片

为了方便排序和管理图片,我们按照 “年月日 + 时间戳 + 尺寸” 来命名图片:

var _dateSymbol = new Date().toLocaleDateString().split(&#39;-&#39;).join(&#39;&#39;);
var _timeSymbol = new Date().getTime().toString();

至于图片尺寸 使用 gm的 size() 方法来获取,处理方式如下:

gm(uploadedPath).size(function(err, size) {
  var dstPath = &#39;./public/file/&#39; + _dateSymbol + _timeSymbol 
    + &#39;_&#39; + size.width + &#39;x&#39; + size.height + &#39;.&#39; 
    + _img.originalFilename.split(&#39;.&#39;)[1];
  var _port = process.env.PORT || &#39;9999&#39;;
    relPath = &#39;http://&#39; + req.hostname + ( _port!==80 ? &#39;:&#39; + _port : &#39;&#39; ) 
    + &#39;/file/&#39; + _dateSymbol + _timeSymbol + &#39;_&#39; + size.width + &#39;x&#39; 
    + size.height + &#39;.&#39; + _img.originalFilename.split(&#39;.&#39;)[1];
  // 重命名
  fs.rename(uploadedPath, dstPath, function(err) {
    if (err) {
      reject(err)
    } else {
      console.log(&#39;rename ok!&#39;);
    }
  });
});

总结

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

【相关推荐】

1. 免费js在线视频教程

2. JavaScript中文参考手册

3. php.cn独孤九贱(3)-JavaScript视频教程

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn