整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

CSS酷炫跑马灯等待时加载效果

CSS酷炫跑马灯等待时加载效果

个视频用CSS来写一个酷炫的跑马灯加载等待的效果。

·来看一下代码:<span style="--:html结构非常简单,加载的这些小圆点等一下就通过这些width的尾元素来写,一共有二十个,每个里面都定义了一个变量i。当然这一堆东西是可以通过JS来生成的,也非常简单,大家可以自己试着去改一下。

·样式现在写了一些基本的样式,其它样式重新来写。<span style="首先是加载的这个区域,给它一个相对定位,大小是120乘以120。<div class="loader"然后写一下这些span。

·现在还没有什么东西,因为这些小圆点还没有写,就一个背景,这些小圆点通过width的尾元素来写,大小给15像素就可以了,给个背景颜色还有圆角。现在这些小圆点是重叠在一起了,可以把它旋转开。

·width进行一个load,用一下计算的函数,用定义好的这个变量乘以18度。然后给这些小圆点加一些阴影,一共五层阴影非常简单,就给它叠加起来。

·接下来就是动画的效果。首先先让背景颜色可以不断的切换起来,给它绑定一个动画,来写一下这个动画,非常简单。width:1通过滤镜去调整它的色相就可以了。position:relativ开始在0度的位置,把色相的角度刚好转一圈,现在背景颜色就已经有变化了。

·最后就是怎么样让这些小圆点有一个跑马灯的加载效果。这里很简单,再来写一个动画,对它进行缩放,一开始保持原来的大小,然后到百分之八十一,直到后面就让它消失,把这个动画绑定在伪元素上面。

现在这些小圆点也有动画了,但是它们是整体同时出现同时消失的,没有那种跑马灯的加载效果。其实也非常简单,只要让这些小圆点的动画起始时间不一样就可以了,也就是给它加一个动画的延迟。这里同样要计算一下,用定义好的变量i每个都乘以0.1秒。

来看一下最终的效果,没有问题,这种跑马灯的加载效果就完成了。

这个视频就到这里,感谢大家的收看。

于最近的工作原因对图标有了更加全方位的认知,虽然之前写过《如何绘制功能图标基础篇?》《如何系统的学习功能图标?》这两篇文章,但里面还是缺少了理论依据和系统做图标的思维。通过不断在学习的过程中不断有了新的认知,希望和大家一起分享。

图标设计原则

1. 表意准确

功能图标的第一原则是表意准确,要让用户看到图标第一时间就能理解它的含义。同时,功能图标还具有通用性,符合所有的用户的使用习惯,不要试图去改变用户日积月累沉淀下来的记忆。

微信底部的Tab栏,已经很多年没换过了,由于微信用户群体庞大机构复杂,牵一发而动全身,谁也不敢随便的改变用户多年积累的认知记忆。可能从美观角度还有很大发挥的空间,但是用户更多的会认为,熟悉的就是最好的。

而爱心图标在用户的认知里更多的是喜欢,当朋友在微信朋友圈发了照片或更新动态,点击爱心来表达自己的喜欢。

2. 可预见性

预见性是指人对事物发展的预判和前瞻,而人对功能图标预见性的强弱取决于用户对该图标的认知强弱,当我们把绘制好的图标放入页面时我们要考虑用户是否可以很快的找到该图标?

当用户找到该图标时,用户是否会很快的理解图标代表什么意思?当用户在点击图标前是否已经大约预测到点击该图标后的界面大体样式或内容?

根据上图的icon我们可以预测这是一款音乐类app的图标,因为图标有有明显的音符和音乐播放按钮等。

根据上图的icon我们可以预测这是一款购物电商类app的图标,因为图标中有分类查找和购物车图标。

上图中当前显示页面为店铺页面,当我们看到客服图标时能大体的想象得到点击客服图标会跳转到聊天工具的页面,这就是图标的可预见性。

3. 统一性

(1)大小的统一

