Vue에서 Crop 지원 Cloudinary 이미지 업로드 프로그램 만들기

약 1년여 전에 저는 클라우드 나리를 만났습니다. 그 이후로 저는 대부분의 개인 프로젝트의 주요 이미지 납부 네트워크가 되었습니다. 왜냐하면 합리적인 비용 지불 계획을 제공하는 것 외에 매우 광범위한 무료 계획도 제공했기 때문입니다.나에게 있어서 또 다른 장점은 설치하고 사용하기 쉽다는 것이다.
한동안 사용해 왔고, 많은 Vue 프로젝트에서 일하고 있음을 감안하여, 이 문제를 해결하기 위해 Vue 구성 요소를 만들기로 결정했습니다.
그림을 클라우드에 업로드하기 전에 재단 기능을 추가할 수 있습니다.
완성된 후에 구성 요소가 어떻게 운행될 것인지에 대한 예상은 다음과 같다.

laravel과 함께 작업하는 유사한 Vue 구성 요소도 만들었습니다. 저도 이 글을 쓸 것입니다.
계속하기 전에 먼저 cloudinary에 계정을 만들어야 합니다.
그럼 업로드 프로그램을 만듭니다.

cloudinary 설정


cloudinary 계정에 로그인하여 업로드 설정을 만들고 업로드된 이미지를 처리합니다.업로드 사전 설정을 설정하기 전에 업로드 중인 이미지를 저장할 미디어 라이브러리 폴더를 먼저 만듭니다.
미디어 라이브러리에 가기

새 폴더 만들기

그런 다음 업로드 사전 설정을 계속 작성합니다.
설정 > 업로드(탭)

아래로 스크롤하고 를 클릭하여 업로드 사전 설정을 추가합니다.

업로드 사전 설정 이름을 설정하고, 서명하지 않은 서명 모드를 선택하고, 위에서 만든 폴더의 이름을 입력합니다.

주의: 클라이언트 프로그램에서 업로드 기능을 실현할 때 서명하지 않은 업로드 설정을 사용합니다.
업로드된 이미지를 삭제하려면 Upload control로 이동하여 Return delete token (삭제 영패로 되돌아가기) 을 on (열기) 으로 전환하십시오.

주의: 파일을 업로드한 후, 업로드한 파일을 삭제하려면 10분 안에만 삭제 영패를 사용할 수 있습니다.
나머지 설정을 기본 설정으로 설정한 다음 저장을 클릭하여 새 업로드 사전 설정을 저장합니다.
설정 페이지와 업로드 탭으로 돌아가면 새 업로드 사전 설정이 기존 사전 설정에 나열됩니다.

Vue 구성 요소


Vue 구성 요소를 만듭니다. 이 구성 요소는 생성된 업로드 설정을 통해 이미지를cloudinary에 업로드합니다.

거푸집


<template>
  <div id="vue-cloudinary-uploader">
      <input type="hidden" :value="uploadedImageData.secureUrl" :name="inputName">

      <button v-if="uploadedImageData.secureUrl" class="vcu-button button-danger" type="button" @click="deleteImageFromCloud()">Change Image</button>
      <button id="uploader-button" class="vcu-button button-info" type="button" v-else @click.prevent="showModal()" :disabled="processingUpload || modelVisible">Select Image File</button>

    <div id="modal-wrapper" v-show="modelVisible">
      <div class="image-cropper">
        <div class="editor">
          <div class="input">
            <div>
              <input type="file" ref="photo" accept="image/*" @change="addLocalImage()" id="vcu-file-input">
            </div>
          </div>
          <div v-if="showUploadProgress" class="vcu-progress-wrapper">
            <div class="vcu-progress" :style="'width: ' + uploadProgress + '%'"></div>
          </div>
          <div class="img">
            <img ref="working_image" id="image" :src="localFileDataUrl">
          </div>
        </div>
        <div class="options">
          <div>
            <button class="vcu-button button-danger" type="button" @click="destroyUploaderInstance(true)" :disabled="processingUpload">CANCEL</button>
          </div>
          <div>
            <button class="vcu-button button-info" type="button" @click="getCroppedCanvas()" :disabled="processingUpload">CROP</button>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
위의 템플릿은 uploader 모드의 시작을 촉발하는 단추를 포함하고, 이 모드에는 이미지 재단 기능을 제공하는 재단기 용기가 설치되어 있습니다.

