前臺(tái)nuxt+后臺(tái)eggjs實(shí)現(xiàn)el-upload上傳并且vue-cropper剪裁輸出到阿里云oss

nuxt使用vue-cropper

  1. 下載vue-cropper薪寓;
npm install vue-cropper
  1. nuxt.config.js配置載入vue-cropper全局組件
module.exports = {
  plugins: [
    '@/plugins/element-ui',
    {
      src: '@/plugins/axios',
      ssr: true
    },
    '~/plugins/router.js',
    { src: '~/plugins/cropper.js', ssr: false }
  ],
}
  1. 實(shí)現(xiàn)plugins/cropper.js旷痕,剪裁組件是純前端的操作,所以不需要ssr捌年,也沒(méi)有必要加載到服務(wù)端渲染
import Vue from 'vue';
import vueCropper from 'vue-cropper';
if(process.browser){
    Vue.use(vueCropper);
}

vue-cropper屬性介紹

  1. vue-cropper剪裁組件的屬性介紹谴轮;
    屬性名稱 描述 可選值
    img 裁剪圖片的地址 url地址,支持blob和base64
    output-size 裁剪生成圖片的質(zhì)量 默認(rèn)值1.可選值0~1之間
    output-type 裁剪生成圖片的格式 默認(rèn)jpeg jpeg, png, webp
    info 裁剪框的大小信息 默認(rèn)是true识窿,可選true和false
    canScale 圖片是否允許滾輪縮放 默認(rèn)是true斩郎,可選true和false
    can-move 上傳圖片是否可以移動(dòng) 默認(rèn)是true,可選true和false
    can-move-box 截圖框能否拖動(dòng) 默認(rèn)是true喻频,可選true和false
    fixed-box 固定截圖框大小 不允許改變
    auto-crop 是否默認(rèn)生成截圖框 默認(rèn)是false缩宜,可選true和false
    auto-crop-width 默認(rèn)生成截圖框?qū)挾?/td> 默認(rèn)是容器的 80%
    auto-crop-height 默認(rèn)生成截圖框高度 默認(rèn)容器的 80%
    center-box 截圖框是否被限制在圖片里面 默認(rèn)是false,可選true和false
    high 是否按照設(shè)備的dpr 輸出等比例圖片 默認(rèn)是true甥温,可選true和false
    info-true true 為展示真實(shí)輸出圖片寬高 false 展示看到的截圖框?qū)捀?/td> 默認(rèn)是false锻煌,可選true和false
    enlarge 圖片根據(jù)截圖框輸出比例倍數(shù) 默認(rèn)是1,可選0~max
    fixed 是否開(kāi)啟截圖框?qū)捀吖潭ū壤?/td> 默認(rèn)是false姻蚓,可選true和false
    fixed-number 截圖框的寬高比例 默認(rèn)[1,1]宋梧,可選寬度和高度

實(shí)現(xiàn)cropper剪裁組件的封裝

  1. 剪裁組件的封裝;

<template>
<div class="Cropper">
 <el-dialog :visible.sync="dialogVisible" width="740px" title="圖片裁剪" :before-close="handleClose"
   :close-on-click-modal="dialogVisible" top="5vh">
   <div class="cropper-subtitle">
     <div>
       <span class="cropper-babel">溫馨提示1:</span>
       <span>可以通過(guò)鼠標(biāo)滑動(dòng)滾輪放大圖片進(jìn)行裁剪</span>
     </div>
     <div>
       <span class="cropper-babel">溫馨提示2:</span>
       <span>圖片寬度必須在300以上</span>
     </div>
   </div>
   <div class="cropper-container">
     <div class="cropper-el">
       <vue-cropper ref="cropper"
       :img="cropperImg"
       :fixed-box="option.fixedBox"
       :auto-crop="option.autoCrop"
       :auto-crop-width="option.autoCropWidth"
       :auto-crop-height="option.autoCropHeight"
       :info-true="option.infoTrue"
       :center-box="option.centerBox"
       :high="option.high"
       :fixed-number="option.fixedNumber"
       :limitMinSize="option.limitMinSize" />
     </div>
   </div>
   <span slot="footer" class="dialog-footer">
     <el-button @click="uploadBth">重新上傳</el-button>
     <el-button @click="handleClose">取 消</el-button>
     <el-button type="primary" @click="saveImg">確 定</el-button>
   </span>
 </el-dialog>
</div>
</template>