图标的主流尺寸有16×16, 24×24, 32×32, 48×48, 64×64, 96×96, 128×128, 192×192, 256×256, 512×512,1024×1024……

(2)偶数规则

元素周期表中相邻的两元素,原子序数为偶数的,其在地壳中的平均含量常大于奇数元素的含量。对于同一元素而言,质量数为偶数的同位素,在地壳中的平均含量大于相邻奇数同位素的平均含量。

这是人们根据分析的实际数据,经验归纳而得出的元素和同位素在地壳中的分布规则之一,称为偶数规则。

在UI界面设计对于偶数原则基本保持一致态度。

在图标设计中主要就是两种声音,4的倍数和8的倍数?48之间的争斗不仅体现在图标尺寸的规范上也体现在珊格系统的规范制定中。

那么怎么根据强有力的依据去决定到底是用4的倍数还是8的倍数呢?换言之就是到底用ios的规范还是用Material design的规范?

(3)ios的规范4的倍数

iPhone上最小的点击区域,官方推荐是44px×44px。

为什么ios的规范4的倍数?因为苹果改变了游戏的规则,以前大家一起玩耍的时候都用px物理像素(physical pixel)来定义大小的尺寸,突然苹果推出retina屏幕改变了普通屏幕的物理尺寸。在不同的屏幕上(普通屏幕 vs retina屏幕),css像素所呈现的大小(物理尺寸)是一致的,不同的是1个css像素所对应的物理像素个数是不一致的。

在普通屏幕下,1个css像素 对应 1个物理像素(1:1);在retina 屏幕下,1个css像素对应 4个物理像素(1:4)。

(4)Material design的规范8点网格

Material design建立8点为一个单位的网格,所有的元素尺寸都是8的倍数。有些屏幕会很难调整适应这个系统,比如iPhone6开始的375×667的尺寸,但是解决方法也很简单。

保持填充和空隙(padding & margin)的尺寸统一遵循规则,剩余的空间可以用块状的元素来填充。有一些元素的尺寸是奇数的也没关系,只要他们能让整体遵守这套规则就好。

(5)数字8拆解分析

  • 加减法:2+2+2+2=8;2+3+3=8;2+6=8;3+5=8;4+4=8
  • 乘除法:2×4=8
  • 次方:2的3次方等于8
  • 比例关系:2/8=1/4;3/8;4/8=1/2;5/8;6/8=3/4

(6)黄金螺旋线/斐波那契数列

斐波那契数列(FibonacciSequence)数列是这样一个数列1、1、2、3、5、8……

在数学上,斐波那契数列是以递归的方法来定义:

F0=1

F1=1

Fn=F(n-1)+F(n-2)

(n>=2,n∈N*)

为什么谷歌的Material design和Ant design都把8点一个单位的网格,根据我上面的一些数学方法的推理,斐波那契数列中数字1/2/3/5/8占了很大的比重。

举个列子2+6=8,可以继续拆解成1+3+1+3=8,但是2:6=1:3;同理 2×4=8,但是2:4=1:2,里面细拆数字都符合斐波那契数列,符合斐波那契数列意味着就符合了黄金分割比。

最后得出的结论就是8的倍数为主,4的倍数为辅;除非你设计的app只需要适配ios系统可以使用4的倍数,当既要适配ios系统又要适配安卓系统时且没有设计两套界面分别适配ios跟安卓时选择8的倍数是做好的选择。

(7)颜色统一

图标在选取颜色的时候尽量不要超过4种颜色,且每个图标的配色需要根据对应的行业背景进行配色,利用色彩心理学比如红色可使用在美食餐饮上,橙色用在美食上多指甜美,绿的代表食物多指健康绿色产品等

(8)风格统一

风格已经在《如何系统的学习功能图标?》归纳的很全了,直角图标和圆角图标基础上适当添加一种符合的图标风格;最好不要出现两种风格相加,很容易乱,也不够简洁,主次不明。

