Vue 进阶系列丨大文件切片上传

Posted HFun TM

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Vue 进阶系列丨大文件切片上传相关的知识,希望对你有一定的参考价值。

Vue 进阶系列教程将在本号持续发布,一起查漏补缺学个痛快!若您有遇到其它相关问题,非常欢迎在评论中留言讨论,达到帮助更多人的目的。若感本文对您有所帮助请点个赞吧!


2013年7月28日,尤雨溪第一次在 GItHub 上为 Vue.js 提交代码;2015年10月26日,Vue.js 1.0.0版本发布;2016年10月1日,Vue.js 2.0发布。

最早的 Vue.js 只做视图层,没有路由, 没有状态管理,也没有官方的构建工具,只有一个库,放到网页里就可以直接用了。

后来,Vue.js 慢慢开始加入了一些官方的辅助工具,比如路由(Router)、状态管理方案(Vuex)和构建工具(Vue-cli)等。此时,Vue.js 的定位是:The Progressive Framework。翻译成中文,就是渐进式框架。

Vue.js2.0 引入了很多特性,比如虚拟 DOM,支持 JSX 和 TypeScript,支持流式服务端渲染,提供了跨平台的能力等。Vue.js 在国内的用户有阿里巴巴、百度、腾讯、新浪、网易、滴滴出行、360、美团等等。

Vue 已是一名前端工程师必备的技能,现在就让我们开始深入学习 Vue.js 内部的核心技术原理吧!


文件上传

在前端中,对于文件上传是一个很常见的功能,我们可以上传图片、文件等到服务器,对于一般的小型的文件, 例如小图片,我们可以选择基于 formdata 进行文件上传,也可以基于 base64 格式去上传,直接发送表单请求即可。

但是对于大文件来说,比如几个G甚至几十G的文件来说,我们如果还是按小文件来处理的话,就显得不是很恰当了。本篇文章就是告诉大家如何实现大文件上传的,包括切片上传、断点续传、进度把控等方面,废话不多说,开始吧。


大文件上传思路

这里先简单说一下整体的思路:

  1. 针对文件内容生成的唯一的hash值,作为文件名称。确保相同文件,不同的文件名的情况下,生成的hash值是一样的。

  2. 将大文件切成若干个小的分片,并且每一个分片的命名格式为 “文件hash值+当前切片索引值”,方便后端合并这些切片。

  3. 上传之前,将当前文件的hash值传递给后台,拿到当前文件在数据库里已经存在的切片,如果有切片已经存在,那么前端就不传递这些了,这就是断点续传的原理。

  4. 上传过程中,通过监听当前传递的切片数除以切片总数,获取当前上传进度。当进度条百分百时,自动通知后端将这些文件合并。

  5. 上传完成。



下面进行代码展示:

  • 生成文件名

这里我们使用一个第三方库:spark-md5,根据文件二进制流,生成hash值。

在vue中,通过:npm i spark-md5 安装

import SparkMD5 from 'spark-md5'


/**
 * @description: 拿到文件的hash值
 * @Author: hfunteam
 * @param * file 文件,File类型
 * @return 
 *   buffer: 文件的二进制流
 *   hash: 生成的文件hash值
 *   suffix:文件后缀名
 *   filename: 文件名称 hash + suffix
 *  
 */ 
function changeBuffer(file: File) 
  return new Promise(resolve => 
    let fileReader = new FileReader();
    fileReader.readAsArrayBuffer(file);
    fileReader.onload = ev => 
      let buffer: ArrayBuffer = ev.target!.result as ArrayBuffer,
        spark = new SparkMD5.ArrayBuffer(),
        HASH,
        suffix;
      spark.append(buffer);
      // 拿到文件的hash值
      hash = spark.end();
      // 匹配文件后缀名
      suffix = /\\.([a-zA-Z0-9]+)$/.exec(file.name)![1];
      resolve(
        buffer,
        hash,
        suffix,
        filename: `$HASH.$suffix`
      );
    ;
  );
