避免异步混乱的NodeJS(Javascript)设计模式

时间:2019-06-27 01:31:19

标签: javascript node.js design-patterns async-await async.js

我在JS的大型项目中工作有点新,我觉得很难保持代码干净,几乎所有内容都是异步的。
我正在创建大量Promises,并将几乎每个函数都声明为异步函数,并且几乎在每一行上都使用await,我觉得这不是管理它的正确方法。
示例:

    var mysql = require('mysql');
module.exports = class MyClass {

    constructor() {

    }

    async init(){
        await this._initDbConnection();
    }

    _initDbConnection(){
        return new Promise(function(resolve, reject){
            this.db = mysql.createConnection({
                ...
            });
            this.db.connect(function(err) {
                ...    
            });    
        });
    }

    tableExists(tableName){
        return new Promise...            
    }

    createTable(tableName){
        return new Promise...
    }
    async save(data){
        try{
            if( ! (await this.tableExists()) ){
                await this.createTable();
            }
            return new Promise(function(resolve, reject){
                this.db.query(sql, function (err, result) {
                    ...                    
                });
            });
        }
        catch(e){

        }
    }

};

const myclass = new MyClass();
await myclass.init();
await myclass.save();
await 
await 
await !

每个查询或执行异步的任何查询都相同。
这是一个非常丑陋的解决方案。
我的意思是,如果我需要数据库中的某些东西,我想在第一行中连接到数据库,然后在第二行中执行查询,然后在第三行中处理结果。使用JS来做到这一点,我需要创建很多回调或在每行上使用await?

3 个答案:

答案 0 :(得分:1)

db.js

const options = require('../options')
var mysql = require('mysql');

class DataBase {
  constructor(options){
    this.options = options
    this.db = mysql.createConnection(this.options)
  }

  connect(){
    if(this.db.connected){
      return Promise.resolve(this.db)
    }
    return new Promise(function(resolve, reject){
      this.db.connect(function(err) {
        if (err) {
          reject(err);
        } else {
          console.log("Connected to MySQL!");
          resolve(this.db);
        }
      });  
    })
  }
}

module.exports = new Database(options)

index.js

const db = require('./db')

db.connect()

anywhere.js

 const db = require('../db')

 async function(){
   await db.connect()
   db.db.doWhatever()
 }

很明显,您只需要在启动时要执行的操作中等待db.connect()的冗余,例如,在路由中,例如,您已经知道它是从启动连接的:

routes.js

const db = require('../db').db

