Skip to content

拖拽、文件上传、下载、缓冲、懒加载 ​

拖拽 ​

  • 事件拖拽

    利用 offset 值来计算

  • 拖拽的问题:如果拖拽的地方有文字并且在拖拽 div 的时候,文字是被选中的状态,浏览器会认为是拖拽文字,或者如果要拖拽的元素是图片,浏览器会认为是保存图片效果,这都是因为浏览器的默认行为;

  • 解决拖拽问题

    • 普通浏览器可以利用 return false,阻止浏览器默认行为
    • IE 浏览器需要使用事件捕获来阻止浏览器默认行为,事件捕获在普通浏览器里面会出错,所以需要做兼容

拖拽 API ​

  1. 属性

draggable 将 div 设置为 draggable=true 可以被拖拽,img 和 a 标签不加这个属性就可以拖拉,一般可以给这两个标签设置 false;

js
<div draggable="true">此区域可拖拉</div>
<div draggable="true">此区域可拖拉</div>
  1. 拖拽事件

(1)被拖动的元素事件

dragstart 开始拖动时触发

drag 拖动过程中持续触发

dragend 拖动结束触发

(2)目标元素事件

dragenter 进入目标时触发,该事件会冒泡,必须阻止冒泡和默认行为;

dragover 在目标区域拖拽持续触发,若想被拖拉的节点放进来,必须阻止默认动作 event.preventDefault(),防止拖拉效果被重置;

dragleave 离开目标时触发

drop 释放目标时触发,在该节点阻止默认行为 event.preventDefault(),比如:打开其他链接;

  1. dataTransfer对象

(1)主要用来读写需要传递的数据

e.dataTransfer.setData('传输名',传输对象) 用于提交传输对象,可以在 dragstart 事件上面添加

e.dataTransfer.getData('传输名') 用于获取传输对象,可以在目标上面的 ondrop 事件添加

e.dataTransfer.files 获取被 drop 的外部文件

拖拽组件库:react-dnd

事件捕获(IE 独有) ​

obj.setCapture();页面中和元素一样的事件都捕获起来,将页面中其他相同的事件触发时都执行元素的事件处理函数;让其他元素都执行调用捕获元素的条件;

  1. 释放捕获:obj.releaseCapture();取消继续捕获;每次点击都会捕获一次,所以抬起需要取消捕获;

限制盒子移动范围 ​

拖拽不能超出屏幕,通过下图我们能得到:oDiv.left+oDiv.offsetWidth 不能大于屏幕宽。

鼠标按下时:

js
var x = ev.clientX - obj1.offsetLeft
var y = ev.clientY - obj1.offsetTop
var x = ev.clientX - obj1.offsetLeft
var y = ev.clientY - obj1.offsetTop

鼠标移动时:

js
var l = ev.clientX - x
var t = ev.clientY - y
var l = ev.clientX - x
var t = ev.clientY - y

判断是否有碰撞:

js
if (l < 0) {
  l = 0
} // 大于 0 并且小于屏幕宽再回来自己的宽度
if (l > document.documentElement.clientWidth - obj1.offsetWidth) {
  l = document.documentElement.clientWidth - obj1.offsetWidth
}
if (t < 0) {
  t = 0
} //
if (t > document.documentElement.clientHeight - obj1.offsetHeight) {
  t = document.documentElement.clientHeight - obj1.offsetHeight
}
if (l < 0) {
  l = 0
} // 大于 0 并且小于屏幕宽再回来自己的宽度
if (l > document.documentElement.clientWidth - obj1.offsetWidth) {
  l = document.documentElement.clientWidth - obj1.offsetWidth
}
if (t < 0) {
  t = 0
} //
if (t > document.documentElement.clientHeight - obj1.offsetHeight) {
  t = document.documentElement.clientHeight - obj1.offsetHeight
}

赋值给 div;

js
obj1.style.top = t + “px”;
obj1.style.left = l + “px”;
obj1.style.top = t + “px”;
obj1.style.left = l + “px”;

当鼠标抬起时:

js
document.onmousemove = document.onmouseup = “”;
if(obj2.setCapture){
    obj2.releaseCapture();
}
document.onmousemove = document.onmouseup = “”;
if(obj2.setCapture){
    obj2.releaseCapture();
}

image-20240705103821310

碰撞检测; ​

中间使用||连接;

左:move.offsetLeft+move.offsetWidth < box.offsetLeft

上:move.offsetTop+move.offsetHeight < box.offsetTop

右:move.offsetLeft > box.offsetLeft+box.offsetWidth

下:move.offsetTop > box.offsetTop + box.offsetHeight

image-20240705103812468

缓冲运动 ​

公式(目标点 - 当前位置)/ 缩放系数

如果改变的样式是 opacity 需要乘以 100

取整 如果大于 0,则向上取整,如果小于 0 则向下取整。

根据运动是否完成来设定 flag 开关。

  1. 做小盒子缓冲移动 -----每次加的值也是一个变量;
  2. 封装
  3. 改变透明度 ----判断透明度的值 不能为小数,所以*100;
  4. 做对象 做多值缓冲运动
  5. 改变清除定时器的时间;
  6. 加一个回调函数,动画完成;

懒加载 ​

先只加载可视窗口区域的图片,当用户向下拖动滚动条时再继续加载后面可视图片;

