В проекте у меня есть цикл, проходящий через список URL-адресов. Он загружает файл с каждого URL-адреса и выполняет какой-либо пост-процесс над загруженным файлом.
После всего процесса (как процесса загрузки, так и послепроцесса) я хочу выполнить функцию обратного вызова. Поскольку пост-процесс включает в себя некоторую поточную задачу, он имеет близкое событие. Если последний элемент можно идентифицировать, я могу передать функцию обратного вызова в событие закрытия. Однако, поскольку цикл является асинхронным, я не могу отслеживать, какой элемент выполняется последним.
На данный момент я использую 5-секундный тайм-аут, чтобы убедиться, что обратный вызов выполняется после всего процесса. Очевидно, что это не является устойчивым. Какой хороший способ справиться с этим?
код цикла:
exports.processArray = (items, process, callback) => {
var todo = items.concat();
setTimeout(function() {
process(todo.shift());
if(todo.length > 0) {
// execute download and post process each second
// however it doesn't guarantee one start after previous one done
setTimeout(arguments.callee, 1000);
} else {
setTimeout(() => {callback();}, 5000);
}
}, 1000);
};
processArray(
// First param, the array
urlList,
// Second param, download and post process
(url) => {
if(url.startsWith('http')) {
getDataReg(url, uid);
}
else if(url.startsWith('ftp')) {
getDataFtp(url, uid);
}
else {
console.log('not a valid resource');
}
},
// Third param, callback to be executed after all done
() => {
Request.get('${config.demouri}bound=${request.query.boundary};uid=${uid}', {
method: 'GET',
auth: auth
})
.on('response', (response) => {
console.log('response event emmits');
zipFiles(uid)
.then((path) => {
reply.file(path, { confine: false, filename: uid + '.zip', mode: 'inline'}).header('Content-Disposition');
});
});
}
);
Загрузите и опубликуйте процесс:
exports.getDataFtp = (url, uid) => {
console.log('get into ftp');
var usefulUrl = url.split('//')[1];
var spliter = usefulUrl.indexOf('/');
var host = usefulUrl.substring(0, spliter);
var dir = usefulUrl.substring(spliter+1, usefulUrl.length);
var client = new ftp();
var connection = {
host: host
};
var fileNameStart = dir.lastIndexOf('/') + 1;
var fileNameEnd = dir.length;
var fileName = dir.substring(fileNameStart, fileNameEnd);
console.log('filename: ', fileName);
client.on('ready', () => {
console.log('get into ftp ready');
client.get(dir, (err, stream) => {
if (err) {
console.log('get file err:', err);
return;
} else{
console.log('get into ftp get');
stream.pipe(fs.createWriteStream(datadir + 'download/${uid}/${fileName}'));
stream.on('end', () => {
console.log('get into ftp close');
unzipData(datadir + 'download/${uid}/', fileName, uid);
client.end();
});
}
});
});
client.connect(connection);
};
exports.getDataReg = (url, uid) => {
console.log('get into http');
var fileNameStart = url.lastIndexOf('/') + 1;
var fileNameEnd = url.length;
var fileName = url.substring(fileNameStart, fileNameEnd);
var file = fs.createWriteStream(datadir + 'download/${uid}/${fileName}');
if (url.startsWith('https')) {
https.get(url, (response) => {
console.log('start piping file');
response.pipe(file);
file.on('finish', () => {
console.log('get into http finish');
unzipData(datadir + 'download/${uid}/', fileName, uid);
});
}).on('error', (err) => { // Handle errors
fs.unlink(datadir + 'download/${uid}/${fileName}');
console.log('download file err: ', err);
});
} else {
http.get(url, (response) => {
console.log('start piping file');
response.pipe(file);
file.on('finish', () => {
unzipData(datadir + 'download/${uid}/', fileName, uid);
});
}).on('error', (err) => {
fs.unlink(datadir + 'download/${uid}/${fileName}');
console.log('download file err: ', err);
});
}
};
function unzipData(path, fileName, uid) {
console.log('get into unzip');
console.log('creating: ', path + fileName);
fs.createReadStream(path + fileName)
.pipe(unzip.Extract({path: path}))
.on('close', () => {
console.log('get into unzip close');
var filelist = listFile(path);
filelist.forEach((filePath) => {
if (!filePath.endsWith('.zip')) {
var components = filePath.split('/');
var component = components[components.length-1];
mv(filePath, datadir + 'processing/${uid}/${component}', (err) => {
if(err) {
console.log('move file err: ');
} else {
console.log('move file done');
}
});
}
});
fs.unlink(path + fileName, (err) => {});
});
}
То, что вы хотите сделать, - сделать все ваши асинхронные процессы сходящимися в одно обещание, которое вы можете использовать для выполнения обратного вызова в нужный момент.
Давайте начнем с точки завершения каждого процесса, который, как я полагаю, находится в unzipData()
функции mv()
в unzipData()
. Вы хотите обернуть каждое из этих асинхронных действий в Promise, которое решает в .forEach()
и вы также захотите использовать эти обещания позже, и для этого вы используете метод .map()
для сбора обещаний в массиве (вместо .forEach()
).
Здесь код:
var promises = filelist.map((filePath) => {
if (!filePath.endsWith('.zip')) {
var components = filePath.split('/');
var component = components[components.length-1];
return new Promise((resolve, reject) =>
mv(filePath, datadir + 'processing/${uid}/${component}', (err) => {
if(err) {
console.log('move file err: ');
reject(); // Or resolve() if you want to ignore the error and not cause it to prevent the callback from executing later
} else {
console.log('move file done');
resolve();
}
}));
}
return Promise.resolve();
});
(если асинхронное действие не должно выполняться, вместо него возвращается обещание, которое разрешается немедленно)
Теперь мы можем превратить этот список обещаний в одно обещание, которое решает, когда все обещания в списке разрешились:
var allPromise = Promise.all(promises);
Далее, нам нужно посмотреть дальше в коде. Мы можем видеть, что код, который мы только что рассматривали, сам по себе является частью обработчика событий асинхронного действия, то есть fs.createReadStream()
. Вам нужно обернуть это обещанием, которое разрешится, когда будут устранены внутренние обещания, и это обещание unzipData()
функцию unzipData()
:
function unzipData(path, fileName, uid) {
console.log('get into unzip');
console.log('creating: ', path + fileName);
return new Promise((outerResolve) =>
fs.createReadStream(path + fileName)
.pipe(unzip.Extract({path: path}))
.on('close', () => {
console.log('get into unzip close');
var filelist = listFile(path);
// Code from previous examples
allPromise.then(outerResolve);
}));
}
Затем мы рассмотрим функции, которые используют unzipData()
: getDataReg()
и getDataFtp()
. Они выполняют только одно асинхронное действие, поэтому все, что вам нужно сделать, это заставить их вернуть обещание, которое разрешается, когда unzipData()
обещать, возвращаемое unzipData()
.
Упрощенный пример:
exports.getDataReg = (url, uid) => {
return new Promise((resolve, reject) => {
// ...
https.get(url, (response) => {
response.pipe(file);
file.on('finish', () => {
unzipData(datadir + 'download/${uid}/', fileName, uid)
.then(resolve);
});
}).on('error', (err) => { // Handle errors
fs.unlink(datadir + 'download/${uid}/${fileName}');
reject(); // Or resolve() if you want to ignore the error and not cause it to prevent the callback from executing later
});
// ...
});
}
Наконец, мы processArray()
к функции processArray()
и здесь вам нужно сделать то же самое, что мы сделали для начала: сопоставить процессы в списке обещаний. Во-первых, переданная функция process
должна возвращать обещания, возвращаемые getDataReg()
и getDataFtp()
:
// Second param, download and post process
(url) => {
if(url.startsWith('http')) {
return getDataReg(url, uid);
}
else if(url.startsWith('ftp')) {
return getDataFtp(url, uid);
}
else {
console.log('not a valid resource');
}
return Promise.reject(); // or Promise.resolve() if you want invalid resources to be ignored and not prevent the callback from executing later
}
Теперь ваша processArray()
может выглядеть так:
exports.processArray = (items, process, callback) =>
Promise.all(items.map(process))
.then(callback)
.catch(() => console.log('Something went wrong somewhere'));
Обратный вызов будет вызван, когда все асинхронные действия будут завершены независимо от того, в каком порядке они выполняются. Если какое-либо из обещаний отклоняется, обратный вызов никогда не будет выполнен, поэтому соответствующим образом откажитесь от своих обещаний.
Здесь JSFiddle с полным кодом: https://jsfiddle.net/upn4yqsw/
После всего процесса (как процесса загрузки, так и послепроцесса) я хочу выполнить функцию обратного вызова.
Интересная вещь о серии асинхронных процессов заключается в том, что вы никогда не узнаете, когда будут завершены все процессы. Поэтому установка тайм-аута для обратного вызова - это быстрый и грязный способ сделать это, но он не является надежным.
Вместо этого вы можете использовать counter
для решения этой проблемы. Скажем, у вас есть 10 операций для выполнения. Вначале вы устанавливаете счетчик на counter = 10
После завершения каждого процесса, независимо от того, как (он может либо преуспеть или сработает), вы можете уменьшить счетчик на 1, как counter -= 1
а сразу после него вы можете проверить, счетчик равен 0, если это означает, что все процессы завершены, и мы достигли конца. Теперь вы можете безопасно запускать свою функцию обратного вызова, например if(counter === 0) callback();
Если бы я был вами, я бы сделал что-то вроде этого:
* Обратите внимание, что вызываемый процесс должен вернуть обещание, чтобы я мог знать, когда он закончит (опять же, независимо от того, как)
* Если вам нужна помощь в отношении обещаний, эта полезная статья может вам помочь: https://howtonode.org/promises
* О, и еще одна вещь, вам следует избегать использования arguments.callee
, потому что она устарела. Вот почему почему свойство arguments.callee.caller устарело в JavaScript?
exports.processArray = (items, process, callback) => {
var todo = [].concat(items);
var counter = todo.length;
runProcess();
function runProcess() {
// Check if the counter already reached 0
if(checkCounter() === false) {
// Nope. Counter is still > 0, which means we got work to do.
var processPromise = process(todo.shift());
processPromise
.then(function() {
// success
})
.catch(function() {
// failure
})
.finally(function() {
// The previous process is done.
// Now we can go with the next one.
--counter;
runProcess();
})
}
};
function checkCounter() {
if(counter === 0) {
callback();
return true;
} else {
return false;
}
}
};
В общем, поскольку nodejs
, похоже, не реализовал Streams Standard для Promise
, по крайней мере, из того, что может собраться; а использует механизм событий или обратного вызова, вы можете использовать конструктор Promise
в вызове функции, чтобы return
выполненный объект Promise
при отправке определенного события
const doStuff = (...args) => new Promise((resolve, reject)) => {
/* define and do stream stuff */
doStreamStuff.on(/* "close", "end" */, => {
// do stuff
resolve(/* value */)
})
});
doStuff(/* args */)
.then(data => {})
.catch(err => {})
Promise.all()
и Array.prototype.map()
вместо Array.prototype.forEach()
, см. Какой смысл обещаний в JavaScript?