使用Vue3实现一个Upload组件的示例代码

2022-04-15 0 837
目录
  • 通用上传组件开发
  • 我们需要实现如下功能
    • 自定义模版
    • 支持文件上传列表
    • 支持一系列生命周期钩子事件,上传事件
  • 拖拽支持
    • 写在最后

      通用上传组件开发

      开发上传组件前我们需要了解:

      • FormData上传文件所需API
      •  dragOver文件拖拽到区域时触发
      • dragLeave  文件离开拖动区域
      • drop文件移动到有效目标时

      首先实现一个最基本的上传流程:

      基本上传流程,点击按钮选择,完成上传

      代码如下:

      <template>
        <div class="app-container">
            <!--使用change事件-->
            <input type="file" @change="handleFileChange">
        </div>
      </template>
      <script lang="ts">
      import { defineComponent } from 'vue'
      import axios from 'axios'
      export default defineComponent({
        name: 'App',
        setup() {
          const handleFileChange = (e: Event) => {
            // 断言为HTMLInputElement
            const target = e.target as HTMLInputElement
            const files = target.files
            if(files) {
              const uploadedFile = files[0]
              const formData = new FormData()
              formData.append('file', uploadedFile)
              // 使用node模拟上传接口
              axios.post('http://localhost:3001/upload', formData, {
                headers: {
                  "Content-Type": 'multipart/form-data'
                }
              }).then(resp=> {
                console.log('resp', resp)
              }).catch(error=> {
      
              })
            }
          }
      
          return {
            handleFileChange  
          }
        }
      })
      </script>
      
      <style>
      .page-title {
        color: #fff;
      }
      </style>

      结果如下:

      使用Vue3实现一个Upload组件的示例代码

      到这里我们基本的上传已经处理完成了,相对来说还是比较简单的,接下来我们创建Uploader.vue文件用来封装Upload组件。

      我们需要实现如下功能

      自定义模版

      我们知道使用<input type=”file”>系统自带样式比较难看所用我们需要如下处理:

      • 对样式进行优化,隐藏input
      • 点击<div class=”upload-area” @click=”triggerUpload”></div>使用js出发input的click事件
      • 处理上传状态

      代码如下:

      <template>
        <div class="file-upload">
          <div class="upload-area" @click="triggerUpload"></div>
          <span v-if="fileStatus==='loading'">正在上传</span>
          <span v-else-if="fileStatus==='success'">上传成功</span>
          <span v-else-if="fileStatus==='error'">上传失败</span>
          <span v-else>点击上传</span>
          <input ref="fileInput" type="file" name="file" style="display: none" />
        </div>
      </template>
      <script lang="ts">
          import { computed, defineComponent, PropType, reactive, ref } from 'vue'
          import axios from 'axios'
          type UploadStatus = 'ready' | 'loading' | 'success' | 'error'
          export default defineComponent({
              props: {
                  action: { // url地址
                      type: String,
                      required: true
                  }
              },
              setup(props) {
                  // input实例对象,通过与ref="fileInput"的关联获取到input实例对象
                  const fileInput = ref<null | HTMLInputElement>(null)
                  const fileStatus = ref<UploadStatus>('ready')
                  
                  // 1.div点击事件
                  const triggerUpload = () => {
                      if(fileInput.value) {
                          fileInput.value.click()
                      }
                  }
                  // 通过div来触发input的change事件
                  const handleFileChange = (e: Event) => {
                    const target = e.target as HTMLInputElement
                    const files = target.files
                    if(files) {
                      const uploadedFile = files[0]
                      const formData = new FormData()
                      formData.append('file', uploadedFile)
                      readyFile.status = 'loading' // 上传之前将状态设置为loading
                      axios.post(props.actions, formData, {
                        headers: {
                          "Content-Type": 'multipart/form-data'
                        }
                      }).then(resp=> {
                        console.log('resp', resp)
                        readyFile.status = 'success' // 上传成功将状态设置为success
                      }).catch(error=> {
                      readyFile.status = 'error' // // 上传失败将状态设置为error
                      })
                    }
                  }
                  return {
                      fileInput,
                      triggerUpload,
                      handleFileChange,
                      fileStatus
                  }
      
              }
          })
      </script>
      
      

      现在我们已经完成了对上传组件样式的优化,和上传状态的处理,接下来我们需要处理文件上传列表

      支持文件上传列表

      处理文件上传列表我们需要有如下实现:

      • 显示文件名称
      • 状态
      • 可删除
      • 显示上传进度
      • 有可能有更丰富的显示

      在上一步代码的基础上做一些修改:

      <template>
        <div class="file-upload">
          <div class="upload-area" @click="triggerUpload"></div>
          <!-- 点击上传态 -->
            <slot v-if="isUploading" name='loading'>
              <button disabled>正在上传</button>
            </slot>
            <!-- 上传完毕态 -->
            <slot name="uploaded" v-else-if="lastFileData && lastFileData.loaded" :uploadData="lastFileData.data">
              <button disabled>点击上传</button>
            </slot>
            <!-- 默认态 -->
            <slot v-else name='default'>
              <!-- <button disabled>点击上传</button> -->
              <span>点击上传</span>  
            </slot>
          <input ref="fileInput" type="file" name="file" style="display: none" />
          <!--展示fileList-->
          <ul>
            <li 
            v-for="file in filesList"
            :key="file.uid"
            :class="`uploaded-file upload-${file.status}`"
            >
              <span class="filname">
                {{ file.name }}
              </span>
              <button class="delete-icon" @click="reomveFile(file.uid)">Del</button>
            </li>
          </ul>
        </div>
      </template>
      <script lang="ts">
          import { last } from 'lodash-es'
          import { v4 as uuidv4 } from 'uuid';
          // 定义上传状态
          type UploadStatus = 'ready' | 'loading' | 'success' | 'error' 
          // step1 定义上传文件对象接口类
          export interface UploadFile {
            uid: string; // 文件唯一id
            size: number; // 文件大小
            name: string; // 文件名称
            status: UploadStatus; // 上传状态
            raw: File;            // 文件
            progress?: string;    // 文件上传进度
            resp?: any;           // 服务端返回数据
            url?: string          // 对应展示的url
          }
          export default defineComponent({
              props: {
                  action: { // url地址
                      type: String,
                      required: true
                  }
              },
              setup(props) {
                  // input实例对象,通过与ref="fileInput"的关联获取到input实例对象
                  const fileInput = ref<null | HTMLInputElement>(null)
                  // step2 上传文件列表
                  const filesList = ref<UploadFile[]>([])
                  // step4-1 判断是否正在上传中
                  const isUploading = computed(()=> {
                      return filesList.value.some((file)=>file.status==='loading')
                  })
                  //step4-2 获取上传文件的最后一项
                  const lastFileData = computed(()=>{
                    const lastFile = last(filesList.value)
                    if(!lastFile) return false
                    return {
                      loaded: lastFile?.status === 'success',
                      data: lastFile?.resp
                    }
                  })
                  // 1.div点击事件
                  const triggerUpload = () => {
                      if(fileInput.value) {
                          fileInput.value.click()
                      }
                  }
                  // 通过div来触发input的change事件
                  const handleFileChange = (e: Event) => {
                    const target = e.target as HTMLInputElement
                    const files = target.files
                    if(files) {
                      const uploadedFile = files[0]
                      const formData = new FormData()
                      formData.append('file', uploadedFile)
                      // step3 设置响应式对象,存储到filesList中,以便在页面中展示
                      const fileObj = reactive<UploadFile>({ 
                        uid: uuid(); // 文件唯一id
                        size: uploadedFile.size,
                        name: uploadedFile.name,
                        status: 'loading',
                        raw: uploadedFile
                      })
                      filesList.value.push(fileObj)
                      
                      axios.post(props.actions, formData, {
                        headers: {
                          "Content-Type": 'multipart/form-data'
                        },
                        //step6 处理上传进度
                        onUploadProgress: (progressEvent)=> {
                            const complete = (progressEvent.loaded / progressEvent.total * 100 | 0) + '%'
                            fileObj.progress = complete
                        }
                      }).then(resp=> {
                        console.log('resp', resp)
                        fileObj.status = 'success'
                        
                      }).catch(error=> {
                          fileObj.status = 'error'
                      }).finally(()=> {
                          // 一张图片重复上传时无法继续上传bug
                          if(fileInput.value) {
                              fileInput.value.value = ''
                          }
                      })
                    }
                  }
                  // step7 处理删除
                  const reomveFile = (uid: string) => {
                       filesList.value = filesList.value.filter(file=>file.uid!==uid)
                  }
                  return {
                      fileInput,
                      triggerUpload,
                      handleFileChange,
                      fileStatus,
                      isUploading,
                      filesList,
                      lastFileData
                  }
              }
          })
      </script>
      
      • 首先我们定义上传文件对象接口类UploadFile
      • 创建了一个filesList响应式对象
      • 去掉了fileStatus,因为在UploadFile接口中我们已经有定义status
      • 修改模版中的状态,利用computed来判读是否是处于上传状态,并增加slot进行自定义
      • 展示上传的图片
      • 处理上传进度
      • 处理删除

      注意点:如果选择相同的图片不会进行上传操作,因为我们使用的是input的change事件,所以我们需要在上传后将input的value设置为null

      支持一系列生命周期钩子事件,上传事件

      beforeUpload

      <template>
       ...
      </template>
      <script lang="ts">
          import { last } from 'lodash-es'
          import { v4 as uuidv4 } from 'uuid';
          // 定义上传状态
          type UploadStatus = 'ready' | 'loading' | 'success' | 'error'
          // 定义上传文件为boolean或promsie且接受一个File
          type CheckUpload = ()=> boolean | Promise<File>
          export interface UploadFile {
            ...
          }
          export default defineComponent({
              props: {
                  action: { // url地址
                      type: String,
                      required: true
                  },
                  beforeUpload: {
                      type: Function as PropType<CheckUpload>
                  }
              },
              setup(props) {
                  const fileInput = ref<null | HTMLInputElement>(null)
                  const filesList = ref<UploadFile[]>([])
                  // step4-1 判断是否正在上传中
                  const isUploading = computed(()=> {
                      return filesList.value.some((file)=>file.status==='loading')
                  })
                  const lastFileData = computed(()=>{
                    const lastFile = last(filesList.value)
                    if(!lastFile) return false
                    return {
                      loaded: lastFile?.status === 'success',
                      data: lastFile?.resp
                    }
                  })
                  const triggerUpload = () => {
                      if(fileInput.value) {
                          fileInput.value.click()
                      }
                  }
                  const handleFileChange = (e: Event) => {
                    const target = e.target as HTMLInputElement
                    const files = target.files
                    const uploadedFile = files[0]
                    if(props.beforeUpload) {
                        const result = props.beforeUpload(uploadedFile)
                        if(result && result instanceof Promise) {
                            result.then((processFile)=> {
                                if(uploadedFile instanceof File) {
                                    postFile(uploadedFile)
                                }
                            }).catch(error=>{
                                console.error(error)
                            })  
                        } esle if(result===true) {
                        postFile(uploadedFile)
                        }
                    }else{
                        postFile(uploadedFile)
                    }
                    
                    
                  }
                  const reomveFile = (uid: string) => {
                       filesList.value = filesList.value.filter(file=>file.uid!==uid)
                  }
                  // 处理文件上传
                  const postFile = (readyFile:UploadFile)=> {
                      const formData = new FormData()
                      formData.append('file', readyFile.raw)
                      readyFile.status = 'loading'
                      axios.post(props.action, formData, {
                headers: {
                  "Content-Type": 'multipart/form-data'
                },
                onUploadProgress: (progressEvent)=> {
                    const complete = (progressEvent.loaded / progressEvent.total * 100 | 0) + '%'
                  //   console.log('上传 ' + complete)
                  readyFile.progress = complete
                }
              }, ).then(resp=> {
                console.log('resp', resp)
                // fileStatus.value = 'success'
                readyFile.status = 'success'
                readyFile.resp = resp.data
              }).catch(error=> {
                  // fileStatus.value = 'error'
                  readyFile.status = 'error'
              })
              .finally(()=> {
                // 一张图片重复上传时无法继续上传bug
                if(fileInput.value) {
                  fileInput.value.value = ''
                }
              })
                  }
                  return {
                      fileInput,
                      triggerUpload,
                      handleFileChange,
                      fileStatus,
                      isUploading,
                      filesList,
                      lastFileData
                  }
              }
          })
      </script>
      

      实现步骤:

      • 在poops处定义属性beforeUpload,同时定义上传文件为boolean或promsie且接受一个File
      • 将原上传方法进行封装为postFile
      • 根据beforeUpload返回结果,进行接下来的流程处理

      onProgress,onSuccess,onError,onChange与之类似

      拖拽支持

      大致流程如下图:

      使用Vue3实现一个Upload组件的示例代码

      • dragover和dragleave添加和删除对应的class
      • drop拿到正在被拖拽的文件,删除class,并且触发上传
      • 只有在属性drag为true才能触发

      需要注意v-on的使用具体可参考vue官方文档

      上代码:

      <template>
        <div class="file-upload">
          <div 
            :class="['upload-area',  drag && isDragOver ? 'is-dragover': '' ]"
            v-on="events"
            >
            <!-- 点击上传态 -->
            <slot v-if="isUploading" name='loading'>
              <button disabled>正在上传</button>
            </slot>
            <!-- 上传完毕态 -->
            <slot name="uploaded" v-else-if="lastFileData && lastFileData.loaded" :uploadData="lastFileData.data">
              <button disabled>点击上传</button>
            </slot>
            <!-- 默认态 -->
            <slot v-else name='default'>
              <!-- <button disabled>点击上传</button> -->
              <span>点击上传</span>  
            </slot>
          </div>
      
          <input ref="fileInput" type="file" name="file" style="display: none" @change="handleFileChange" />
      
          <ul>
            <li 
            v-for="file in filesList"
            :key="file.uid"
            :class="`uploaded-file upload-${file.status}`"
            >
            <img
                v-if="file.url && listType === 'picture'"
                class="upload-list-thumbnail"
                :src="file.url"
                :alt="file.name"
              >
              <span class="filname">
                {{ file.name }}
              </span>
              <span class="progress">
                {{ file.progress }}
              </span>
              <button class="delete-icon" @click="reomveFile(file.uid)">Del</button>
            </li>
          </ul>
          
        </div>
      </template>
      
      <script lang="ts">
      import { computed, defineComponent, PropType, reactive, ref } from 'vue'
      import axios from 'axios'
      import { v4 as uuidv4 } from 'uuid';
      import { last } from 'lodash-es'
      type UploadStatus = 'ready' | 'loading' | 'success' | 'error' 
      type fileListType = 'text' | 'picture'
      type CheckUpload = ()=> boolean | Promise<File>
      
      export interface UploadFile  {
        uid: string; // 文件唯一id
        size: number; // 文件大小
        name: string; // 文件名称
        status: UploadStatus; // 上传状态
        raw: File;            // 文件
        progress?: string;
        resp?: any;           // 服务端返回数据
        url?: string          // 对应展示的url
      
      }
      // dragOver  文件拖拽到区域时触发
      // dragLeave  文件离开拖动区域
      // drop 事件拿到正在被拖拽的文件删除class并且触发上传
      // 只有设置drag的时候才有效
       
      export default defineComponent({
        name: 'Uploader',
        props: {
          action: { // url地址
              type: String,
              required: true
          },
          beofreUpload:{ // 上传之前的处理
            type: Function as PropType<CheckUpload>
          },
          drag: { // 是否拖拽
            type: Boolean,
            default: false
          },
          autoUpload: { // 自动上传
            type: Boolean,
            default: true
          },
          listType: {
            type: String as PropType<fileListType>,
            default: 'text'
          }
        },
        
        setup(props) {
          const fileInput = ref<null | HTMLInputElement>(null)
          const fileStatus = ref<UploadStatus>('ready')
          // 存储上传的文件
          const filesList = ref<UploadFile[]>([])
          // 定义了一个isDragOver标识,处理拖动后样式.upload-area显示
          const isDragOver = ref<boolean>(false)
          const triggerUpload = () => {
              if(fileInput.value) {
                  fileInput.value.click()
              }
          }
      
          let events: {[key:string]: (e: any)=>void} = {
            'click': triggerUpload,
            
          }
          // 只要有一个文件状态是loading态就说明是正在上传
          const isUploading = computed(()=>{
            return filesList.value.some((file)=> file.status==='loading')
          })
      
          // 获取上传文件的最后一项
          const lastFileData = computed(()=>{
            const lastFile = last(filesList.value)
            if(!lastFile) return false
            return {
              loaded: lastFile?.status === 'success',
              data: lastFile?.resp
            }
          })
      
          // 处理dragover,dragleave
          const handleDrag = (e: DragEvent,over: boolean)=> {
          // 阻止默认事件
            e.preventDefault()
            // dragover为true,dragleave为fasle
            isDragOver.value = over
          }
          // 处理drop
          const handleDrop = (e: DragEvent) => {
            e.preventDefault()
            isDragOver.value = false
            if(e.dataTransfer) {
              beforeUploadCheck(e.dataTransfer.files)
            }
          }
         
          if(props.drag){
            events =  {
              ...events,
               'dragover': (e: DragEvent) => { handleDrag(e, true)},
              'dragleave': (e: DragEvent) => { handleDrag(e, false)},
              'drop': handleDrop
            }
            // console.log(events)
          }
          // 删除文件
          const reomveFile = (uid: string)=> {
            filesList.value = filesList.value.filter(file=>file.uid!==uid)
          }
      
          const postFile = (readyFile:UploadFile) => {
             const formData = new FormData()
              formData.append('file', readyFile.raw)
              readyFile.status = 'loading'
              // 选择文件后push到存储对象里面
              axios.post(props.action, formData, {
                headers: {
                  "Content-Type": 'multipart/form-data'
                },
                onUploadProgress: (progressEvent)=> {
                    const complete = (progressEvent.loaded / progressEvent.total * 100 | 0) + '%'
                  //   console.log('上传 ' + complete)
                  readyFile.progress = complete
                }
              }, ).then(resp=> {
                console.log('resp', resp)
                // fileStatus.value = 'success'
                readyFile.status = 'success'
                readyFile.resp = resp.data
              }).catch(error=> {
                  // fileStatus.value = 'error'
                  readyFile.status = 'error'
              })
              .finally(()=> {
                // 一张图片重复上传时无法继续上传bug
                if(fileInput.value) {
                  fileInput.value.value = ''
                }
              })
          }
          const addFileToList = (uploadedFile: File) => {
            const fileObj = reactive<UploadFile>({
              uid: uuidv4(),
              size: uploadedFile.size,
              name: uploadedFile.name,
              status: 'ready',
              raw: uploadedFile
            })
             // 处理图片格式并显示
            if(props.listType==='picture') {
              // try {
              //   fileObj.url = URL.createObjectURL(uploadedFile)
              // }catch(err) {
              //   console.error('upload file err', err)
              // }
              const fileReader = new FileReader()
              fileReader.readAsDataURL(uploadedFile)
              fileReader.addEventListener('load', ()=> {
                fileObj.url = fileReader.result as string
              })
            }
      
            filesList.value.push(fileObj)
            if(props.autoUpload) {
              postFile(fileObj)
            }
          }
      
          const uploadFiles = ()=> {
            // filesList.value.filter(file => file.status === 'ready').forEach(readyFile => postFile(readyFile))
            filesList.value.filter(file => file.status === 'ready').forEach(readyFile=>postFile(readyFile))
          }
      
          const beforeUploadCheck = (files: null | FileList ) => {
            if(files) {
                fileStatus.value = 'loading'
              const uploadedFile = files[0]
              if(props.beofreUpload) {
                const result = props.beofreUpload()
                // 如果有返回值
                if(result && result instanceof Promise) {
                  result.then(processedFile=> {
                    if(processedFile instanceof File) {
                      addFileToList(processedFile)
                    } else {
                      throw new Error('beforeUpload promise should return file object')
                    }
                  }).catch(err=> {
                    console.log(err)
                  })
                } else if(result === true) {
                    addFileToList(uploadedFile)
                }
              } else {
                addFileToList(uploadedFile)
              }
            }
          }
      
          const handleFileChange = (e: Event) => {
            const target = e.target as HTMLInputElement
            const files = target.files
            beforeUploadCheck(files)
          }
          return {
              fileInput,
              triggerUpload,
              handleFileChange,
              isUploading, 
              filesList,
              reomveFile,
              lastFileData,
              beforeUploadCheck,
              isDragOver,
              events
          }
        }
      })
      </script>
      
      

      以上就是基本的基于vue3实现的上传通用组件
      另附上上传接口代码:

      // 引入模块
      const Koa = require('koa');
      const fs = require('fs');
      const path = require('path');
      const router = require('koa-router')();
      const koaBody = require('koa-body');
      const static = require('koa-static');
      const cors = require('koa2-cors')
       
      // 实例化
      const app = new Koa();
       
      app.use(koaBody({
        multipart: true, // 支持文件上传
        formidable: {
          maxFieldsSize: 2 * 1024 * 1024, // 最大文件为2兆
          multipart: true // 是否支持 multipart-formdate 的表单
        }
      }));
       
      const uploadUrl = "http://localhost:3001/static/upload";
       
      // 配置路由
      router.get('/', (ctx) => {
        // 设置头类型, 如果不设置,会直接下载该页面
        ctx.type = 'html';
        // 读取文件
        const pathUrl = path.join(__dirname, '/static/upload.html');
        ctx.body = fs.createReadStream(pathUrl);
      });
       
      // 上传文件
      router.post('/upload', (ctx) => {
        // 获取上传文件
        const file = ctx.request.files.file;
        console.log(file);
        // 读取文件流
        const fileReader = fs.createReadStream(file.path);
        // 设置文件保存路径
        const filePath = path.join(__dirname, '/static/upload/');
        // 组装成绝对路径
        const fileResource = filePath + `/${file.name}`;
       
        /**
         * 使用 createWriteStream 写入数据,然后使用管道流pipe拼接
         */
        const writeStream = fs.createWriteStream(fileResource);
        // 判断 /static/upload 文件夹是否存在,如果不在的话就创建一个
        if (!fs.existsSync(filePath)) {
          fs.mkdir(filePath, (err) => {
            if (err) {
              throw new Error(err);
            } else {
              fileReader.pipe(writeStream);
              ctx.body = {
                url: uploadUrl + `/${file.name}`,
                code: 0,
                message: '上传成功1'
              };
            }
          });
        } else {
          fileReader.pipe(writeStream);
          ctx.body = {
            url: uploadUrl + `/${file.name}`,
            code: 0,
            message: '上传成功1'
          };
        }
      });
       
      // 配置静态资源路径
      app.use(static(path.join(__dirname)));
      // 开启跨域
      app.use(cors())
      // 启动路由
      app.use(router.routes()).use(router.allowedMethods());
      
      // 监听端口号
      app.listen(3001, () => {
        console.log('server is listen in 3001');
      });
      
      

      写在最后

      已上只是一个简单的实现,当然了我们也可以添加自定义headers,自定义传递数据data,accecpt等等

      到此这篇关于使用Vue3实现一个Upload组件的示例代码的文章就介绍到这了,更多相关Vue3 Upload组件内容请搜索NICE源码以前的文章或继续浏览下面的相关文章希望大家以后多多支持NICE源码!

      免责声明:
      1、本网站所有发布的源码、软件和资料均为收集各大资源网站整理而来;仅限用于学习和研究目的,您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。 不得使用于非法商业用途,不得违反国家法律。否则后果自负!

      2、本站信息来自网络,版权争议与本站无关。一切关于该资源商业行为与www.niceym.com无关。
      如果您喜欢该程序,请支持正版源码、软件,购买注册,得到更好的正版服务。
      如有侵犯你版权的,请邮件与我们联系处理(邮箱:skknet@qq.com),本站将立即改正。

      NICE源码网 JavaScript 使用Vue3实现一个Upload组件的示例代码 https://www.niceym.com/34305.html