假设要把项目做成微服务架构,然后单独做个静态资源服务器。


假如项目要做成微服务架构,像我最近做的一个国际项目,里头的日志,静态资源等需要抽离成独立的服务。。。巴拉巴拉省略一万字。

静态资源

假如把静态资源单独做成一个微服务, 可以选择搭建一个HDFS 。。。具体步骤(省略很多字)

文件服务hdfs

HDFS(Hadoop Distributed File System),作为Google File System(GFS)的实现,是Hadoop项目的核心子项目,是分布式计算中数据存储管理的基础,是基于流数据模式访问和处理超大文件的需求而开发的,可以运行于廉价的商用服务器上。它所具有的高容错、高可靠性、高可扩展性、高获得性、高吞吐率等特征为海量数据提供了不怕故障的存储,为超大数据集(Large Data Set)的应用处理带来了很多便利。

适用、不适用的场景
1
2
3
4
高容错性、可构建在廉价机器上
适合批处理
适合大数据处理
流式文件访问
HDFS局限
1
2
3
4
不支持低延迟访问
不适合小文件存储
不支持并发写入
不支持修改

为什么要将文件分片

1、为了充分利用网络带宽,加快上传速度
2、避免断网,页面关闭等引起的文件上传失败导致文件丢失,需要重新上传

分片上传原理

将一个文件切割为一系列特定大小的小数据片,然后将这些小数据片分别上传到服务端,全部上传完后再由服务端将这些小数据片合并成为一个完整的资源。在上传的过程中即使遭受一些不确定因素影响导致上传中断,在恢复之后将继续该文件上次上传的进度继续上传,也就是断点续传。

举个栗子:
直接上传

1
2
3
传一个100M的文件,开始上传...0%..2%...99%...(断网) => 上传失败
然后再传一次,开始上传...0%..2%...99%...(某些原因造成页面关闭) => 上传失败
重新上传一次,开始上传...0%...

分片

1
2
100M的文件,每个数据片大小5M,分20片上传
开始上传...第一片...第二片...(断网)...第三片...(页面关闭)...第四片...(传完20片重组成文件)上传成功

后端:实现分片上传的前提是需要服务器记录某文件的上传进度,根据该文件的唯一标识判读是否是同一个文件,可以利用文件内容根据(修改时间+文件名称+最后修改时间)求MD5码,如果上传的文件过大,求MD5码也是个稍微漫长的过程,所以对于太大的文件,只能针对其中某一段数据进行MD5,加上其他的鉴权得到唯一的key。

前端:将文件按照按照一定大小进行分片,一般每片大小在5M左右,可以同时发起多个请求,但一次同时请求的连接数不宜过多,否则服务器负载过重。可以利用H5强大的File Api,通过File对象的slice方法切出文件的一部分,切出的数据片是Blob。

实现流程

1、选择文件
2、根据(修改时间+文件名称+最后修改时间)计算MD5,以此来实现断点续传及秒传的功能,所以要等MD5计算完毕之后,再开始文件上传的操作。

可以在这里做文件效验进度

3、通过文件的MD5查询文件是否已经存在
4、检查并上传MD5

可以在这里做文件上传进度

5、上传完所有分片之后通知服务器进行合并

具体实现

实现分片上传的方式有很多种:
百度的分片上传组件:WebUploader
vue-simple-uploader: vue-simple-uploader是基于 simple-uploader.js 封装的vue上传插件。使用之前瞄一眼vue-simple-uploader官方文档simple-uploader.js官方文档…以及其他的花里胡哨

使用vue-simple-uploader

vue-simple-uploader的使用方法很简单,基本上就是配置参数

1
npm i vue-simple-uploader

main.js或者其他导入的地方

