theme: fancy
一. 引言
背景: 随着语言技术的发展和工作需求,为了实现方便,快速,高效的开发体验,越来越多的框架,中间件等工具层出不穷,像
nodejs
下的express、egg、koa
,javascript
下的react、vue等
,的却这些工具的出现让我们的开发越来越高效,上手也越来越快,但是,这反而让人们失去了学习语言的原理,并且自己去思考的动力,遇到需求直接找一些已经开发好的工具直接使用,虽然会用,但是从不知道它背后的逻辑。
好了,废话不多说,通过本节学习,你将学会处理各种类型的数据传输,无论是文字,图片还是其他文件,都通通适用。
在学习之前让我们先去了解以下几个知识点
javascript:
- File
- Blob
nodejs :
- Buffer
二. File
从字面意义来看,file
也就是我们要上传的文件,在前端中,我们唯一访问文件的情况,就是使用input
标签的type="file"
,用户选择文件后会返回一个FileList
对象。我们来看一下,就以上传图片为例。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pcqffo0H-1658473722806)(?)]FileList
是一个类数组类型,其中存放着用户选择的文件,用户可以选择多个文件,所以使用类数组保存,在这我们只上传一个图片,所以可以通过FileList[0]
来访问具体文件File
,在File
中有如图所示的属性,再此就不做介绍了。
三. Blob
用MDN的解释来说,Blob
对象表示一个不可变、原始数据的类文件对象。正常来说,我们在保存数据时,都是使用我们熟悉的utf-8
的格式,但是计算机在处理数据时,更熟悉的是二进制数据,并且我们在操作非文本语言时,无法用utf-8
的形式来操作数据,所以我们可以用Blob
以二进制数据的形式来处理任意数据。因此我们可以把Blob
当作一个存储并且操作二进制数据的一个对象。
1. 构建Blob
通过构造函数的形式来创建一个Blob
对象,第一个参数为array
,第二个为一个options
const blob = new Blob(array, options)
2. 使用Blob
首先我们先来初步了解以下Blob
的一些方法和属性
- size:访问Blob的大小,单位为字节
- type: 文件的 MIME 类型
- arrayBuffer(): 返回一个Promise对象,用二进制来访问数据
- text(): 返回一个Promise对象,用 utf-8 格式来访问数据
- slice(): 类似于数组的slice 方法,来截取一段指定范围的新Blob对象
const blob = new Blob(['点个赞吧哈哈'],{type:'text/plain'})
console.log(blob.size, 'size')
console.log(blob.type, 'type')
blob.text().then(console.log)
blob.arrayBuffer().then(console.log)
const blobCopy = blob.slice(0, 6)
blobCopy.text().then(console.log)
打印结果如下:我们截取了6个字节blob,也就是点个
两个字,对字符串来说,该方法好像多此一举,String.prototype
中已经实现,但是在传输大文件时,我们可以很好的利用该功能实现分片传输。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t1lymq5E-1658473722810)(?)]
四.Buffer
Buffer
是nodejs
中内置的一个对象,它与前边提过的Blob
类似,用来处理像图片,文件这样的数据,它的操作方式跟数组类似,可以通过下标访问,其中数据是用两位的十六进制
来保存的。
1. Buffer 内存分配
首先要了解的一点是,Buffer
是javascript
和c++
相结合的一个模块,所以,它的性能远远比单独javascript
要快的多。除此外,Buffer
内存是由C++
申请,而不是由V8
来分配,属于堆外内存
。而js
只控制其内存调度,也就是使用。
我们来了解一个新的概念slab
,slab
是由C++
申请的一块8kb
的内存,可以把它理解为内存的最小调度单位,每次js
使用Buffer
来存储一段数据时,会将数据存到同一个slab
中,直到下次Buffer
的使用空间大于slab
剩余的大小时,C++
继续申请一块新的slab
。优点:
如果每次使用一点Buffer
就申请内存,频繁操作为造成严重性能问题,使用动态内存管理类机制,很好的解决了该问题。
2. Buffer 的使用
const buf = Buffer.alloc(1024, '来个关注吧', 'utf-8')
console.log(buf)
console.log(buf.length)
console.log(buf[0])
我们看下如下打印结果。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jZ0NGPzq-1658473722812)(?)]
方法和参数
- alloc: 接收三个参数,分别是申请空间大小(单位是字节),数据填充内存,编码方式
- length: 占用空间大小
- 打印第一个字节,由于是两位十六进制,所以范围为 0 - ff,也就是 0 - 255
- concat: 同数组的该方法类似,用来将每一个小段
Buffer
拼接起来 - toString(encoding):该方法用来将
buffer
转为其他编码方式
了解了上边内容后,我们进入今天的主题,上传和下载,其所上边讲的一切都是为了数据在 tcp 通道中传输做准备。
五. 文件下载
我们先来理下思路,文件下载,肯定先由客户端发起下载请求,然后后端将文件传送到前端并下载,在此用到了两个知识点。\
-
node
使用fs
来读取文件 -
客户端
使用a
标签的download
属性实现实现下载 - 使用
URL.createObjectURL
来创建可下载url
注:以上知识请大家自行了解,在此不做太多解释
直接上代码(完整代码,可直接复制测试一下,记得目录下放文件)
// client
<body>
<button id="btn">文件下载</button>
<h1 id="fileSize">文件大小:</h1>
</body>
<script>
const btn = document.getElementById('btn')
btn.onclick = function () {
fetch('/down', {method: 'get'})
.then(res => res.blob())
.then(data => {
getFileSize(data)
fileDown(data)
})
}
const fileDown = function (data) {
const link = document.createElement('a')
link.download = '1.jpg'
link.href = URL.createObjectURL(data)
link.style.display = 'none'
link.text = '文件下载'
document.body.appendChild(link)
link.click()
URL.revokeObjectURL(link.href)
document.body.removeChild(link)
}
const getFileSize = function(blob){
document.getElementById('fileSize').innerText = `文件大小:${Math.round(blob.size/(1024*1024))}MB`
}
</script>
// sever
const http = require('http')
const fs = require('fs')
class App {
source
constructor() {
this.source = {}
}
use(path, fn) {
this.source[path] = fn
}
}
app.use('/down', (req, res) => {
const rs = fs.createReadStream('./1.zip', {
highWaterMark: 1024 * 8
})
const data = []
let size = 0
rs.on('data', chunk => {
size += chunk.length
data.push(chunk)
})
rs.on('end', () => {
const buf = Buffer.concat(data, size)
res.setHeader('Content-Type', 'application/octet-stream')
res.setStatus = 200
res.setHeader('content-length', size)
res.end(buf)
})
})
http.createServer(function handleRequest(req, res) {
if (app.source[req.url]) {
app.source[req.url](req, res)
} else {
res.statusCode = 404
res.end()
}
}).listen(3000, () => {
console.log('running')
})
看完代码可能有点懵逼,我来简单说一下:
client
- 直接发起下载请求拿回返回数据
- 执行方法
getFileSize
将数据大小转为MB 显示到页面上(草率写了下)- 实现下载,这一步是最重要的,将获取到的数据流转为
blob
类型,然后利用URL.createObjuectURL
创建一个指向该数据的url
,并赋值个a
标签的href
属性,再利用download
的属性,实现下载文件( 这里文件名我写死了,根据文件尾缀会生成不同类型文件,这一定要注意 )
sever
- 处理路由我做了个简单封装用来注册路由,不要理会
- 首先是文件读取,使用
fs
的createReadStream
以数据流的形式读取文件,通过监听数据流读入,每次读取一个chunk
都是一个小的Buffer
,用数组将这些小段的Buffer
收集起来,当监听到数据读取完毕,将小的Buffer
拼接起来,一起发回前端。
以上就是文件下载的前后端实现,大家可以自己测试,可以下载任何类型文件,一定要记住修改生成文件尾缀。接下来我们说一下上传
六.文件上传
// client
<body>
<input type="file" onchange="handleFileChange(this)">
</body>
<script>
const handleFileChange = function (file) {
const source = file.files[0]
upload(source)
}
const upload = function (source) {
const blob = new Blob([source], { type: source.type })
fetch('/upload', {
method: 'post',
body: blob
})
}
</script>
// server
app.use('/upload', (req, res) => {
const data = []
req.on('data', chunk => {
data.push(chunk)
})
req.on('end', () => {
const buffer = Buffer.concat(data)
fs.writeFile('./peg.jpg', buffer, (err) => {
console.log(err)
})
})
res.end()
})
简单阐述下思路:
client
- 首先前端通过
input
标签获取用户要上传的文件,再将其改为Blob
数据格式,此处其实不需要做更改,file
对象本身实现了Blob
对象的原型链继承,可以直接使用其方法- 使用
fetch
将数据发送
server
- 通过
req.on
方法来监听数据的传输,同数据下载一下,此处读取的每一个chunk
都是小的二进制流,保存在buffer
中,当监听到数据全部传输完毕,使用Buffer.concat
方法将所有小段数据组合- 最后使用
fs.writeFile
将Buffer
数据写入任意路径下,完美实现了文件上传
七. 结尾
使用以上方法,我们可以处理任何类型的文件,但是这里写的都是文件一次性上传,如果遇到大文件上传,就需要分片上传,大家可以利用Blob
和Buffer
的方法slice
,将数据分段上传,可以自己尝试,再此就不多说啦。有机会之后文章中在聊。
在学习任何语言中,我们一定不要急于求成,一定要先学习语言的原生方法特性,将基础打好,再去学习对应框架从而事半功倍。