在整个产品或者系统中,可以适当使用2到3种风格不同的图标就行差异化对待。

(9)图标设计规范

圆角规范:外圆角半径-线的粗细=内圆角半径

外圆角半径大小:圆角半径是整个图标大小的十分之一左右

(10)图标的物理平衡和视觉平衡

为什么我们再同样的大小区域去绘制正方形、圆形、三角形,虽然符合了统一的物理大小规范,但是从视觉上看上去却很不均衡?

关于这一点Material design给出了很好的解决办法规范化的去绘制图标。

正方形18dp*18dp ; 圆形直径20dp大小的规范

垂直矩形20dp*16dp ; 水平矩形16dp*20dp

通过Google系统图标规范绘制出来的图标可以达到视觉平衡

打破规则:当视觉平衡和物理平衡发生冲突时,我们应该优先选择视觉平衡。上图中是微信的界面图标,仔细观察我们发现通讯录的图标已经超出物理规定的大小,但是整个图标在界面中是可以达到视觉平衡的。

所以我们在绘制的过程中可以打破规则,这也是每个优秀的设计师应该具备的。

(11)图标网格系统

在主流的图标绘制中,线性图标的粗细大小有1px、2px、3px。所以我们在建立图标网格系统是使用了8的倍数,上面已经通过对数字8拆解中得知8的倍数非常适合1px、2px、3px粗细大小的。

在二倍图下使用48*48px的尺寸大小,在一倍图下使用了24*24px的尺寸大小来绘制图标。

空间的呼吸感:在绘制图标时,我们不但要确定图标的大小,还要考虑图标的内呼吸感,就是所谓的正负形,图标的负空间也有规则,Material design内呼吸感以2px为基准进行绘制的。

通过字体字重感受线性图标粗细:

字体字重从细到粗会给人轻盈到沉稳的感觉,无论中文还是西文,文字越细其可读性越强,文字越粗其视认性越高。

通过列举线性图标的粗细大小有1px、2px、3px、4px。可以看到图标粗细变化给人的视觉感受也是不一样的,具体使用多大取决于界面内容,最好的方法就是通过对比来验证那个粗细更适合当前界面。

关于2倍尺寸下使用3px,在3倍尺寸下会变成4.5px,会出现0.5px的问题。这方面的技术已经可以实现了,当然最好使用svg矢量格式。比如上图的爱心图标,弧线肯定是存在小数点问题,所以使用svg矢量格式是最好的选择。

(12)怎么画一条0.5px的边

比较了在高清屏上画0.5px的几种方法——可以通过直接设置宽高border为0.5px、设置box-shadow的垂直方向的偏移量为0.5px、借助线性渐变linear-gradient、使用transform: scaleY(0.5)的方法,使用SVG的方法。

最后发现SVG的方法兼容性和效果都是最好的,所以在viewport是1的情况下,可以使用SVG画0.5px,而如果viewport的缩放比例不是1的话,那么直接画1px即可。

更详细的请参考链接:怎么画一条0.5px的边。

4. 层次明确

图标具有可点击性和标识性。可点击性就会有点击前、点击时、点击后三种状态,主流底部标签栏会在点击前使用线性图标,点击时和点击后使用面性图标;也有使用颜色来区分。

5. 延展性

图标应该具有很强的延展性,好的图标可以直接当应用图标或者logo来使用,好的图标还可以通过点线面动效变化做下拉加载动画。

图标的功能分类

按图标功能还可以细分为动作图标、警示图标、内容图标、设备图标、文件图标、编辑图标、导航图标、通知图标、社交图标、切换图标等……

为什么我们在设计图标的时候很少去系统的这样去区分,更多的原因可能我们做的C端产品,图标种类和数量相对较少,当我们接触到B端产品,由于B端产品的业务复杂程度对应的图标数量也随之增加,为了更好的管理图标需要更加详细的设置分类。

图标的命名规范