优点:

(1)这样减少了加载时的线程数量,使可视区域内的图片也能够快速加载,优化了用户体验。

(2)减少了同一时间发向服务器的请求数,服务器压力剧减。

  1. 在写网页<img>标签时,将图片的路径放到自定义属性_src 路径中,在加载页面时,这个图片一开始是无法加载,当浏览器的可视区域移动到此图片上时,将_src 中的路径赋值给 src 属性,并开始加载,即浏览器的可视距离加上浏览器被卷起的高度大于等于图片到浏览器顶部的距离;
js
$(window).scroll(function (e) {
  $('img').each(function () {
    //如果浏览器可视高度加上浏览器被卷起的高度 大于等于 图片到浏览器顶部的距离,则显示
    if ($(window).height() + $(window).scrollTop() >= $(this).offset().top + 300) {
      $(this).attr('src', $(this).attr('_src'))
    }
  })
})
$(window).scroll(function (e) {
  $('img').each(function () {
    //如果浏览器可视高度加上浏览器被卷起的高度 大于等于 图片到浏览器顶部的距离,则显示
    if ($(window).height() + $(window).scrollTop() >= $(this).offset().top + 300) {
      $(this).attr('src', $(this).attr('_src'))
    }
  })
})

自定义滚动条 ​

  1. 设置滚动条条儿的高度

猜想:滚动条的高度随着内容的高度而变化,因此它们需要成正比;

结果:列表可见的高度 / 列表实际的高度 = x / 列表可见的高度

x = 列表可见的高度 / 列表实际的高度 * 列表可见的高度

js
function setBarHeight() {
  var scrollHeight = listDom.scrollHeight
  var h = (listClientHeight / scrollHeight) * listClientHeight
  if (h >= listClientHeight) {
    h = 0 //内容过少,滚动条高度超过了可见区域,不需要滚动条
  }
  barDom.style.height = h + 'px'
}
function setBarHeight() {
  var scrollHeight = listDom.scrollHeight
  var h = (listClientHeight / scrollHeight) * listClientHeight
  if (h >= listClientHeight) {
    h = 0 //内容过少,滚动条高度超过了可见区域,不需要滚动条
  }
  barDom.style.height = h + 'px'
}
  1. 设置滚动条的位置

猜想:滚动条移动的位置要与列表被卷起的高度成正比;滚动条的可区域相当于列表可见的高度;

结果:列表被卷起的高度 / 列表实际内容的高度 = x / 列表可见的高度;

x = 列表被卷起的高度 / 列表实际内容的高度 * 列表可见的高度;

js
function setBarTop() {
  var top = (listDom.scrollTop / listDom.scrollHeight) * listClientHeight
  barDom.style.top = top + 'px'
}
function setBarTop() {
  var top = (listDom.scrollTop / listDom.scrollHeight) * listClientHeight
  barDom.style.top = top + 'px'
}
  1. 判断鼠标滚动事件

根据 onmousewheel 传入一个 e,取值 e.detailY,用列表被卷起的高度去相加

js
listDom.onmousewheel = function (e) {
  setListScrollTop(listDom.scrollTop + e.detailY) //调用设置列表被卷起的高度的高度;
}
listDom.onmousewheel = function (e) {
  setListScrollTop(listDom.scrollTop + e.detailY) //调用设置列表被卷起的高度的高度;
}
  1. 设置列表的 scrolltop
js
function setListScrollTop(newScrollTop) {
  // 这里传入的值可能为负数,因为会向上或向下滚动;

  clearInterval(timer) //避免重复调用函数,停止之前的移动
  //使用动画实现滚动
  //例如:scrollTop  300 -> 100
  var spd = 0.8 //速度:1毫秒变化的距离(自定义的)
  var dis = newScrollTop - listDom.scrollTop //移动的总距离
  var tick = 10 //多少毫秒移动一次
  var duration = Math.abs(dis / spd) //计算移动的总时间,因为可能是负数,所以需要取绝对值;
  var times = Math.ceil(duration / tick) //一共可以移动多少次
  var curTimes = 0 //当前移动了多少次
  var everyDis = dis / times //每次移动的距离 = 总距离/总次数
  timer = setInterval(function () {
    curTimes++
    listDom.scrollTop += everyDis
    setBarTop()
    if (curTimes === times) {
      clearInterval(timer)
    }
  }, tick)
}
function setListScrollTop(newScrollTop) {
  // 这里传入的值可能为负数,因为会向上或向下滚动;

  clearInterval(timer) //避免重复调用函数,停止之前的移动
  //使用动画实现滚动
  //例如:scrollTop  300 -> 100
  var spd = 0.8 //速度:1毫秒变化的距离(自定义的)
  var dis = newScrollTop - listDom.scrollTop //移动的总距离
  var tick = 10 //多少毫秒移动一次
  var duration = Math.abs(dis / spd) //计算移动的总时间,因为可能是负数,所以需要取绝对值;
  var times = Math.ceil(duration / tick) //一共可以移动多少次
  var curTimes = 0 //当前移动了多少次
  var everyDis = dis / times //每次移动的距离 = 总距离/总次数
  timer = setInterval(function () {
    curTimes++
    listDom.scrollTop += everyDis
    setBarTop()
    if (curTimes === times) {
      clearInterval(timer)
    }
  }, tick)
}
  1. 拖动滚动条条儿事件,根据滚动条 条儿位置,直接设置列表滚动的高度;