<script>
export default {
name: 'Cropper',
props: {
 // 彈框
 dialogVisible: {
   type: Boolean,
   default: false
 },
 // 剪裁完成之后返回圖片的數(shù)據(jù)格式狰挡,默認(rèn)為blob
 imgType: {
   type: String,
   default: 'blob'
 },
 // 需要剪裁圖片的路徑
 cropperImg: {
   type: String,
   default: ''
 },
 // 裁剪比例捂龄,默認(rèn)1:1
 zoomScale: {      
   type: Array,
   default: () => ([1, 1])
 }
},
data() {
 return {
   previews: {},
   option: {
     img: '', // 裁剪圖片的地址
     fixedBox: false, // 固定截圖框大小 不允許改變
     autoCrop: true, // 是否默認(rèn)生成截圖框
     autoCropWidth: 300, // 默認(rèn)生成截圖框?qū)挾?     autoCropHeight: 320, // 默認(rèn)生成截圖框高度
     centerBox:true,// 截圖框是否被限制在圖片里面
     limitMinSize: 600, // 更新裁剪框最小屬性
     high:false,// 是否按照設(shè)備的dpr 輸出等比例圖片
     infoTrue: true, // true 為展示真實(shí)輸出圖片寬高 false 展示看到的截圖框?qū)捀?     fixedNumber: this.zoomScale // 截圖框的寬高比例
   },
 };
},
methods: {
 // 重新上傳
 uploadBth() {
   this.$emit('update-cropper');
 },
 // 取消關(guān)閉彈框
 handleClose() {
   this.$emit('colse-dialog', false);
 },
 // 獲取裁剪之后的圖片释涛,默認(rèn)blob,也可以獲取base64的圖片
 saveImg() {
   if (this.imgType === 'blob') {
     // 返回blob的數(shù)據(jù)格式倦沧,cropper默認(rèn)只支持兩種返回的數(shù)據(jù)唇撬,具體后面可以轉(zhuǎn)成file
     this.$refs.cropper.getCropBlob(data => {
       // 返回?cái)?shù)據(jù)和圖片寬度和高度
       this.$emit('upload-img', {data,cro:{width:this.$refs.cropper.cropW,height:this.$refs.cropper.cropH}});
     });
   } else {
     // 返回base64的數(shù)據(jù)格式
     this.$refs.cropper.getCropData(data => {
       // 返回?cái)?shù)據(jù)和圖片寬度和高度
       this.$emit('upload-img', {data,cro:{width:this.$refs.cropper.cropW,height:this.$refs.cropper.cropH}});
     });
   }
 },
}
};
</script>

<style lang="scss" scoped>
.Cropper {
margin:0;
.cropper-el {
 height: 500px;
 width: 700px;
 flex-shrink: 0;
}
.cropper-subtitle{
 margin-top: -25px;
 margin-bottom: 10px;
 .cropper-babel{
   color:red;
 }
}
.cropper-container {
 display: flex;
 justify-content: space-between;
 .prive-el {
   flex: 1;
   align-self: center;
   text-align: center;
   .prive-style {
     margin: 0 auto;
     flex: 1;
     -webkit-flex: 1;
     display: flex;
     display: -webkit-flex;
     justify-content: center;
     -webkit-justify-content: center;
     overflow: hidden;
     background: #ededed;
     margin-left: 40px;
   }
   .preview {
     overflow: hidden;
   }
   .el-button {
     margin-top: 20px;
   }
 }
}
}
</style>

el-upload組件和cropper組件的聯(lián)合使用

  1. el-upload組件和cropper組件;
<template>
  <div>
    <!-- 注意:必須關(guān)閉自動(dòng)上傳屬性 auto-upload -->
    <el-upload
      :http-request="Upload"
      name="files"
      :multiple="false"
      list-type="picture-card"
      :limit="12"
      :before-upload="beforeAvatarUpload"
      ref="fileUpload"
      :auto-upload="false"
      :on-change="selectChange"
      action="aaa"
      class="cropper-upload-box"
    >
      <i slot="default" class="el-icon-plus"></i>
    </el-upload>
 
    <cropper
      v-if="showCropper"
      :dialog-visible="showCropper"
      :cropper-img="cropperImg"
      :zoomScale="zoomScale"
      @update-cropper="updateCropper"
      @colse-dialog="closeDialog"
      @upload-img="uploadImg"
    />
  </div>
</template>
 
