前言
先說一下上一篇NodeJS文章
NodeJS 後端開發 06 連接配接MySQL
,這一篇展示了一個資料庫連結的db.js 工具庫。
該工具庫提供了一個runSql函數,它的運作方式是先送出SQL,然後再異步把查詢結果回調傳遞給callback函數的。
這個函數隻适用于提前加載資料的情況,比如預先緩存批量的查庫結果。然後使用者請求WebAPI的時候,直接讀查詢緩存。
這樣會有下面的問題:
新資料入庫了,本地緩存需要更新。
而且無法支援動态查詢。
說這麼多,直接用Koa做個接口示範一下查詢效果吧。
代碼展示,KOA做接口查詢資料庫
const koa = require('Koa');
const app = new koa()
var router = require('koa-router')();
var db = require('./db-lite');
//做個簡單接口
router.get('/', function(ctx, next){ctx.body='Hello, geeklevin!'});
//做個接口查詢資料庫
router.get('/query', function(ctx,next){
db.runSql("SELECT * FROM COMPANY; ", function(result,fields){
console.log('查詢結果:', result);
ctx.body = JSON.stringify(result);
console.log('設定響應body成功!');
});
console.log('processing');
});
//啟動伺服器
app.use(router.routes());
const PORT = 8081
console.log('start app at port %s', PORT)
app.listen(PORT);
先檢視簡單接口展示效果:
再看看/query接口
日志看到設定了響應體ctx.body成功了,可這時候請求已經結束了。
因為db.runSql函數不是同步執行了,/query這個接口執行這個函數,碰到函數結束了,目前請求處理完畢,沒有設定請求體,是以響應Not Found。
看看Promise吧
var data = new Promise(function(resolve, reject){
asynFunction(function(error, result){
if(error){
reject(error);
}else{
resolve(result);
}
});
});
async getData(){
var result = await data();
console.log('result is:', result);
}
就兩段代碼,第一段是構造Promise對象;第二段為調用Promise對象處理異步請求。
先說Promise。data是一個Promise對象,它内部調用了一個異步函數asynFunction(這個函數可以是查庫,請求其他網站接口,消息隊列擷取資料等),但是它能夠保證兩個結果:
處理異步請求成功的時候調用resolve函數,傳遞result對象;
出錯的時候調用reject函數,傳遞error對象。
就這麼簡單。
第二段代碼就是一個聲明了異步響應的函數,雖然内部調用的Promise對象,又套娃調用了異步函數,但是await文法糖,能夠保證result變量指派這一行同步執行,也就是getData會等待result指派這一行異步調用執行完畢,再執行console.log。
這裡稍微有點繞了,請讀者停下來,好好思考整理一下。
簡單來說就是,在async聲明的異步函數中,同步等待Promise内部執行結果,就是,同步等待異步請求。
這樣,異步轉同步,很神奇吧!
像查詢資料庫這樣的異步操作就能夠在API中被等待,并響應給使用者了,下面來代碼了。
好,用koa接口得怎麼做呢?需要在資料庫調用端怎麼調整呢?
下面是runSql的簡化實作代碼,我們要改造它
const runSql = function(sql, callback){
console.log('will run sql:', sql);
//這裡是異步調用的,但是不是Promise實作,沒有辦法使用asyn,await來實作同步等待異步請求。
db.query(sql, function(error, result, fields){
try{
if(error) throw error;
console.log('query result:', result);
}finally{
if(callback) callback(result, fields);
}
});
}
先看koa怎麼搞?
這個好辦, 下面是一個簡單的router(路由web API)。
我們看到在router.get 第二個參數加了async關鍵字修飾的方法。
根據ES6,我們知道async函數内可以使用await強制等待異步Promise函數的相應結果。
很明顯,上面的runSql函數傳回不是一個Promise對象的,是以這裡先調用一個未實作的函數:prunSql。
router.get('/asynQuery', async function(ctx, next){
//參考上面Promise使用,聲明同步等待一個傳回Promise對象的函數。
var data = await prunSql('SELECT * FROM COMPANY;');
ctx.body = JSON.stringify(data)
});
這是一個新的接口, 跟其他接口不一樣的就是,它處理請求的方法是用async聲明的異步函數。
我們需要改造runSql函數
const prunSql = function(sql){
console.log('will run sql:', sql);
return new Promise(function(resolve, reject){
console.log('asyn run sql: ', sql);
db.query(sql, function(error, result, fields){
if(error){
console.log('[%s] asyn error:', error);
reject(error);
}else{
console.log('asyn result:', result);
resolve(result);
}
});
});
};
這就是改造好的prunSql,這裡用了resolve函數把資料庫查詢結果傳遞出去。
這樣在使用Promise的then可以擷取到異步查詢結果。
或者在一個異步響應的方法内await Promise對象,等待查詢傳回值。
看看使用Promise處理後的效果:
問題至此,迎刃而解!
具體用什麼架構做接口不重要,了解整個思想才是重點。
貼一下代碼, 下面的資料庫查詢庫(db-lite.js),讀者可自行更換成其他異步查詢的庫,進行改造,不必過多糾結一定要使用本文的runSql函數。
const koa = require('Koa');
const app = new koa()
var router = require('koa-router')();
var db = require('./db-lite');
router.get('/', function(ctx, next){ctx.body='Hello, geeklevin!'});
router.get('/query', function(ctx,next){
db.runSql("SELECT * FROM COMPANY; ", function(result,fields){
console.log('查詢結果:', result);
ctx.body = JSON.stringify(result);
console.log('設定響應body成功!');
});
console.log('processing');
});
router.get('/asynQuery', async function(ctx, next){
var data = await db.prunSql('SELECT * FROM COMPANY;');
ctx.body = JSON.stringify(data)
});
app.use(router.routes());
const PORT = 8081
console.log('start app at port %s', PORT)
app.listen(PORT);
db-lite.js 代碼:
const mysql = require('mysql');
const db = mysql.createConnection({
host: "localhost",
port: 3306,
user: "root",
password: "12345678"
});
const logDbStat = function(){
console.log("db state %s and threadID %s", db.state, db.threadId);
// console.log("db detail:", db);
}
logDbStat();
console.log('start to connect mysql');
db.connect(function(err){
if(err){
console.log('fail to connect db',err.stack);
throw err;
}
logDbStat();
});
const close = function(){
db.destroy();
console.log('db disconnected');
logDbStat();
};
var counter = 0;
const runSql = function(sql, callback){
counter++;
var cv = counter;
console.log('run sql[%s] :[%s]',cv, sql);
db.query(sql, function(error, result, fields){
try{
if(error) throw error;
}finally{
if(callback) callback(result, fields);
}
});
}
const prunSql = function(sql){
console.log('will sql:', sql);
return new Promise(function(resolve, reject){
counter++;
var cv = counter;
console.log('asyn run sql[%s] :[%s]', cv, sql);
db.query(sql, function(error, result, fields){
if(error){
console.log('[%s] asyn error:', cv, error);
reject(error);
}else{
// console.log('[%s] asyn result:', cv, result);
resolve(result);
}
});
});
};
const myDb = 'demo20210330';
runSql("USE " + myDb);
module.exports.runSql = runSql;
module.exports.prunSql = prunSql;
總結
Promise結合async和await結合能夠産生神奇的效果,也是很多新手過不去的坎,希望讀者都能夠熟練運用。
上面展示代碼,讀者可以自行思考,改造工作中使用的異步請求轉化為同步調用。
篇幅有限,當然這本質上還是異步執行的,希望讀者自行鞭策,思考了解。