猜想:列表滚动被卷起的高度要 与 滚动条条儿与列表可见的高度的顶部距离成正比;

结果:条儿的位置 / 列表可见的高度 = x / 列表总高度 (成正比)x 为被卷起的高度;

js
function setListScrollTopFromBarTop(top) {
  // 这个top需要被调用;
  var sh = (top / listClientHeight) * listDom.scrollHeight
  listDom.scrollTop = sh
}

barDom.onmousedown = function (e) {
  var y = e.pageY //相对于网页的y坐标
  var top = parseFloat(getComputedStyle(this).top) //按下时的top值
  var h = barDom.clientHeight //滚动条条儿自身的高度
  window.onmousemove = function (e) {
    var newY = e.pageY
    var newTop = top + (newY - y)
    if (newTop < 0) {
      newTop = 0
    } else if (newTop > listClientHeight - h) {
      newTop = listClientHeight - h
    }
    barDom.style.top = newTop + 'px'
    setListScrollTopFromBarTop(newTop)
  }
  window.onmouseup = function () {
    window.onmousemove = null
  }
}
function setListScrollTopFromBarTop(top) {
  // 这个top需要被调用;
  var sh = (top / listClientHeight) * listDom.scrollHeight
  listDom.scrollTop = sh
}

barDom.onmousedown = function (e) {
  var y = e.pageY //相对于网页的y坐标
  var top = parseFloat(getComputedStyle(this).top) //按下时的top值
  var h = barDom.clientHeight //滚动条条儿自身的高度
  window.onmousemove = function (e) {
    var newY = e.pageY
    var newTop = top + (newY - y)
    if (newTop < 0) {
      newTop = 0
    } else if (newTop > listClientHeight - h) {
      newTop = listClientHeight - h
    }
    barDom.style.top = newTop + 'px'
    setListScrollTopFromBarTop(newTop)
  }
  window.onmouseup = function () {
    window.onmousemove = null
  }
}

监听图片的 error 事件 ​

  1. 当一个图片资源加载出错时,可以替换成默认的图片
html
<img src="" onerror="" alt="" />
<img src="" onerror="" alt="" />
  1. 全局监听:当一个页面过多图片时,可以使用全局监听,需要使用冒泡
js
window.addEventListener('error',()=>{
    if(e.target.)
},true)
window.addEventListener('error',()=>{
    if(e.target.)
},true)
  1. 解决无限触发网络出现异常
js
window.addEventListener('error',function(e){
    let times = Number(e.target.dataset.times) || 0, 以失败的次数,默认为0
        allTimes = 3; //总失败次数
    if(e.target.tagName.toUpperCase() === 'IMG'){
        if(times >= allTimes){
            e.target.src = '' 提供一个base64的默认图片
        } else {
            e.target.dataset.times = times++
            e.target.src = '//default.jpg'
        }
    }
},true)
window.addEventListener('error',function(e){
    let times = Number(e.target.dataset.times) || 0, 以失败的次数,默认为0
        allTimes = 3; //总失败次数
    if(e.target.tagName.toUpperCase() === 'IMG'){
        if(times >= allTimes){
            e.target.src = '' 提供一个base64的默认图片
        } else {
            e.target.dataset.times = times++
            e.target.src = '//default.jpg'
        }
    }
},true)

文件对象-FileReader 读取文件 ​

  1. 回调方法:

onload:文件读取完成

onabort:中断时触发

onerror:出错时触发

onloadstart:文件上传开始

onloadend:读取结束时,不论成功或失败

onprogress:读取中

  1. 读取操作:传入参数是 file 对象;

readAsArrayBuffer

readAsDataURL:从文件读取 URL 数据

readAsBinaryString:将文件读取为二进制码

readAsText:从文件读取字符串

abort():终止读取操作

js
// 读取图片并显示到页面上
files.addEventListener(
  'change',
  function (e) {
    const { files } = event.target
    var fr = new FileReader()
    fr.readAsDataURL(files[0])
    fr.onload = function (e) {
      console.log(fr.result) // 拿到图片的base64
    }
  },
  false,
)

// 读取大文件并显示进度
files.addEventListener(
  'change',
  function (e) {
    const { files } = event.target
    for (let file of files) {
      var fr = new FileReader()
      fr.onprogress = function (e) {
        // lengthComputable表示过程是否可以测量,需要进行判断
        if (!e.lengthComputable) return
        // 四舍五入计算百分比
        let percent = Math.round((e.loaded / e.total) * 100)
        console.log(`${file.name},progress is ${percent}%`)
      }
      fr.readAsArrayBuffer(file)
    }
  },
  false,
)
// 读取图片并显示到页面上
files.addEventListener(
  'change',
  function (e) {
    const { files } = event.target
    var fr = new FileReader()
    fr.readAsDataURL(files[0])
    fr.onload = function (e) {
      console.log(fr.result) // 拿到图片的base64
    }
  },
  false,
)