关于图标的命名为什么要用英语正规化?因为我们用的整个系统都是基于英语开发的,设计师的业务下游主要是前端工程师,如果我们不能规范的命名每个图标肯定会增加前端的工作量,如何提高合作效率应该也是设计师用户体验的范畴。英语差的打开谷歌翻译基本没任何问题的。

切图命名以模块为前缀,如:模块_类别_功能_状态.png

  • 模块:登陆页面(login) 公共(common) 需求a(need) 需求b(demand) 发现(discover) 消息(message) 我(me)
  • 类别:导航栏(nav) 菜单栏(tab) 按钮(btn) 图标(icon) 背景图片(bg) 默认图片(def)
  • 功能:菜单(menu) 返回(back) 关闭(close) 编辑(eidt) 消息(message) 删除(delete)
  • 状态:选中(selected) 不可点(disabled) 按下(pressed) 正常(normal)

图标的制作上线

在app产品中,以美团app为例整个产品中图标使用了多种风格,首页金刚区图标作为首页流量分发的重要分支,在视觉设计要吸引用户的眼球做的更艳丽一点,而在标签栏导航图标和内页的功能图标需要设计的更加简洁。

我们在绘制图标的时候首页金刚区复杂的图标单独绘制一套,其他系统需要绘制线面两种风格,为了更好的适配页面,方面以后更好的使用我们在Sketch中使用Symbol系统的制作图标。

以爱心图标为例,我们使用Symbol绘制线面两套图标,关于图标的配色可以添加黑白灰+主色,可以有警示的橙色/成功的绿色/删除的红色等,后续复制添加也很方便。

建立图标库和颜色库,每次有新增的图标和新增的颜色,只需再新增一个Symbol就可以很好的管理自己产品中的图标库了。

团队协作:

目前团队协作按照图标功能分类上传到sketch的插件craft,方便团队其他成员一起使用。

设计的下游——前端开发:

为了很好的方便前端开发工作,我们需要根据上面的规范进行命名自己的图标。

前端开发主流做法就是把图标变成一个字体,上传到团队共享的icon网站,通过输出svg矢量格式的图标,让前端开发工程师直接调用。

国内主流的平台是iconfont,如果sketch制作的图标,导出svg格式在illustrator软件里面重新安装1024的尺寸进行绘制,上传到项目中。

所有的路径都需要扩展成面性图标,多色图标不支持后期代码修改颜色,单色图标后期可支持自定义图标颜色。

总结

我们在设计的过程中,随之对设计的认知水平提高,我们的知识体系也在不断的完善,这时候就需要我们对了解的知识进行深挖,多问自己为什么?了解背后的逻辑。这样才会更加深刻。

#参考文献#

  • Material Design规范:https://material.io
  • 怎么画一条0.5px的边:https://segmentfault.com/a/1190000013998884

#相关文章#

如何系统学习功能图标

作者:水手哥,公众号:水手哥学设计

本文由 @水手哥 原创发布于人人都是产品经理。未经许可,禁止转载

题图来自 Unsplash ,基于 CC0 协议

文件上传

前景提要

在工作中,经常会遇到上传文件的功能,但是当文件体积大时,如果使用把该文件直接在一个请求体中提交,会出现一些问题,以nginx为例:

  • 其默认允许1MB以内的文件
  • 超过1MB的文件,需要设置client_max_body_size放开体积限制

但是这样会存在一个问题,就是如果上传的文件体积很大,就会出现一些问题,最明显的问题是:

服务器的存储和网络带宽压力都会非常大

当服务器、产品、用户忍不了时,就需要对大文件上传进行优化。

1、大文件切片上传

逻辑梗概

  • 将大文件分割成多个文件块
  • 逐个上传文件块
  • 服务端将文件块顺序合并成完整文件

