Logo Vincent
返回文章列表

Node.js-开发实践:下载文件

Node.js
Node.js-开发实践:下载文件

【前言】

下载文件是Node.js中最常见的功能,

但实际开发中下载文件也会隐藏各种各样的坑。

【原始代码】

如果在网络搜索Node.js下载文件代码,

大概会搜到类似下面的代码片段,

本文从这里开始,陆续优化下载文件这个功能。

const http = require('http');
const https = require('https');
const fs = require('fs');

function downloadFile(url, dest) {
  let protocol = url.startsWith('https') ? https : http;
  let file = fs.createWriteStream(dest);
  protocol
    .get(url, (res) => {
      res.pipe(file);
      file.on('finish', () => {
        file.close();
        console.log('Download complete.');
      });
    })
    .on('error', (err) => {
      fs.unlink(dest);
      console.error(`Download failed: ${err.message}`);
    });
}

上面的代码片段可以看到:

1.兼容了Node.js原生的http和https请求

2.通过get请求获取url内容后pipe到本地文件写流中

3.捕获了请求的异常,异常时删除已经下载的文件

【校验url合法性】

可以看到上面代码是直接使用url的,

其实这里应该校验一下url的合法性,

代码如下:

// url
import { URL } from 'url';

/**
 * check url
 * @param {*} checkUrl
 * @returns
 */
export const checkUrl = (inputUrl) => {
  // check
  if (!inputUrl) return;

  // check url
  const url = new URL(inputUrl);
  if (!url) return;

  // check protocol
  return url.protocol === 'http:' || url.protocol === 'https:';
};

这里使用了Node.js原生的url模块进行校验

【校验dest合法性】

同样的上面代码直接使用dest目标路径,

这里同样的需要进行一些校验,

代码如下:

// fs
import { path, mkdir } from 'qiao-file';

/**
 * check dest
 * @param {*} dest
 * @returns
 */
export const checkDest = async (dest) => {
  // check dest
  if (!dest) return;

  // absolute dest
  const destIsAbsolute = path.isAbsolute(dest);
  if (!destIsAbsolute) dest = path.resolve(__dirname, dest);

  // mkdir
  const dir = path.dirname(dest);
  const mkdirRes = await mkdir(dir);
  if (!mkdirRes) return;

  // return
  return dest;
};

1. 这里兼容了dest为相对路径的情况

2. 以及如果dest的文件夹不存在则自动创建

添加url和dest校验后的代码如下:

// http
import http from 'http';
import https from 'https';

// fs
import { fs } from 'qiao-file';

// check
import { checkUrl } from './check-url.js';
import { checkDest } from './check-dest.js';

/**
 * download
 * @param {*} url
 * @param {*} dest
 * @returns
 */
export const download = async (url, dest) => {
  // check url
  if (!checkUrl(url)) return Promise.reject(new Error('url is not valid'));

  // check dest
  const newDest = await checkDest(dest);
  if (!newDest) return Promise.reject(new Error('dest is not valid'));

  // p and file
  const file = fs.createWriteStream(newDest);
  const protocol = url.startsWith('https') ? https : http;

  //
  return new Promise((resolve, reject) => {
    protocol
      .get(url, (res) => {
        // pipe
        res.pipe(file);

        // file
        file.on('finish', () => {
          file.close();

          resolve(newDest);
        });
        file.on('error', (err) => {
          fs.unlink(newDest);
          reject(err);
        });
      })
      .on('error', (err) => {
        fs.unlink(newDest);
        reject(err);
      });
  });
};

【校验文件流异常】

上面的代码中文件流的写入异常监听,

放在了res.pipe后,

如果写入的是一个没权限的路径,

则会抛出一个未捕获的异常,

修改后如下:

return new Promise((resolve, reject) => {
  // file write stream
  const file = fs.createWriteStream(newDest);
  file.on('error', (err) => {
    reject(err);
  });
  file.on('finish', () => {
    file.close();
    resolve(newDest);
  });

  // get
  const protocol = url.startsWith('https') ? https : http;
  protocol
    .get(url, (res) => {
      // pipe
      res.pipe(file);
    })
    .on('error', (err) => {
      fs.unlink(newDest);
      reject(err);
    });
});

【校验请求异常】

上面代码中么有校验请求的异常,

例如请求响应code非200,

如果code非200,但是不会走到error的监听,

只会下载一个错误的文件,

修改代码如下:

// get
const protocol = url.startsWith('https') ? https : http;
protocol
  .get(url, (res) => {
    // check res
    if (!res || res.statusCode !== 200) {
      return reject(new Error('response status code is not 200'));
    }

    // pipe
    res.pipe(file);
  })
  .on('error', async (err) => {
    await rm(newDest);
    reject(err);
  });

【设置timeout】

接着来设置下载的超时时间,

这里并不是直接在request中设置timeout就行,

nodejs的http模块中的timeout属性,

代表的是建立连接的超时时间,

例如设置timeout 100ms,

代表建立连接超过100ms,

而不是整个下载过程超过100ms,

这里需要自己实现一下,

代码如下:

// timeout
let timeoutId;
if (timeout) {
  timeoutId = setTimeout(() => {
    if (res) res.destroy();
    clearFile(newDest);
    reject(new Error('timeout'));
  }, timeout);
}

const file = fs.createWriteStream(newDest);
file.on('error', (err) => {
  clearTimeoutFn(timeoutId, timeout);
  reject(err);
});
file.on('finish', () => {
  file.close();

  clearTimeoutFn(timeoutId, timeout);
  resolve(newDest);
});

// pipe
res.pipe(file);

1. 首先如果传入timeout参数,就启动timeout

2. 如果提前完成,则清理timeout

3. 如果没有提前,清理文件,并返回异常

【校验文件完整性】

下载完文件后需要校验文件的完整性,

这里简单校验一下文件大小,

代码如下:

export const checkFileSize = (res, dest) => {
  // check
  if (!res || !dest) return;

  // res length
  const resLength = res.headers['content-length'];
  if (!resLength) return;

  // fs length
  try {
    const s = fs.statSync(dest);
    if (!s || !s.size) return;

    return parseInt(resLength, 10) === s.size;
  } catch (error) {
    debug(error);
  }
};

【文件下载进度】

下载文件的过程中,

实时的反馈下载进度也很重要,

这里可以监听res的data事件实现,

代码如下:

// data
let data = 0;

/**
 * on progress
 * @param {*} res
 * @param {*} onProgress
 * @returns
 */
export const onDownloadProgress = (res, onProgress) => {
  // check res
  if (!res) return;

  // check progress
  if (!onProgress) return;

  // check content
  const contentLength = res.headers['content-length'];
  if (!contentLength) return;

  // total
  const total = parseInt(contentLength, 10);

  // on data
  res.on('data', (chunk) => {
    data = data + chunk.length;

    const p = (data / total).toFixed(3);
    const n = parseFloat(p);
    onProgress(n);
  });
};

【qiao-downloader】

封装了一个npm包,欢迎使用,

https://www.npmjs.com/package/qiao-downloader

1.基础的下载文件能力

2.请求异常校验

3.文件流异常校验

4.定制timeout能力

5.下载完成后文件大小校验

6.支持下载进度回调

© 2026 Vincent. 保留所有权利。