app.get('/posts', async(req, res) => {
  const posts = await db.query('select * from posts')
  res.send(posts)
}

答案 1 :(得分:1)

如果某些内容是异步的,则无论如何都必须使用“然后”异步/等待或回调来处理。 现在,您在JavaScript中拥有“类”这一事实并不意味着您必须使用它们。 我不是班级和古典OOP的忠实粉丝。
我写的东西不一样...有些人皱眉,但无论如何就是生活。 您编写的类似乎没有任何状态,我也看不到使用类的意义,但这与首选项有关。
看起来像是Service类。
不使用类的好处是,您无需在所有内容前加上丑陋的“ this”狗屎。您可以在仅具有功能的模块中编写以上代码。

请记住,如果函数是异步的,则不需要显式返回Promise

const { log, error } = console;

async function promiseMe(shouldIthrow) {
  if (!shouldIthrow) {
    return 'I Promise you'; //See? no Promise, it will be wrapped in a promise for you
  } else throw Error('I promise an Error')
}

// somewhere else
(async function run() {
  try {
    const result = await promiseMe(false)
    log('Look mum, a promise', result);

  } catch (r) {

  }

})();
// Or "then"
promiseMe(false).then(value => log('Look mum, a promise'));
promiseMe(true).then(_ => { }).catch(e => error('Oh men!'));

现在,这就是我要编写的代码的方式(它实际上是有效的代码,尽管没用)

const db = {
  query: function (sql, callback) {
    //sanitze your sql
    callback && callback({ result: 'database deleted' });
  },
  initConnection: async function () {
    !dbStarted && (dbStarted = true) && (log('DB Started'));
    return db; 
  }
}

function Completer() {
  let resolve, reject;
  const promise = new Promise((res, rej) => {
    resolve = res;
    reject = rej;
  });
  return { resolve, reject, promise };
}

//Higher order function to decorate anything that uses a db
// to ensure there's a db connection 
function withDb(decorated) {
  return async function decorator() {
    await  db.initConnection();
    decorated() 
  }
}
const tableExists = withDb(async function tableExists() {
  log('tableExists');
  return false ///whatever code you need here
});

async function createTable() {
  log('createTable');
  return false ///whatever code you need here
}

function saveHandler(completer){
  return function (data) {
      data.result && completer.resolve(data.result);
      data.error && completer.reject(data.result);
    }
}

async function save(data) {
  try {
    (!await tableExists()) && await createTable();

    const completer = Completer();
    db.query('DROP DATABASE databasename;', saveHandler(completer)); 

    return completer.promise;
  }
  catch (e) {
    //Nah no errors
  }
}

save('blah blah').then(result => { log('[Saved?] oh no:', result) });

// or
(async function run() {
  const result = await save('blah blah');
  log('[Saved?] oh no:', result);
})();

答案 2 :(得分:1)

对于初始化异步资源的非常特殊的情况,您可以使用几种设计模式。请注意,这些设计模式对异步代码的其他用例将无济于事

1。初始化功能

正如您在自己的代码中所演示的那样,这是一种实现方法。基本上,您有一个异步方法来初始化资源。这类似于jQuery的.ready()函数。有几种编写init函数的方法。最简单的方法可能是接受回调,让您继续执行逻辑:

class Foo {
    init (callback) {
        connectToDB().then(db => {
            this.db = db;
            callback(this);
        });
    }
}

用法:

let foo = new Foo();
foo.init(async function(){
    await foo.save();
});

2。生成器模式

这种设计模式在Java世界中更为常见,而在javascript中却很少见。当对象需要复杂的初始化时,将使用构建器模式。需要异步资源正是一种非常适合于构建器模式的复杂性:

class Foo {
    constructor (db) {
        if (typeof db === 'undefined') {
            throw new Error('Cannot be called directly');
        }
        this.db = db;
    }

    static async build () {
        let db = await connectToDB();
        return new Foo(db);
    }
}

用法:

Foo.build().then(foo => {
    foo.save();
});

3。按需初始化/隐藏init

如果您的初始化混乱或复杂,并且您希望使用更简洁的API,则此设计模式非常有用。想法是缓存资源并仅在尚未初始化时对其进行初始化:

class Foo {
    constructor () {
        this.db = null;
    }

    db () {
        if (this._dbConnection !== null) {
            return Promise.resolve(this._dbConnection);
        }
        else {
            return connectToDB().then(db => {
                this._dbConnection = db;
                return db;
            })
        }
    }

    async save (data) {
        let db = await this.db();
        return db.saveData(data);
    }

}

用法:

async function () {
    let foo = new Foo();
    await foo.save(something);  // no init!!
    await foo.save(somethingElse);
}

奖金

如果回顾一下init函数示例,您会发现回调看起来像是一种控制结构-类似于while()if()。这是匿名函数的杀手级功能之一-创建控制结构的能力。在标准javascript中,有很好的例子,例如.map().forEach()甚至是陈旧的.sort()

您可以自由创建异步控制结构(Coalan / async和async-q库就是很好的例子)。代替:

if( ! (await this.tableExists()) ) { ...

您可以将其编写为:

this.ifTableNotExist(()=>{
    return this.createTable();
})
.then(()=>{ ...

可能的实现:

  ifTableNotExist (callback) {
      return new Promise((ok,err) => {
          someAsyncFunction((table) => {
              if (!table) ok(callback());
          });
      });
  }

async / await只是异步编程中的一种工具。并且本身就是一种设计模式。因此,将自己限制为异步/等待会限制软件设计。熟悉匿名函数,您将看到很多重构异步代码的机会。

第2次奖励

在按需初始化模式的示例中,使用示例通过使用await依次保存两个数据。这是因为如果我们不等待它完成,代码将初始化数据库连接两次。

但是,如果我们想加速代码并并行执行两个保存,该怎么办?如果我们想这样做怎么办:

// Parallel:
await Promise.all([
    foo.save(something),
    foo.save(somethingElse)
]);

我们可以做的是,我们可以使用.db()方法检查是否有未完成的承诺:

// method to get db connection:
db () {
    if (this._dbConnection !== null) {
        return Promise.resolve(this._dbConnection);
    }
    else {
        if (this._dbPromise === null) {
            this._dbPromise = connectToDB().then(db => {
                this._dbConnection = db;
                return db;
            })
        }
        return this._dbPromise;
    }
}

实际上,由于对Promise可以调用.then()的次数没有限制,因此我们实际上可以简化该过程并仅缓存诺言(不知道为什么我以前没有想到过):

// method to get db connection:
db () {
    if (this._dbPromise === null) {
        this._dbPromise = connectToDB();
    }
    return this._dbPromise;
}