NodeJS (Javascript) 避免异步混乱的设计模式
NodeJS (Javascript) Design Patterns to avoid async mess
我对使用 JS 的大型项目有点陌生,我觉得很难保持我的代码干净,几乎所有东西都是异步的。
我创建了很多 Promises 并将几乎每个函数都声明为 async 并在几乎每一行都使用 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???
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()
}
显然,您只需要在启动时希望执行的操作中使用冗余 await 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)
}
如果某些东西是异步的,您无论如何都必须使用 "then" async/wait 或回调来处理它。
现在您在 JavaScript 中有 "classes" 并不意味着您必须使用它们。
我不是 classes 和经典 OOP 的忠实粉丝。
我写的东西不同……人们不喜欢的东西,但无论如何这就是生活。
您写的 class 似乎没有任何状态我也看不到使用 class 的意义,但这是偏好问题。
看起来是一个服务 class.
不使用 classes 的好处是你不需要在所有东西前都加上难看的 "this" 前缀。您可以在仅包含函数的模块中编写上面的代码。
另外请记住,如果函数是异步的,您不需要 return 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);
})();
对于初始化异步资源的非常具体的情况,您可以使用多种设计模式。请注意,这些设计模式 不会真正帮助 异步代码的其他用例。
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。 On-demand初始化/隐藏初始化
如果您的初始化混乱或复杂并且您更喜欢更简洁的 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()
甚至 good-old .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 只是异步编程中的一种工具。并且本身就是一种设计模式。因此,将自己限制在 async/await 会限制您的软件设计。熟悉匿名函数,您会看到很多重构异步代码的机会。
奖金第二
在on-demand初始化模式的示例中,使用示例使用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()
的次数没有限制,我们实际上可以简化它并只缓存 promise(不知道为什么我没有想到它之前):
// method to get db connection:
db () {
if (this._dbPromise === null) {
this._dbPromise = connectToDB();
}
return this._dbPromise;
}
我对使用 JS 的大型项目有点陌生,我觉得很难保持我的代码干净,几乎所有东西都是异步的。
我创建了很多 Promises 并将几乎每个函数都声明为 async 并在几乎每一行都使用 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???
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()
}
显然,您只需要在启动时希望执行的操作中使用冗余 await 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)
}
如果某些东西是异步的,您无论如何都必须使用 "then" async/wait 或回调来处理它。
现在您在 JavaScript 中有 "classes" 并不意味着您必须使用它们。
我不是 classes 和经典 OOP 的忠实粉丝。
我写的东西不同……人们不喜欢的东西,但无论如何这就是生活。
您写的 class 似乎没有任何状态我也看不到使用 class 的意义,但这是偏好问题。
看起来是一个服务 class.
不使用 classes 的好处是你不需要在所有东西前都加上难看的 "this" 前缀。您可以在仅包含函数的模块中编写上面的代码。
另外请记住,如果函数是异步的,您不需要 return 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);
})();
对于初始化异步资源的非常具体的情况,您可以使用多种设计模式。请注意,这些设计模式 不会真正帮助 异步代码的其他用例。
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。 On-demand初始化/隐藏初始化
如果您的初始化混乱或复杂并且您更喜欢更简洁的 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()
甚至 good-old .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 只是异步编程中的一种工具。并且本身就是一种设计模式。因此,将自己限制在 async/await 会限制您的软件设计。熟悉匿名函数,您会看到很多重构异步代码的机会。
奖金第二
在on-demand初始化模式的示例中,使用示例使用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()
的次数没有限制,我们实际上可以简化它并只缓存 promise(不知道为什么我没有想到它之前):
// method to get db connection:
db () {
if (this._dbPromise === null) {
this._dbPromise = connectToDB();
}
return this._dbPromise;
}