서문에
express 의 보수적인 것에 비해,koa 는 비교적 급진적입니다. 현재 Node Stable 은 이미 v7.10.0 이며,async&await 는 v7.6 에서 호화 런치에 추가되었습니다. 이러한 훌륭한 것은 반드시 사용해야 합니다
현재 역사에서 보면,순서 형식으로 비동기 코드를 작성하는 것은 자연스러운 선택의 결과입니다. Microsoft 제품 일련의 언어,예를 들어 F# 2.0(2010 년) 은 이 특성을 서포트했으며,C# 5.0(2012 년) 도 이 특성을 추가했습니다. 한편 JS 는 ES2016 이 되어서야 처음으로 async&await 의 서포트를 검토했으며,그 사이 생태계에는 몇 가지 과도기 제품이 나타났습니다. 예를 들어 EventProxy, Step, Wind 등의 비동기 제어 라이브러리,ES2015 에서推出된 Promise, yield, 및 이를 기초로 실현된 co 모듈 등은,모두 비동기 플로 제어를 더욱 심플하게 하기 위한 것입니다
async&await 는가장 자연스러운 방식(순서 형식,동기 코드와 형식적으로 차이가 없음) 으로,현재 가장 최적의 방안입니다
P.S. JS 비동기 프로그래밍에 관한 더 많은 정보는,이하를 참조:
-
[EventProxy 를 시뮬레이트_Node 비동기 플로 제어 1](/articles/模拟 eventproxy-node 异步流程控制 1/)
-
[Step 소스 코드 해석_Node 비동기 플로 제어 2](/articles/step 소스 코드 해석-node 异步流程控制 2/)
-
[Promise 를 시뮬레이트_Node 비동기 플로 제어 3](/articles/模拟 promise-node 异步流程控制 3/)
-
[WindJS 에致敬_Node 비동기 플로 제어 4](/articles/向 windjs 致敬-node 异步流程控制 4/)
一.미들웨어
PHP 가 쿼리 문자열 해석,리퀘스트 바디 수신,Cookie 해석 주입 등의 기본적인 상세 처리 서포트를 내장하고 있는 것과는 달리
Node 가 제공하는 것은 적나라한 HTTP 연결로,이러한 상세 처리環節을 내장하고 있지 않아,수동으로 실현할 필요가 있습니다. 예를 들어 먼저 루팅으로 리퀘스트를配信하고,다음에 Cookie, 쿼리 문자열,리퀘스트 바디를 해석하며,대응하는 루팅 처리完毕后,리퀘스트에 응답할 때 먼저 원시 데이터를 포장하고,레스폰스 헤더를 설정하며,JSONP 서포트를 처리하는 등입니다. 리퀘스트가 올 때마다,이 전과정의 각環節의 처리는 불가결하며,각環節은 모두 미들웨어입니다
미들웨어의 작업 방식은车间流水线와 유사하며,1 장의 주문 (원시 리퀘스트 데이터) 이 와서,루팅이 대응하는 부서에配信하고,Cookie 필드를 꺼내어,해석完毕后 결과를 기입하며,쿼리 문자열을 꺼내어,각 파라미터 쌍을 해석하여 기입하고,리퀘스트 바디를 읽어,해석하여 포장하여 기입합니다……주문에 보충된 정보에 기반하여,车间이 1 개의 제품을 토해냅니다……통일 규격의 간단한 포장 (원시 데이터를 포장) 을添え,라벨을 붙이고 (레스폰스 헤더),精装인지 平装인지를 고려하며 (JSONP 서포트를 처리),마지막으로 출하합니다
따라서 미들웨어는底層의 상세를 캡슐화하고,기초 기능을 조직하며,인프라와 업무 로직을 분리하기 위해 사용됩니다
尾触发
가장 일반적인 미들웨어 조직 방식은尾触发로,예를 들어:
// 一般中间件的结构:尾触发下一个中间件
var middleware = function(err, req, res, next) {
// 把处理结果挂到请求对象上
req.middlewareData = handle(req);
// 通过 next 传递 err,捕获异步错误
if (errorOccurs) {
return next(error);
}
next();
};
모든 미들웨어를 순서대로繋ぎ,업무 로직環節에 이를 때,필요한 모든 입력 항목은 사전에 준비되어 리퀘스트 오브젝트에 걸려 있습니다 (리퀘스트 관련 미들웨어가 완성). 업무 로직 실행完毕后 레스폰스 데이터를 취득하여,직접 뒤에 던지며,레스폰스 관련 일련의 미들웨어를 통해,최종적으로 리퀘스트 측은 예상에 부합하는 레스폰스 콘텐츠를 취득합니다. 실제로는 우리는 업무 로직에만 주목하면 되며,전후의 일은 일련의 미들웨어가 완성합니다
尾触发은 모든 미들웨어를 직렬 실행하며,2 개의 문제가 존재합니다:
-
병행 최적화가 결여
-
에러 캡처 메커니즘이 번잡
미들웨어를 의존 관계에 기반하여 그룹화하고,병행 실행하면,퍼포먼스를 향상할 수 있습니다. 1 층의 추상을 추가하면 해결할 수 있습니다. 에러는 수동으로 뒤에 던질 필요가 있으며,미들웨어 체인을 따라 수동으로 전달하며,비교적 번거롭고,해결하기 어렵습니다
koa2.0 미들웨어
매우 깔끔하게 보입니다:
app.use(async (ctx, next) => {
const start = new Date();
await next();
const ms = new Date() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});
심플한 레스폰스耗时 기록 미들웨어로,만약 미들웨어隊首에 두면,모든 미들웨어 실행의 총耗时를 취득할 수 있습니다
위에서 소개한 尾触发과는 달리,await 가 있으면 임의의 위치에서 후속 미들웨어를触发할 수 있습니다. 예를 들어 위의 2 개의 타임스탬프 간의 next() 와 같이,이로써 매우 엄격한 순서로 미들웨어를 조직할 필요가 없어지며,유연해집니다
이전에 尾触发을 사용한 이유는,비동기 미들웨어가 즉시 반환하기 때문에,콜백 함수를 통해 제어할 수밖에 없었기 때문입니다. 그래서 尾触发 순서 실행 각 미들웨어를 약정했습니다
한편 async&await 는 비동기 조작의 종료를 대기할 수 있습니다 (여기서의 대기는真正意义上的 대기로,메커니즘은 yield 와 유사). 더 이상 비동기 미들웨어를 특별히关照할 필요가 없어,尾触发은 그다지 필요하지 않습니다
二.루팅
루팅도 일종의 미들웨어로,리퀘스트의配信을 담당합니다. 예를 들어:
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) {
// ...
});
일반적인 RESTful API 로,리퀘스트를 method 와 url 에 기반하여 대응하는 route 에配信합니다. 루팅과 일반 미들웨어의 차이는루팅은 통상 주요 업무 로직과 밀접하게 관련되어 있으며,리퀘스트 처리 과정을 3 단으로 나눌 수 있습니다:
리퀘스트 전처리 -> 주요 업무 로직 -> 레스폰스 포장 처리
대응하는 미들웨어 타입:
리퀘스트 관련 미들웨어 -> 루팅 -> 레스폰스 관련 미들웨어
기능은 다르지만,구조에서 보면,루팅과 일반 미들웨어에는 아무런 차이도 없습니다. router 는 리퀘스트配信 미들웨어로,url 에서 route 로의 관계를 유지하며,리퀘스트를 대응하는 route 에渡すために 사용됩니다
三.에러 캡처
await myPromise ��식 중 reject 의 에러는 외층 try...catch 로 캡처할 수 있습니다. 예를 들어:
(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');
주의,try...catch 에러 캡처는 reject(err) 에만 한정되며,직접 throw 된 것 또는 런타임 이상은 캡처할 수 없습니다. 더욱이,비동기 함수가 작성된 그 층의 스코프의 try...catch 만이 이상을 캡처할 수 있으며,외층의 것은 할 수 없습니다. 예를 들어:
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);
}
비동기 함수 자신은 실행 후 즉시 반환하기 때문에,외층 try...catch 는 이러한 비동기 이상을 캡처할 수 없으며,먼저 first log here 를 보고,100ms 후에 캡처되지 않은 이상을 던집니다
한편 Promise 에는 특수한 메커니즘이 있습니다:
特殊的:如果 resolve 的参数是 Promise 对象,则该对象最终的 [[PromiseValue]] 会传递给外层 Promise 对象后续的 then 的 onFulfilled/onRejected
([Promise 를 완전히 이해하다](/articles/完全理解 promise/) 에서 발췌)
즉 resolve(nextPromise) 를 통해建立된 Promise 체인상의 임의의 1 環의 reject 에러는 모두 Promise 체인을 따라 밖에 던져집니다. 예를 들어:
(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)
}
})();
仍然에 가장 내층의 에러를 캡처할 수 있습니다
미들웨어 에러를 캡처
이 특성을 이용하여,미들웨어 에러를 캡처하기 위한 미들웨어를 실현할 수 있습니다. 다음과 같습니다:
// 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;
}
};
이 미들웨어를 최전면에 두면,후속의 모든 미들웨어의 reject 에러 및 동기 에러를 캡처할 수 있습니다
글로벌 에러 캡처
위에서는 reject 의 에러와 동기 실행 과정에서 발생한 에러를 캡처했지만,비동기 throw 의 에러 (비동기 런타임 에러를 포함) 는仍然에 캡처할 수 없습니다
한편 가벼운 Uncaught Error 만으로 Node 서비스 전체를挂게 할 수 있기 때문에,마지막 보장으로서 글로벌 에러 처리를 추가할 필요가 있습니다:
// global catch
process.on('uncaughtException', (error) => {
console.error('uncaughtException ' + error);
});
이는 자연히 모든 코드 전에 실행하도록 배치하며,かつ 자신에 에러가 없는 것을 보증할 필요가 있습니다
粗暴한글로벌 에러 캡처는 만능이 아닙니다. 예를 들어 에러 발생 후에 500 을 레스폰스하는 것은 할 수 없으며,이 부분은 에러 캡처 미들웨어의职责입니다
四.示例 Demo
심플한 RSS 서비스로,미들웨어 조직은 다음과 같습니다:
middleware/
header.js # 레스폰스 헤더를 설정
json.js # 레스폰스 데이터를 규격 통일의 JSON 으로 변환
onerror.js # 미들웨어 에러를 캡처
route/
html.js # /index 대응의 루팅
index.js # /html/:url 대응의 루팅
pipe.js # /pipe 대응의 루팅
rss.js # /rss/:url 대응의 루팅
순서대로 각 미들웨어를 적용:
// 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)
리퀘스트 전처리와 레스폰스 데이터 포장은 모두 전후의 미들웨어가 완성하며,루팅은 출력 (원시 레스폰스 데이터) 을 생성하는 것만을 담당합니다. 예를 들어:
// 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();
};
抓取成功后,data 를 ctx.state 에걸고,resolve() 로 대기 종료를통지하며,next() 로다음미들웨어에레스폰스데이터를포장시키고,매우상쾌합니다
프로젝트주소:https://github.com/ayqy/RSSHelper/tree/master/node
아직 댓글이 없습니다