优势分析

  1. 减轻服务器压力:如果一次性上传大文件,服务器的存储和网络带宽压力都会非常大,而通过切片,可以将这些压力分散到多个小文件中,减轻服务器的压力。
  2. 断点续传、错误重试:因为大文件被肢解了,如果因为一些原因中断、错误了,已经上传的部分就不用再重新上传了,只需要把后续的传上就好了。

前端部分

1.1 切文件(前端)

1.2 判定切片是否完成上传完成(前端)

  • 客户端记录切片的上传状态,只需要上传未成功的切片

1.3 断点、错误续传(前端)

  • 客户端上传文件时,记录已上传的切片位置
  • 下次上传时,根据记录的位置,继续上传

后端部分

1.1 收切片、存切片

  • 将相关切片保存在目标文件夹

1.2 合并切片

  • 服务端根据切片的顺序,将切片合并成完整文件

1.3 文件是否存在校验

  • 服务端根据文件Hash值、文件名,校验该文件是否已经上传

代码实现

1、搭建基础项目

服务器(基于express)

const express=require('express')
const app=express()
app.listen(3000, ()=> {
    console.log('服务已运行:http://localhost:3000');
})


前端

基础页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        input{
            display: block;
            margin: 10px 0;
        }
    </style>
</head>
<body>
    <input type="file" id="file">
    <input type="button" id="upload" value="上传">
    <input type="button" id="continue" value="继续上传">
</body>
</html>


引入资源

<script type="module" src="./spark-md5.js"></script>
<script type="module" src="./operate.js"></script>


operate.js

// 获取文件域
const fileEle=document.querySelector("#file");
const uploadButton=document.querySelector("#upload");
const continueButton=document.querySelector("#continue");
uploadButton.addEventListener("click", async ()=> {
    console.log("点击了上传按钮")
})
continueButton.addEventListener('click', async ()=> {
    console.log("点击了继续上传按钮")
})


3、静态资源托管(server)

app.use(express.static('static'))


4、上传接口

搭建上传接口(server)

使用body-parser中间价解析请求体

// 导入中间件
const bodyParser=require('body-parser')
// 使用中间件
// 处理URL编码格式的数据
app.use(bodyParser.urlencoded({ extended: false })); 
// 处理JSON格式的数据
app.use(bodyParser.json()); 


上传接口

app.post('/upload', (req, res)=> {
    res.send({
        msg: '上传成功',
        success: true
    })
})


测试接口(前端)

// 单个文件上传
const uploadHandler=async (file)=> {
    fetch('http://localhost:3000/upload', {
        method: "POST",
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            fileName: '大文件',
        }),
    })
}
uploadButton.addEventListener("click", async (e)=> {
    uploadHandler()
})


5、文件上传接口存储文件(server)

使用multer中间件处理上传文件

设置uploadFiles文件夹为文件存储路径

const multer=require('multer')
const storage=multer.diskStorage({
    destination: function (req, file, cb) {
        cb(null, './uploadFiles');
    },
});
const upload=multer({
    storage
})

app.post('/upload', upload.single('file'), (req, res)=> {
    
})


测试

// 单个文件上传
const uploadHandler=async (file)=> {
    let fd=new FormData();
    fd.append('file', file);
    fetch('http://localhost:3000/upload', {
        method: "POST",
        body: fd
    })
}
uploadButton.addEventListener("click", async ()=> {
    let file=fileEle.files[0];
    uploadHandler(file)
})


6、文件切片

注意

假设切片大小为1M 保存切片顺序(为了合成大文件时正确性) 上传状态(为了断点续传、前端显示进度条)

// 使用单独常量保存预设切片大小 1MB
const chunkSize=1024 * 1024 * 1; 
// 文件切片
const createChunks=(file)=> {
    // 接受一个文件对象,要把这个文件对象切片,返回一个切片数组
    const chunks=[];
    // 文件大小.slice(开始位置,结束位置)
    let start=0;
    let index=0;
    while (start < file.size) {
        let curChunk=file.slice(start, start + chunkSize);
        chunks.push({
            file: curChunk,
            uploaded: false,
            chunkIndex: index,
        });
        index++;
        start +=chunkSize;
    }
    return chunks;
}


