Node.js async functions 最佳實踐

白色風車發表於2018-01-19

Node.js async functions 最佳實踐

從 Node.js 7.6 開始, Node.js 採用了支援 async functions 的新版 V8 引擎。2017年10月31日, Node.js 8 正式成為長期支援的版本,我們沒有理由不在程式碼中使用 async functions 了。在這篇文章中,我會簡單地向你說明什麼是 async functions 以及它們是如何改變我們編寫 Node.js 應用的方式的。

什麼是 async functions ?

async functions 能讓你像寫同步程式碼一樣編寫基於 PromisePromise-based ) 的程式碼。當你用 async 關鍵字定義了一個函式,你就可以在該函式內部使用 await 關鍵字。當這個 async function 被呼叫時,它會返回一個 Promise。當這個 async function 返回一個值,被返回的 Promise 的狀態會變為 fufilled,若 async function 丟擲一個錯誤, Promise 的狀態變為 rejected

await 關鍵字用來等待一個 Promise 被解決( resolved )並返回結果( the fulfilled value )。如果傳遞給 await 關鍵字的值不是一個 Promise,那麼它會被轉換成一個 resolved Promise

const rp = require( 'request-promise' );

async function main() {
  const result = await rp( 'https://google.com' );
  const twenty = await 20;

  // sleeeeeeeeping for a second ?

  await new Promise( resolve => {
    setTimeout( resolve, 1000 );
  } );

  return result;
}

main.then( console.log ).catch( console.error );

遷移到 async functions

如果你的 Node 應用已經在使用 Promise,那就只需要開始 await 你的 Promise,而不是像以往一樣把它們連結起來。

如果你的應用是使用 callback 構建的,那向 async functions 的遷移就應該逐步進行。你可以在新增新功能的時候使用這個新技術。如果你不得不使用之前的程式碼,那就簡單地用 Promise 把它們包裝起來。

可以使用內建的 util.promisify 方法來完成這件事情:

const util         = require( 'util' );
const { readFile } = require( 'fs'   );

const readFileAsync = util.promisify( readFile );

async function main() {
  const result = await readFileAsync( '.gitignore' );

  return result;
}

main.then( console.log ).catch( console.error );

async functions 的最佳實踐

express 中使用 async functions

由於 expressPromise 的支援非常好,在 express 中使用 asyncs functions 非常簡單:

const express = require( 'express' );

const app = express();

app.get( '/', async ( request, response ) => {
  // awaiting Promise here
  // if you just wait a single promise, you could simply return with it,
  // no need to await for it

  const result = await getContent();

  response.send( result );
} );

app.listen( process.env.PORT );

上面的例子有一個嚴重的問題,如果 Promiserejectexpress 的路由處理函式會被掛起,因為沒有任何處理錯誤的行為。

為了解決這個問題,你需要把你的非同步處理函式放在一個能處理異常的函式中:

const awaitHandlerFactory = ( middleware ) => {
  return async ( req, res, next ) => {
    try {
      await middleware( req, res, next );
    } catch ( err ) {
      next( err );
    }
  };
}

// and use it this way:

app.get( '/', awaitHandlerFactory( async ( request, response ) => {
  const result = await getContent();

  response.send( result );
} ) );

並行執行

想象你在做類似的事情,一個操作需要兩個輸入,一個來自資料庫,另一個來自外部服務:

async function main() {
  const user    = await Users.fetch( userId );
  const product = await Products.fetch( productId );

  await makePurchase( user, product );
}

在這個例子中,會發生如下事件:

  • 你的程式碼會先獲取 user 資訊,
  • 然後獲取 product 資訊,
  • 最後完成購買

如你所見,你可以同時做前兩件事情,因為它們之間並沒有依賴關係。你可以通過 Promise.all 方法來做這件事:

  async function main() {
    const [ user, product ] = await Promise.all( [
      Users.fetch( userId );
      Products.fetch( productId );
    ] );

    await makePurchase( user, product );
  }

在另外一些情況下,你可能只需要最先被 resolvePromise 的結果,在上面的例子中,你可以使用 Promise.race 方法。

錯誤處理

考慮下面的程式碼示例:

async function main() {
  await new Promise( ( resolve, reject ) => {
    reject( new Error( '?' ) );
  } );
}

main.then( console.log );

在執行這個程式碼段時,你會在你的終端中看到類似這樣的資訊:

(node:69738) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 2): Error : ?
(node:69738) [DEP0018] DeprecationWarning: Unhandled promise rejections deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

在比較新的版本的 Node.js 中,如果 Promise rejections 沒有被處理,它會讓整個 Node 程式掛掉。正因為如此,在必要的時候,你應該使用 try-catch 塊:

const util = require( 'util' );

async function main() {
  try {
    await new Promise( ( resolve, reject ) => {
      reject new Error( '?' );
    } );
  } catch ( err ) {
    // handle error case
    // maybe throwing is okay depending on your use-case
  }
};

main.then( console.log ).catch( console.error );

但是,使用 try-catch 塊可能會隱藏一些重要的異常,比如你希望被丟擲的系統錯誤。

更復雜的控制流

async 是最早的關於 Node.js 非同步控制流的庫之一,它是由 Caolan McMahon 建立的。它提供一些 asynchronous helpers,比如:

  • mapLimit,
  • filterLimit,
  • concatLimit,
  • priorityQueue.

如果你不想為了實現同樣的邏輯而重複造輪子,而且你也希望使用一個月下載量達到5千萬、久經實踐驗證的庫,你可以在使用 async functions 的時候結合 util.promisify 利用上述的方法:

const util  = require( 'util'  );
const async = require( 'async' );

const numbers = [ 1, 2, 3, 4, 5 ];

mapLimitAsync = util.promisify( async.mapLimit );

async function main() {
  return await mapLimitAsync( numbers, 2, ( number, done ) => {
    setTimeout( function () {
      done( null, number * 2 )
    }, 100 );
  } );
}

main.then( console.log ).catch( console.error );

相關文章