Как разравнять Пирамиду смерти
Итак вы открыли ноду и увидели, что почти все функции «из коробки» последним аргументом принимают колбэк.
var fs = require("fs");
fs.readdir(__dirname, function(error, files) {
if (error) {
console.error(error);
} else {
for (var i = 0, j = files.length; i < j; i++) {
console.log(files[i]);
}
}
});
Пирамида смерти
А в чем собственно проблема? Проблема в том, что на маке с ретиной порой заканчивается место под пробелы (конечно можно сказать, что 4 пробела на таб — роскошь) и весь код маячит далеко справа при использовании хотя бы десятка таких функций подряд.
var fs = require("fs");
var path = require("path");
var buffers = [];
fs.readdir(__dirname, function(error1, files) {
if (error1) {
console.error(error1);
} else {
for (var i = 0, j = files.length; i < j; i++) {
var file = path.join(__dirname, files[i]);
fs.stat(file, function(error2, stats) {
if (error2) {
console.error(error2);
} else if (stats.isFile()) {
fs.readFile(file, function(error3, buffer) {
if (error3) {
console.error(error3);
} else {
buffers.push(buffer);
}
});
}
});
}
}
});
console.log(buffers);
Так что же c этим можно сделать? Не применяя библиотек, для наглядности, так как с ними все примеры не займут и строчки кода, дальше будет показано как с этим справиться используя сахар es6 и es7.
Promise
Встроенный объект позволяющий немного разровнять пирамиду:
var fs = require("fs");
var path = require("path");
function promisify(func, args) {
return new Promise(function(resolve, reject) {
func.apply(null, [].concat(args, function(error, result) {
if (error) {
reject(error);
} else {
resolve(result);
}
}));
});
}
promisify(fs.readdir, [__dirname])
.then(function(items) {
return Promise.all(items.map(function(item) {
var file = path.join(__dirname, item);
return promisify(fs.stat, [file])
.then(function(stat) {
if (stat.isFile()) {
return promisify(fs.readFile, [file]);
} else {
throw new Error("Not a file!");
}
})
.catch(function(error) {
console.error(error);
});
}));
})
.then(function(buffers) {
return buffers.filter(function(buffer) {
return buffer;
});
})
.then(function(buffers) {
console.log(buffers);
})
.catch(function(error) {
console.error(error);
});
Кода стало немного больше, но зато сильно сократилась обработка ошибок.
Обратите внимание .catch был использован два раза потому, что Promise.all использует fail-fast стратегию и бросает ошибку, если ее бросил хотя бы один промис на практике такое пременение далеко не всегда оправдано, например если нужно проверить список проксей, то нужно проверить все, а не обламываться на первой «дохлой». Этот вопрос решают библиотеки Q и Bluebird и тд, поэтому его освещать не будем.
Теперь перепишем это все с учетом arrow functions, desctructive assignment и modules.
import fs from "fs";
import path from "path";
function promisify(func, args) {
return new Promise((resolve, reject) => {
func.apply(null, [...args, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
}]);
});
}
promisify(fs.readdir, [__dirname])
.then(items => Promise.all(items.map(item => {
const file = path.join(__dirname, item);
return promisify(fs.stat, [file])
.then(stat => {
if (stat.isFile()) {
return promisify(fs.readFile, [file]);
} else {
throw new Error("Not a file!");
}
})
.catch(console.error);
})))
.then(buffers => buffers.filter(e => e))
.then(console.log)
.catch(console.error);
Generator
Теперь совсем хорошо, но…ведь есть еще какие-то генераторы, которые добавляют новый тип функций function* и ключевое слово yeild, что будет если использовать их?
import fs from "fs";
import path from "path";
function promisify(func, args) {
return new Promise((resolve, reject) => {
func.apply(null, [...args, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
}]);
});
}
function getItems() {
return promisify(fs.readdir, [__dirname]);
}
function checkItems(items) {
return Promise.all(items.map(file => promisify(fs.stat, [path.join(__dirname, file)])
.then(stat => {
if (stat.isFile()) {
return file;
} else {
throw new Error("Not a file!");
}
})
.catch(console.error)))
.then(files => {
return files.filter(file => file);
});
}
function readFiles(files) {
return Promise.all(files.map(file => {
return promisify(fs.readFile, [file]);
}));
}
function * main() {
return yield readFiles(yield checkItems(yield getItems()));
}
const generator = main();
generator.next().value.then(items => {
return generator.next(items).value.then(files => {
return generator.next(files).value.then(buffers => {
console.log(buffers);
});
});
});
Цепочки из generator.next ().value.then не лучше чем колбэки из первого примера однако это не значит, что генераторы плохие, они просто слабо подходят под эту задачу.
Async/Await
Еще два ключевых слова, с мутным значением, которые можно попробовать прилепить к решению, уже надоевшей задачи по чтению файлов- Async/Await
import fs from "fs";
import path from "path";
function promisify(func, args) {
return new Promise((resolve, reject) => {
func.apply(null, [...args, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
}]);
});
}
function getItems() {
return promisify(fs.readdir, [__dirname]);
}
function checkItems(items) {
return Promise.all(items.map(file => promisify(fs.stat, [path.join(__dirname, file)])
.then(stat => {
if (stat.isFile()) {
return file;
} else {
throw new Error("Not a file!");
}
})
.catch(console.error)))
.then(files => {
return files.filter(file => file);
});
}
function readFiles(files) {
return Promise.all(files.map(file => {
return promisify(fs.readFile, [file]);
}));
}
async function main() {
return await readFiles(await checkItems(await getItems()));
}
main()
.then(console.log)
.catch(console.error);
Пожалуй самый красивый пример, все функции заняты своим делом и нету никаких пирамид.
Если писать этот код не для примера, то получилось бы как-то так:
import bluebird from "bluebird";
import fs from "fs";
import path from "path";
const myFs = bluebird.promisifyAll(fs);
function getItems(dirname) {
return myFs.readdirAsync(dirname)
.then(items => items.map(item => path.join(dirname, item)));
}
function getFulfilledValues(results) {
return results
.filter(result => result.isFulfilled())
.map(result => result.value());
}
function checkItems(items) {
return bluebird.settle(items.map(item => myFs.statAsync(item)
.then(stat => {
if (stat.isFile()) {
return [item];
} else if (stat.isDirectory()) {
return getItems(item);
}
})))
.then(getFulfilledValues)
.then(result => [].concat(...result));
}
function readFiles(files) {
return bluebird.settle(files.map(file => myFs.readFileAsync(file)))
.then(getFulfilledValues);
}
async function main(dirname) {
return await readFiles(await checkItems(await getItems(dirname)));
}
main(__dirname)
.then(console.log)
.catch(console.error);
Комментарии (4)
30 июня 2016 в 14:18
0↑
↓
У меня велосипед получился точно такой же. С точностью до названий функций. А переименую-ка я их как у Вас…
30 июня 2016 в 14:18
–1↑
↓
Кто нибудь скажет мне чем promise лучше «классического ajax»?30 июня 2016 в 14:28
+1↑
↓
Тем же, чем мягкое лучше теплого.На самом деле, профит от промизов в том, что это асинхронный компонуемый примитив. Они легко выстраиваются параллельно или последовательно или в любой комбинации, да еще обработка ошибок довольно удобная.
Справедливости ради, если не лепить анонимные функции, а грамотно все декомпозировать и разносить по модулям, то и без промисов все выглялит вполне прилично. Но промизы действительно легко компонуются.
30 июня 2016 в 14:25 (комментарий был изменён)
+1↑
↓
это не из разряда лучше, а скорее вместе