圖片裁剪上傳示例(node + react)

?醬發表於2018-12-18

場景

因為公司內部平臺非常多,很多開發的站點地址沒有一個統一的入口,所以作者基於 egg + mongodb + redies + umi +antd 搭建了一個簡單的入口平臺。 由於各個平臺各有特點如果能輸入名字的話還是不太好區分,logo上傳必然是一個必須的功能。 一起來看一下整個前後端功能實現的過程。

node部分

依賴安裝

yarn add  await-stream-ready  stream-wormhole 
複製程式碼
  • await-stream-ready 非同步二進位制 寫入流
  • stream-wormhole 管道讀入一個蟲洞。

路由宣告

module.exports = app => {
  const { router, controller } = app;
  router.get(‘/api/file/upload’, controller.file.upload)
};
複製程式碼

Egg宣告路由

controller

'use strict';
//node.js 檔案操作物件
const fs = require('fs');
//node.js 路徑操作物件
const path = require('path');
//故名思意 非同步二進位制 寫入流
const awaitWriteStream = require('await-stream-ready').write;
//管道讀入一個蟲洞。
const sendToWormhole = require('stream-wormhole');
//當然你也可以不使用這個 哈哈 個人比較賴
//還有我們這裡使用了egg-multipart
const Controller = require('egg').Controller;

class FileController extends Controller {
  async upload() {
    const ctx = this.ctx;
    //egg-multipart 已經幫我們處理檔案二進位制物件
    // node.js 和 php 的上傳唯一的不同就是 ,php 是轉移一個 臨時檔案
    // node.js 和 其他語言(java c#) 一樣操作檔案流
    const stream = await ctx.getFileStream();
    //新建一個檔名
    const filename = stream.filename
    // const filename = md5(stream.filename) + path
    //   .extname(stream.filename)
    //   .toLocaleLowerCase();
    //檔案生成絕對路徑
    //當然這裡這樣市不行的,因為你還要判斷一下是否存在檔案路徑
    const target = path.join(this.config.baseDir, 'app/public/uploads', filename);
    //生成一個檔案寫入 檔案流
    const writeStream = fs.createWriteStream(target);
    try {
      //非同步把檔案流 寫入
      await awaitWriteStream(stream.pipe(writeStream));
    } catch (err) {
      //如果出現錯誤,關閉管道
      await sendToWormhole(stream);
      throw err;
    }
    const url = `/public/uploads/${filename}`
    //檔案響應
    ctx.body = { url };
  }
}
module.exports = FileController;
複製程式碼

首先 egg-multipart 已經幫我們處理了二進位制物件,在前端發起請求姿勢沒有問題的情況下,後端部分只要呼叫ctx.getFileStream 就可以得到一個流

const stream = await ctx.getFileStream();
複製程式碼

然後指定寫入的資料夾,注意這邊如果沒有找到資料夾會直接報錯!

   const filename = stream.filename
    //當然這裡這樣市不行的,因為你還要判斷一下是否存在檔案路徑
    const target = path.join(this.config.baseDir, 'app/public/uploads', filename);
複製程式碼

然後建立一個檔案寫入流

  const writeStream = fs.createWriteStream(target);
複製程式碼

接下來我們引用的兩個庫就派上用場了,一個是用來非同步完成寫入流,另外一個是用來報錯的時候關閉管道,這部分不瞭解的請移步node。

    try {
      //非同步把檔案流 寫入
      await awaitWriteStream(stream.pipe(writeStream));
    } catch (err) {
      //如果出現錯誤,關閉管道
      await sendToWormhole(stream);
      throw err;
    }
複製程式碼

等待檔案寫入流結束之後,檔案就在目標資料夾下了,就可以把檔案的地址返回給前端

   const url = `/public/uploads/${filename}`
    //檔案響應
    ctx.body = { url };
複製程式碼

總結

Egg部分已經幫我們把處理二進位制物件處理完了,我們需要做的事情其實很簡單,拿到檔案流,指定寫入的資料夾,建立檔案寫入流,等待寫入流結束之後返回檔案寫入的地址給前端。

前端部分

依賴安裝

本示例涉及到圖片裁剪,如果沒有這個需求的請略過

yarn add react-image-crop
複製程式碼

PlatformModal.jsx

upload(上傳模組)