;
  • 分片上传+断点续传

// html 部分
<input type="file" id="input" >


// js 部分
import axios from 'axios'
// 监听input按钮事件
document.getElementById('input').addEventListener('change', async function () 
    // 获取文件的HASH
    let alreadyUploadChunks = [], // 当前已经上传的切片
        
            hash,
            suffix
         = changeBuffer(file); 
    
    // 获取已经上传的切片信息
     let data = await axios.get('/upload_already', 
          params: 
              hash
          
      );
     // 从后端拿到已经上传的切片列表
     alreadyUploadChunks = data.fileList
    
    // 实现文件切片处理 「固定数量 或者 固定大小」
    let max = 1024 * 100, // 切片大小
        count = Math.ceil(file.size / max), // 切片总数
        index = 0,   // 当前上传的切片索引值
        chunks = []; // 存放切片的数组
    if (count > 100) 
        max = file.size / 100;
        count = 100;
    
    // 存放切片,注意此处的切片名称,hash+index+suffix
    while (index < count) 
        chunks.push(
            file: file.slice(index * max, (index + 1) * max),
            filename: `$HASH_$index+1.$suffix`
        );
        index++;
    
    // 把每一个切片都上传到服务器上
    chunks.forEach(chunk => 
        // 这里进行断点续传:已经上传的无需在上传
        if (alreadyUploadChunks.length > 0 && alreadyUploadChunks.includes(chunk.filename)) 
            // 管控进度条
            manageProgress();
            return;
        
        let formdata = new FormData();
        formdata.append('file', chunk.file);
        formdata.append('filename', chunk.filename);
        instance.post('/upload_chunk', formdata).then(data => 
            // 管控进度条
            manageProgress();
        ).catch(() =>             
            alert('当前切片上传失败,请您稍后再试');
        );
    );
)
  • 进度条控制+通知合并

当进度条到达百分之百时,调用通知后台合并切片接口,合并成功,则上传成功。

async function manageProgress()
    // 管控进度
    document.getElementById('progress').style.width = `$index/count*100%`;
    // 当所有切片都上传成功,我们合并切片
    if (index < count) return;
    document.getElementById('progress').style.width = `100%`;
    try 
        data = await instance.post('/upload_merge', 
            hash,
            count
        , 
            headers: 
                'Content-Type': 'application/x-www-form-urlencoded'
            
        );
        if (data.code === 0) 
            alert(`恭喜您,文件上传成功`);
            return;
        
        throw data.codeText;
     catch (err) 
        alert('切片合并失败,请您稍后再试');
    
;

Vue 进阶系列教程将在本号持续发布,一起查漏补缺学个痛快!若您有遇到其它相关问题,非常欢迎在评论中留言讨论,达到帮助更多人的目的。若感本文对您有所帮助请点个赞吧!

叶阳辉

HFun 前端攻城狮

往期精彩:

Vue 进阶系列丨虚拟DOM和VNode

Vue 进阶系列教程将在本号持续发布, 一起查漏补缺学个痛快!若您有遇到其它相关问题,非常欢迎在评论中留言讨论,达到帮助更多人的目的。若感本文对您有所帮助请点个赞吧!



2013年7月28日,尤雨溪第一次在 GItHub 上为 Vue.js 提交代码;2015年10月26日,Vue.js 1.0.0版本发布;2016年10月1日,Vue.js 2.0发布。

最早的 Vue.js 只做视图层,没有路由, 没有状态管理,也没有官方的构建工具,只有一个库,放到网页里就可以直接用了。

后来,Vue.js 慢慢开始加入了一些官方的辅助工具,比如路由(Router)、状态管理方案(Vuex)和构建工具(Vue-cli)等。此时,Vue.js 的定位是:The Progressive Framework。翻译成中文,就是渐进式框架。

