upload component encapsulation of element UI

Keywords: Javascript Attribute axios Vue

Picture upload

No matter what project, it's probably necessary to upload pictures. As a common requirement, it will be used in many places. An upload component should be packaged separately to facilitate reuse.

Here, use Vue + element UI upload + qiniu cloud to finish the upload

Front end calls qinniu API

At present, the mainstream upload method of qiniu cloud is about authorized upload, which is about the following process:

  • Request the backend interface to obtain the upload voucher token (the backend generates the token through accessKey, secretKey, bucket)
  • Request the interface address of qinniu cloud to finish uploading
  • Qiniu cloud server returns the image address (returns hash and key, which needs to be spliced by itself)

If the back-end is very good, call the seven cattle upload interface directly on the server side, then the front-end only needs to pass a file to the back-end, and the front-end aspect will be much simpler.

However, I didn't encounter it, and the front-end calls the upload interface, which is more controllable.

About qiniu cloud's upload address:

After testing, the above four interfaces are available (https or http). My space here is South China region, and different regions will be different. Can refer to

Seven cattle API, with three parameters, token, file and key (optional), where key is the file name, which will be generated automatically if not transferred

First of all, analyze the requirements. The completed upload component should be combined with the form, which means to realize two-way binding. When calling this component, only the value (picture url) attribute needs to be bound. After the internal upload of the component is completed, change the value through $emit('input', url). This is very convenient.

The following El upload components are introduced:

  1. The action attribute is the upload interface address, directly using the upload address of qiniu cloud above.
  2. The name field is the parameter field name of the file stream. The default value is file. The parameter field of the file stream uploaded by qinniu cloud is file, so it can be ignored here.
  3. The data attribute is the parameter to be passed. The parameters required for qiniu cloud's upload address include file (automatically passed by the plug-in), key and token. The key can not be passed (automatically generated by qiniu). We just need to get the token and plug the token into data before calling qiniu.
  4. The before upload attribute is the hook before uploading the file. Here, call the back-end interface to get the token needed for upload, plug it into data, and then don't forget to resolve. Because obtaining the token is an asynchronous process, Promise is returned in the hook. After the request is successful, resolve will upload (if there is a need to verify the file size and file type, it can also be done in the hook. Return false in advance)

Here is the code:

<template>
  <el-upload
    v-loading="loading"
    class="uploader"
    :class="{'hover-mask': value}"
    action="https://up-z2.qiniup.com"
    :show-file-list="false"
    :data="param"
    accept="image/*"
    :on-success="handleSuccess"
    :before-upload="handlebeforeUpload">
      <img v-if="value" :src="value" class="avatar">
      <i class="el-icon-plus uploader-icon"></i>
  </el-upload>
</template>

<script>
import axios from 'axios'
export default {
  props: {
    value: String,
    required: true
  },
  data() {
    return {
      loading: '',
      param: {
        token: ''
      }
    }
  },
  methods: {
    handleSuccess(res, file) {
      this.loading = false
      // If the key parameter is not passed, the hash value automatically generated by 7n is used. If the key parameter is passed, the returned key value is used.
      const { hash } = res
      // Splicing to get picture url
      const imageUrl = 'your domain prefix' + hash
      // When the event input is triggered, the parent component will modify the value value of the binding
      this.$emit('input', imageUrl)
    },
    handlebeforeUpload(file) {
      // File verification can be done here
      const isImg = /^image\/\w+$/i.test(file.type)
      if (!isImg) {
        this.$message.error('Upload only JPG,PNG,GIF format!')
        return false
      }
      return new Promise((resolve, reject) => {
        this.loading = true
        // Get token
        const tokenUrl = 'http://xxx/upload'
        axios.get(tokenUrl).then(res => {
          const { token } = res.data.data
          this.param.token = token
          resolve(true)
        }).catch(err => {
          this.loading = false
          reject(err)
        })
      })
    }
  }
}
</script>