测试文件切片函数

// 存储当前文件所有切片
let chunks=[];
uploadButton.addEventListener("click", async ()=> {
    let file=fileEle.files[0];
    chunks=createChunks(file);
    console.log(chunks);
})


注意:将来要把这些切片全部都上传到服务器,并且最后需要把这些切片合并成一个文件,且要做出文件秒传功能,需要保留当前文件的hash值和文件名,以辨别文件和合并文件。

在页面中引入spark-md5.js

<script type="module" src="./spark-md5.js"></script>


获取文件Hash值

const getHash=(file)=> {
    return new Promise((resolve)=> {
        const fileReader=new FileReader();
        fileReader.readAsArrayBuffer(file);
        fileReader.onload=function (e) {
            let fileMd5=SparkMD5.ArrayBuffer.hash(e.target.result);
            resolve(fileMd5);
        }
    });
}


把文件的hash值保存在切片信息中

// 文件hash值
let fileHash="";
// 文件名
let fileName="";
// 创建切片数组
const createChunks=(file)=> {
    // 接受一个文件对象,要把这个文件对象切片,返回一个切片数组
    const chunks=[];
    // 文件大小.slice(开始位置,结束位置)
    let start=0;
    let index=0;
    while (start < file.size) {
        let curChunk=file.slice(start, start + chunkSize);
        chunks.push({
            file: curChunk,
            uploaded: false,
            fileHash: fileHash,
            chunkIndex: index,
        });
        index++;
        start +=chunkSize;
    }
    return chunks;
}
// 上传执行函数
const uploadFile=async(file)=> {
    // 设置文件名
    fileName=file.name;
    // 获取文件hash值
    fileHash=await getHash(file);
    chunks=createChunks(file);
    console.log(chunks);
}


7、上传逻辑修改

前端部分

单个文件上传函数修改:

插入文件名、文件Hash值、切片索引

上传成功之后修改状态标识(可用于断点续传、上传进度回显)

// 单个文件上传
const uploadHandler=(chunk)=> {
    return new Promise(async (resolve, reject)=> {
        try {
            let fd=new FormData();
            fd.append('file', chunk.file);
            fd.append('fileHash', chunk.fileHash);
            fd.append('chunkIndex', chunk.chunkIndex);
            let result=await fetch('http://localhost:3000/upload', {
                method: 'POST',
                body: fd
            }).then(res=> res.json());
            chunk.uploaded=true;
            resolve(result)
        } catch (err) {
            reject(err)
        }
    })
}


批量上传切片

限制并发数量(减轻服务器压力)

// 批量上传切片
const uploadChunks=(chunks, maxRequest=6)=> {
    return new Promise((resolve, reject)=> {
        if (chunks.length==0) {
            resolve([]);
        }
        let requestSliceArr=[]
        let start=0;
        while (start < chunks.length) {
            requestSliceArr.push(chunks.slice(start, start + maxRequest))
            start +=maxRequest;
        }
        let index=0;
        let requestReaults=[];
        let requestErrReaults=[];

        const request=async ()=> {
            if (index > requestSliceArr.length - 1) {
                resolve(requestReaults)
                return;
            }
            let sliceChunks=requestSliceArr[index];
            Promise.all(
                sliceChunks.map(chunk=> uploadHandler(chunk))
            ).then((res)=> {
                requestReaults.push(...(Array.isArray(res) ? res : []))
                index++;
                request()
            }).catch((err)=> {
                requestErrReaults.push(...(Array.isArray(err) ? err : []))
                reject(requestErrReaults)
            })
        }
        request()
    })
}


抽离上传操作