// 读取大文件并显示进度
files.addEventListener(
  'change',
  function (e) {
    const { files } = event.target
    for (let file of files) {
      var fr = new FileReader()
      fr.onprogress = function (e) {
        // lengthComputable表示过程是否可以测量,需要进行判断
        if (!e.lengthComputable) return
        // 四舍五入计算百分比
        let percent = Math.round((e.loaded / e.total) * 100)
        console.log(`${file.name},progress is ${percent}%`)
      }
      fr.readAsArrayBuffer(file)
    }
  },
  false,
)
  1. 大文件分片读取

由于 file 是 blob 的一个特例,blob 上有一个 slice 方法,和字符串 slice 方法一样用法

写好 hello world 内容的 txt 文件,截取 hello 五个字符,代码中每个英文字母占 1 个字节,通过 file.size 可以获取到所有的字节

js
files.onchange = function (e) {
  let { files } = e.target
  let blob = files[0].slice(0, 5)
  let fr = new FileReader()
  fr.onload = (e) => {
    console.log(e.target.result)
  }
  console.log(blob)
  fr.readAsText(blob)
}
files.onchange = function (e) {
  let { files } = e.target
  let blob = files[0].slice(0, 5)
  let fr = new FileReader()
  fr.onload = (e) => {
    console.log(e.target.result)
  }
  console.log(blob)
  fr.readAsText(blob)
}

接收 input 上传事件 ​

  1. 使用 input 标签的 type 类型为 file;
js
;<input type="file" id="files" multiple />

// 判断兼容
if (!(window.File && window.FileReader && window.FileList && window.Blob)) {
  throw new Error('当前浏览器对FileAPI的支持不完善')
}
;<input type="file" id="files" multiple />

// 判断兼容
if (!(window.File && window.FileReader && window.FileList && window.Blob)) {
  throw new Error('当前浏览器对FileAPI的支持不完善')
}
  1. 监听文件的选择,选择文件之后会触发 change 事件,input 事件 file 保存在 event.target.files 对象里面,是一个伪数组;
js
files.addEventListener(
  'change',
  function (e) {
    const { files } = event.target
    console.log('上传的文件是', files)
  },
  false,
)
files.addEventListener(
  'change',
  function (e) {
    const { files } = event.target
    console.log('上传的文件是', files)
  },
  false,
)
  1. 由于没有使用 form 表单,上传的事件是需要创建一个 formData 事件,并将文件添加进去,就可以发送到后端了; 注意:使用 formdata 上传后台不需要
js
let fd = new FormData()
fd.append('files', file)
// 将fd对象发送给后端就可以了
let fd = new FormData()
fd.append('files', file)
// 将fd对象发送给后端就可以了
  1. 获取 formData 的值

方法一:可以直接使用[...formdata]可以获取到二维数组,查看 formdata 的值;

方法二:使用formdata.keys()返回一个 iterator 对象, 并使用 for of 循环遍历就可以了

使用 formData 里面的 getAll 或 get 方法获取, get 获取的是一个,getAll 获取全部

js
let arr = fd.keys()
for (var props of arr) {
  fd.getAll(props)
}
let arr = fd.keys()
for (var props of arr) {
  fd.getAll(props)
}
  1. formData 添加数组

直接将数组放进 formData 里面将会是[object ..],需要循环添加

js
let arr = [1, 2]
let fd = new FormData()
arr.forEach((i) => {
  fd.append('arr', i)
})
let arr = [1, 2]
let fd = new FormData()
arr.forEach((i) => {
  fd.append('arr', i)
})

拖拽上传 ​

对于 input 标签,监听 onchange 事件,存在 e.target.files 上面;

对于拖拽事件,fileList 存放在拖拽事件的回调函数 e.dataTransfer.files 上面

js
// 使得drop事件可以触发
document.ondragover = (e) => e.preventDefault()
// 防止新窗口打开或下载
document.ondrop = (e) => e.preventDefault()
// 监听指定区域文件拖拽
div.ondrop = (e) => {
  let { files } = e.dataTransfer
  let fr = new FileReader()
  fr.readAsDataURL(files[0])
  fr.onload = (e) => {
    div.style.backgroundImage = `url(${fr.result})`
  }
}
// 使得drop事件可以触发
document.ondragover = (e) => e.preventDefault()
// 防止新窗口打开或下载
document.ondrop = (e) => e.preventDefault()
// 监听指定区域文件拖拽
div.ondrop = (e) => {
  let { files } = e.dataTransfer
  let fr = new FileReader()
  fr.readAsDataURL(files[0])
  fr.onload = (e) => {
    div.style.backgroundImage = `url(${fr.result})`
  }
}

粘贴上传 ​

js
;<input id="upload" type="text" />
upload.addEventListener('paste', function (e) {
  // 需要直接取到files才能读取到
  var files = e.clipboardData.files
})
;<input id="upload" type="text" />
upload.addEventListener('paste', function (e) {
  // 需要直接取到files才能读取到
  var files = e.clipboardData.files
})

文件切片 ​

blob 对象也支持 slice 方法,可以使用 slice 方法进行切片

js
/**
 * file 需要切片的文件
 * chunkSize 每个切片大小
 */
function createFileChunk(file, chunkSize = 10 * 1024 * 1024) {
  // 创建数组保留切片数组
  const fileChunkList = []
  let cur = 0 // 切片当前数据
  let i = 0 // 下标
  while (cur < file.size) {
    fileChunkList.push({
      chunk: file.slice(cur, cur + chunkSize),
      hash: file.name + '-' + i,
      index: i,
      percentage: 0,
    })
    cur += chunkSize
    i++
  }
  // 返回最后切片的数组
  return fileChunkList
}
/**
 * file 需要切片的文件
 * chunkSize 每个切片大小
 */