1
2
3
import vueSimpleUploader from 'vue-simple-uploader';
Vue.use(vueSimpleUploader);
`

专门写个.vue文件来装它,叫upload-vue-simple-uploader.vue
template部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<uploader
:options="options"
:file-status-text="statusText"
@file-complete="fileComplete"
@complete="complete"
class="uploader-example"
>
<uploader-unsupport></uploader-unsupport>
<uploader-drop>
<p>请将文件拖拽到这里,或者</p>
<uploader-btn>请选择文件</uploader-btn>
<uploader-btn :attrs="attrs">请选择图片</uploader-btn>
<uploader-btn :directory="true">请选择目录</uploader-btn>
</uploader-drop>
<uploader-list></uploader-list>
</uploader>
</template>

js部分,options,attrs,statusText等都是它的配置项,可以翻看它的文档进行配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
export default {
data () {
return {
options: {
target: 'http://127.0.0.1:3000/upload', //上传地址
chunkSize: 5 * 1024 * 1024, //分片大小
testChunks: false, // 是否开启服务器分片校验(秒传)
maxChunkRetries: 3, //最大自动失败重试上传次数
checkChunkUploadedByResponse: function (chunk, message) {// 服务器分片校验,断点续传基础
let objMessage = JSON.parse(message);
if (objMessage.skipUpload) {
return true;
}
return (objMessage.uploaded || []).indexOf(chunk.offset + 1) >= 0
},
headers: {
Authorization: "Bearer " + "your accessToken"
},
},
attrs: {
//接受的文件类型,形如['.png', '.jpg', '.jpeg', '.gif', '.bmp'...]
accept: 'image/*'
},
statusText: { //对应的状态说明
success: '上传成功',
error: '上传出错了',
uploading: '正在上传',
paused: '暂停中',
waiting: '等待中'
}
}
},
methods: {
complete () {
console.log('上传完成 complete', arguments)
},
fileComplete () {
console.log('文件上传完成 complete', arguments)
}
}
}
`

style

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.uploader-example {
width: 880px;
padding: 15px;
margin: 40px auto 0;
font-size: 12px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.4);
}
.uploader-example .uploader-btn {
margin-right: 4px;
}
.uploader-example .uploader-list {
max-height: 440px;
overflow: auto;
overflow-x: hidden;
overflow-y: auto;
}

使用的时候直接import然后在components里头注册然后直接在页面上用,当然这只是方便快捷简单粗暴的demo

1
import UploadVueSimpleUploader from '@/components/upload-vue-simple-uploader'

使用效果
使用效果
上传一个mp3文件
上传一个mp3文件

上传一个我比较喜欢的千与千寻《神隐》的10M大小的mp3文件,发现它上传成功了,然后发了两次请求,因为设置的分片大小是5M所以会发两次请求
然后此时后端接收到的数据是这个样子的,然后后端再把它重组就成了文件

后端接收到
后端接收到

原生的方式

原生的方式比上面那个方式稍微复杂一些,但是比较可控,首先是有DOM,当然都是比较随意的布局,好看可以用ElementUI或者自己写写样式。

1
npm install spark-md5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div>
<div>
<input
type="file"
ref="file"
@change="fileChange"
>
</div>

<div class="process-wrap">
<div class="progress"><span :style="style"></span>校验进度:{{checkProcessStyle}}</div>
</div>

<br>

