# 图片文件上传的相关操作

# 上传图片

<template>
   <div class="uploadpic" id="uploadpic">
  <croppa
        v-model="croppa" 
        class="croppa"
        placeholder="选择或拖拽图片到此处"
        :width="200"
        :height="200"
        :placeholder-font-size="16"  
        :zoom-speed="10"    
        :loading-size="50" 
       :show-loading="true"    
  >
  </croppa>

  <i class="el-icon-close close" @click="closemes()" ></i>
  <el-button type="primary" @click="upload" class="upload">上传</el-button>
  <el-button type="primary"  @click="croppa.rotate()" class="rotate">旋转90°</el-button>
     

</div>
</template>
<script>

import Storage from "../../static/js/storage.js"
export default {
    data(){
        return{
           picsrc:"",
           croppa:{}
           
        }
    },
    mounted(){
        // this.$nextTick(function(){
            vuedrag("uploadpic")
        // })
    },
    methods: {
        upload() {
            let that=this;
        if (!this.croppa.hasImage()) {
            this.$message.error("请选择完图片再提交")
            return
        }
        
        this.croppa.generateBlob((blob) => {
            console.log(blob)
            var fd = new FormData()
            fd.append('upload', blob,'filename.jpg')
            console.log(fd)
            console.log(this.$store.state.selfmes.uid)
            fd.append('uid',this.$store.state.selfmes.uid);
            //  var freader = new FileReader(); 
            //  freader.readAsDataURL(blob); 
            //  freader.onload = function(e) { 
            //    let s= URL.createObjectURL(blob);
            //    console.log(s)
           
            console.log(that.picsrc)
            console.log(fd.upload)
  			
            let config = {
                        headers: {'Content-Type': 'multipart/form-data'}
                    }

                    this.$axios.post('/file/uploadHead',
                            fd,config
                    ).then(function(res){
                        //打印整体信息
                            console.log(res.data)
                        
                        if(res.data.meta.success==false){
                            that.$message.error(res.data.meta.message)
                        }
                        else if(res.data.meta.success==true){
                            that.$message.success('成功');
                            that.picsrc=res.data.data.sPic;
                            let tem=that.$store.state.selfmes;
                            tem.photo= that.picsrc;
                            console.log(tem)
                            that.$store.commit("getphoto",tem)
                            Storage.change("state","selfmes",tem);
                            console.log(that.$store.state.selfmes)
                          
                           
                            // 把图片传给vuex
                            that.$emit('eswitch',false)
                           
                            
                        }
                    }).catch(function(err){
                        that.$message.error(err)
                        
                        console.log(err)
                    })
            //  }
        
        })
        },
        closemes(){
            this.$emit('eswitch',false)
        }
 
  }
}
</script>
<style scoped lang="stylus">
    @import "../../static/styl/main.styl"    
</style>

//使用了插件croppa
this.croppa.generateBlob((blob) => {
	
	var fd = new FormData()
	fd.append('upload', blob,'filename.jpg')
	//参数
	fd.append('uid',this.$store.state.selfmes.uid);
	
	//config可以不配置
	let config = {
				headers: {'Content-Type': 'multipart/form-data'}
			}

			this.$axios.post('/file/uploadHead',
					fd,config
			).then(function(res){
				//打印整体信息
					console.log(res.data)
				
				if(res.data.meta.success==false){
					that.$message.error(res.data.meta.message)
				}
				else if(res.data.meta.success==true){
					that.$message.success('成功');
					that.picsrc=res.data.data.sPic;
					let tem=that.$store.state.selfmes;
					tem.photo= that.picsrc;
					console.log(tem)
					that.$store.commit("getphoto",tem)
					Storage.change("state","selfmes",tem);
					console.log(that.$store.state.selfmes)
				  
				   
					// 把图片传给vuex
					that.$emit('eswitch',false)
				   
					
				}
			}).catch(function(err){
				that.$message.error(err)
				
				console.log(err)
			})
	//  }

})

# 原生ajax拿到后台给的图片