function createFileChunk(file, chunkSize = 10 * 1024 * 1024) {
  // 创建数组保留切片数组
  const fileChunkList = []
  let cur = 0 // 切片当前数据
  let i = 0 // 下标
  while (cur < file.size) {
    fileChunkList.push({
      chunk: file.slice(cur, cur + chunkSize),
      hash: file.name + '-' + i,
      index: i,
      percentage: 0,
    })
    cur += chunkSize
    i++
  }
  // 返回最后切片的数组
  return fileChunkList
}

切片上传 ​

js
async function uploadChunks(file, fileChunkList) {
  // 创建Promise数组进行切片上传
  const requestList = fileChunkList
    .map(({ chunk, hash, fileHash }) => {
      const formData = new FormData()
      formData.append('chunk', chunk)
      formData.append('hash', hash)
      formData.append('filename', file.name)
      formData.append('fileHash', fileHash)
      return { formData }
    })
    // request封装的请求方法
    .map(async ({ formData }, index) =>
      request({
        url: `${baseURL}msg/upload`,
        data: formData,
        // 显示进度函数
        onProgress: createProgressHandler(fileChunkList[index], file, fileChunkList),
      }),
    )
  // 上传
  await Promise.all(requestList)
  // 上传完成发送合并请求
  await mergeRequest(file.name, setIsSelectFile, socket)
}

// xhr
function request({ url, method = 'post', data, headers = {}, onProgress = (e) => e, requestList }) {
  return new Promise((resolve) => {
    const xhr = new XMLHttpRequest()
    xhr.upload.onprogress = onProgress
    xhr.open(method, url)
    Object.keys(headers).forEach((key) => xhr.setRequestHeader(key, headers[key]))
    xhr.send(data)
    xhr.onload = (e) => {
      resolve({
        data: e.target.response,
      })
    }
  })
}

function createProgressHandler(item) {
  return (e) => {
    item.percentage = parseInt(String((e.loaded / e.total) * 100))
  }
}
async function uploadChunks(file, fileChunkList) {
  // 创建Promise数组进行切片上传
  const requestList = fileChunkList
    .map(({ chunk, hash, fileHash }) => {
      const formData = new FormData()
      formData.append('chunk', chunk)
      formData.append('hash', hash)
      formData.append('filename', file.name)
      formData.append('fileHash', fileHash)
      return { formData }
    })
    // request封装的请求方法
    .map(async ({ formData }, index) =>
      request({
        url: `${baseURL}msg/upload`,
        data: formData,
        // 显示进度函数
        onProgress: createProgressHandler(fileChunkList[index], file, fileChunkList),
      }),
    )
  // 上传
  await Promise.all(requestList)
  // 上传完成发送合并请求
  await mergeRequest(file.name, setIsSelectFile, socket)
}

// xhr
function request({ url, method = 'post', data, headers = {}, onProgress = (e) => e, requestList }) {
  return new Promise((resolve) => {
    const xhr = new XMLHttpRequest()
    xhr.upload.onprogress = onProgress
    xhr.open(method, url)
    Object.keys(headers).forEach((key) => xhr.setRequestHeader(key, headers[key]))
    xhr.send(data)
    xhr.onload = (e) => {
      resolve({
        data: e.target.response,
      })
    }
  })
}

function createProgressHandler(item) {
  return (e) => {
    item.percentage = parseInt(String((e.loaded / e.total) * 100))
  }
}

断点续传 ​

断点续传的原理在于前端/服务端需要记住已上传的切片,这样下次上传就可以跳过之前已上传的部分,有两种方案实现记忆的功能

  • 前端使用 localStorage 记录已上传的切片 hash
  • 服务端保存已上传的切片 hash,前端每次上传前向服务端获取已上传的切片

第一种是前端的解决方案,第二种是服务端,而前端方案有一个缺陷,如果换了个浏览器就失去了记忆的效果,所以这里选取后者

通过 blob 获取 url 地址 ​

URL.createObjectURL 传入 blob

js
// 第一种
var obj_url = window.URL.createObjectURL(blob)
// 使用createObjectURL之后建议使用revokeObjectURL方法释放生成的文件url,解除浪费性能
window.URL.revokeObjectURL(obj_url)

// 第二种
let fr = new FileReader()
fr.readAsDataURL(files[0])
fr.onload = (e) => {
  let url = fr.result
}
// 第一种
var obj_url = window.URL.createObjectURL(blob)
// 使用createObjectURL之后建议使用revokeObjectURL方法释放生成的文件url,解除浪费性能
window.URL.revokeObjectURL(obj_url)

// 第二种
let fr = new FileReader()
fr.readAsDataURL(files[0])
fr.onload = (e) => {
  let url = fr.result
}

文章地址 ​

好玩的 blob 对象 ​

手工创建 blob 对象:

js
var obj = { name: 'hhh' }
var blob = new Blob([JSON.stringify(obj)], { type: 'application/json' })
var obj = { name: 'hhh' }
var blob = new Blob([JSON.stringify(obj)], { type: 'application/json' })