<script>
import Cropper from "@/components/cropper/index.vue";
export default {
  name: "UploadCropper",
  data() {
    return {
      showCropper: false, // 是否顯示裁剪框
      cropperImg: "", // 需要裁剪的圖片
      noCanUpload:false,//當(dāng)前沒(méi)有在上傳的狀態(tài)
      currentFile:[],//當(dāng)前已經(jīng)上傳成功并且剪裁成功的圖片文件列表
      uploadFile:null,//剪裁之后返回的圖片數(shù)據(jù)(Blob數(shù)據(jù)格式對(duì)象)
      cro:{},//這里存放的是剪裁完成之后圖片的寬度和高度
    };
  },
  props: {
    // 裁剪比例展融,默認(rèn)1:1
    zoomScale: {        
      type: Array,
      default: [1, 1]
    }
  },
  components: {
    Cropper
  },
  methods: {
    // 上傳圖片之前校驗(yàn)圖片大小是否超過(guò)了2M
    beforeAvatarUpload(file) {
      // 原圖片
      const isLt2M = file.size / 1024 / 1024 < 2;    
      //裁剪后的圖片(會(huì)比原圖片大很多窖认,應(yīng)該是轉(zhuǎn)成Blob的原因?qū)е拢?      if (!isLt2M) {
        this.$message.error("上傳圖片大小不能超過(guò) 2MB!");
        this.noCanUpload = true     // 如果這里被攔截,將自動(dòng)刪除不能上傳的圖片
        return false
      }
      return isLt2M
    },
    // 手動(dòng)實(shí)現(xiàn)上傳告希,將剪裁完畢的圖片和圖片寬高屬性派發(fā)給調(diào)用組件
    Upload(file) {
      let obj = {
        ...this.cro,
        file:this.uploadFile
      }
      this.$emit('getUploadImg',obj)
    },
    // 點(diǎn)擊重新上傳圖片進(jìn)行剪裁
    updateCropper() {
      if(!this.noCanUpload){
        let fileList = this.$refs.fileUpload.uploadFiles        // 獲取文件列表
        let index02 = fileList.findIndex(item => {        // 把取消裁剪的圖片刪除
          return item.uid == this.currentFile.uid;
        });
        fileList.splice(index02, 1)
      }
 
      let index = this.$refs.fileUpload.$children.length - 1;
      this.$refs.fileUpload.$children[index].$el.click();
    },
    // 關(guān)閉窗口,刪掉剪裁的圖片
    closeDialog() {
      this.showCropper = false;
      if(!this.noCanUpload){
        // 獲取文件列表
        let fileList = this.$refs.fileUpload.uploadFiles;
        let index = fileList.findIndex(item => {        
          return item.uid == this.currentFile.uid;
        });
        fileList.splice(index, 1)
      }
    },
    // 拿到剪裁之后的圖片,這里返回的圖片文件是Blob格式
    uploadImg({data,cro}) {
        this.uploadFile = data;
        this.cro = cro;
        //裁剪后的圖片寬扑浸,高  ==> 取最小值
        let minProp = Math.min(cro.width, cro.height)  
        // 如果最小值比設(shè)置的最小值(默認(rèn)為300)小
        if( minProp < 300){    
          this.$message.error(`圖片寬度最小不能小于300px`);
          return false
        }
        this.$refs.fileUpload.submit();
        this.closeDialog();
    },
    // 監(jiān)聽(tīng)上傳改變,把上傳的圖片轉(zhuǎn)換blob數(shù)據(jù)格式并且生成url路徑傳遞給cropper組件
    selectChange(file) {
      this.noCanUpload = false
      let files = file.raw;
      var reader = new FileReader();
      reader.onload = e => {
        let data;
        if (typeof e.target.result === "object") {
          // 把Array Buffer轉(zhuǎn)化為blob 如果是base64不需要
          data = window.URL.createObjectURL(new Blob([e.target.result]));
        } else {
          data = e.target.result;
        }
        this.cropperImg = data;
      };
      //轉(zhuǎn)換file文件為緩存buffer流,方便生成blob數(shù)據(jù)格式
      reader.readAsArrayBuffer(files);
      // 默認(rèn)開(kāi)啟裁剪
      this.showCropper = true;
      // 組裝數(shù)據(jù)燕偶,重新上傳的時(shí)候會(huì)用到
      let list = this.$refs.fileUpload.uploadFiles.length>0?this.$refs.fileUpload.uploadFiles:[];
      list.forEach(item=>{
        this.currentFile.push(item)
      })  
    }
  }
};
</script>
 
<style lang="scss">
.cropper-upload-box{
  display: flex;
  .el-upload{
    width: 148px;
    height: 148px;
  }
}
</style>

調(diào)用上傳剪裁組件推送服務(wù)端

  1. 調(diào)用上傳剪裁組件首装,這里使用的是eggjs,下面我會(huì)貼出egg代碼杭跪;
<template>
  <div class="main-container">
    <upload-cropper :zoomScale="zoomScale" @getUploadImg="getUploadImg"></upload-cropper>
    <img :src="cropperImage.src" alt="" :style="{'width':cropperImage.width+'px'}">
  </div>
</template>


