amandakelake / blog

think more!learn more!
2.25k stars 183 forks source link

JS文件:读取与拖拽、转换bsae64、预览、FormData上传、七牛上传、分割文件 #40

Open amandakelake opened 6 years ago

amandakelake commented 6 years ago

一、读取文件

前端无法向原生App那样直接操作本地文件,不然一个网页就能偷光用户电脑上的文件,用户想要触发文件操作,主要有三种

1、<input type='file' />选择本地文件 2、通过拖拽,利用drop事件 3、在编辑框里面复制粘贴(这里不讲,有兴趣的同学可当做延伸题目自己去了解)

1、<input type='file' />

<input id="file1" type="file">
// 监听input的change事件,通过e拿到文件
$('#file1').change(e => {
  const files = e.target.files || e.dataTransfer.files;
});

可以打印files看一下是什么,可以看到是一个对象,以files[0]表示具体的文件,还有一个长度属性是1 (如果同时导入多个文件,估计就是files[1]、files[2]的读法了,长度就是对应的文件数量,这里纯靠猜,本人没实践过,有兴趣的同学可以自己研究) 78601ae6-8da6-4561-a127-ee334136a768

这里要注意的是,这个files在很多时候都是不可用的,还需要转换成base64格式或者其他格式才能使用,怎么转换等下就讲,总之就是目前拿到的files没*用

2、拖拽

// 一个空的div就可以了,样式自己写吧
<div id="dropbox" />
const dropbox = $('#dropbox');
// jQuery的监听事件用on,不用addEventListener
dropbox.on({
  // 阻止dragenter和dragover事件的默认行为
  dragenter: dragenter,
  dragover: dragover,
  drop: drop
})

function dragenter(e) {
  e.stopPropagation();
  e.preventDefault();
}
function dragover(e) {
  e.stopPropagation();
  e.preventDefault();
}

function drop(e) {
  e.stopPropagation();
  e.preventDefault();
  // 在jQuery里面,得来originalEvent才拿得到
  const files = e.originalEvent.dataTransfer.files;
}

在jQuery里面,需要去originalEventdataTransfer里面才拿得到文件; 拖拽事件,还需要处理掉默认行为

到目前为主,我们只是拿到了两类没*用的files,还需要转换为能在页面上预览的格式以及可以上传到服务器的格式才行,传说中的base64。

二、处理文件(bsae64)与预览

图片的 base64 编码就是可以将一副图片数据编码成一串字符串,使用该字符串代替图像地址。(省去了一次HTTP请求,但只针对于小图片,大图片转码后可能占用的内存更大)

转换文件,目前有两种常用的方式:FileReaderObjectURL

FileReader

老惯例,先上MDN FileReader - Web API 接口 | MDN

FileReader 对象允许Web应用程序异步读取存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用 File 或 Blob 对象指定要读取的文件或数据。

其中File对象可以是来自用户在一个元素上选择文件后返回的FileList对象,也可以来自拖放操作生成的 DataTransfer对象,还可以是来自在一个HTMLCanvasElement上执行mozGetAsFile()方法后返回结果。

FileReader 包括四个异步读取文件的选项(具体请查阅) FileReader.readAsArrayBuffer()FileReader.readAsBinaryString()FileReader.readAsDataURL()FileReader.readAsText() 对FileReader 对象调用其中某一种读取方法后,可使用 onloadstartonprogressonloadonabortonerroronloadend 跟踪其进度

具体步骤: 1、实例化FileReader对象 2、FileReader.readAsDataURL() 开始读取内容 2、事件处理,通过onload事件读取文件(注意,是个异步过程,如果想要拿到数据,需要注册回调事件callback)

我这里封装好了一个转换函数

function transferImgToBase64(files, cb) {
    // 先判断浏览器是否支持,一般都支持的啦,求个心安而已
  if (typeof FileReader === 'undefined') {
    alert('您的浏览器不支持图片上传,请升级您的浏览器');
    return false;
  }
    // 实例化实例化`FileReader`对象
  let reader = new FileReader();
    // 读取内容
  reader.readAsDataURL(files[0]);
    // 通过onload事件拿到文件,并注册一个回调事件cb,这个回调事件就是拿来上传文件、预览文件等等的操作,回调的参数就是处理好的base64格式文件
  reader.onload = e => {
    cb ? cb(e.target.result) : null;
  };
}

预览就比较简单了

$('#file1').change(e => {
  const files = e.target.files || e.dataTransfer.files;
  $('#img').attr('src', objectURL)
    // 这里的res就是传入的base64数据,上面的e.target.result
  transferImgToBase64(files, res => {
    console.log('转码后', res);
    $('#img').attr('src', res);
  });
});