如果将 blob 放到 HTTP 请求发送给服务端,相当于向服务器发送了一个 json 文件

下面例子相当于创建了一个 script 标签,放到页面上

js
const jsCode = 'console.log("hello")'
const blob = new Blob([jsCode], { type: 'text/javascript' })
const script = document.createElement('script')
script.src = URL.createObjectURL(blob)
document.body.appendChild(script)
const jsCode = 'console.log("hello")'
const blob = new Blob([jsCode], { type: 'text/javascript' })
const script = document.createElement('script')
script.src = URL.createObjectURL(blob)
document.body.appendChild(script)

动态引入模块

html
<script type="module">
  function importCode(code) {
    const blob = new Blob([code], { type: 'text/javascript' })
    return import(URL.createObjectURL(blob))
  }
  const code = `
		export default {
			foo: 'bar'
		}
	`
  importCode(code).then((m) => {
    console.log(m.default)
  })
</script>
<script type="module">
  function importCode(code) {
    const blob = new Blob([code], { type: 'text/javascript' })
    return import(URL.createObjectURL(blob))
  }
  const code = `
		export default {
			foo: 'bar'
		}
	`
  importCode(code).then((m) => {
    console.log(m.default)
  })
</script>

参考文章:https://github.com/akira-cn/FE_You_dont_know/issues/12

下载 ​

  1. 创建 a 标签,之后给 a 标签添加 download 属性;
js
<a href="./1.docs" download="文件.docs">
  下载
</a>
<a href="./1.docs" download="文件.docs">
  下载
</a>

注意:

如果是 txt 文件和 img 文件,则谷歌浏览器会直接打开,而不是下载

如果是服务器方式运行网页,文件在项目文件夹中,则无论是 img 还是 txt 都会直接进行下载操作

  1. 配合后端下载也可以让后端给响应头添加:

    js
    header('Content-type: image/jpeg')
    header("Content-Disposition: attachment; filename='download.jpg'")
    header('Content-type: image/jpeg')
    header("Content-Disposition: attachment; filename='download.jpg'")
  2. 后端下载链接如果提示非法下载 office,需要修改 content-type,

https://blog.csdn.net/topc2000/article/details/79793057

  1. 前后端分离下载,前端自己处理下载

    • 流的形式:

      js
      function downLoad(blob) {
        let a = document.createElement('a')
        a.href = URL.createObjectURL(blob)
        a.download = true
        a.click()
        a = null
      }
      function downLoad(blob) {
        let a = document.createElement('a')
        a.href = URL.createObjectURL(blob)
        a.download = true
        a.click()
        a = null
      }
    • url 形式

      js
      function urlToBase64(url) {
        return new Promise((resolve) => {
          let canvas = document.createElement('canvas')
          let ctx = canvas.getContext('2d')
          let img = new Image()
          img.setAttribute('crossOrigin', 'Anonymous')
          img.onload = () => {
            ctx.width = img.width
            ctx.height = img.height
            ctx.drawImage(img, 0, 0)
            let base64 = canvas.toDataURL('image/png')
            resolve(base64)
            canvas = null
          }
          img.src = url
        })
      }
      function base64ToFile(base64, fileName) {
        let arr = base64.split(',')
        let mime = arr[0].match(/:(.*?);/)[1]
        let bstr = atob(arr[1])
        let n = bstr.length
        let u8arr = new Uint8Array(n)
        while (n--) {
          u8arr[n] = bstr.charCodeAt(n)
        }
        return new File([u8arr], fileName, { type: mime })
      }
      function urlToBase64(url) {
        return new Promise((resolve) => {
          let canvas = document.createElement('canvas')
          let ctx = canvas.getContext('2d')
          let img = new Image()
          img.setAttribute('crossOrigin', 'Anonymous')
          img.onload = () => {
            ctx.width = img.width
            ctx.height = img.height
            ctx.drawImage(img, 0, 0)
            let base64 = canvas.toDataURL('image/png')
            resolve(base64)
            canvas = null
          }
          img.src = url
        })
      }
      function base64ToFile(base64, fileName) {
        let arr = base64.split(',')
        let mime = arr[0].match(/:(.*?);/)[1]
        let bstr = atob(arr[1])
        let n = bstr.length
        let u8arr = new Uint8Array(n)
        while (n--) {
          u8arr[n] = bstr.charCodeAt(n)
        }
        return new File([u8arr], fileName, { type: mime })
      }
  2. 解决浏览器直接打开下载问题(兼容 IE)

