简体   繁体   中英

Using native ES6 Promises to de-Nodeify / compact standard callbacks

The Q library ( https://github.com/kriskowal/q ) provides very helpful adaptors for functions that follow Node's standard callback layout, ie last argument is function(err, result) .

return Q.nfcall(FS.readFile, "foo.txt", "utf-8");
return Q.nfapply(FS.readFile, ["foo.txt", "utf-8"]);

That's discussed further in the "Adapting Node" section of the README.

When using native ES6 Promises to accomplish the same, one often ends up with this unwieldy kind of trainwreck:

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

const server = http.createServer((req, res) => {
    new Promise((resolve, reject) => { 
        fs.readFile('/etc/motd', (err, data) => {
            if(err) { 
                reject(err.toString());
                return;
            }

            resolve(data);
        });
    }).then(data => {
        res.writeHead(200);
        res.end(data);
    }).catch(e => {
        res.writeHead(500);
        res.end(e);
    });
}).listen(8000);

While this does flatten worst-case callback hell, it's still visually cluttered and hard to follow.

Obviously, one could decompose this into functions and inline less code to make it more readable, but that solution works fairly well for rectifying the very callback hell that promises are supposed to help solve in the first place. :-)

Is there anything I'm missing about the standard ES2015/6 Promise feature set that could allow one to save some mess here? Failing that, suggestions for low-calorie polyfills would be appreciated.

Browsers and Node.js 7 and lower

Most popular callback-based packages have their promisified counterparts, eg fs-extra and mz/fs for fs .

pify is widely known solution in for promisification that uses native Promise by default. Other promise-related packages from this maintainer can be helpful as well, for example p-event to promisify one-time event listeners.

http example involves a callback that is triggered multiple times, something that cannot be replaced with a promise-based function. But it's obviously possible to promisify callback-based things beforehand like fs (as shown in pify-fs package ):

const pify = require('pify');
const fs = pify(require('fs'), {
  exclude: [/^exists/, /.+(Stream|Sync)$/, /watch/],
  excludeMain: true
});
...
http.createServer((req, res) => {
    let code;
    let body;

    fs.readFile('/etc/motd')
    .then(
      data => {
        body = data;
        code = 200;
      },
      err => {
        body = String(err);
        code = 500;
      }
    )
    .then(() => {
      res.writeHead(code);
      res.end(body);
    });
})

Without third-party promisification solution the developer is forced to reinvent the wheel, this involves promise construction with new Promise , like shown in original example.

It should be noticed that Bluebird is a popular alternative to ES6 promises particularly because it provides demanded features out of the box, including promisification .

Node.js 8

As of 8.0.0, Node has built-in util.promisify to promisify Node-style callbacks. A recipe for batch promisification of a plain object like fs is

const util = require('util');
const fs = Object.assign({}, require('fs'),
    Object.entries(require('fs'))
    .filter(([, val]) => typeof val === 'function')
    .filter(([key]) => !/^[A-Z_]|^exists|.+(Stream|Sync)$|watch/.test(key))
    .reduce((fs, [key, val]) => Object.assign(fs, { [key]: util.promisify(val) }), {})
);

I guess one can always invent one's own ...

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

function promiseAdapt(func, ...args) {
    return new Promise((resolve, reject) => {
        func.apply(this, args.concat((err, res) => {
            if(err) {
                reject(err.toString());
                return;
            }

            resolve(res || undefined);
        }));
    });
}

const server = http.createServer((req, res) => {
    let data = undefined;

    promiseAdapt(fs.readFile, '/etc/motd', { 
        encoding: 'utf8' 
    }).then(d => {
        data = d;

        let str = `${req.method} ${req.url} (${req.headers['user-agent'] || '?'}) from ${req.connection.remoteAddress}` + "\n";

        return promiseAdapt(fs.writeFile, 'access.log', str, {
            encoding: 'utf8',
            flag: 'a',
            mode: '0755'
        });
    }).then(() => {
        res.writeHead(200);
        res.end(data);
     }).catch(e => {
        res.writeHead(500);
        res.end(e);
    });
}).listen(8000);

You can avoid promises altogether and just execute your code synchronously via nsynjs . Your code will transform as follows:

Step 1. Wrap slow functions with callbacks into nsynjs-aware wrappers:

// wrappers.js

var fs=require('fs');
exports.readFile = function (ctx,name) {
    console.log("reading config");
    var res={};
    fs.readFile( name, "utf8", function( error , data){
        if( error ) res.error = error;
        res.data = data;
        ctx.resume(error);
    } );
    return res;
};
exports.readFile.nsynjsHasCallback = true;

Step 2: Write your logic as if it was synchronous, and put it into function:

const synchronousCode = function(req,res,wrappers) {
    try {
        var data = wrappers.readFile(nsynjsCtx,'/etc/motd').data;
        res.writeHead(200);
        res.end(data);
    }
    catch(e) {
        res.writeHead(500);
        res.end(e);
    };
}

Step 3. Execute that function via nsynjs:

// index.js
const fs = require('fs');
const http = require('http');
const nsynjs = require('nsynjs');
const wrappers = require('./wrappers');

const synchronousCode = function(req,res,wrappers) {
    ...
};

const server = http.createServer(function(req, res){
    nsynjs.run(synchronousCode,{},req,res,wrappers,function(){
        console.log('synchronousCode is done');
    })
}).listen(8000);

Please see similar example here https://github.com/amaksr/nsynjs/tree/master/examples/node-module-loading

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM