Использование Promises с fs.readFile в цикле

Я пытаюсь понять, почему настройки ниже обещания не работают.

(Примечание: я уже решил эту проблему с async.map. Но я хотел бы узнать, почему мои попытки ниже не работают.)

Правильное поведение должно быть: bFunc должен запускать столько времени, сколько необходимо, чтобы fs читал все файлы изображений (bFunc ниже запускается дважды), а затем консоль cFunc печатает "End".

Спасибо!

Попытка 1: выполняется и останавливается в cFunc().

var fs = require('fs');

bFunc(0)
.then(function(){ cFunc() }) //cFunc() doesn't run

function bFunc(i){
    return new Promise(function(resolve,reject){

        var imgPath = __dirname + "/image1" + i + ".png";

        fs.readFile(imgPath, function(err, imagebuffer){

            if (err) throw err;
            console.log(i)

            if (i<1) {
                i++;
                return bFunc(i);
            } else {
                resolve();
            };

        });

    })
}

function cFunc(){
    console.log("End");
}

Попытка 2: В этом случае я использовал цикл for, но он не работает. Консольные отпечатки: End, bFunc done, bFunc done

var fs = require('fs');

bFunc()
        .then(function(){ cFunc() })

function bFunc(){
    return new Promise(function(resolve,reject){

        function read(filepath) {
            fs.readFile(filepath, function(err, imagebuffer){
                if (err) throw err;
                console.log("bFunc done")
            });
        }

        for (var i=0; i<2; i++){
            var imgPath = __dirname + "/image1" + i + ".png";
            read(imgPath);
        };

        resolve()
    });
}


function cFunc(){
    console.log("End");
}

Спасибо за помощь заранее!

Ответы

Ответ 1

Поэтому, когда у вас есть несколько асинхронных операций для какой-либо координации, я сразу же хочу перейти к обещаниям. И лучший способ использовать обещания для координации ряда асинхронных операций - заставить каждую асинхронную операцию возвращать обещание. Асинхронная операция самого низкого уровня, которую вы показываете, - fs.readFile(). Поскольку я использую библиотеку обещаний Bluebird, в ней есть функция для "обещания" целого модуля асинхронных функций.

var Promise = require('bluebird');
var fs = Promise.promisifyAll(require('fs'));

Это создаст новые параллельные методы для объекта fs с суффиксом "Async", которые возвращают обещания вместо использования прямых обратных вызовов. Итак, будет fs.readFileAsync(), который возвращает обещание. Вы можете прочитать больше об обещании Bluebird здесь.

Итак, теперь вы можете создать функцию, которая довольно просто получает изображение и возвращает обещание, значением которого являются данные из изображения:

 function getImage(index) {
     var imgPath = __dirname + "/image1" + index + ".png";
     return fs.readFileAsync(imgPath);
 }

Затем в вашем коде создается впечатление, что вы хотите сделать bFunc() функцией, которая читает три из этих изображений и вызывает cFunc() после их завершения. Вы можете сделать это следующим образом:

var Promise = require('bluebird');
var fs = Promise.promisifyAll(require('fs'));

 function getImage(index) {
     var imgPath = __dirname + "/image1" + index + ".png";
     return fs.readFileAsync(imgPath);
 }

 function getAllImages() {
    var promises = [];
    // load all images in parallel
    for (var i = 0; i <= 2; i++) {
        promises.push(getImage(i));
    }
    // return promise that is resolved when all images are done loading
    return Promise.all(promises);
 }

 getAllImages().then(function(imageArray) {
    // you have an array of image data in imageArray
 }, function(err) {
    // an error occurred
 });

Если вы не хотите использовать Bluebird, вы можете вручную сделать обещанную версию fs.readFile() следующим образом:

// make promise version of fs.readFile()
fs.readFileAsync = function(filename) {
    return new Promise(function(resolve, reject) {
        fs.readFile(filename, function(err, data){
            if (err) 
                reject(err); 
            else 
                resolve(data);
        });
    });
};

Или в современных версиях node.js вы можете использовать util.promisify(), чтобы создать обещанную версию функции, которая следует соглашению об асинхронных вызовах node.js:

const util = require('util');
fs.readFileAsync = util.promisify(fs.readFile);

Тем не менее, вы быстро обнаружите, что как только вы начнете использовать обещания, вы захотите использовать их для всех асинхронных операций, так что вы будете "обещать" много вещей и иметь библиотеку или, по крайней мере, универсальную функцию, которая будет делать это. потому что вы сэкономите много времени.


В еще более новых версиях node.js (версия 10. 0+) вы можете использовать встроенную версию библиотеки fs, которая поддерживает обещания:

const fsp = require('fs').promises;

fsp.readFile("someFile").then(data => {
    console.log(data);
});

Ответ 2

Ваш код должен выглядеть примерно так:

// promisify fs.readFile()
fs.readFileAsync = function (filename) {
    return new Promise((resolve, reject) => {
        fs.readFile(filename, (err, buffer) => {
            if (err) reject(err); else resolve(buffer);
        });
    });
};

const IMG_PATH = "foo";

// utility function
function getImageByIdAsync(i) {
    return fs.readFileAsync(IMG_PATH + "/image1" + i + ".png");
}

Использование с одним изображением:

getImageByIdAsync(0).then(imgBuffer => {
    console.log(imgBuffer);
}).catch(err => {
    console.error(err);
});

Использование с несколькими изображениями:

var images = [1,2,3,4].map(getImageByIdAsync);

Promise.all(images).then(imgBuffers => {
    // all images have loaded
}).catch(err => {
    console.error(err);
});

Обещать функцию - значит взять асинхронную функцию с семантикой обратного вызова и извлечь из нее новую функцию с семантикой обещания.

Это можно сделать вручную, как показано выше, или - предпочтительно - автоматически. Среди прочего, библиотека обещаний Bluebird имеет помощника для этого, см. http://bluebirdjs.com/docs/api/promisification.html

Ответ 3

Узел v10 имеет API fs Promises

const fsPromises = require('fs').promises

const func = async filenames => {

  for(let fn of filenames) {
    let data = await fsPromises.readFile(fn)
  }

}

func(['file1','file2'])
  .then(res => console.log('all read', res))
  .catch(console.log)

https://nodejs.org/api/fs.html#fs_fs_promises_api

Или, если вы хотите прочитать больше файлов одновременно:

const func = filenames => {
  return Promise.all(
    filenames.map(f => fsPromises.readFile(f))
  )
}

func(['./a','./b'])
  .then(res => console.log('all read', res))
  .catch(console.log)

Ответ 4

Вы также можете использовать этот модуль: 'fs-readfile-обещание'

var readFile = require('fs-readfile-promise');
readFile(__dirname + '/file1.txt','utf-8').then(function (data){
    console.log("file name:", data)
    return readFile(__dirname +'/'+data, 'utf-8')
}).then(function (data1){
    console.log('Content data:', data1)
}).catch( function (err){
    console.log(err)
})