Javascript


설치cropperjs는 이미지를 재단하는 데 사용되고, 설치axios는 이미지를 업로드하는 http 클라이언트에 사용됩니다.
#install cropperjs
npm i cropperjs

#install axios
npm i axios
다음은 js 코드의brekdown입니다.
  import axios from 'axios'
  import Cropper from 'cropperjs'
  import 'cropperjs/dist/cropper.css'
  export default {
    name: "CloudinaryVueUploader",
    data(){
      return {
        showFileSelector: true,
        showImageCropper: false,
        showUploadProgress: false,
        modelVisible: false,
        processingUpload: false, // this will be true when image is being uploaded to prevent any other upload request

        cropperInstance: null,
        uploadProgress: 0,
        localFileDataUrl: '',
        cloudinaryUploadUrl: '',
        cloudinaryDeleteUrl: '',
        uploadedImageData: {
          deleteToken: '',
          publicId: '',
          secureUrl: ''
        }
      }
    },
어셈블리에서 사용할 변수를 시작합니다.
    props: {
      CloudinaryCloudName: {
        type: String,
        default: 'CLOUDINARY_CLOUD_NAME',
        validator: (x) => x !== ''
      },
      cloudinaryUploadPreset: {
        type: String,
        default: 'CLOUDINARYY_UPLOAD_PRESET_NAME',
        validator: (x) => x !== ''
      },
      aspectRatio: {
        type: Number,
        default: 0
      },
      inputName: {
        type: String,
        default: 'imageToUpload'
      },
    },
이것은 Vue 구성 요소이기 때문에, 상기 도구는 Cloudinar CycloudName과cloudinary UploadPreset 변수 데이터의 전달을 간소화할 것입니다.
Cloudinar CycloudName은cloudinary 대시보드의 계정 상세 정보 아래 첫 번째 항목입니다.
    mounted(){
      this.cloudinaryUploadUrl = `https://api.cloudinary.com/v1_1/${this.CloudinaryCloudName}/upload`
      this.cloudinaryDeleteUrl = `https://api.cloudinary.com/v1_1/${this.CloudinaryCloudName}/delete_by_token`
    },
구성 요소를 불러올 때,cloudinary UploadUrl과cloudinary DeleteUrl 변수를 채웁니다.
    methods: {
      showModal(){
        this.modelVisible = true
      },
      hideModal(){
        this.modelVisible = false
      },
이 두 가지 방법은 구성 요소의 모드를 전환하는 것을 책임질 것이다.
      editImage(){
        this.showImageCropper = true
        if(this.cropperInstance){
          this.cropperInstance.destroy()
          this.showImageCropper = false
        }
        this.$nextTick(() => {
          this.cropperInstance = new Cropper(this.$refs.working_image, {
            aspectRatio: this.aspectRatio,
            viewMode: 2,
            background: false,
            crop(event) {},
            ready(){
            this.showImageCropper = true
          }
          });
        })
      },
editImage(): 선택한 이미지에서 재단기 인스턴스를 시작합니다.
      async addLocalImage(){
        if(this.$refs.photo.files.length < 1){
          console.log('No photo selected')
          return false
        }
        let photo = this.$refs.photo.files[0];
        this.localFileDataUrl = window.URL.createObjectURL(photo)
        this.$nextTick(this.editImage())
      },
addLocalImage(): 이미지를 선택할 때 editImage()를 호출합니다.
      async getCroppedCanvas(){
        if(!this.cropperInstance){
          alert("Select Image File!")
          return false
        }
        if(!this.cropperInstance.getCroppedCanvas()){
          alert("No Image Detected!")
          return false
        }
        if(this.processingUpload){ // don't initiate another upload while one is running
          alert("Previous upload not completed!")
          return false
        }
        let canvas = this.cropperInstance.getCroppedCanvas()
        await canvas.toBlob( (blob) => {
          let formData = new FormData()
          formData.append('file', blob)
          formData.append('upload_preset', this.cloudinaryUploadPreset)
          this.uploadImageToCloud(formData)
        })
      },
getcroppedCanvas (): 재단기의 캔버스를 가져오고 이 데이터를 uploadImageTocloud () 에 전달합니다.
      destroyUploaderInstance(closeCropper = false){
        // destroy cropper instance
        if(this.cropperInstance && closeCropper){
          this.cropperInstance.destroy()
        }
        // set all other variables to their defaults
        this.cropperInstance = null
        this.localFileDataUrl = ''
        this.processingUpload = false
        this.showFileSelector = true
        this.showImageCropper = false
        this.showUploadProgress = false
        this.uploadProgress = 0
        document.getElementById("vcu-file-input").value = "";
        this.uploadedImageData = { deleteToken: '', publicId: '', secureUrl: '' }
        this.$emit('uploaderDestroyed', "" )
        if(closeCropper){
          this.hideModal()
        }
      },
destroyUploaderInstance(): 어셈블리의 변수를 재설정합니다.
      uploadImageToCloud(formData){
        this.showUploadProgress = true
        this.processingUpload = true
        this.uploadProgress = 0
        axios.post(this.cloudinaryUploadUrl, formData, {
          onUploadProgress: (progressEvent) => {
            this.uploadProgress = progressEvent.lengthComputable ? Math.round( (progressEvent.loaded * 100) / progressEvent.total ) : 0 ;
          }
        })
        .then( (response) => {
          this.uploadedImageData = {
            secureUrl: response.data.secure_url,
            deleteToken: response.data.delete_token,
            publicId: response.data.public_id
          }
          this.showUploadProgress = false
          this.processingUpload = false
          this.$emit('imageUrl', response.data.secure_url )
          this.hideModal()
        })
        .catch( (error) => {
          if(error.response){
              console.log(error.message)
          }else{
              console.log(error)
          }
          this.showUploadProgress = false
          this.processingUpload = false
        })
      },
uploadImageTocloud (): 이 방법은 수정된 이미지를 업로드하고,cloudinary에서 업로드된 이미지 URL, 영패, 공공 id를 삭제합니다.
      deleteImageFromCloud(){
        if(this.uploadedImageData.deleteToken === ''){ // if delete token is not provided
          console.log("uploadedImageData ", this.uploadedImageData)
          alert("No Delete token")
        }
        let formData = new FormData()
        formData.append('token', this.uploadedImageData.deleteToken)
        axios.post(this.cloudinaryDeleteUrl, formData)
          .then(response => {
            if(this.cropperInstance){
              this.cropperInstance.destroy()
            }
            this.destroyUploaderInstance()
            this.showModal()
            this.$emit('remoteImageDeleted')
          })
          .catch(error=>{
            console.log(error)
            return false
          })
      }
    }
  }