// 文件上传
const uploadFile=async (file)=> {
    // 设置文件名
    fileName=file.name;
    // 获取文件hash值
    fileHash=await getHash(file);
    // 获取切片
    chunks=createChunks(file);
    try {
        await uploadChunks(chunks)
    } catch (err) {
        return {
            mag: "文件上传错误",
            success: false
        }
    }
}


后端部分

修改上传接口,增加功能

使用一个文件Hash值同名的文件夹保存所有切片

这里使用了node内置模块path处理路径

使用fs-extra第三方模块处理文件操作

const path=require('path')
const fse=require('fs-extra')
app.post('/upload', upload.single('file'), (req, res)=> {
    const { fileHash, chunkIndex }=req.body;
    // 上传文件临时目录文件夹
    let tempFileDir=path.resolve('uploadFiles', fileHash);
    // 如果当前文件的临时文件夹不存在,则创建该文件夹
    if (!fse.pathExistsSync(tempFileDir)) {
        fse.mkdirSync(tempFileDir)
    }
    // 如果无临时文件夹或不存在该切片,则将用户上传的切片移到临时文件夹里
    // 如果有临时文件夹并存在该切片,则删除用户上传的切片(因为用不到了)
    // 目标切片位置
    const tempChunkPath=path.resolve(tempFileDir, chunkIndex);
    // 当前切片位置(multer默认保存的位置)
    let currentChunkPath=path.resolve(req.file.path);
    if (!fse.existsSync(tempChunkPath)) {
        fse.moveSync(currentChunkPath, tempChunkPath)
    } else {
        fse.removeSync(currentChunkPath)
    }
    res.send({
        msg: '上传成功',
        success: true
    })
})


8、合并文件

编写合并接口(server)

合并成的文件名为 文件哈希值.文件扩展名

所以需要传入文件Hash值、文件名

app.get('/merge', async (req, res)=> {
    const { fileHash, fileName }=req.query;
    res.send({
        msg: `Hash:${fileHash},文件名:${fileName}`,
        success: true
    });
})


请求合并接口(前端)

封装合并请求函数

// 合并分片请求
const mergeRequest=(fileHash, fileName)=> {
    return fetch(`http://localhost:3000/merge?fileHash=${fileHash}&fileName=${fileName}`, {
        method: "GET",
    }).then(res=> res.json());
};


在切片上传完成后,调用合并接口

// 文件上传
const uploadFile=async (file)=> {
    // 设置文件名
    fileName=file.name;
    // 获取文件hash值
    fileHash=await getHash(file);
    // 获取切片
    chunks=createChunks(file);
    try {
        await uploadChunks(chunks)
        await mergeRequest(fileHash, fileName)
    } catch (err) {
        return {
            mag: "文件上传错误",
            success: false
        }
    }
}


合并接口逻辑

1、根据文件Hash值,找到所有切片

app.get('/merge', async (req, res)=> {
    const { fileHash, fileName }=req.query;
    // 最终合并的文件路径
    const filePath=path.resolve('uploadFiles', fileHash + path.extname(fileName));
    // 临时文件夹路径
    let tempFileDir=path.resolve('uploadFiles', fileHash);
    // 读取临时文件夹,获取所有切片
    const chunkPaths=fse.readdirSync(tempFileDir);
    console.log('chunkPaths:', chunkPaths);
    res.send({
        msg: "合并成功",
        success: true
    });
})


合并接口逻辑

2、遍历获取所有切片路径数组,根据路径找到切片,合并成一个文件,删除原有文件夹

