Preface
Compared to express's conservatism, koa is relatively radical. Currently Node Stable is already v7.10.0, async&await was added to the luxury lunch in v7.6, such a good thing must be used
From historical perspective, writing asynchronous code in sequential form is the result of natural selection. A series of languages from Microsoft, such as F# 2.0 (2010) already supported this feature, C# 5.0 (2012) also added this feature, while JS only considered supporting async&await in ES2016, during this period some transition products appeared in the ecosystem, such as EventProxy, Step, Wind and other asynchronous control libraries, Promise, yield launched in ES2015, and co module implemented on this basis, are all to make asynchronous flow control simpler
async&await is the most natural way (sequential form, no difference from synchronous code in form), and is currently the optimal solution
P.S. For more information about JS asynchronous programming, please check:
-
[Simulating EventProxy_Node Asynchronous Flow Control 1](/articles/模拟 eventproxy-node 异步流程控制 1/)
-
[Step Source Code Interpretation_Node Asynchronous Flow Control 2](/articles/step 源码解读-node 异步流程控制 2/)
-
[Simulating Promise_Node Asynchronous Flow Control 3](/articles/模拟 promise-node 异步流程控制 3/)
-
[Salute to WindJS_Node Asynchronous Flow Control 4](/articles/向 windjs 致敬-node 异步流程控制 4/)
1. Middleware
Unlike PHP which has built-in support for basic detail processing like query string parsing, request body receiving, Cookie parsing injection etc.
Node provides bare HTTP connections, doesn't have these detail processing links built-in, need to implement manually, for example first come with routing to distribute requests, then parse Cookie, query string, request body, after corresponding route processing is complete, when responding to request need to first wrap original data, set response headers, handle JSONP support etc. Every time a request comes, each link processing in this entire process is indispensable, each link is middleware
Middleware's working method is similar to workshop assembly line, when an order comes (original request data), routing distributes to corresponding department, take out Cookie field, after parsing complete fill in the result, take out query string, parse out each parameter pair, fill in, read request body, parse and wrap it, fill in... According to information supplemented on the order, workshop spits out a product... Add unified specification simple packaging (wrap original data), stick labels (response headers), consider whether hardcover or plain packaging (handle JSONP support), finally ship
So middleware is used to encapsulate underlying details, organize basic functions, separate infrastructure and business logic
Tail Trigger
Most common middleware organization method is tail trigger, for example:
// 一般中间件的结构:尾触发下一个中间件
var middleware = function(err, req, res, next) {
// 把处理结果挂到请求对象上
req.middlewareData = handle(req);
// 通过 next 传递 err,捕获异步错误
if (errorOccurs) {
return next(error);
}
next();
};
String all middleware together in order, when walking to business logic link, all needed input items are prepared in advance and hung on request object (completed by request-related middleware), after business logic execution completes get response data, directly throw backwards, go through response-related series of middleware, finally requester gets response content meeting expectations, while actually we only need to focus on business logic, before and after things are all completed by a series of middleware
Tail trigger serially executes all middleware, exists 2 problems:
-
Lacks parallel optimization
-
Error capture mechanism is cumbersome
Group middleware by dependency relationship, execute in parallel, can improve performance, can solve by adding one layer of abstraction. Errors need to be manually thrown backwards, manually passed along middleware chain, quite troublesome, not easy to solve
koa2.0 Middleware
Looks very beautiful:
app.use(async (ctx, next) => {
const start = new Date();
await next();
const ms = new Date() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});
A simple response time consumption recording middleware, if placed at front of middleware queue, can get total time consumption of all middleware execution
Different from tail trigger introduced above, with await can trigger subsequent middleware at any position, for example next() between above two timestamps, this way don't need to organize middleware according to very strict order, flexible a lot
Previously used tail trigger, because asynchronous middleware would return immediately, can only control through callback function, so agreed tail trigger to sequentially execute each middleware
While async&await can wait for asynchronous operation to end (waiting here is true waiting, mechanism similar to yield), no longer need to specially care about asynchronous middleware, tail trigger is not that necessary
2. Routing
Routing is also a kind of middleware, responsible for distributing requests, for example:
router
.get('/', function (ctx, next) {
ctx.body = 'Hello World!';
})
.post('/users', function (ctx, next) {
// ...
})
.put('/users/:id', function (ctx, next) {
// ...
})
.del('/users/:id', function (ctx, next) {
// ...
})
.all('/users/:id', function (ctx, next) {
// ...
});
Common RESTful API, distribute requests by method and url to corresponding route. Difference between routing and general middleware is routing is usually closely related to main business logic, can divide request processing process into 3 segments:
请求预处理 -> 主要业务逻辑 -> 响应包装处理
Corresponding to middleware types:
请求相关的中间件 -> 路由 -> 响应相关的中间件
Although functions are different, from structure perspective, routing and general middleware have no difference. router is request distribution middleware, used to maintain url to route relationship, hand over requests to corresponding route
3. Error Capture
reject errors in await myPromise method can be captured by outer try...catch, for example:
(async () => {
try {
await new Promise((resolve, reject) => {
setTimeout(() => {
let err = new Error('err');
reject(err);
}, 100);
});
} catch (ex) {
console.log('caught ' + ex);
}
})();
console.log('first log here');
Note, try...catch error capture is limited to reject(err), directly throw or runtime exceptions cannot be captured. Additionally, only try...catch in the layer of scope where asynchronous function is created can capture exceptions, outer layers cannot, for example:
try {
(async () => {
await new Promise((resolve, reject) => {
setTimeout(() => {
let err = new Error('err');
reject(err);
}, 100);
});
})();
console.log('first log here');
} catch (ex) {
console.log('caught ' + ex);
}
Because asynchronous function itself executes and returns immediately, outer try...catch cannot capture such asynchronous exceptions, will first see first log here, 100ms later throws uncaught exception
While Promise has a special mechanism:
特殊的:如果 resolve 的参数是 Promise 对象,则该对象最终的 [[PromiseValue]] 会传递给外层 Promise 对象后续的 then 的 onFulfilled/onRejected
(From [Completely Understanding Promise](/articles/完全理解 promise/))
That is to say, reject errors on any link of Promise chain established through resolve(nextPromise) will be thrown out along Promise chain, for example:
(async () => {
try {
await new Promise((resolve, reject) => {
resolve(new Promise((rs, rj) => {
rs(new Promise((s, j) => {
setTimeout(() => {
j(new Error('err'));
}, 100);
}))
}))
});
} catch (ex) {
console.log('caught ' + ex)
}
})();
Can still capture innermost error
Capture Middleware Errors
Using this characteristic, can implement middleware used to capture middleware errors, as follows:
// middleware/onerror.js
// global error handling for middlewares
module.exports = async (ctx, next) => {
try {
await next();
} catch (err) {
err.status = err.statusCode || err.status || 500;
let errBody = JSON.stringify({
code: -1,
data: err.message
});
ctx.body = errBody;
}
};
Put this middleware at very front, can capture reject errors and synchronous errors of all subsequent middleware
Global Error Capture
Above captured reject errors and errors produced during synchronous execution process, but asynchronous throw errors (including asynchronous runtime errors) still cannot be captured
While a light Uncaught Error can make entire Node service hang, so necessary to add global error handling as last guarantee:
// global catch
process.on('uncaughtException', (error) => {
console.error('uncaughtException ' + error);
});
This naturally should be placed before all code to execute as much as possible, and guarantee itself has no errors
Rough global error capture is not omnipotent, for example cannot respond a 500 after error occurs, this part is error capture middleware's responsibility
4. Example Demo
A simple RSS service, middleware organization as follows:
middleware/
header.js # 设置响应头
json.js # 响应数据转规格统一的 JSON
onerror.js # 捕获中间件错误
route/
html.js # /index 对应的路由
index.js # /html/:url 对应的路由
pipe.js # /pipe 对应的路由
rss.js # /rss/:url 对应的路由
Apply each middleware in order:
// global catch for middles error
app.use(onerror);
// router
router
.get('/', function (ctx, next) {
ctx.body = 'RSSHelper';
})
.get('/index', require('./route/index.js'))
.get('/rss/:url', require('./route/rss.js'))
.get('/html/:url', require('./route/html.js'))
.get('/pipe', require('./route/pipe.js'))
app
.use(router.routes())
.use(router.allowedMethods())
// custom middlewares
app
.use(header)
.use(json)
Request preprocessing and response data wrapping are all completed by before and after middleware, routing only responsible for producing output (original response data), for example:
// route /html
const fetch = require('../fetch/fetch.js');
module.exports = async (ctx, next) => {
await new Promise((resolve, reject) => {
const url = ctx.params.url;
let onsuccess = (data) => {
data = data || {};
ctx.state.data = data;
resolve();
}
let onerror = reject;
fetch('html', url)
.on('success', onsuccess)
.on('error', onerror)
});
next();
};
After capture succeeds, hang data on ctx.state, resolve() notifies waiting ends, next() hands over to next middleware to wrap response data, very clean
Project address: https://github.com/ayqy/RSSHelper/tree/master/node
No comments yet. Be the first to share your thoughts.