deleteImageFromCloud(): 이 방법은 이미지를 업로드할 때 얻은 deleteToken을 사용하여 이전에 업로드한 이미지를 삭제합니다.
주의: deleteToken을 사용하여 업로드 삭제는 업로드 이미지 후 10분 이내에만 가능합니다.

콘셉트


이후 구성 요소의 끝에 다음과 같은 스타일을 추가하거나 자신의 취향에 따라 수정합니다.
  :root{
    --default-font-family: Arial, Helvetica, sans-serif;
    --default-font-weight-small: 300;
    --default-font-weight-medium: 600;
    --default-font-weight-heavy: 900;

    --default-space-x-small: 5px;
    --default-space-small: 10px;
    --default-space-medium: 15px;
    --default-space-large: 25px;

    --color-primary: rgb(233,233,239);
    --color-secondary: rgb(248,248,250);
    --color-tertiary: rgb(255,255,255);
    --color-danger: rgb(220, 20, 60);
    --color-danger-dark: rgb(200, 20, 60);
    --color-danger-light: rgb(240, 20, 60);
    --color-info: rgb(13, 125, 216);
    --color-info-dark: rgb(10, 94, 196);
    --color-info-light: rgb(10, 94, 236);
    --color-success: rgb(16, 190, 10);
    --color-text-button: aliceblue;
  }

  * {
    font-family: var(--default-font-family);
  }

  .vcu-progress-wrapper{
    height: 30px;
    width: 100%;
    padding: 0;
  }
  .vcu-progress{
    margin: 0;
    height: inherit;
    background: var(--color-success)
  }

