用vue写轮子的一些心得(十)——upload图片上传组件

需求分析

  • 支持正常的图片上传功能;
  • 支持图片多选上传;
  • 支持图片预览;
  • 支持配置长传图片大小校验,格式校验;
  • 支持图片上传时提示不同的状态,成功、失败、上传中;
  • 支持可X掉某一张图片;

 

方法实现

一、思路:

1、首先图片上传是需要有服务器支持的,这里我们用node在本地起一个服务,模拟服务器接收上传图片并保存,再通过img标签的url链接拿到服务器的图片地址。

2、多文件上传在前端只需要在类型为file的input的属性中增加multiple属性即可多选,文件格式同理在input上增加accept定义文件格式。服务器上也需要做相应的配置才能支持多文件上传。

3、整个图片上传的流程是:用户点击上传按钮,组件触发类型为file的input,用户选择完文件确定后,组件循环拿到文件数组中的文件并上传给服务器,此时文件的状态为上传中,上传成功后服务器保存文件后接口便返回给前端url路径,此时文件状态为成功,若上传过程中失败则状态为失败。上传成功后,img标签展示接口返回的url链接完成图片预览。整个图片上传流程结束。

那我们依次来看看每一个步骤的代码吧。

 

二、本地起一个存储图片的node服务器

index.js

const express = require('express')
const multer = require('multer')
const cors = require('cors')
const upload = multer({dest: 'images/'})
const app = express()

app.get('/', (req, res) => {
    res.send('hello word')
})

app.options('/upload', cors())
app.post('/upload', cors(), upload.single('file'), (req, res)=> {
    res.send(JSON.stringify({id: req.file.filename}))
})

app.get('/preview/:key', cors(), (req, res)=> {
    res.sendFile(`images/${req.params.key}`, {
        root: __dirname,
        headers: {
            'Content-Type': 'image/jpeg',
        }
    }, (error)=>{
        console.log(error)
    })
})
var port = process.env.PORT || 3000
console.log(port)
app.listen(port)

启动命令 node index

三、upload组件传参定义

<template>
    <div class="box">
        <t-upload
            accept="image/*"
            method="POST"
            action="http://127.0.0.1:3000/upload"
            name="file"
            :fileList.sync="fileList"
            :parseResponse="parseResponse"
            @update:fileList="xxx"
        >
            <template>
                <t-button icon="upload">上传</t-button>
            </template>
        </t-upload>
    </div>
</template>
export default {
    data() {
        return {
            fileList: [] //图片上传成功的url数组
        }
    },
    methods: {
        parseResponse(response) {
            let object = JSON.parse(response)
            return `http://127.0.0.1:3000/preview/${object.id}`
        },
        xxx(fileList) {
            // console.log('监听到了 update:fileList 事件')
            // console.log(fileList, 'fileList')
        }
    },
}

四、upload组件内部实现

<template>
    <div class="t-uploader">
        <div @click="onClickUpload">
            <slot></slot>
        </div>
        <ol class="t-uploader-fileList">
            <li v-for="file in fileList" :key="file.name">
                <template v-if="file.status === 'uploading'">
                    <t-icon name="loading" class="t-uploader-spin"></t-icon>
                </template>
                <template v-else-if="file.type.indexOf('image') > -1">
                    <img class="t-uploader-image" :src="file.url" width="32" height="32" alt="">
                </template>
                <template v-else>
                    <div class="t-uploader-defaultImage"></div>
                </template>
                <span class="t-uploader-name" :class="{[file.status]: file.status}">{{file.name}}</span>
                <button class="t-uploader-remove" @click="onRemoveFile(file)">X</button>
            </li>
        </ol>
        <div ref="box" style="height:0; height: 0; overflow: hidden;"></div>
    </div>
