-- Address: http://blog.chrisyip.im/nodejs-101-package-promise-and-async
First, let's review the Sagase project structure:
lib/ cli.js sagase.jsGruntfile.jspackage.json
I spoke about it in the previous article.package.json
, This articlelib/sagase.js
.
Because the code is relatively long, we will talk about it separately in one section. For the complete code, open GitHub.
'use strict';
Notifies the compiler to enter strict mode. The main function is to enable the compiler to help you check some pitfalls. Experienced JavaScript developers can ignore them.
var fs = require('fs-extra'), async = require('async'), _ = require('lodash'), Promise = require('bluebird'), path = require('path')
Reference all dependencies.--harmony
, Recommendedconst
Replacevar
Keyword to avoid variable modification.
var mar = 'March'mar = 'June'console.log(mar) // 'June'const july = 'July'july = 'May'console.log(july) // 'July'
function readFiles (opts) {}
Contains a lot of information, one by one.
return new Promise(function (resolve, reject) {}
Returns a Promise object.
Because Node. js is characterized by Asynchronization, it usually needs to be processed asynchronously:
// get all files in DIRfs.readdir(DIR, function (err, files) { if (err) { return errorHandler(err) } // loop files files.forEach(function (file) { // get the stat of each files fs.stat(file, function (err, stat) { // if it's file if (stat.isFile()) { // get content of file fs.readFile(file, function (err, buff) { // do whatever you want }) } }) })})
If you need to process a lot of things in a function, or even make it possible for the function to return results in multiple files, it is difficult to rely only on callback-I don't know when to use the returned results of a file.
If Promise is used, it will be much simpler:
// in a.jsvar Promise = require('bluebird'), readFilemodule.exports = new Promise(function (resolve, reject) { fs.readdir(DIR, function (err, files) { err ? reject(err) : resolve(files) })})
// in b.jsvar readFile = require('./a.js')readFile .then(function (files) { // do something with files return NEW_RESULT; }, function (err) { // handle error here }) .then(function (data) { // do something with NEW_RESULT }, function (err) { // handle error here })
// in c.jsvar readFile = require('./a.js')readFile.then(function (files) { // the files still exist and accessable})
With Promise encapsulation, you canresolve
Results across different files, do not need to process everything in a callback function; in addition, through.then()
The second function processingreject
To avoid multiple judgments.
Note that a third-party Promise library named bluebird is introduced here. If the Node. js version is>= 0.11.13
Third-party libraries are not required.
async.each( files, function (file, cb) { }, function (err) { err ? reject(err) : resolve(res) })
Async is a library that improves the asynchronous callback process and assigns the asynchronous processing capability to common array processing functions, suchasync.each
It is equivalent to a multi-threaded version.Array.forEach
But in actual use, do not expect the execution order to be out of order or forward order. The key is the third parameter.
async.each([1, 2, 3], function (item, next) { console.log(item) next()}, function (err) { console.log('all tasks done')})// output:// 1// 2// 3// "all tasks done"
Generally, becausefs.stat
Is called asynchronously, soArray.forEach
After traversing the array, it is difficult to ensure whether all the tasks in it have been completed.Promise.resolve()
Data correctness cannot be guaranteed. And passasync.each
The third parameter of, you can know the status of the task, and ensure that Promise can get the correct data.
path.resolve(opts.folder + '/' + file).replace(process.cwd(), '.')
path
Is a library used to solve directory-related problems.path.resolve
Will./dir
To/Users/USER/PATH/TO/dir
(Mac) format.
process.cwd()
Will return the directory of the process that calls this script; in addition, there is__dirname
Indicates the directory where the script is located:
Suppose there are the following files:
lib/index.jsindex.js
// in lib/index.jsmodule.exports = function () { return __dirname}
// in index.jsconsole.log(process.cwd()) // '/Users/USER/PATH/'console.log(require('./lib')()) // '/Users/USER/PATH/lib/'
The remaining code is not explained one by one, and the following work is roughly done:
- Read all files in the specified directory first (
fs.readdir
)
- Use
async.each
Traverse the obtained results
- Determine whether the stat of each file is a directory (
fs.stat
)
- If yes, if the check operator does not meet the condition, the next round of recursion (
readFiles
)
- If no, the check operator does not meet the conditions. If yes, it is added to the final result array (
res.push()
)
- Repeat 1-3 until all objects are traversed.
Note that:
cb()
Appears in multiple places for notificationsasync.each
This task has been executed
- Use Promise in Recursion
readFiles.then()
First parameter Processingres
Is returned and usedreadFiles.catch
Processingerror
, UsereadFiles.finally
Processingcb()
(Because the async task must be notified to be completed, it should be handled here)
Promise.finally
It is a unique API of bluebird. The native Promise needs to implement it like this:readFiles.then().catch().then()
, Secondthen
Equivalentfinally
(But not intuitive enough)
function formatOptions (opts) {}
The parameters used for formatting are not explained much.
Note thatopts
Object To include all parameters and pass themreadFiles
. Because of JavaScript features, it is much easier to pass parameters with a JSON object, for example:
function formatOptions (folder, pattern, ignoreCase, nameOnly, exclude, excludeNameOnly, recusive) {}
Because V8 in Node. js does not contain the default function parameter values, the most convenient method is to use a JSON object:
var options = _.assign({}, { key_1: default_value_1, key_2: default_value_2 key_3: default_value_3}, opts)
JSON objects also facilitate expansion-no interface needs to be changed whether to add or delete keys.
Finally,Sagase
Class, and create a newSagase
Object:
function Sagase () { Object.defineProperties( this, { find: { enumerable: true, value: function (opts) { return readFiles(formatOptions(opts)) } } } )}module.exports = new Sagase()
Object.defineProperty
AndObject.defineProperties
It is a new feature added to ECMAScript 5. Through these features, you can create many interesting things, such as the traditionaljQuery.fn.size()
:
jQuery.fn.size = function () { return Number.MAX_SAFE_INTEGER}var body = $('body')body.length // 1body.size() // 9007199254740991
Replace it with ES 5:
Object.defineProperties( jQuery.fn, { size: { enumerable: true, get: function () { return this.length } } })var body = $('body')body.length // 1body.size // 1jQuery.fn.size = function () { return Number.MAX_SAFE_INTEGER}body.size // 1body.size() // TypeError: number is not a function
Rational Useconst
AndObject.defineProperty
You can avoid unexpected situations and ensure program robustness.
Slavelib/sagase.js
Node. the asynchronous feature of js makes the function layer by layer ('fs. readdir-> fs. stat-> isDirectory (), which is not easy to write or understand, for example:
function getJSON () { var json fs.readFile('demo.json', function (err, buff) { json = JSON.parse(buff.toString()) }) return json}getJSON() // undefined
Of course, you can also use the synchronous interface in Node. js, suchfs.readdirSync
,:
- Node. js synchronous interfaces are not necessarily more efficient than Python, Ruby, and other languages.
- Not all interfaces have synchronous versions, such
child_process.exec
To take advantage of Node. js, you must use Promise and async to write programs correctly. For example, in such a scenario, the browser needs to obtain the data of all the items and gifts in the shopping cart. The common steps are: Find the product data and get the gifts through the product ID to find the promotion rules, calculates the total price and returns the result. These steps can be concatenated by multiple requests to the database in the backend language. In the RESTful API mode, you can also initiate multiple requests and finally splice them in the browser.
What if I add asynchronous processing on the browser and server?
var router = express.Router()router.get('/cart', function (req, res, next) { async.parallel( [ // get all products data function (next) { request('/api/products', OPTIONS) .then(function (data) { next(null, data) }) }, // get products gifts function (next) { async.map( PRODUCT_LIST, function (p, cb) { request('/api/product/:id/gifts', OPTIONS) .then(function (data) { cb(null, data) }) }, function (err, results) { next(null, results) } ) } ], function (err, results) { RESPONSE_BODY = { products: results[0], gifts: results[1], total: calcTotal(results[0]) } res.send(RESPONSE_BODY) } )})
$.ajax('/cart').then(function () { // handle products and gifts here})
Through asynchronous processing, the browser can use a single request to complete the effects of multiple requests without damaging the RESTful API structure. This is very advantageous for mobile terminals with tight resources and changing network environments. For computer terminals, the interaction response speed and user experience are improved by reducing the request time.
This article focuses on how to bypass Node. js callback function traps using libraries such as Promise and async. The callback function is the most confusing part of Node. js. If you cannot control it, the written Node. js code will be ugly and difficult to maintain.
To make the Code look better, a new feature-Generator was added to ECMAScript 6. Koa has started to use generator to build a project. The next article describes how to use it.