js
function doSave(value, name, type) {
  var blob
  name = name || ''
  if (typeof window.Blob == 'function') {
    blob = new Blob([value], { type: type })
  } else {
    var BlobBuilder =
      window.BlobBuilder ||
      window.MozBlobBuilder ||
      window.WebKitBlobBuilder ||
      window.MSBlobBuilder
    var bb = new BlobBuilder()
    bb.append(value)
    blob = bb.getBlob(type)
  }
  var URL = window.URL || window.webkitURL
  var bloburl = URL.createObjectURL(blob)
  var anchor = document.createElement('a')
  if ('download' in anchor) {
    anchor.style.visibility = 'hidden'
    anchor.href = bloburl
    anchor.download = name
    document.body.appendChild(anchor)
    var evt = document.createEvent('MouseEvents')
    evt.initEvent('click', true, true)
    anchor.dispatchEvent(evt)
    document.body.removeChild(anchor)
  } else if (navigator.msSaveBlob) {
    navigator.msSaveBlob(blob, name)
  } else {
    window.location.href = bloburl
  }
}
function doSave(value, name, type) {
  var blob
  name = name || ''
  if (typeof window.Blob == 'function') {
    blob = new Blob([value], { type: type })
  } else {
    var BlobBuilder =
      window.BlobBuilder ||
      window.MozBlobBuilder ||
      window.WebKitBlobBuilder ||
      window.MSBlobBuilder
    var bb = new BlobBuilder()
    bb.append(value)
    blob = bb.getBlob(type)
  }
  var URL = window.URL || window.webkitURL
  var bloburl = URL.createObjectURL(blob)
  var anchor = document.createElement('a')
  if ('download' in anchor) {
    anchor.style.visibility = 'hidden'
    anchor.href = bloburl
    anchor.download = name
    document.body.appendChild(anchor)
    var evt = document.createEvent('MouseEvents')
    evt.initEvent('click', true, true)
    anchor.dispatchEvent(evt)
    document.body.removeChild(anchor)
  } else if (navigator.msSaveBlob) {
    navigator.msSaveBlob(blob, name)
  } else {
    window.location.href = bloburl
  }
}
  1. 使用插件下载

    1. 安装npm i -g file-saver
    2. 引入下载
    js
    import { saveAs } from 'file-saver'
    saveAs(url, fileName)
    import { saveAs } from 'file-saver'
    saveAs(url, fileName)

复制 ​

使用原生 ​

ie 点击复制会到页面最底部,推荐使用第二种方法

js
function copyText(text) {
  let textArea = document.createElement('textarea')
  textArea.value = text
  document.body.appendChild(textArea)
  textArea.select()
  document.execCommand('copy')
  document.body.removeChild(textArea)
  // 复制成功
}
function copyText(text) {
  let textArea = document.createElement('textarea')
  textArea.value = text
  document.body.appendChild(textArea)
  textArea.select()
  document.execCommand('copy')
  document.body.removeChild(textArea)
  // 复制成功
}

另一种方法 ​

https://github.com/feross/clipboard-copy

js
function copyText(text) {
  var span = document.createElement('span')
  span.textContent = text
  document.body.appendChild(span)

  var selection = window.document.getSelection()
  var range = window.document.createRange()
  range.selectNode(span)

  selection.removeAllRanges()
  selection.addRange(range)
  window.document.execCommand('copy')

  document.body.removeChild(span)
}
function copyText(text) {
  var span = document.createElement('span')
  span.textContent = text
  document.body.appendChild(span)

  var selection = window.document.getSelection()
  var range = window.document.createRange()
  range.selectNode(span)

  selection.removeAllRanges()
  selection.addRange(range)
  window.document.execCommand('copy')

  document.body.removeChild(span)
}

使用插件 ​

官网: https://github.com/zenorocha/clipboard.js

安装:npm i clipboard

  1. 使用方法 (1)复制 input 标签里面的内容
js
<input id='foo' value='hhhhhhh'/>
<button class='btn' data-clipboard-target='#foo'>点击复制</button>

//当创建实例时自动给绑定元素添加点击事件
new Clipboard('.btn')
<input id='foo' value='hhhhhhh'/>
<button class='btn' data-clipboard-target='#foo'>点击复制</button>

//当创建实例时自动给绑定元素添加点击事件
new Clipboard('.btn')

(2)react 例子,直接复制传入的内容,创建实例会绑定 click 事件,获取 data-clipboard-text 属性的值;

js
import Clipboard from 'clipboard'
let clipboard = Clipboard('.li')

<li data-clipboard-text='这是复制的内容' className='li'>
import Clipboard from 'clipboard'
let clipboard = Clipboard('.li')

<li data-clipboard-text='这是复制的内容' className='li'>

(3)配置 data-clipboard-action 可以选择是剪切还是复制,copy 是复制,cut 为剪切,不写默认 copy,仅对 input 和 textarea 起作用

js
<input data-clipboard-action="cut" value="hhhhhhh" />
<input data-clipboard-action="cut" value="hhhhhhh" />
  1. 事件,复制之后的回调函数
js
var clipboard = new ClipboardJS('.btn')

clipboard.on('success', function (e) {
  console.info('Action:', e.action) // 目标操作
  console.info('Text:', e.text) // 目标文本
  console.info('Trigger:', e.trigger) // 目标元素

  e.clearSelection()
})

clipboard.on('error', function (e) {
  console.error('Action:', e.action)
  console.error('Trigger:', e.trigger)
})
var clipboard = new ClipboardJS('.btn')

clipboard.on('success', function (e) {
  console.info('Action:', e.action) // 目标操作
  console.info('Text:', e.text) // 目标文本
  console.info('Trigger:', e.trigger) // 目标元素

  e.clearSelection()
})

clipboard.on('error', function (e) {
  console.error('Action:', e.action)
  console.error('Trigger:', e.trigger)
})
  1. 修改默认方法
