# 图片文件上传的相关操作
# 上传图片
<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();
}
});
});
});
})
# 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,
});