Vue.js2.0 引入了很多特性,比如虚拟 DOM,支持 JSX 和 TypeScript,支持流式服务端渲染,提供了跨平台的能力等。 Vue.js 在国内的用户有阿里巴巴、百度、腾讯、新浪、网易、滴滴出行、360、美团等等。

Vue 已是一名前端工程师必备的技能,现在就让我们开始深入学习 Vue.js 内部的核心技术原理吧!




什么是虚拟DOM

作为一个前端,想必或多或少都听说过虚拟 DOM 这个名词,那么虚拟 DOM到底是什么呢?

在 web 早期,页面的交互效果还是比较简单的,不需要频繁的去操作DOM,使用 Jquery 完全可以满足我们的需求。但是随着时代的发展,页面上的东西越来越多,需求也越来越复杂,相应的程序中需要维护的状态也越来越多,我们就要不断的去操作 DOM。此时命令式编程的 Jquery 就显得很臃肿,后期维护困难。

三大框架的诞生就是为了解决这一问题,声明式操作 DOM,我们只需要描述状态和DOM之间的映射关系,就可以将状态渲染成视图。从状态到视图的过程,框架会帮我们做,不需要我们手动去操作DOM。

当状态发生变化时,需要重新渲染页面,也就是框架要去帮助我们操作DOM,那么框架怎么去确定状态发生了什么变化以及 框架要去哪里更新DOM 呢?最简单的做法就是只要有一个状态发生了变化,页面里全部的DOM 元素都重新渲染一遍,但是访问 DOM 的操作是比较费时的,如果按照这么做的话,会造成相当大的浪费。那么 Vue.js 是怎么做的呢?

Vue.js 在渲染页面之前会先根据状态生成一份虚拟 DOM 树,然后使用虚拟DOM 树进行渲染,在渲染之前,会使用新生成的虚拟 DOM 树和上一次生成的虚拟 DOM 树进行对比,只渲染不同的部分。



Vue的虚拟DOM

在 Vue.js1.0 的时候,Vue 并没有引用虚拟 DOM,当时的做法是当状态发生变化是,Vue 已经知道了是哪个节点使用了这个状态,然后将这个节点进行更新操作,根本不需要对比操作。

但是这样的缺点是,对于每一个状态都有一个依赖跟踪器,粒度太细,这样就会有一些内存开销和依赖追踪的开销,当状态被越来越多的节点使用的时候,开销就会非常大。

Vue2.0 的时候选择了一个中等粒度,当状态发生变化的时候,只在组件级别有一个依赖跟踪器,当组件内的状态改变的时候,只能通知到组件层次,组件内部通过虚拟 DOM 进行比对与渲染。Vue 之所以能随意的切换绑定的粒度,本质上还要归功于变化侦测,关于变化侦测的相关知识请移步。

Vue 使用模板来描述状态与 DOM 之间的映射关系,通过编译将模板转换成渲染函数,执行渲染函数生成虚拟节点树(vnode),使用虚拟节点数就可以渲染页面了。当状态发生变化的时候,直接进行 DOM 操作会比较慢,所以首先会先生成虚拟节点树,将新的节点树和旧的节点树进行对比,然后再操作 DOM,更新页面。

所以,虚拟 DOM 主要做了两件事:
  • 提供与真实 DOM 节点所对应的虚拟节点 vnode

  • 将新的虚拟节点和旧的虚拟节点进行对比,然后更新页面




什么是 VNode

VNode 是 Vue.js 的一个类,使用它可以实例化不同的 vnode 实例,我们只需要控制传入的参数的不同就可以了。
class VNode { constructor ( tag, data, children, text, elm, context, componentOptions, asyncFactory ) { this.tag = tag; this.data = data; this.children = children; this.text = text; this.elm = elm; this.ns = undefined; this.context = context; this.fnContext = undefined; this.fnOptions = undefined; this.fnScopeId = undefined; this.key = data && data.key; this.componentOptions = componentOptions; this.componentInstance = undefined; this.parent = undefined; this.raw = false; this.isStatic = false; this.isRootInsert = true; this.isComment = false; this.isCloned = false; this.isOnce = false; this.asyncFactory = asyncFactory; this.asyncMeta = undefined; this.isAsyncPlaceholder = false; } get child () { return this.componentInstance }}