export default {

  mounted() {

    $("#changemes").on('click',function(e){
        var canshu= e.target.innerHTML;

        var mes=JSON.stringify({file:canshu})
       
        var url = `${riliURL}/FileImgDownload/`;
        // var url = "https://192.168.11.50:6688/FileImgDownload/";
            var xhr = new XMLHttpRequest();
            xhr.open("POST", url);
            xhr.setRequestHeader(
              "x-header-token",
              localStorage.getItem("Token")
            );
        xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded");
        xhr.responseType = "blob";  
        xhr.send(mes);

         xhr.onreadystatechange = function() {
              if (xhr.readyState == 4 && xhr.status == 200) {
                   var blob = this.response;
                        if(blob.size>0){
                            var reader = new FileReader();
                            reader.readAsDataURL(blob);   // 转换为base64,可以直接放入a标签href
                            reader.onload = function (e) {
                                // 转换完成,创建一个a标签用于下载
                                var a = document.createElement('a');
                                a.download = canshu;
                                a.href = e.target.result;
                                $("body").append(a);    // 修复firefox中无法触发click
                                a.click();
                                $(a).remove();
                                // window.location.reload();
                            }
                        }else{
                            // window.location.reload();
						}

                    // window.location.href="https://192.168.11.50"+xhr.responseText; 
                    //  console.log(JSON.parse(xhr.responseText).api)
              }
            };
       
    } )


    // 文件上传
    $("#file_upload").on("change",function(){

        var form_data = new FormData();
            var file_info = $("#file_upload")[0].files[0];
            console.log(file_info)
            console.log(file_info.name)
          if(file_info){
            var filename=file_info.name;

            form_data.append("file", file_info);
           
            console.log(form_data);
            var url = `${riliURL}/FileUpload/`;
            // var url = "https://192.168.11.50:6688/FileUpload/";
            var xhr = new XMLHttpRequest();
            xhr.open("POST", url);
            xhr.setRequestHeader(
              "x-header-token",
              localStorage.getItem("Token")
            );
            xhr.send(form_data);
            var addmes = `<div><span class='hrmg'>${filename}</span></div>`
            $("#addchatdoc").append(addmes);
            xhr.onreadystatechange = function() {
              if (xhr.readyState == 4 && xhr.status == 200) {
                     
              }
            };
          }
        
    });
  //  图片上传
       $("#inputPic").on("change",function(){

        var form_data = new FormData();
            var file_info = $("#inputPic")[0].files[0];
          if(file_info){
            var filename=file_info.name;

            form_data.append("file", file_info);
           
            console.log(form_data);
            var url = `${riliURL}/ImgUpload/`;
            var xhr = new XMLHttpRequest();
            xhr.open("POST", url);
            xhr.setRequestHeader(
              "x-header-token",
              localStorage.getItem("Token")
            );
            xhr.send(form_data);
            xhr.onreadystatechange = function() {
              if (xhr.readyState == 4 && xhr.status == 200) {
                    callback1();
              }
            };
          }
        
    });
    function callback1(){
       var url = `${riliURL}/ImgDownload/`;
       var xhr = new XMLHttpRequest();
        xhr.open("POST", url);
        xhr.setRequestHeader(
          "x-header-token",
          localStorage.getItem("Token")
        );
         xhr.responseType = "blob";
         xhr.onload = function() {
                 if (xhr.readyState == 4 && xhr.status == 200) {

                 window.URL = window.URL || window.webkitURL;
                // alert( typeof history.pushState)
                   var blob = this.response;
                   var img = document.createElement("img");
                    img.onload = function(e) {
                      window.URL.revokeObjectURL(img.src); // 清除释放
                    };
                    img.src = window.URL.createObjectURL(blob);
                    that.myselfpic=img.src;
                
              }
        };
        xhr.send();
    }
    callback1();
   
    

    

</script>

# 文件自定义拖拽上传dataTransfer和校验上传内容格式正确

  • 使用drag事件结合drop,e.dataTransfer.files拿到对应文件
  • 校验格式正确
  • gif GIF89a 和GIF87a => '47 49 46 38 39 61' '47 49 46 38 37 61'
  • png 89 50 4E 47 0D 0A 1A 0A
  • jpg (start=='FF D8') && (tail=='FF D9')
handleFileChange(e){

    const [file] = e.target.files
    if(!file) return 
    this.file = file
},
bindEvents(){
    const drag = this.$refs.drag
    drag.addEventListener('dragover',e=>{
    drag.style.borderColor = 'red'
    e.preventDefault()
    })
    drag.addEventListener('dragleave',e=>{
    drag.style.borderColor = '#eee'
    e.preventDefault()
    })
    drag.addEventListener('drop',e=>{
    // //通过e.dataTransfer方法来存储被拖拽元素的id,这样在ondrop的时候可以获取这个id,并未选中元素添加拖拽元素
    const fileList = e.dataTransfer.files
    drag.style.borderColor = '#eee'
    this.file = fileList[0]


    e.preventDefault()

    // const e.dataTrans
    })
}, 
async blobToString(blob){
    return new Promise(resolve=>{
    const reader = new FileReader()
    reader.onload = function(){
        console.log(reader.result)
        const ret = reader.result.split('') //字母G=>charCodeAt:71=>toString(16):47
                    .map(v=>v.charCodeAt())//返回的 Unicode 编码,转成10进制
                    .map(v=>v.toString(16).toUpperCase())//转成16进制 
                    // .map(v=>v.padStart(2,'0')) //补全
                    .join('')
        resolve(ret)
    }
    reader.readAsBinaryString(blob)
    })
} ,
async isGif(file){
    // GIF89a 和GIF87a
    // 前面6个16进制,'47 49 46 38 39 61' '47 49 46 38 37 61'
    // 16进制的抓安环
    const ret = await this.blobToString(file.slice(0,6))
    const isGif = (ret=='47 49 46 38 39 61') || (ret=='47 49 46 38 37 61')
    return isGif
},
async isPng(file){
    const ret = await this.blobToString(file.slice(0,8))
    const ispng = (ret == "89 50 4E 47 0D 0A 1A 0A")
    return ispng
},
async isJpg(file){
    const len = file.size
    const start = await this.blobToString(file.slice(0,2))
    const tail = await this.blobToString(file.slice(-2,len))
    const isjpg = (start=='FF D8') && (tail=='FF D9')
    return isjpg
},
async isImage(file){
    // 通过文件流来判定
    // 先判定是不是gif
    return await this.isGif(file) || await this.isPng(file)
}

# 上传进度条

axios自带上传和下载进度的函数,如果使用原生ajax,也可以自己封装实现相应功能

const form = new FormData()
form.append('name','file')
form.append('file',this.file)
const ret = await this.$http.post('/uploadfile',form,{
onUploadProgress:progress=>{
    this.uploadProgress = Number(((progress.loaded/progress.total)*100).toFixed(2))
}
})
  • 后台处理文件对应参数及存储位置
cnpm i fs-extra --save
const fse = require('fs-extra')
 async uploadfile() {
    // /public/hash/(hash+index)
    // 报错
    // if(Math.random()>0.3){
    //   return this.ctx.status = 500
    // }
    const { ctx } = this
    console.log(ctx.request)
    //解析参数
    const file = ctx.request.files[0]
    const { name } = ctx.request.body
    await fse.move(file.filepath, this.config.UPLOAD_DIR+'/')//UPLOAD_DIR:全局配置常量
    this.message('上传成功')
  }

# 借助webWorker将上传文件切片

    import sparkMD5 from 'spark-md5'
    const CHUNK_SIZE = 10*1024*1024
    createFileChunk(file,size=CHUNK_SIZE){
      const chunks = [] 
      let cur = 0
      while(cur<this.file.size){
        chunks.push({index:cur, file:this.file.slice(cur,cur+size)})
        cur+=size
      }
      return chunks
    },
    async calculateHashWorker(){
      return new Promise(resolve=>{
        this.worker = new Worker('/hash.js')
        this.worker.postMessage({chunks:this.chunks})
        this.worker.onmessage = e=>{
          const {progress,hash} = e.data
          this.hashProgress = Number(progress.toFixed(2))
          if(hash){
            resolve(hash)
          }
        }
      })
    },
    async uploadFile(){
      if(!this.file){
        return 
      }
       this.chunks = this.createFileChunk(this.file)
       this.hash =await this.calculateHashWorker()
       console.log(hash)
    //   const hash = await this.calculateHashSample()
    //   this.hash = hash
    },
  • hash.js

// 引入spark-md5

self.importScripts('spark-md5.min.js')


self.onmessage = e=>{
  // 接受主线程传递的数据
  const {chunks } = e.data
  const spark = new self.SparkMD5.ArrayBuffer()

  let progress = 0
  let count = 0

  const loadNext = index=>{
    const reader = new FileReader()
    reader.readAsArrayBuffer(chunks[index].file)
    reader.onload = e=>{
      count ++
      spark.append(e.target.result)

      if(count==chunks.length){
        self.postMessage({
          progress:100,
          hash:spark.end()
        })
      }else{
        progress += 100/chunks.length
        self.postMessage({
          progress
        })
        loadNext(count)
      }
    }
  }
  loadNext(0)
}

# 借助浏览器空闲时间切片

    // 60fps
    // 1秒渲染60次 渲染1次 1帧,大概16.6ms
    // |帧(system task,render,script)空闲时间  |帧 painting idle   |帧   |帧   |
    // 借鉴fiber架构
    async calculateHashIdle(){
      const chunks = this.chunks
      return new Promise(resolve=>{
        const spark = new sparkMD5.ArrayBuffer()
        let count = 0 

        const appendToSpark = async file=>{
          return new Promise(resolve=>{
            const reader = new FileReader()
            reader.readAsArrayBuffer(file)
            reader.onload = e=>{
              spark.append(e.target.result)
              resolve()
            }
          })
        }
        const workLoop = async deadline=>{
          // timeRemaining获取当前帧的剩余时间
          while(count<chunks.length && deadline.timeRemaining()>1){
            // 空闲时间,且有任务
            await appendToSpark(chunks[count].file)
            count++
            if(count<chunks.length){
              this.hashProgress = Number(
                ((100*count)/chunks.length).toFixed(2)
              )
            }else{
              this.hashProgress = 100
              resolve(spark.end())
            }
          }
          window.requestIdleCallback(workLoop)
        }
        // 浏览器一旦空闲,就会调用workLoop
        window.requestIdleCallback(workLoop)

      })
    },

# 抽样hash计算 和 切片上传

async calculateHashSample(){

      // 布隆过滤器  判断一个数据存在与否
      // 1个G的文件,抽样后5M以内
      // hash一样,文件不一定一样
      // hash不一样,文件一定不一样
      return new Promise(resolve=>{
        const spark = new sparkMD5.ArrayBuffer()
        const reader = new FileReader()

        const file = this.file
        const size = file.size
        const offset = 2*1024*1024
        // 第一个2M,最后一个区块数据全要
        let chunks = [file.slice(0,offset)]

        let cur = offset
        while(cur<size){
          if(cur+offset>=size){
            // 最后一个区快
            chunks.push(file.slice(cur, cur+offset))

          }else{
            // 中间的区块
            const mid = cur+offset/2
            const end = cur+offset
            chunks.push(file.slice(cur, cur+2))
            chunks.push(file.slice(mid, mid+2))
            chunks.push(file.slice(end-2, end))
          }
          cur+=offset
        }
        // 中间的,取前中后各2各字节
        reader.readAsArrayBuffer(new Blob(chunks))
        reader.onload = e=>{
          spark.append(e.target.result)
          this.hashProgress = 100
          resolve(spark.end())
        }
      })
    },
 const chunks = this.createFileChunk(this.file)
 const hash = await this.calculateHashSample()
 this.hash = hash
 this.chunks = chunks.map((chunk,index)=>{
        // 切片的名字 hash+index
        const name = hash +'-'+ index
        return {
          hash,
          name,
          index,
          chunk:chunk.file,
          // 设置进度条,已经上传的,设为100
          progress:uploadedList.indexOf(name)>-1 ?100:0
        }
      })
      await this.uploadChunks(uploadedList)
  }

     async uploadChunks(uploadedList=[]){
      const requests = this.chunks
        .filter(chunk=>uploadedList.indexOf(chunk.name)==-1)
        .map((chunk,index)=>{
          // 转成promise
          const form = new FormData()
          form.append('chunk',chunk.chunk)
          form.append('hash',chunk.hash)
          form.append('name',chunk.name)
          // form.append('index',chunk.index)
          return {form, index:chunk.index,error:0}
        })
        .map(({form,index})=> this.$http.post('/uploadfile',form,{
          onUploadProgress:progress=>{
            // 不是整体的进度条了,而是每个区块有自己的进度条,整体的进度条需要计算
            this.chunks[index].progress = Number(((progress.loaded/progress.total)*100).toFixed(2))
          }
        }))
      // @todo 并发量控制 
      // 尝试申请tcp链接过多,也会造成卡顿
      // await Promise.all(requests)

      await Promise.all(requests)
      await this.mergeRequest()
    },

    computed:{
      cubeWidth(){
        return  Math.ceil(Math.sqrt(this.chunks.length))*16
      },
      uploadProgress(){
        if(!this.file || this.chunks.length){
          return 0
        }
        const loaded = this.chunks.map(item=>item.chunk.size*item.progress)
                          .reduce((acc,cur)=>acc+cur,0)
        return parseInt(((loaded*100)/this.file.size).toFixed(2))
      }
  },

# 切片上传后端操作

 async uploadfile() {
    const { ctx } = this
    console.log(ctx.request)
    const file = ctx.request.files[0]
    //name = hash +'-'+ index
    const { hash, name } = ctx.request.body
    const chunkPath = path.resolve(this.config.UPLOAD_DIR, hash)
    // 通过插件fs-extraapi判断当前有没有文件夹是chunkPath,没有则创建一个
    if (!fse.existsSync(chunkPath)) {
      await fse.mkdir(chunkPath)
    }
    //将文档碎片转移到改文件夹下
    await fse.move(file.filepath, `${chunkPath}/${name}`)
    this.message('切片上传成功')
  }

# 合并文件碎片

async mergefile() {
    const { ext, size, hash } = this.ctx.request.body
    const filePath = path.resolve(this.config.UPLOAD_DIR, `${hash}.${ext}`)
    await this.ctx.service.tools.mergeFile(filePath, hash, size)
    this.success({
      url: `/public/${hash}.${ext}`,
    })
  }
async mergeFile(filepPath, filehash, size) {
    const chunkdDir = path.resolve(this.config.UPLOAD_DIR, filehash) // 切片的文件夹
    let chunks = await fse.readdir(chunkdDir)
    chunks.sort((a, b) => a.split('-')[1] - b.split('-')[1])
    //排序
    chunks = chunks.map(cp => path.resolve(chunkdDir, cp))
    await this.mergeChunks(chunks, filepPath, size)

  }
  async mergeChunks(files, dest, size) {
    //流文件读取
    const pipStream = (filePath, writeStream) => new Promise(resolve => {
      const readStream = fse.createReadStream(filePath)
      readStream.on('end', () => {
        //删除已经读取完的文件
        fse.unlinkSync(filePath)
        resolve()
      })
      readStream.pipe(writeStream)
    })

    await Promise.all(
      files.forEach((file, index) => {
        pipStream(file, fse.createWriteStream(dest, {
          start: index * size,
          end: (index + 1) * size,
        }))
      })
    )
  }

# 文件上传秒传功能

    async uploadFile(){

      if(!this.file){
        return 
      }
      const chunks = this.createFileChunk(this.file)
        // 抽样hash 不算全量
      // 布隆过滤器 损失一小部分的精度,换取效率
      const hash = await this.calculateHashSample()
      this.hash = hash

      // 问一下后端,文件是否上传过,如果没有,是否有存在的切片
      const {data:{uploaded, uploadedList}} = await this.$http.post('/checkfile',{
        hash:this.hash,
        ext:this.file.name.split('.').pop()
      })
      if(uploaded){
        // 秒传
        return this.$message.success('秒传成功')
      }    
      
      this.chunks = chunks.map((chunk,index)=>{
        // 切片的名字 hash+index
        const name = hash +'-'+ index
        return {
          hash,
          name,
          index,
          chunk:chunk.file,
          // 设置进度条,已经上传的,设为100
          // 返回查看是否已经在后台文件夹中有了该文件
          progress:uploadedList.indexOf(name)>-1 ?100:0
        }
      })
      await this.uploadChunks(uploadedList)

    },

# 秒传的后端代码

 async checkfile() {
    const { ctx } = this
    const { ext, hash } = ctx.request.body
    const filePath = path.resolve(this.config.UPLOAD_DIR, `${hash}.${ext}`)

    let uploaded = false
    let uploadedList = []
    if (fse.existsSync(filePath)) {
      // 文件存在
      uploaded = true
    } else {
      uploadedList = await this.getUploadedList(path.resolve(this.config.UPLOAD_DIR, hash))
    }
    this.success({
      uploaded,
      uploadedList,
    })
  }

# 断点续传

前端大文件上传核心是 利用 Blob.prototype.slice 方法,和数组的 slice 方法相似,调用的 slice 方法可以返回 原文件的某个切片

    async uploadFile(){
      const chunks = this.createFileChunk(this.file)

      const hash = await this.calculateHashSample()
      this.hash = hash

      //秒传:代码见上
      // uploadedList=>从后台获取的已经上传的文件名称

      this.chunks = chunks.map((chunk,index)=>{
        // 切片的名字 hash+index
        const name = hash +'-'+ index
        return {
          hash,
          name,
          index,
          chunk:chunk.file,
          // 用来控制页面每个切片进度条状态
          progress:uploadedList.indexOf(name)>-1 ?100:0
        }
      })
      await this.uploadChunks(uploadedList)

    },
    async uploadChunks(uploadedList=[]){
      const requests = this.chunks
        // 如果是已经上传的文件,则需要过滤掉这次请求
        .filter(chunk=>uploadedList.indexOf(chunk.name)==-1)
        .map((chunk,index)=>{
          // 转成promise
          const form = new FormData()
          form.append('chunk',chunk.chunk)
          form.append('hash',chunk.hash)
          form.append('name',chunk.name)
          // 这里的index应该拿chunk.index,因为filter过滤后再去取这个index就不匹配了
          return {form, index:chunk.index,error:0}
        })
        .map(({form,index})=> this.$http.post('/uploadfile',form,{
          onUploadProgress:progress=>{
            // 不是整体的进度条了,而是每个区块有自己的进度条,整体的进度条需要计算
            this.chunks[index].progress = Number(((progress.loaded/progress.total)*100).toFixed(2))
          }
        }))

      await Promise.all(requests)
      await this.mergeRequest()
    }

# 上传并发控制实现和报错重传限制

    // TCP慢启动,先上传一个初始区块,比如10KB,根据上传成功时间,决定下一个区块20K,还是是50K,还是5K
    // 在下一个一样的逻辑,可能编程100K,200K,或者2K
    // 上传可能报错
    // 报错之后,进度条变红,开始重试
    // 一个切片重试失败三次,整体全部终止
    async sendRequest(chunks,limit=4){
      // limit控制并发数
      // 一个数组,长度是limit
      // [task12,task13,task4]
      return new Promise((resolve,reject)=>{
        const len = chunks.length
        let counter = 0 
        let isStop = false
        const start = async ()=>{
          if(isStop){
            return 
          }
          const task = chunks.shift()
          if(task){
            const {form,index} = task

            try{
              await this.$http.post('/uploadfile',form,{
                onUploadProgress:progress=>{
                  // 不是整体的进度条了,而是每个区块有自己的进度条,整体的进度条需要计算
                  this.chunks[index].progress = Number(((progress.loaded/progress.total)*100).toFixed(2))
                }
              })
              if(counter==len-1){
                // 最后一个任务
                resolve()
              }else{
                counter++
                // 启动下一个任务
                start()
              }
            }catch(e){

              this.chunks[index].progress = -1
              if(task.error<3){
                task.error++
                chunks.unshift(task)
                start()
              }else{
                // 错误三次
                isStop = true
                reject()
              }
            }
          }
        }

        while(limit>0){
          // 启动limit个任务
          // 模拟一下延迟
          setTimeout(()=>{
            start()
          },Math.random()*2000)
          limit-=1
        }
      
      })
    },

# 使用canvas压缩图片

<!DOCTYPE html>
<html>
  <head>
  </head>
  <body>
    <input type="file" id="upload">
    <script>
      const ACCEPT = ['image/jpg', 'image/png', 'image/jpeg']; // 限定图片文件类型
      const MAXSIZE = 1024 * 1024 * 3; // 限定图片最大容量
      const MAXSIZE_STR = '3MB';
      function convertImageToBase64(file, cb) {
        let reader = new FileReader();
        reader.addEventListener('load', function(e) {
          const base64Image = e.target.result; // 获取文件内容,等同于 reader.result
          cb(base64Image);
          reader = null;
        });
        reader.readAsDataURL(file); // 读取 file 对象中的内容
      }
      function compress(base64Image, cb) {
        let maxW = 1024;
        let maxH = 1024;

        const image = new Image();
        image.addEventListener('load', function() {
          let ratio; // 压缩比
          let needCompress = false; // 是否需要压缩
          if (maxW < image.naturalWidth) {
            needCompress = true;
            ratio = image.naturalWidth / maxW;
            maxH = image.naturalHeight / ratio;
          }
          if (maxH < image.naturalHeight) {
            needCompress = true;
            ratio = image.naturalHeight / maxH;
            maxW = image.naturalWidth / ratio;
          }
          if (!needCompress) {
            maxW = image.naturalWidth;
            maxH = image.naturalHeight;
          }
          const canvas = document.createElement('canvas');
          canvas.setAttribute('id', '__compress__');
          canvas.width = maxW;
          canvas.height = maxH;
          canvas.style.visibility = 'hidden';
          document.body.append(canvas);

          const ctx = canvas.getContext('2d');
          ctx.clearRect(0, 0, maxW, maxH);
          ctx.drawImage(image, 0, 0, maxW, maxH); // 渲染图片
          const compressImage = canvas.toDataURL('image/jpeg', 0.9); // 压缩图片
          cb(compressImage);
          const _image = new Image();
          _image.src = compressImage;
          document.body.appendChild(_image);
          canvas.remove(); // 移除 canvas
        });
        image.src = base64Image; // 将图片设置到 image 的 src 属性中
        document.body.appendChild(image);
      }
      function uploadImage(compressImage) {
        console.log('upload image to server...', compressImage);
      }

      const upload = document.getElementById('upload');
      upload.addEventListener('change', function(e) {
        const file = e.target.files[0];
        console.log(file);
        if (!file) {
          return;
        }
        const { type: fileType, size: fileSize } = file;
        // 图片类型检查
        if (!ACCEPT.includes(fileType)) {
          alert('不支持上传该格式文件!');
          upload.value = '';
          return;
        }
        // 图片大小检查
        if (fileSize > MAXSIZE) {
          alert('文件超出' + MAXSIZE_STR + '!');
          upload.value = '';
          return;
        }
        // 压缩文件
        convertImageToBase64(file, (base64Image) => compress(base64Image, uploadImage));
      });
    </script>
  </body>
</html>

# html2canvas截图页面内容并上传

html2canvas集合canvas转base64的api将页面某个区域内容变成canvas,然后将canvas转为base64传给后端。 场景:财务截图页面内容上传给系统添加附件,缺点:有时候会有些排版问题

<template>
  <div class="hello">
		<div id='k'></div>
		<div id='do1'>
			1111
			2222<br/>
			3333
			4444
			<table>
				<tr>
					<td>111sdf121</td>
					<td>11sda111</td>
					<td>11ews111</td>
				</tr>
				<tr>
					<td>1sdarf1111</td>
					<td>11sadf111</td>
					<td>111sdfsd132111fdsg2212222222222</td>
				</tr>
			</table>
		</div>
  </div>
</template>

<script>
import h2c from 'html2canvas' 
export default {
  name: 'HelloWorld',
  mounted(){
	  console.log(h2c)
	  let do1 =document.getElementById('do1')
	  h2c(do1).then(function(canvas) {
	      console.log(canvas)
		  document.body.appendChild(canvas);
		   const compressImage = canvas.toDataURL('image/jpeg', 1); // 压缩图片
		   console.log(compressImage)
	  });
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
  margin: 40px 0 0;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: inline-block;
  margin: 0 10px;
}
a {
  color: #42b983;
}
table,tr,td{
	border:1px solid red
}
td{
	width:100px !important;
	word-break:break-all
}
</style>

# html2canvas 配置

async function fun(){
	const options = {
		backgroundColor: null // null或transparent可将canvas背景设置为透明
	}
	const canvas = await html2canvas(pic, options)
}

# canvas截取video一帧

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<video id="video" controls="controls">
    <source src="./skt.mp4" />
</video>
<button id="capture">Capture</button>
<div id="output"></div>

<script src="https://cdn.bootcss.com/jquery/1.10.1/jquery.min.js"></script>
<script>
    (function() {
        "use strict";
        var video, $output;
        var scale = 0.25;
        // var initialize = function() {
            
        // };
		
        var captureImage = function() {
            var canvas = document.createElement("canvas");
            canvas.width = video.videoWidth * scale;
            canvas.height = video.videoHeight * scale;
            canvas.getContext('2d')
                .drawImage(video, 0, 0, canvas.width, canvas.height);
            var img = document.createElement("img");
            img.src = canvas.toDataURL('image/png');
            $output.prepend(img);
        };
		$output = $("#output");
		video = $("#video").get(0);
		$("#capture").click(captureImage);

    }());
</script>
</body>
</html>

# 微信多图上传 wx.chooseImage

项目是在企业微信中使用的,weixin-js-sdk只能在微信和企业微信中使用???

<script type="text/javascript" src='http://res.wx.qq.com/open/js/jweixin-1.2.0.js'></script>
npm install weixin-js-sdk
/** 微信上传图片 */
//判断手机平台 ios 和 android
function checkIOS() {
    var u = navigator.userAgent;
   // var isAndroid = u.indexOf('Android') > -1 || u.indexOf('Adr') > -1; //android终端
    var isiOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); //ios终端
    return isiOS
}

function delfile(id)
{
    Showbo.Msg.confirm('确定删除此附件吗?',function(flag){
        if(flag == "yes"){
            $.ajax({
                type: "POST",
                url: "/rentmgt/api/wx_delfile",
                data: {id:id},
                dataType:'json',
                async: false,
                cache: false,
                success: function(data){
                    showfiles();
                }
            });
        }
    });
}

function showfiles()
{
    var fileid = $("#fileid").val();

//			fileid = "201711091150512511";

    $.ajax({
        type: "POST",
        url: "/rentmgt/api/wx_getfiles",
        data: {fileId:fileid},
        dataType:'json',
        async: false,
        cache: false,
        success: function(data){
            var html = "";
            images.imgCount = data.length;
            $.each(data, function (index, obj) {
                //循环获取数据
                html = html + '<em>' +
                    '<img src="/rentmgt'+obj.msg+'" class="sentpic" onclick="wxpreview(this)">'+
                    '<img src="../img/delelte.png" onclick=delfile(\''+obj.id+'\') class="delpic" />'+
                    '</em>';
            });
            $("#fj").html(html);
        }
    });
}

function createFileId()
{
    if($('#fileid').val() == ''){
        $.ajax({
            url: "/rentmgt/api/wx/createFileId",
            data: { },
            async: false,
            success: function( result ) {
                $('#fileid').val(result.data.fileId);
            }
        });
    }
}

function modifyFile(id){
    Showbo.Msg.confirm('确定删除此附件吗?',function(flag){
        if(flag == "yes"){
            delete images.serverId[id];
            images.imgCount--;
            uploadCount--;
            $("#img"+id).css("display", "none");
        }
    });
}

function getFileName(fileUrl){
    //var fileurl = "a/b/safasdf.jpg";
    var reg = /[^\\\/]*[\\\/]+/g; //匹配文件的名称和后缀的正则表达式
    var name = fileUrl.replace(reg, '');
    var postfix = /\.[^\.]+/.exec(name);//获取文件的后缀
    var text =name.substr(0,postfix['index']);//获取没有后缀的名称
    return text;
}
var uploadCount = 0;

var images = {
    maxCount: 9,
    imgCount: 0,
    previewImgs: [],
    localId: [],
    serverId: {}
};

function setMediaIds(){
    var serverIds = [];
    for( var key in images.serverId){
        serverIds.push(images.serverId[key]);
    }
    $.ajax({
        type: 'POST',
        url: "/rentmgt/api/wx/setMediaId",
        data: {mediaId: serverIds.join(","), fileid: $("#fileid").val()},
        success: function( res ) {
        }
    });
}
/*安卓预览*/
function wxpreview(id) {
    wx.previewImage({
        current: id.src, // 当前显示图片的http链接
        urls: [id.src] // 需要预览的图片http链接列表
    });
}

/*ios预览*/
function wxpreview4ios(id) {
    var isrc = $(id).attr('data-isrc');
    wx.previewImage({
        current: isrc, // 当前显示图片的http链接
        urls: [isrc] // 需要预览的图片http链接列表
    });

}
$(document).ready(function(){
    //showfiles();
    $.ajax({
        url: "/rentmgt/api/wx/ticket",
        async: false,
        data: {
            url: location.href.split('#')[0]
        },
        success: function( result ) {
            wx.config({
                beta: true,
                debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
                appId: result.data.appid, // 必填,公众号的唯一标识
                appid: result.data.appid, // 必填,公众号的唯一标识
                timestamp: result.data.timestamp, // 必填,生成签名的时间戳
                nonceStr: result.data.noncestr, // 必填,生成签名的随机串
                signature: result.data.signature,// 必填,签名,见附录1
                jsApiList: ['chooseImage',
                    'previewImage',
                    'uploadImage',
                    'downloadImage',
                    'chooseWXPay',
                    'getLocalImgData'] // 必填,需要使用的JS接口列表,
            });
        }
    });

    var failImg;
    wx.ready(function(){
        $('#checkJsApi').click(function () {
            wx.chooseImage({
                count: 9, // 默认9
                sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩图,默认二者都有
                sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
                success: function (res) {
                    // 生成文件目录,创建fileid
                    if($("#fileid").val() == ""){
                        createFileId();
                    }

                    images.localId = res.localIds;
                    if (images.localId.length == 0) {
                        return;
                    }

                    var i = 0, length = images.localId.length;
                    uploadCount = uploadCount + length;

                    images.imgCount = images.imgCount + length;
                    if(images.imgCount > images.maxCount){
                        images.imgCount = images.imgCount - length;
                        Showbo.Msg.alert('最多可上传'+images.maxCount+'张图片');
                        return;
                    }

                    function upload() {
                        // 获取文件名做为图片标签ID
                        var localFileIdx = new Date().getTime(); //getFileName(images.localId[i]);
                        // 苹果机上传图片
                        if(checkIOS()){
                            wx.getLocalImgData({
                                localId: images.localId[i], // 图片的localID
                                success: function (res) {
                                    var localData = res.localData; // localData是图片的base64数据,可以用img标签显示
                                    localData = localData.replace('jgp', 'jpeg');//iOS 系统里面得到的数据,类型为 image/jgp,因此需要替换一下

                                    wx.uploadImage({
                                        localId: images.localId[i],
                                        success: function(res) {
                                            images.serverId[localFileIdx] = res.serverId;
                                            if (i <= length) {
                                                $("#fj").append('<em id="img'+localFileIdx+'" emfileId="'+localFileIdx+'"><img src="'+localData+'"  data-isrc="'+images.localId[i]+'" class="sentpic" onclick="wxpreview4ios(this)" ><img  src="../img/delelte.png" onclick="modifyFile('+localFileIdx+')" class="delpic" /></em>');
                                                i++;
                                                uploadCount--;
                                                upload();
                                            }
                                        },
                                        fail: function(res) {
                                            // alert('fail'+JSON.stringify(res));
                                            if(i < length){
                                                $('body').on('click', '#reload'+localFileIdx,  function(){
                                                    failImg = $(this);
                                                    wx.uploadImage({
                                                        localId: $(this).prev().attr('src'),
                                                        success: function(res) {
                                                            images.serverId[localFileIdx] = res.serverId;

                                                            $(failImg).parent().attr("emfileId", res.data.id);
                                                            $(failImg).css("display","none");
                                                            uploadCount--;
                                                        },
                                                        fail: function(res) {
                                                            Showbo.Msg.alert('重新上传失败');
                                                        }
                                                    });
                                                });

                                                $("#fj").append('<em id="img'+localFileIdx+'" ><img src="'+localData+'"  isrc="'+images.localId[i]+'" class="sentpic" onclick="wxpreview4ios(this)"><img id="reload'+localFileIdx+'" src="../img/reload.png" class="reload" style="width:1.3rem;height:1.3rem;position:absolute;top:0;left:0;opacity:0.8"><img  src="../img/delelte.png" onclick="modifyFile('+localFileIdx+')" class="delpic" /></em>');
                                                i++;
                                                upload();
                                            }
                                        }
                                    });
                                }
                            });
                        }else{
                            // 安卓机上传图片
                            wx.uploadImage({
                                localId: images.localId[i],
                                // isShowProgressTips: 0,
                                success: function(res) {
                                    // 微信上传成功, 预览图片
                                    images.serverId[localFileIdx] = res.serverId;
                                    if (i < length) {
                                        // 本地预览图片
                                        $("#fj").append('<em id="img'+localFileIdx+'" emfileId="'+localFileIdx+'"><img src="'+images.localId[i]+'" class="sentpic" onclick="wxpreview(this)"><img  src="../img/delelte.png" onclick="modifyFile('+localFileIdx+')" class="delpic" /></em>');
                                        i++;
                                        uploadCount--;
                                        upload();
                                    }
                                },
                                fail: function(res) {
                                    // alert('fail'+JSON.stringify(res));
                                    if(i < length){
                                        //  重新上传事件
                                        $('body').on('click', '#reload'+localFileIdx,  function(){
                                            failImg = $(this);
                                            wx.uploadImage({
                                                localId: $(this).prev().attr('src'),
                                                success: function(res) {
                                                    images.serverId[localFileIdx] = res.serverId;

                                                    $(failImg).parent().attr("emfileId", res.data.id);
                                                    $(failImg).css("display","none");
                                                    uploadCount--;
                                                },
                                                fail: function(res) {
                                                    Showbo.Msg.alert('重新上传失败');
                                                }
                                            });
                                        });

                                        $("#fj").append('<em id="img'+localFileIdx+'" ><img src="'+images.localId[i]+'" class="sentpic" onclick="wxpreview(this)"><img id="reload'+localFileIdx+'" src="../img/reload.png" class="reload" style="width:1.3rem;height:1.3rem;position:absolute;top:0;left:0;opacity:0.8"><img  src="../img/delelte.png" onclick="modifyFile('+localFileIdx+')" class="delpic" /></em>');
                                        i++;
                                        upload();
                                    }
                                }
                            });
                        }
                    }
                    upload();
                }
            });
        });
    });
})

参考 (opens new window)

# webWorker和断点续传

在 Web Worker 的场景下,由于 Worker 线程执行的脚本文件需要与主线程的文件分开处理(例如,它们可能位于不同的目录中,或者需要被单独打包以优化加载性能),Webpack 需要通过特定的配置或插件来识别这些 Worker 脚本,并将它们打包成合适的格式供 Worker 线程使用。

npm install --save-dev worker-loader
module.exports = {
  publicPath: './',

  chainWebpack: config => {  
    config.module  
       .rule('worker')  
      .test(/\.worker\.js$/)  // 如果需要.worker.js后缀
      .use('worker-loader')  
      .loader('worker-loader')
      .options({ // 可以查阅worker-loader文档,根据自己的需求进行配置
       })
  }  
}
  // 给webworker传file
  worker.postMessage({
      file,
      chunkSize,
      startIndex,
      endIndex,
    });
最后更新: 8/8/2024, 2:30:12 PM