/*button styles*/
  .vcu-button{
    position: relative;
    background: var(--color-primary);
    border: none;
    border-radius: 2px;
    color: var(--color-text-button);
    font-weight: 500;
    padding: var(--default-space-small);
    margin-left: 5px;
    cursor: pointer;
  }
  .vcu-button:disabled{
    background: var(--color-primary) !important;
    color: black !important;
  }

  .close-button{
    position: absolute;
    top: 0; right: 0;
    margin: var(--default-space-x-small);
    padding: var(--default-space-small);
    font-weight: var(--default-font-weight-medium);
    cursor: pointer;
    z-index: 5;
  }
  .close-button:hover{
    background-color: var(--color-danger-light);
  }
  .close-button:active{
    background-color: var(--color-danger-dark);
  }

  .button-danger{
    background-color: var(--color-danger);
  }
  .button-danger:hover{
    background-color: var(--color-danger-light);
  }
  .button-danger:active{
    background-color: var(--color-danger-dark);
  }
  .button-info{
    background-color: var(--color-info);
  }
  .button-info:hover{
    background-color: var(--color-info-light);
  }
  .button-info:active{
    background-color: var(--color-info-dark);
  }

  #modal-wrapper{
    position: fixed;
    top: 10px; bottom: 10px;
    left: 100px; right: 100px;
    border: 1px solid var(--color-primary);
    margin: var(--default-space-small);
    z-index: 99999;
    display: flex;
    box-shadow: 0 0 5px 0 #b1b0b0;
  }

  .image-cropper{
    flex-grow: 1;
    /* padding: var(--default-space-small); */
    display: flex;
    flex-direction: column;
    background-color: var(--color-tertiary);
    max-height: inherit;
    max-width: inherit;
  }

  .image-cropper > .editor{
    flex-grow: 1;
    display: flex;
    flex-direction: column;
    background-color: var(--color-tertiary);
    padding: var(--default-space-small);
    max-height: inherit;
    max-width: inherit;
    overflow: hidden;
  }
  .image-cropper > .editor > .input{
    height: 50px;
    display: flex;
    flex-direction: row;
    align-items: flex-start;
  }
  .image-cropper > .editor > .input{
    height: 50px;
    display: flex;
    flex-direction: row;
    justify-content: center;
  }
  .image-cropper > .editor > div > .input > input, .image-cropper > .editor > .input > button{
    min-height: 20px;
    font-size: 16px;
    margin-left: var(--default-space-small);
  }


  input[type="file"]{
    position: relative !important;
    top: 1% !important;
    z-index: 1 !important;
    width: initial !important;
    height: initial !important;
    -webkit-appearance: initial !important;
    opacity: 1 !important;
    cursor: pointer !important;
  }

  .image-cropper > .editor > .img{
    position: relative;
    max-height: -webkit-fill-available;
    padding: var(--default-space-small);
    flex-grow: 1;
    background: var(--color-tertiary);
    min-height: 20px;
    margin-bottom: 20px;
  }

  .image-cropper > .options{
    display: flex;
    flex-direction: row;
    justify-items: center;
    justify-content: flex-end;
    height: 50px;
    background-color: var(--color-secondary);
    padding: var(--default-space-x-small);
    border-top: 1px solid var(--color-primary);
  }

  .image-cropper > .options > div{
    display: flex;
    justify-content: center;
    align-items: center;
    width: 100px;
    padding: var(--default-space-small);
    margin-left: var(--default-space-x-small);
    cursor: pointer;
    font-weight: var(--default-font-weight-small);
  }


  @media screen and (orientation: portrait){
    #modal-wrapper{
      left: 20px; right: 20px;
    }

    .image-cropper > .options{
      height: 10vmin;
    }
  }

  img {
    max-width: 100%;
  }

/*model styes*/
  .show{
    display: flex !important
  }
  .hide{
    display: none !important
  }

업로드된 이미지 URL 가져오기


이 구성 요소에서 업로드된 이미지의 URL을 얻을 수 있는 두 가지 방법이 있습니다.첫 번째 방법은 구성 요소의 이미지 Url 이벤트를 감청하는 것입니다. 두 번째 방법은 inputName 속성을 전달하는 것입니다. 이 속성은 구성 요소에 숨겨진 입력의name 속성을 채웁니다.
후자는 Vue 환경 밖에서 이 구성 요소를 사용하는 데 유리하다. 예를 들어 칼날 모양 내부에 있다.
지금 나가서 인터넷 파괴해.

좋은 웹페이지 즐겨찾기