Scrivere promesse eleganti in Node.js

-1

Sto attraversando un momento molto difficile scrivere un codice pulito con un semplice login / login manager utente. Sto cercando di evitare l'inferno di callback / callback e non vedo alcun vantaggio nell'usare il presunto async / wait più pulito.

Prendi ad esempio il mio codice di registrazione utente. Deve eseguire 3 promesse in sequenza

  1. Assicurati che l'utente sia unico nel database

  2. Hash la password

  3. Inserisci l'utente nel database

Voglio

  1. Registra eventuali errori in ogni singola chiamata promessa (es. errore db)

  2. registra il risultato finale nella funzione di livello più alto (ad esempio, registra con successo)

  3. Restituisce una risposta HTTP dal chiamante.

Sembra proprio ora

hashPassword(password){
    let self = this;
    return bcrypt.hash(password, 10)
        .then(function(hash) {
            return hash;
        })
        .catch(function(err){
            self.logger.error('failed to hash password');
            self.logger.error(err);
            return Promise.reject(err)
        });
}

insertUser(email, passwordHash){
    let self = this;
    let data = {
        email: email,
        password: passwordHash
    };
    return this.collection.insertOne(data)
        .then(function(res) {
            self.logger.info('inserted ${email} into db');
            return res;
        })
        .catch(function(err){
            self.logger.error('failed to add user to db');
            self.logger.error(err);
            return Promise.reject(err)
        });
}

findUser(email){
    let self = this;
    return this.collection.findOne({email:email})
        .then(function(element) {
            if(!element){
                self.logger.info('user ${email} not found');
            }
            return element;
        })
        .catch(function(err){
            self.logger.error('failed to query user ${email}');
            self.logger.error(err);
            return Promise.reject(err);
        });
}

registerUser(email, password){
    let self = this;
    return this.findUser(email)
        .then(function(element){
            if(element){
                self.logger.error('user ${email} already exists');
                // now what do i do here for the caller?
                // reject the promise? resolve with an err code?
                return Promise.reject()
            }
            // what if this one fails? how will the caller know..
            return self.hashPassword(password);
        })        
        .then(function(hash){
            return self.insertUser(email, hash);
        })
        .then(function(){
            self.logger.info('registered user ${email}');
        })
        .catch(function(err){
            self.logger.error('failed to register user');
            return Promise.reject(err)
        });
}

In realtà sono super constrongvole con le funzioni di base ( findUser , hashPassword , insertUser ). Ma il registerUser ha senso. Il fatto che tu riceva promesse entro una chiamata di then mi infastidisce davvero, per esempio, ma è proprio così che va. Il vero problema qui è che, se una delle tre chiamate nel processo fallisce, non sono sicuro di come dire al mio chiamante quale cosa è fallita.

Questo è il chiamante.

router.post('/register', function(req, res, next){
    if(req.body.email && req.body.password && req.body.confirm){
        userManager.registerUser(req.body.email, req.body.password)
            .then(function(result){
                res.sendStatus(200);
            })
            .catch(function(err){
                 res.sendStatus(400);
            });
    }
    else {
        res.sendStatus(400);
    }
});

Ora ho provato a fare tutto in async / await, ma questo richiede solo un sacco di tentativi di cattura del nocciolo che, a mio parere, sono ancora peggio. Sono solo perso qui cercando di mantenere questo pulito.

    
posta jozenbasin 20.10.2018 - 21:22
fonte

3 risposte

2

Penso che tu stia rifiutando (ha!) l'idea di async / attendi un po 'troppo rapidamente; è esattamente ciò di cui hai bisogno per il problema che stai affrontando, in termini di semplificazione delle cose.

Puoi anche creare un errore personalizzato (o più ...) per rendere le cose più pulite da vedere.

Ad esempio:

class WrappedError extends Error {
  constructor(previousError, ...params) {
    super(...params);
    if (Error.captureStackTrace) {
      Error.captureStackTrace(this, CustomError);
    }

    this.previousError = previousError;
  }
}

async function hashPassword(password){
   try {
     const hashed = await bcrypt.hash(password, 10);
     return hashed;
   catch (e) {
     throw new WrappedError(e, 'Could not hash the password');
   }
}

async function insertUser(email, passwordHash){
    let data = {
        email: email,
        password: passwordHash
    };
    try {
       const result = await this.collection.insertOne(data);
       return result;
    catch (e) {
       throw new WrappedError(e, 'Could not insert ${email} into DB');
    }
}

async function findUser(email) {
  try {
    const user = await this.collection.findOne({ email });
    return user;
  } catch (e) {
    throw new WrappedError(e, 'Could not find user in the DB');
  }
}

async function registerUser(email, password){
  try {
    let user = await this.findUser(email);
    if (user) {
       const e = new Error('User ${email} already exists!');
       throw e;
    } // user doesn't exist, proceed
    const hashed = await this.hashPassword(password);
    user = await insertUser(email, hashed);
    this.logger.info('inserted ${email} into db');

    return user;
  catch (e) {
    this.logger.error(e);
  }
}

Probabilmente, potresti saltare la maggior parte delle tue funzioni in questo modo e chiamare direttamente la logica interna, gestendo tutto nella funzione RegisterUser poiché la maggior parte del contenuto è in grado di correggere gli errori e la registrazione. In alternativa, è possibile creare ulteriori errori personalizzati per ogni caso di errore.

    
risposta data 07.12.2018 - 19:15
fonte
1

Puoi lanciare invece di restituire Promise.reject

Puoi sostituire le funzioni di callback con le frecce per ridurre il rumore ed evitare il trucco self=this

Potresti anche creare alcune funzioni di supporto per la registrazione.

Sarebbe quindi simile a questo:

const info = message => value => (this.logger.info(message), value);
const error = message => value => {
    this.logger.error(message);
    throw value;
};

hashPassword(password){
    return bcrypt.hash(password, 10)
        .catch(this.error('failed to hash password'))
}

insertUser(email, password){
    let data = { email, password };

    return this.collection.insertOne(data)
        .then(this.info('inserted ${email} into db'))
        .catch(this.error('failed to add user to db'));
}

findUser(email){
    return this.collection.findOne({email:email})
        .then(element => {
            if(!element){
                this.logger.info('user ${email} not found');
            }
            return element;
        })
        .catch(this.error('failed to query user ${email}'));
}

registerUser(email, password){
    return this.findUser(email)
        .then(element => {
            if(element){
                this.logger.error('user ${email} already exists');
                throw 'user ${email} already exists';
            }
            // if this one fails the last catch in this promise will catch it
            return this.hashPassword(password);
        })        
        .then(hash => this.insertUser(email, hash))
        .then(this.info('registered user ${email}'))
        .catch(this.error('failed to register user'));
}
    
risposta data 07.12.2018 - 17:28
fonte
0

Il ritorno delle promesse respinte mi sembra soddisfacente. Assicurati di rifiutarli con errori che possono essere compresi dal chiamante. Questo può includere codici di errore, oggetti specializzati contenenti dati dettagliati o semplicemente testo. Puoi anche annidare gli errori rifiutando con errori contenenti una proprietà cause che quindi contiene il motivo più profondo (ancora un altro oggetto errore).

Per trovare la struttura dell'errore che si adatta alla tua situazione, considera ciò che vorresti ricevere sul sito di chiamata.

    
risposta data 20.10.2018 - 21:59
fonte

Leggi altre domande sui tag