Як розрівняти Піраміду смерті

Налаштувати webpack по мануали, запрограмувати ангуляр і навіть послати json з ajax — кажись кожен може, але як глянеш на сам код… В цьому пості буде показана різниця між нововведеннями.

Отже ви відкрили ноду і побачили, що майже всі функції «з коробки» останнім аргументом приймають колбек.

var fs = require("fs");
fs.readdir(__dirname, function(error, files) {
if (error) {
console.помилка(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);


Так що ж з цим можна зробити? Не застосовуючи бібліотек, для наочності, так як з ними всі приклади не займуть і строчки коду, далі буде показано, як з цим впоратися використовуючи цукор 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);
});
}));
})
.then(function(buffers) {
return buffers.filter(function(buffer) {
return buffer;
});
})
.then(function(buffers) {
console.log(buffers);
})
.catch(function(error) {
console.помилка(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);

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.