<style scoped lang="scss">
  .uploader {
    width: 130px;
    height: 130px;
    border: 1px dashed #d9d9d9;
    border-radius: 6px;
    cursor: pointer;
    &:hover {
      border-color: #409EFF;
    }
    /deep/ .el-upload {
      position: relative;
      width: 100%;
      height: 100%;
      overflow: hidden;
    }
  }
  .uploader-icon {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    line-height: 128px;
    text-align: center;
    font-size: 28px;
    color: #8c939d;
  }
  .avatar + .uploader-icon {
    opacity: 0;
  }
  .avatar {
    width: 128px;
    height: 128px;
    display: block;
    border-radius: 6px;
  }
  .hover-mask:hover .uploader-icon {
    opacity: 1;
    background-color: rgba(0, 0, 0, .2);
    color: #fff;
  }
</style>

How to use:

<template>
  <el-form ref="form" :model="form">
    <el-form-item label="Head portrait" prop="avatar">
      <upload v-model="form.avatar"></upload>
      </el-form-item>
    <el-form-item label="Full name" prop="userName">
      <el-input v-model="form.userName"></el-input>
      </el-form-item>
  </el-form>
    <el-button type="primary" @click="onSubmit">Determine</el-button>
</template>
<script>
import Upload from '@/components/Upload'
export default {
  components: { Upload },
  data () {
    return {
      form: {
        avatar: '',
        userName: ''
      }
    }
  },
  methods: {
    onSubmit() {
      this.$refs.form.validata(valid => {
        if (valid) {
          // Pass this.form to the backend
        }
      })
    }
  }
}
</script>

After the implementation of two-way binding, it is very convenient to collect data. It is ok to call the back-end interface to directly pass the binding value.

Front end directly calls back end upload interface

If the back end is very good (iron man), the front end only needs to pass the file object, which is easier. In the upload front hook, you don't need to get the token. This part of the work is handled by the back end. We just need to call the upload interface.

<template>
  <el-upload
    v-loading="loading"
    class="uploader"
    :class="{'hover-mask': value}"
    action="your upload api"
    :show-file-list="false"
    :on-success="handleSuccess"
    :before-upload="handlebeforeUpload">
      <img v-if="value" :src="value" class="avatar">
        <i class="el-icon-plus uploader-icon"></i>
  </el-upload>
</template>

<script>
import axios from 'axios'
export default {
  props: {
    value: String,
    required: true
  },
  data() {
    return {
      loading: ''
    }
  },
  methods: {
    handleSuccess(res, file) {
      this.loading = false
      const { hash } = res
      const imageUrl = 'your domain prefix' + hash
      this.$emit('input', imageUrl)
    },
    handlebeforeUpload(file) {
      // Return true directly without operation
      return true
    }
  }
}
</script>

In fact, when we use v-model on components, it is actually as follows:

<custom-upload
  :value="form.avatar"
  @input="form.avatar = $event"
></custom-upload>

For it to work properly, within the custom upload component you must:

  • Bind its value property to a prop named value (here is the image address)
  • When the value needs to be fixed, throw the new value through the custom input event (here is the $emit('input ', image address) after the upload is successful).

So v-model is a grammar sugar, a shorthand form. If you want to bind two-way and customize it, you can use the above method:

<custom-upload
  :url="form.avatar"
  @update:url="form.avatar = $event"
></custom-upload>

Then the url attribute is accepted inside the sub component as the image address. When updating the url, it should also be consistent with the bound event name $emit('update:url ', image address). The event name using update:propName is the recommended style of vue, which is to remind developers that this is a two-way binding attribute.

Of course, for convenience, vue provides an abbreviation for this pattern, the. sync modifier:

<custom-upload :url.sync="form.avatar"></custom-upload>

A value with a. sync modifier cannot be an expression (for example: url.sync="domain + form.avatar" is not valid)

Multi map uploading

Sometimes the requirements like uploading data and vouchers require uploading multiple images. We can encapsulate another component for uploading multiple images.