我们可以看到 vnode 仅仅只是 VNODE 类的一个实例而已,通过传入的参数不同,就可以生成不同类型的 vnode 实例对象了。vnode 可以理解成节点描述对象,它描述了应该怎样去创建真实的 DOM 节点。



VNode 的种类

  • 注释节点

const createEmptyVNode = (text = '') => { const node = new VNode(); node.text = text; node.isComment = true; return node};


我们可以看到注释节点只有两个有效属性,text 和 isComment,其余属性都是默认的 undefined 或者 false。


例如,一个真实的注释节点:

<!-- 注释节点 -->


所对应的 vnode 是这样的:

{ text:'注释节点',  isComment:true}



  • 文本节点

function createTextVNode (val) { return new VNode(undefined, undefined, undefined, String(val))}


文本节点只有一个 text 属性,它的 vnode 是这样的:

{  text:'我是文本内容',}



  • 克隆节点

function cloneVNode (vnode) { const cloned = new VNode( vnode.tag,    vnode.data, vnode.children && vnode.children.slice(), vnode.text, vnode.elm, vnode.context, vnode.componentOptions, vnode.asyncFactory ); cloned.ns = vnode.ns; cloned.isStatic = vnode.isStatic; cloned.key = vnode.key; cloned.isComment = vnode.isComment; cloned.fnContext = vnode.fnContext; cloned.fnOptions = vnode.fnOptions; cloned.fnScopeId = vnode.fnScopeId; cloned.asyncMeta = vnode.asyncMeta; cloned.isCloned = true; return cloned}


克隆节点就是将一个节点的内容全部复制到新的节点中。主要用来优化静态节点和插槽节点。当静态节点需要重新渲染的时候,并不需要重新再次走一遍渲染函数,然后重新生成 vnode,只需要使用克隆节点的方法将之前的 vnode 克隆一份即可,一定程度上优化了程序的性能。


克隆节点和被克隆节点的唯一区别是 isCloned 属性,克隆节点的 isCloned为 true,被克隆的原始节点的 isCloned 为 false。



  • 元素节点


元素节点一般包含 4 个有效属性:

  1. tag:就是节点的名称,例如 div、p、img

  2. data:该属性包含一些节点上的数据,例如 class、style

  3. children:该节点的子节点列表

  4. context:当前组件的 vue.js 实例


通常,函数式组件的 vnode 是这样子的:

{  children:[VNode,VNode],  context:{...},  data:{...},  tag:{...}}



  • 组件节点


和元素节点类似,有两个独有的属性 componentInstance 和 componentOptions。


通常,函数式组件的 vnode 是这样子的:

{  componentInstance:{...},  componentOptions:{...}, context:{...}, data:{...}, tag:{...}}


  • 函数式组件


和组件节点类似,有两个独有的属性 functionalContext 和 functionalOptions。


通常,函数式组件的 vnode 是这样子的:

{ functionalContext :{...}, functionalOptions:{...}, context:{...}, data:{...}, tag:{...}}




Vue 进阶系列教程将在本号持续发布,一起查漏补缺学个痛快!若您有遇到其它相关问题,非常欢迎在评论中留言讨论,达到帮助更多人的目的。若感本文对您有所帮助请点个赞吧!

叶阳辉
HFun 前端攻城狮

往期精彩:

以上是关于Vue 进阶系列丨大文件切片上传的主要内容,如果未能解决你的问题,请参考以下文章

VUE 前端大文件上传如何实现?

----vue中实现上传文件给后台----

「免费开源」基于Vue和Quasar的前端SPA项目crudapi后台管理系统实战之文件上传

前端大文件分片上传(Vue)

Vue系列教程之Vue进阶

Vue系列教程之Vue进阶