</template>
export default {
    props: {
        name: {
            type: String,
            required: true
        },
        action: {
            type: String,
            required: true
        },
        accept: {
            type: String,
            default: 'image/*'
        },
        method: {
            type: String,
            default: 'POST'
        },
        parseResponse: {
            type: Function,
            required: true
        },
        fileList: {
            type: Array,
            default: ()=> []
        },
        sizeLimit: {type: Number}
    },
    methods: {
        onClickUpload() {
            let input = this.createInput()
            input.addEventListener('change', ()=> {
                this.uploadFiles(input.files) //单文件
                input.remove()
            })
            input.click()
        },
        uploadFiles(rawFiles) {
            let newNames = []
            for (let i=0; i<rawFiles.length; i++) {
                let rawFile = rawFiles[i]
                let {name, size, type} = rawFile
                newNames[i] = this.generateName(name)
            }
            if (!this.beforeUploadFiles(rawFiles, newNames)) {return}
            for (let i=0; i<rawFiles.length; i++) {
                let rawFile = rawFiles[i]
                let newName = newNames[i]
                let formData = new FormData()
                formData.append(this.name, rawFile)
                this.doUploadFile(formData, (response)=> {
                    let url = this.parseResponse(response)
                    this.afterUploadFiles(newName, url)
                }, (xhr)=> {
                    this.uploadError(xhr, newName)
                })
            }
        },
        beforeUploadFiles(rawFiles, newNames) {
            rawFiles = Array.from(rawFiles)
            for (let i=0; i< rawFiles.length; i++) {
                let {size, type} = rawFiles[i]
                if (size > this.sizeLimit) {
                    this.$emit('error', `文件不能大于${this.sizeLimit * 1024}M`)
                    return false
                }
            }
            let x = rawFiles.map((rwaFile, i)=> {
                let {type, size} = rwaFile
                return {name: newNames[i], type, size, status: 'uploading'}
            })
            this.$emit('update:fileList', [...this.fileList, ...x])
            return true
        },
        afterUploadFiles(newName, url) {
            let file = this.fileList.filter(f=> f.name === newName)[0]
            let index = this.fileList.indexOf(file)
            let fileCopy = JSON.parse(JSON.stringify(file))
            fileCopy.url = url
            fileCopy.status = 'success'
            let fileListCopy = [...this.fileList]
            fileListCopy.splice(index, 1, fileCopy)
            this.$emit('update:fileList', fileListCopy)
            this.$emit('uploaded') //全部上传完成的回调
        },
        generateName(name) {
            while (this.fileList.filter(f => f.name === name).length > 0) {
                let duIndexOf = name.lastIndexOf('.')
                let nameBefore = name.substring(0, duIndexOf)
                let nameAfter = name.substring(duIndexOf)
                name = nameBefore + '(1)' + nameAfter
            }
            return name
        },
        uploadError(xhr, newName) {
            let file = this.fileList.filter(f => f.name === newName) [0]
            let index = this.fileList.indexOf(file)
            let fileCopy = JSON.parse(JSON.stringify(file))
            fileCopy.status = 'fail'
            let fileListCopy = [...this.fileList]
            fileListCopy.splice(index, 1, fileCopy)
            this.$emit('update:fileList', fileListCopy)
            let error = ''
            if (xhr.status === 0) { // xhr === XMLHttpRequest
                error = '网络无法连接'
            }
            this.$emit('error', error)
        },
        onRemoveFile(file) {
            let answer = window.confirm('你确定要删除它吗?')
            if (answer) {
                let copy = [...this.fileList]
                let index = copy.indexOf(file)
                copy.splice(index, 1)
                this.$emit('update:fileList', copy)
            }
        },
        createInput() {
            this.$refs.box.innerHTML = ''
            let input = document.createElement('input')
            input.accept = this.accept
            input.type= 'file'
            input.multiple = true
            this.$refs.box.appendChild(input)
            return input
        },
        doUploadFile(formData, success, fail) {
            $http[this.method.toLowerCase()](this.action, {success, fail, data: formData}) //本地模拟接口请求
        }
    }
}

 

完整代码可参考我的GitHub:https://github.com/A-Tione/tione/blob/master/src/upload/upload.vue

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值