<script>
import uploadCropper from "@/components/upload/index.vue";
export default {
  components: {
    uploadCropper
  },
  data() {
    return {
      zoomScale: [1, 1.2],
      cropperImage:{
        width:300,
        height:300,
        src:''
      }
    }
  },
  methods: {
    getUploadImg({file,width,height}) {
      let formData = new FormData();
      let fileOfBlob  = new File([file],"img.jpeg", {type:"image/jpeg"})
      formData.append('files',fileOfBlob );
      this.$axios({
        url: '/upload/stream',
        method: 'post',
        headers: {
          'Content-Type': 'multipart/form-data;boundary='+new Date().getTime()
        },
        data: formData,
      }).then(res => {
        if(res.res.status===200){
          // 返回?cái)?shù)據(jù)剪裁圖片的url,剪裁圖片的寬度和剪裁圖片的高度
          this.cropperImage ={
            src:res.url,
            width,
            height
          }
        }
      })
    }
  }
}
</script>
<style lang="scss">
.main-container {
  width: 1200px;
  margin: -10px auto 0 auto;
  .main-wrapper {
    display: flex;
  }
}
</style>

egg服務(wù)端接收到數(shù)據(jù)之后推送到阿里云oss

  1. 服務(wù)端接收數(shù)據(jù)仙逻;
'use strict';
const Controller = require('egg').Controller;
const fs = require('fs')
const path = require('path')
class CommonController extends Controller {

  //上傳流文件推送oss
  async uploadStream(){
    const { ctx } = this;
    //上傳的不能是文件,必須是二進(jìn)制流
    const stream = await ctx.getFileStream();
    // 生成指定的圖片文件名稱
    const name = 'egg-oss-baiyan/'+ctx.getID(10)+ path.basename(stream.filename);
    let result;
    try{
      // 推送阿里云
      result = await ctx.oss.put(name, stream);
    }catch(err){
        console.log(err);
    }
    // 在ctx擴(kuò)展封裝返回前臺(tái)的數(shù)據(jù)
    return ctx.apiSuccess(result);
  }
}
module.exports = CommonController;

補(bǔ)充egg對(duì)數(shù)據(jù)流的配置

  1. egg對(duì)數(shù)據(jù)流的配置涧尿;
  // 配置egg前臺(tái)上傳文件服務(wù)端接收的數(shù)據(jù)格式類型為流惡方式
  config.multipart = {
    fileSize:'50mb',
    mode: 'stream',
  }
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末系奉,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子姑廉,更是在濱河造成了極大的恐慌缺亮,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,386評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件桥言,死亡現(xiàn)場(chǎng)離奇詭異萌踱,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)号阿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門并鸵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人扔涧,你說(shuō)我怎么就攤上這事园担。” “怎么了枯夜?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,704評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵弯汰,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我湖雹,道長(zhǎng)咏闪,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,702評(píng)論 1 294
  • 正文 為了忘掉前任摔吏,我火速辦了婚禮鸽嫂,結(jié)果婚禮上织鲸,老公的妹妹穿的比我還像新娘。我一直安慰自己溪胶,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,716評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布稳诚。 她就那樣靜靜地躺著哗脖,像睡著了一般。 火紅的嫁衣襯著肌膚如雪扳还。 梳的紋絲不亂的頭發(fā)上才避,一...
    開(kāi)封第一講書(shū)人閱讀 51,573評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音氨距,去河邊找鬼桑逝。 笑死,一個(gè)胖子當(dāng)著我的面吹牛俏让,可吹牛的內(nèi)容都是我干的楞遏。 我是一名探鬼主播,決...
    沈念sama閱讀 40,314評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼首昔,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼寡喝!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起勒奇,我...
    開(kāi)封第一講書(shū)人閱讀 39,230評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤预鬓,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后赊颠,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體格二,經(jīng)...
    沈念sama閱讀 45,680評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,873評(píng)論 3 336
  • 正文 我和宋清朗相戀三年竣蹦,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了顶猜。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,991評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡痘括,死狀恐怖驶兜,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情远寸,我是刑警寧澤抄淑,帶...
    沈念sama閱讀 35,706評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站驰后,受9級(jí)特大地震影響肆资,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜灶芝,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,329評(píng)論 3 330
  • 文/蒙蒙 一郑原、第九天 我趴在偏房一處隱蔽的房頂上張望唉韭。 院中可真熱鬧,春花似錦犯犁、人聲如沸属愤。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,910評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)住诸。三九已至,卻和暖如春涣澡,著一層夾襖步出監(jiān)牢的瞬間贱呐,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,038評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工入桂, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留奄薇,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,158評(píng)論 3 370
  • 正文 我出身青樓抗愁,卻偏偏與公主長(zhǎng)得像馁蒂,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蜘腌,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,941評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容