app.get('/merge', async (req, res)=> {
    const { fileHash, fileName }=req.query;
    // 最终合并的文件路径
    const filePath=path.resolve('uploadFiles', fileHash + path.extname(fileName));
    // 临时文件夹路径
    let tempFileDir=path.resolve('uploadFiles', fileHash);

    // 读取临时文件夹,获取所有切片
    const chunkPaths=fse.readdirSync(tempFileDir);

    console.log('chunkPaths:', chunkPaths);

    // 将切片追加到文件中
    let mergeTasks=[];
    for (let index=0; index < chunkPaths.length; index++) {
        mergeTasks.push(new Promise((resolve)=> {
            // 当前遍历的切片路径
            const chunkPath=path.resolve(tempFileDir, index + '');
            // 将当前遍历的切片切片追加到文件中
            fse.appendFileSync(filePath, fse.readFileSync(chunkPath));
            // 删除当前遍历的切片
            fse.unlinkSync(chunkPath);
            resolve();
        }))
    }
    await Promise.all(mergeTasks);
    // 等待所有切片追加到文件后,删除临时文件夹
    fse.removeSync(tempFileDir);
    res.send({
        msg: "合并成功",
        success: true
    });
})


10、断点续传

封装continueUpload方法

在continueUpload方法中,只上传 uploaded 为true的切片

修改后此功能对用户来说即是黑盒,用户只需要重复调用continueUpload方法即可

// 文件上传
const continueUpload=async (file)=> {
    if(chunks.length==0 || !fileHash || !fileName){
        return;
    }
    try {
        await uploadChunks(chunks.filter(chunk=> !chunk.uploaded))
        await mergeRequest(fileHash, fileName)
    } catch (err) {
        return {
            mag: "文件上传错误",
            success: false
        }
    }
}


2、大文件秒传

逻辑梗概

  • 客户端上传文件时,先提交文件的哈希值,
  • 服务端根据哈希值查询文件是否已经上传,如果已上传,则直接返回已上传状态
  • 客户端收到已上传状态后,直接跳过上传过程

优势分析

  • 提高上传效率:秒传可以提高上传效率,因为文件已经在上传过程中被上传过了,直接返回已上传状态,省要再次上传,提高效率。

代码实现

校验接口,校验是否已经存在目标文件

逻辑:根据文件Hash值和文件名组成 “文件Hash.文件扩展名” ,以保证文件名唯一

app.get('/verify', (req, res)=> {
    const { fileHash, fileName }=req.query;
    const filePath=path.resolve('uploadFiles', fileHash + path.extname(fileName));
    const exitFile=fse.pathExistsSync(filePath);
    res.send({
        exitFile
    })
})


校验函数

// 校验文件、文件分片是否存在
const verify=(fileHash, fileName)=> {
    return fetch(`http://localhost:3000/verify?fileHash=${fileHash}&fileName=${fileName}`, {
        method: "GET",
    }).then(res=> res.json());
};

// 文件上传
const uploadFile=async (file)=> {
    // 设置文件名
    fileName=file.name;
    // 获取文件hash值
    fileHash=await getHash(file);
    // 校验是否已经上传该文件
    let { exitFile }=await verify(fileHash, fileName);
    if (exitFile) {
        return {
            mag: "文件已上传",
            success: true
        }
    }
    // 获取切片
    chunks=createChunks(file);
    try {
        await uploadChunks(chunks.filter(chunk=> !chunk.uploaded))
        await mergeRequest(fileHash, fileName)
    } catch (err) {
        return {
            mag: "文件上传错误",
            success: false
        }
    }
}


3、提取为公共方法

封装

编写 bigFileUpload.js 文件,暴露uploadFile和continueUpload

// bigFileUpload.js
export default {
    uploadFile,
    continueUpload
}


使用

导入资源并调用

import bigUpload from './bigFileUpload.js'
uploadButton.addEventListener("click", async ()=> {
    let file=fileEle.files[0];
    bigUpload.uploadFile(file)
})
continueButton.addEventListener('click', async ()=> {
    bigUpload.continueUpload()
})


4、可优化

前端:

封装形式可优化,采用类的方式封装,以保证数据的独立性、可定制性

切片Hash的计算可以通过抽样切片的方式来进行

...

后端:

文件Hash校验可增加用户ip地址以保证文件唯一性

待合并项可定时删除

...

欢迎大家补充!


作者:JSNoob
链接:https://juejin.cn/post/7323883238896058387