這裡直接使用的是antd upload的元件,如果你後端部分寫好了,直接貼入程式碼,updateUrl 為你上傳檔案的api介面。這邊介面響應之後的格式根據你的情況定義,拿到的url可以直接寫在

<img src={this.state.iconUrl}> 
複製程式碼

既可。 到了這邊一個圖片上傳的示例就結束了,後面我們將裁減模組。

  renderUpdate = () => {
    const uploadProps = {
      name: 'icon',
      action: updateUrl,
      onChange: (info) => {
        if (info.file.status !== 'uploading') {
          console.log(info.file, info.fileList);
        }
        if (info.file.status === 'done') {
          message.success(`${info.file.name} LOGO 上傳成功!`);
          this.setState({
            iconUrl: info.file.response.data.url,
            crop: {}
          })
        } else if (info.file.status === 'error') {
          message.error(`${info.file.name} LOGO 上傳失敗!`);
        }
      }
    }
    return <Upload {...uploadProps}>
      <Button><Icon type="upload" />選擇圖片</Button>
    </Upload>
  }
複製程式碼

renderReactCrop(裁減模組)

圖片裁減部分我們引用了 react-image-crop 這個react元件,這部分功能的一個思路是這樣的。

  • 圖片地址設定
  • 裁減圖片動作觸發並帶有裁減範圍的引數
  • 通過canvas將圖片根據裁減範圍轉化為base64
  • 將 base64 資料暫時存起來
  • 在提交的時候,將base64轉化為檔案
  • 通過 formData 建立檔案物件提交到後端介面
  • 後端返回新的url地址
  • 更改平臺的圖片地址
import ReactCrop from 'react-image-crop'
import 'react-image-crop/dist/ReactCrop.css'
function getBlobBydataURI(dataURI, type) {
  var binary = atob(dataURI.split(',')[1]);
  var array = [];
  for (var i = 0; i < binary.length; i++) {
    array.push(binary.charCodeAt(i));
  }
  return new Blob([new Uint8Array(array)], { type: type });
}

複製程式碼
  renderReactCrop = () => {
    const { iconUrl, crop } = this.state
    const loadImage = imgSrc =>
      new Promise((resolve, reject) => {
        const img = new Image()
        img.setAttribute('crossOrigin', 'anonymous')
        img.src = imgSrc
        img.onload = e => {
          resolve(img)
        }
      })

    const cropImage = async (imgSrc, crop) => {
      const img = await loadImage(imgSrc)
      let canvas, cropX, cropY, cropWidth, cropHeight
      // return this.loadImage(imgSrc, cropAfterLoad.bind(this))
      const imageWidth = img.naturalWidth
      const imageHeight = img.naturalHeight
      cropX = (crop.x / 100) * imageWidth
      cropY = (crop.y / 100) * imageHeight
      cropWidth = (crop.width / 100) * imageWidth
      cropHeight = (crop.height / 100) * imageHeight
      canvas = document.createElement('canvas')
      canvas.width = cropWidth
      canvas.height = cropHeight
      const _2d = canvas.getContext('2d')
      _2d.drawImage(img, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight)
      return canvas.toDataURL('image/jpeg')
    }

    const handleCropComplete = (crop, pixelCrop) => {
      cropImage(iconUrl, crop)
        .then(result => {
          message.success('裁剪成功!')
          this.setState({
            iconBase64: result,
            crop,
          })
        })
        .catch(err => {
          message.error(err.message)
        })
    }
    const handleCropChange = (crop) => {
      this.setState({ crop })
    }
    return <ReactCrop
      src={iconUrl}
      onComplete={handleCropComplete.bind(this)}
      onChange={handleCropChange}
      crop={crop}
    />
  }
複製程式碼

然後是提交的時候的一個處理方式,將base64 轉化為一個blob物件,然

  • 將base64 轉化為一個blob物件
  • 建立一個 formData 用於提交
  • 向fornData裡面新增檔案,注意這邊圖片名稱記得帶上時間戳
  • 用fetch向檔案上傳介面發起請求
  • 拿到url
const blob = getBlobBydataURI(iconBase64, 'image/png')
        let formData = new FormData();
        formData.append('files', blob, `${name}_${Date.parse(new Date())}_icon.png`)
        fetch(updateUrl, {
          method: 'POST',
          body: formData
        })
複製程式碼

結束

部分細節程式碼參考了網上的,一些細節沒有深入研究,整理了以下整個流程。希望對別人有幫助。

相關文章