在input上面放个空的img标签,通过DOM操作,用attr对src属性进行操作即可看到页面上的预览效果(样式就不写了)

<img id="img" src="">
<input id="file1" type="file">

最后一起看看效果 2018-04-01 21 50 47

ObjectURL(一个实验中的功能,只能实现预览)

ObjectURL相当于文件的一个临时路径,此临时路径可随时生成、随时释放,在本地浏览器使用起来时,与普通的url无异

URL.createObjectURL() - Web API 接口 | MDN

// blog是用来创建 URL 的 File 对象或者 Blob 对象​
objectURL = URL.createObjectURL(blob);

在每次调用 createObjectURL() 方法时,都会创建一个新的 URL 对象,即使你已经用相同的对象作为参数创建过。当不再需要这些 URL 对象时,每个对象必须通过调用 URL.revokeObjectURL() 方法来释放。浏览器会在文档退出的时候自动释放它们,但是为了获得最佳性能和内存使用状况,你应该在安全的时机主动释放掉它们。

通过window.URL.createObjectURL(files[0])拿到URL对象,方式比FileReader更简单一点

$('#file1').change(e => {
  const files = e.target.files || e.dataTransfer.files;
  console.log('file1',files)
  const objectURL = window.URL.createObjectURL(files[0]);
  $('#img').attr('src', objectURL)
});

可以打印看下是什么格式的东西

console.log('objectURL',objectURL);

控制台可以看到如下输出

objectURL blob:null/bfefecc5-6c63-4465-84a2-8dc6c2ac48fd

2018-04-01 20 49 07

直接赋值给图片的src属性,即可实现预览,但是,它没有事件处理钩子,实现不了上传进度等业务功能,也没有完成转换base64格式,但如果只是单纯的想要实现预览,倒也足够了。

另外,要记得在合适的时候释放浏览器记忆

window.URL.revokeObjectURL($('#img').src);

三、FormData上传文件

<span href="" class="uploadBtn">
    {{uploadText}}
    <input type="file" name='file' @change="onFileChange" class="file-input">
</span>

通过一个spaninput隐藏起来,加以美化

.uploadBtn {
    position: relative;
    display: inline-block;
    width: 80px;
    text-align: center;
    line-height: 30px;
    border: 1px solid #e7e7eb;
    border-radius: 5px;
    padding: 2px 20px;
    color: #fff;
    background: #08cc6a;
    margin-right: 10px;
    &:hover {
        background: #07b35d;
    }
    input {
        position: absolute;
        left: 0;
        top: 0;
        width: 130px;
        height: 45px;
        opacity: 0;
        cursor: pointer;
    }
}

通过opacity: 0;把真正的input控件隐藏起来 然后让整个span的大小覆盖到input的范围大小

从丑丑的 a256c986-e96e-408a-b936-cd9bac03c442

变成了 25fddd30-cbb5-4b31-86e1-4d267c5a78ec

上面的@change="onFileChange"是vue的写法,用jQuery的同学直接加个监听事件就好了

vm.sendRequest是我自己封装的网络请求方法,$.ajax()的方法也写在下面的注释中了

onFileChange(e) {
    const vm = this;
    const files = e.target.files || e.dataTransfer.files;
    let formData = new FormData();
    console.log('文件', files, files[0].name);
    formData.append(files[0].name, files[0]);
    // const settings = {
    //  "async": true,
    //  "crossDomain": true,
    //  "url": "/shop/reseller/order/upload",
    //  "method": "POST",
    //  "headers": {
    //      "Content-Type": "application/x-www-form-urlencoded",
    //      "Cache-Control": "no-cache",
    //      // "Postman-Token": "cd09edb9-13ea-e249-9efb-aa7d9027b441"
    //  },
    //  "processData": false,
    //  "contentType": false,
    //  "mimeType": "multipart/form-data",
    //  "data": formData,
    // }
    // $.ajax(settings).done(function (response) {
    //  console.log('看看回调',response);
    // });
    vm.uploadText = '上传中...';
    vm.sendRequest({
        url: `/shop/reseller/order/upload`,
        data: formData,
        success(d) {
        toast('文件上传成功', true);
            vm.uploadText = '上传订单';
            vm.wrap = true;
            vm.showResultBtn = true;
            vm.orderCheckResult = d.data;
            console.log('看看回调',d);
        },
        failed(err) {
                toast('文件上传失败', false);
                vm.uploadText = '上传订单';
        toast(err.cnmsg, false);
    }        
}); 
},

有几点要注意的

四、上传到七牛(网络请求,非表单)