js
new ClipboardJS('.btn', {
  target: function (trigger) {
    return trigger.nextElementSibling // 需要返回node
  },
  text: function (trigger) {
    return trigger.getAttribute('aria-label') // 需要返回string
  },
})
new ClipboardJS('.btn', {
  target: function (trigger) {
    return trigger.nextElementSibling // 需要返回node
  },
  text: function (trigger) {
    return trigger.getAttribute('aria-label') // 需要返回string
  },
})
  1. 如果是单页面应用,需要清理创建的事件,单页面需要写在生命周期触发
js
var clipboard = new ClipboardJS('.btn')
clipboard.destroy()
var clipboard = new ClipboardJS('.btn')
clipboard.destroy()

图片压缩 ​

一种方案:安装:npm install exif-js;

js
// 图像压缩
import EXIF from 'exif-js'

// 获取图片信息
function getImageInfo(img, callback) {
  let Orientation = 1
  EXIF.getData(img, function () {
    Orientation = EXIF.getTag(this, 'Orientation')
    callback(Orientation)
  })
}

// 旋转画布
function rotate(ctx, Orientation) {
  switch (Orientation) {
    case 3:
      //旋转180度
      ctx.rotate(Math.PI)
      break
    case 6:
      //旋转90度
      ctx.rotate(Math.PI / 2)
      break
    case 8:
      //旋转270度
      ctx.rotate(Math.PI * 1.5)
      break
    default:
  }
}

// canvas 绘制图片
function drawImage(img, quality, Orientation, callback) {
  const { width, height } = img
  //生成canvas
  var canvas = document.createElement('canvas')
  var ctx = canvas.getContext('2d')

  if (Orientation === 3 || Orientation === 6) {
    canvas.width = height
    canvas.height = width
  } else {
    canvas.width = width
    canvas.height = height
  }
  if (Orientation !== 1) {
    ctx.translate(canvas.width / 2, canvas.width / 2)
    rotate(ctx, Orientation)
    ctx.translate(-canvas.width / 2, -canvas.width / 2)
  }

  ctx.drawImage(img, 0, 0)
  // 图像质量
  if (!(quality && quality <= 1 && quality > 0)) {
    quality = 0.7
  }
  // quality值越小,所绘制出的图像越模糊
  canvas.toBlob(callback, 'image/jpeg', quality)
}

// 图片渲染
function canvasDataURL(file, quality = 0.7, callback) {
  var img = new Image()
  img.src = window.URL.createObjectURL(file)

  img.onload = function () {
    getImageInfo(img, (Orientation) => {
      drawImage(img, quality, Orientation, callback)
    })
  }
}

function compressionImg(file, callback) {
  let newFile = null
  canvasDataURL(file, 0.7, (blob) => {
    // 处理后的file
    newFile = new File([blob], file.name, { type: blob.type })
    if (!newFile || newFile.size > file.size) {
      newFile = file
    }
    callback(newFile)
  })
}

export default compressionImg
// 图像压缩
import EXIF from 'exif-js'

// 获取图片信息
function getImageInfo(img, callback) {
  let Orientation = 1
  EXIF.getData(img, function () {
    Orientation = EXIF.getTag(this, 'Orientation')
    callback(Orientation)
  })
}

// 旋转画布
function rotate(ctx, Orientation) {
  switch (Orientation) {
    case 3:
      //旋转180度
      ctx.rotate(Math.PI)
      break
    case 6:
      //旋转90度
      ctx.rotate(Math.PI / 2)
      break
    case 8:
      //旋转270度
      ctx.rotate(Math.PI * 1.5)
      break
    default:
  }
}

// canvas 绘制图片
function drawImage(img, quality, Orientation, callback) {
  const { width, height } = img
  //生成canvas
  var canvas = document.createElement('canvas')
  var ctx = canvas.getContext('2d')

  if (Orientation === 3 || Orientation === 6) {
    canvas.width = height
    canvas.height = width
  } else {
    canvas.width = width
    canvas.height = height
  }
  if (Orientation !== 1) {
    ctx.translate(canvas.width / 2, canvas.width / 2)
    rotate(ctx, Orientation)
    ctx.translate(-canvas.width / 2, -canvas.width / 2)
  }

  ctx.drawImage(img, 0, 0)
  // 图像质量
  if (!(quality && quality <= 1 && quality > 0)) {
    quality = 0.7
  }
  // quality值越小,所绘制出的图像越模糊
  canvas.toBlob(callback, 'image/jpeg', quality)
}

// 图片渲染
function canvasDataURL(file, quality = 0.7, callback) {
  var img = new Image()
  img.src = window.URL.createObjectURL(file)

  img.onload = function () {
    getImageInfo(img, (Orientation) => {
      drawImage(img, quality, Orientation, callback)
    })
  }
}

function compressionImg(file, callback) {
  let newFile = null
  canvasDataURL(file, 0.7, (blob) => {
    // 处理后的file
    newFile = new File([blob], file.name, { type: blob.type })
    if (!newFile || newFile.size > file.size) {
      newFile = file
    }
    callback(newFile)
  })
}

export default compressionImg
  1. 二种方案

github 地址: https://github.com/WangYuLue/image-conversion(支持按照指定 kb 来压缩,也支持按照质量压缩)

图片预览插件 ​

教程: https://www.cnblogs.com/Jimc/p/10132177.html

官网: https://github.com/fengyuanchen/viewerjs

h5 唤起客户端 ​

地址:https://github.com/suanmei/callapp-lib