<div class="process-wrap">
<div class="progress"><span :style="style1"></span>上传进度:{{uploadProcessStyle}}</div>
</div>
</div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
let SparkMD5 = require('spark-md5')
export default {
data () {
return {
checkProcessStyle: 0,
uploadProcessStyle: 0,
baseUrl: 'http://localhost:3000',
chunkSize: 5 * 1024 * 1024,
fileSize: 0,
file: null,
hasUploaded: 0,
chunks: 0
}
},
computed: {
style () {
return {
width: this.checkProcessStyle + '%'
}
},
style1 () {
return {
width: this.uploadProcessStyle + '%'
}
}
},
methods: {
/* 选择文件 */
fileChange () {
this.file = this.$refs.file.files[0]
this.fileSize = this.file.size
this.resChange(this.file)
},
async resChange (file) {
// 1:按照 修改时间+文件名称+最后修改时间计算MD5
let fileMd5Value = await this.md5File(file)
// 2:校验文件的MD5
let result = await this.checkFileMD5(file.name, fileMd5Value)
// 如果文件已存在, 就秒传
if (result.file) {
console.log('文件已存在,秒传')
return false
}
// 显示上传进度
this.uploadProcessStyle = '100%'
// 3:检查并上传MD5
await this.checkAndUploadChunk(fileMd5Value, result.chunkList)
// 4: 通知服务器所有分片已上传完成
this.notifyServer(fileMd5Value)
},
/* 1、修改时间 + 文件名称 + 最后修改时间 => MD5 */
md5File (file) {
let _this = this
return new Promise((resolve, reject) => {
let blobSlice =
File.prototype.slice ||
File.prototype.mozSlice ||
File.prototype.webkitSlice
let currentChunk = 0
let _spark = new SparkMD5.ArrayBuffer()
let _fileReader = new FileReader() // H5强大的File Api
let _chunks = 100
let _chunkSize = file.size / 100
_this.chunks = _chunks
_this.chunkSize = _chunkSize

/* 读文件 */
_fileReader.onload = function (e) {
console.log('read chunk nr', currentChunk + 1, 'of', _chunks)
_spark.append(e.target.result)
currentChunk++
if (currentChunk < _chunks) {
loadNext()
} else {
let curr = +new Date()
console.log('上传完成')
let result = _spark.end()
resolve(result)
}
}
/* 出错 */
_fileReader.onerror = function () {
console.warn('something wrong!!!!')
}

function loadNext () {
let start = currentChunk * _chunkSize
let end =
start + _chunkSize >= file.size ? file.size : start + _chunkSize
_fileReader.readAsArrayBuffer(blobSlice.call(file, start, end))
_this.checkProcessStyle = currentChunk + 1 + '%'
}
loadNext()
})
},
/* 2、校验文件的MD5 */
checkFileMD5 (fileName, fileMd5Value) {
let _this = this
return new Promise((resolve, reject) => {
let url = `${_this.baseUrl}/check/file?fileName=${fileName}&fileMd5Value=${fileMd5Value}`
_this._$.getJSON(url, function (data) {
resolve(data)
})
})
},
/* 3.1、上传chunk */
async checkAndUploadChunk (fileMd5Value, chunkList) {
this.chunks = Math.ceil(this.fileSize / this.chunkSize)
this.hasUploaded = chunkList.length

for (let i = 0; i < this.chunks; i++) {
let exit = chunkList.indexOf(i + '') > -1
// 如果已经存在, 则不用再上传当前块
if (!exit) {
let index = await this.upload(i, fileMd5Value, this.chunks)
this.hasUploaded = this.hasUploaded + 1
let radio = Math.floor((this.hasUploaded / this.chunks) * 100)
console.log(radio, ':radio')
this.uploadProcessStyle = radio + '%'
}
}
},
/* 3.2上传chunk2 */
upload (i, fileMd5Value, chunks) {
let _this = this
return new Promise((resolve, reject) => {
let end =
(i + 1) * _this.chunkSize >= _this.file.size
? _this.file.size
: (i + 1) * _this.chunkSize
let _formData = new FormData() // 构造表单
_formData.append('data', _this.file.slice(i * _this.chunkSize, end)) // 切出文件的一部分
_formData.append('total', _this.chunks) // 总片数
_formData.append('index', i) // 当前是第几片
_formData.append('fileMd5Value', fileMd5Value)
_this._$.ajax({
url: `${_this.baseUrl}/upload`,
type: 'POST',
data: _formData,
async: true,
processData: false,
contentType: false,
success: function (data) {
resolve(data.desc)
}
})
})
},
/* 4、通知服务器所有分片上传完成 */
notifyServer (fileMd5Value) {
let url = `${this.baseUrl}/merge?md5=${fileMd5Value}&fileName=${this.file.name}&size=${this.file.size}`
this._$.getJSON(url, function (data) {
alert('上传成功')
})
}
}
}

style lang=”stylus”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.process-wrap {
width: 300px;
p {
width: 100%;
}
.progress {
background: #c5c8ce;
height: 20px;
position: relative;
span {
display: block;
background: #19be6b;
height: 100%;
width: 0;
}
}
}

github上的源码地址:https://github.com/HHardyy/vue-upload