直接上文档(相信很多同学都不会点开看,但我任性,就是要贴官方文档,原因不想说) 如何上传base64编码图片到七牛云 - 七牛开发者中心 FAQ 常见问题 - 七牛开发者中心

1、封装七牛上传方法

// key和token是向后端请求,后端向七牛请求回来的验证,七牛说:兄dei,拿好key和token,我帮你留了个坑,等会你上传文件的时候带上它们,我帮你把文件填坑里去
// 前端向七牛上传文件,带上key和token,说:牛哥,刚刚后端那兄弟帮我申请了个坑,就是这文件,你帮我存好哈
// 最后两个参数是成功与失败的回调
function uploadToQiniu(key, token, files, sucCB = () => {}, errCB = () => {}) {
    // 判断https还是http,encode64方法是把key进行Base64编码,自己可以查一下
    // 这个请求链接,上面两个链接中有,不然就找你们后端同学要
  const url =
  document.location.protocol === 'https:'
    ? `https://upload.qbox.me/putb64/-1/key/${encode64(key)}`
    : `http://upload.qbox.me/putb64/-1/key/${encode64(key)}`
  const header = {
    'Content-Type': 'application/x-www-form-urlencoded',
    Authorization: 'UpToken ' + token,
    Host: 'upload.qbox.me'
  };
    // 转换files到base64格式,跟上面的大同小异,区别看下面
  transferImgToBase64WithoutHead(files, (r) => {
    const body = r;
      // fetch ,网络请求API,不会?同学,就此别过了😀
    fetch(url, {
      method: 'POST',
      headers: header,
      body: body
    })
      .then(res => {
          // 上传成功后拿到回调,你自己定义的处理办法
        ...yourOwnMethod(res)
      })
  });
}

2、图片数据处理

注意,这个方法多了WithoutHead几个字母

export function transferImgToBase64WithoutHead(files, cb) {
  if (typeof FileReader === 'undefined') {
    alert('您的浏览器不支持图片上传,请升级您的浏览器');
    return false;
  }
  let reader = new FileReader();
  reader.readAsDataURL(files[0]);
  reader.onload = e => {
      // 这里是唯一的区别,把转换后的base64编码的前面那一部分字符串去掉,参见下图,七牛只接受这样的格式作为body
    let result = e.target.result.replace(/^data:image\/\w+;base64,/, '');
    cb ? cb(result) : null;
  };
}

cb48c095-c858-4878-8ff5-48ea0375dfd6

3、基本用法

function onFileChange(e) {
  // 如何生成预览图,相信不用再说了吧
    // 如何生成`files`,也不用我说了吧
  // `sendRequest`是我们自己封的请求API,向后端请求key和token
  this.sendRequest({
    url: '********',
    success(d) {
      vm.token = d.data.data.token;
      vm.key = d.data.data.key;
      vm.productAdd.preview = d.data.data.token;
      // 上传图片到七牛
      uploadToQiniu(
        d.data.data.key,
        d.data.data.token,
        files,
        res => {
          console.log('成功回调', res);
          toast('图片上传成功', true)
        },
        err => {
          console.log('失败回调', err);
          toast('图片上传失败',false)
        }
      );
    }
  });
}

五、文件分割

利用Blobslice方法进行分割,大概思路如下:

  1. 读入文件
  2. 按固定size分片
  3. 给每个片段加入id,blob的内容提取成base64,数据封装
  4. 发送网络请求到后台(使用队列防止浏览器卡住),
  5. 后台收到后拼装(注意文件锁和顺序)

代码? 不存在的……延伸思考题 哈哈哈,我是不是很坑…… 骚年,自己动手,丰衣足食……

六、后记

感谢您耐心看到这里,希望有所收获!

如果不是很忙的话,麻烦右上角点个star⭐,举手之劳,却是对作者莫大的鼓励。

我在学习过程中喜欢做记录,分享的是自己在前端之路上的一些积累和思考,希望能跟大家一起交流与进步,更多文章请看【amandakelake的Github博客】

参考 前端本地文件操作与上传 – 人人网FED博客 阅读以 JavaScript 编写的本地文件 - HTML5 Rocks 【前端攻略】:玩转图片Base64编码 - ChokCoco - 博客园 基于JS的大文件分片 - CSDN博客

weberandphper commented 6 years ago

文件上次这块东西其实很多的,如果结合后端的话,多文件上传,单文件上传,文件读取,上传进度,大文件上传,后端配置,web服务器相关设置,还有文件安全,光这块的探究其实可以写很多

amandakelake commented 6 years ago

@weberandphper 嗯嗯 的确 还有很多要学的 我目前对后端这块比较薄弱 之后会慢慢补上的

imbant commented 5 years ago

受教了,今天也在研究这方面的东西