For El upload, please pay attention to the following points when uploading multiple images:

  1. The value of props is no longer a string. It should be an array. The array members are the picture address ['url1 ','url2']
  2. The file list property is the list of uploaded files. We can't assign value to it directly. The file list should be an array, such as [{name: 'foo.jpg', url: 'xxx'}, {name: 'bar.jpg', url: 'xxx'}]. This is different from the data we have passed in. We need to deal with value (of course, when we use components, we can directly pass the required format, so we don't need to deal with it.)
  3. Show file list is set to true. Of course, it can not be transmitted. It defaults to true.
  4. List type can be set to 'picture card', and pictures will be displayed in card form.
  5. The on remove attribute is the hook when removing a file from the file list. Here you need to trigger an event to update the value.
  6. The on preview property is the hook when clicking the uploaded file in the file list to preview the image.
  7. The limit attribute can specify the maximum number of uploads allowed. It is used in combination with the on exceeded attribute (the hook when the number of files exceeds the limit). When the upload exceeds the specified value, make some prompts in the hook.

Here is the code:

<template>
  <div>
    <el-upload
      :action="QINIU_UPLOAD_URL"
      :data="param"
      :file-list="fileList"
      list-type="picture-card"
      :limit="limit"
      :on-exceed="handleUploadExceed"
      :on-preview="handlePictureCardPreview"
      :on-remove="handleRemove"
      :before-upload="handlebeforeUpload"
      :on-success="handleSuccess">
      <i class="el-icon-plus"></i>
    </el-upload>
    <el-dialog :visible.sync="dialogVisible">
      <img width="100%" :src="dialogImageUrl" alt="">
    </el-dialog>
  </div>
</template>

<script>
import axios from 'axios'
export default {
  props: {
    value: {
      type: Array,
      default: () => []
    },
    limit: {
      type: Number,
      default: 4
    }
  },
  data() {
    return {
      dialogImageUrl: '', // Current preview image address
      dialogVisible: false, // Preview frame visible
      param: {
        token: ''
      }
    }
  },
  computed: {
    // ['xxx ','xxx'] converted to [{url: 'xxx'}, {url: 'xxx'}]
    fileList() {
      return this.value.map(url => ({ url }))
    }
  },
  methods: {
    handleUploadExceed() {
      this.$message.error(`Upload at most ${this.limit}Zhang picture`)
    },
    handleRemove(file, fileList) {
      // fileList is the list of deleted files
      const value = fileList.map(v => v.url)
      this.$emit('input', value)
    },
    handlePictureCardPreview(file) {
      this.dialogImageUrl = file.url
      this.dialogVisible = true
    },
    handlebeforeUpload(file) {
      return new Promise((resolve, reject) => {
        axios.get('/upload/qiniuToken').then(res => {
          const { token } = res.data
          this.param.token = token
          resolve(true)
        }).catch(err => {
          reject(err)
        })
      })
    },
    handleSuccess(res, file) {
      const { hash } = res
      const imageUrl = this.QINIU_PREFIX + hash
      // Here, if this.value.push(imageUrl) is written like this, vue will give a warning, which roughly means that value as props should not be modified in subcomponents.
      // this.value.concat(imageUrl) can also be used. The concat method returns a new array
      this.$emit('input', [...this.value, imageUrl])
    }
  }
}
</script>

How to use:

<template>
  <el-form ref="form" :model="form">
    <el-form-item label="Repayment amount" prop="amount">
      <el-input v-model="form.amount"></el-input>
    </el-form-item>
    <el-form-item label="voucher" prop="voucherUrlList">
      <multi-upload v-model="form.voucherUrlList"></multi-upload>
    </el-form-item>
  </el-form>
  <el-button type="primary" @click="onSubmit">Determine</el-button>
</template>
<script>
import MultiUpload from '@/components/MultiUpload'
export default {
  components: { MultiUpload },
  data () {
    return {
      form: {
        amount: '',
        voucherUrlList: []
      }
    }
  },
  methods: {
    onSubmit() {
      this.$refs.form.validata(valid => {
        if (valid) {
          // Pass this.form to the backend
        }
      })
    }
  }
}
</script>

After encapsulating the upload component in the form of two-way binding, we will be more convenient to use and reuse.

Posted by dgs on Sun, 27 Oct 2019 09:05:40 -0700