Skip to content

NodeJS实现文件下载 #18

Description

@jappp

最近遇见一个Node实现文件下载的需求,需要实现如下的feature:

  1. 返回实时的下载进度
  2. 能够准备的判断文件是否下载完成
  3. 下载出错时,能够实现断点继续下载
  4. 准确捕获下载时出现的错误

在现有场景下,因为不希望因为一个下载小功能就引入第三方库,并且也没有找到完全符合要求的库,所以自己实现了文件下载功能。

1. 获取一个可读流

实现下载功能的核心模块是 Stream

NodeJs中的 Stream 一共有四种类型,我们常用的是可读流 Readable 和可写流 Writable 。实现下载功能的第一步,就是获取到一个可读流(数据源)
既然是下载功能,可读流大部分情况都是通过网络请求来得到的。我们可以使用axios、request这样的三方的库,也可以使用Node自带的http(s)模块来发送请求

1.1 使用axios获取可读流

使用axios时记得指定responseType为stream

const{ data }=awaitaxios.get('https://abc.com/test.exe',{responseType: 'stream'});console.log(data);// data就是一个可读流

1.2 使用http模块获取可读流

使用http(s)时,记得对 ClientRequest 的timeout和error事件做处理,因为请求时很可能出现错误

在回调函数中拿到的res实际上是一个 incommingMessage 的实例,它在可读流的基础上进行了一次扩展,我们可以把它当做可读流来使用,完全没问题

constreq=http.get({hostname: 'abc.com',protocol: 'https:',path: 'test.exe',},(res)=>{console.log(res);// res就是一个可读流})// 请求出错req.on('error',(err)=>{});// 请求超时req.on('timeout',(err)=>{});

2. 将可读流存储到本地文件

现在,我们有了一个可读流,我们需要将它的数据写入到我们本地文件中,这时候可写流就要出场了。写入文件分为以下步骤:

  1. 设置可读流的编码 setEncoding ,默认会使用Buffer来传输数据。下载图片、安装包等二进制文件的时候,使用Buffer即可,如果下载json、txt这种文件,就需要自行设置编码
  2. 创建一个可写流,传入一个本地文件的路径,例如/my/test.exe。这里文件的后缀名一般来说,要和你下载的文件保持一致。将一个图片文件的数据写入到.txt文件里面,会出现问题
  3. 处理数据流动过程中的事件
// 将数据写入文件constreadStream=awaitrequest('balabala');// 我是获取到的可读流constwriteStream=fs.createWriteStream('/abc/test.exe');// 新建一个可读流,传入我们需要保存的文件// 使用可读流的pipe方法,所有数据将流动到test.exe中readStream.pipe(writeStream);

上述代码执行完之后,其实文件下载就已经开始了。不过只下载文件可不行,还有很多feature没有实现呢

2.1 断点继续下载

一般可读流的数据出问题之后,会进入pause状态,我们需要在可读流恢复正常之后继续下载,而不是重新下载整个文件。有两种实现方法:

  1. 手动调用 readable.resume() 方法,可以将暂停的流恢复。但是不适用当前的场景,因为我们不知道流恢复正常的时机
  2. 监听可读流的 readable事件 reabable event , 在可读流的数据准备好给外界读取的时候,该事件都会触发
reabable.on('readable',()=>{readable.read();// 数据可以被读取时,调用read方法读取数据})

2.2 获取下载进度

代码中储存本地已经下载完毕的chunk大小 A ,通过header中的content-length来获取远程文件的大小 B,文件的下载进度即为 A / B

// 远程文件的大小let{'content-length': remoteFileSize}=headers;remoteFileSize=parseInt(remoteFileSize,10);// let chunkSize = 0 ; // 本地已经下载完的chunk大小readable.on('data',(chunk)=>{chunkSize+=chunk.length;// 每次数据的流出,都会触发data事件,将流出数据的大小累加在chunkSize上event.emit('progress',chunkSize/remoteFileSize);// 通过事件抛出文件下载的进度});

2.3 文件下载完毕

一开始,我以为可读流的end事件触发的时候,文件就已经下载完毕了,但是却踩了坑,见下面的代码

readable.on('end',()=>{// 我以为文件已经下载完毕shell.open(distPath);// 尝试打开这个文件// Error:file is busy});

当我想打开这个文件的时候,却发现偶尔会出现 文件正在被另一个程序 占用的错误
翻了一下文档,发现可读流end事件触发时,只表明可读流的数据已经消耗完毕 ,并不能保证数据已经全部写入到了本地磁盘
那么回忆一下,我们是用什么来写入数据到本地文件的?没错,就是可写流

writeableStream.on('finish',()=>{// 文件是真的已经写入到本地磁盘了shell.open(distPath);// 尝试打开这个文件// 成功打开,Yes!});

总结

NodeJs中的Stream是一个非常强大的功能,下载文件功能是一个Stream的经典应用场景
最后附上所有的download模块源码

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions