├── demo
├── chapter-01-05
│ ├── .eslintignore
│ ├── .eslintrc
│ ├── example.js
│ ├── index.js
│ ├── package.json
│ └── example-simple.js
├── chapter-01-06
│ ├── .eslintignore
│ ├── .eslintrc
│ ├── example.js
│ ├── package.json
│ └── index.js
├── chapter-01-07
│ ├── .eslintignore
│ ├── .eslintrc
│ ├── example.js
│ ├── compose.js
│ ├── package.json
│ └── index.js
├── chapter-03-02
│ ├── .eslintignore
│ ├── .eslintrc
│ ├── res.js
│ ├── req.js
│ ├── package.json
│ └── ctx.js
├── chapter-04-01
│ ├── .eslintignore
│ ├── .eslintrc
│ ├── example.js
│ ├── index.js
│ └── package.json
├── chapter-04-02
│ ├── .eslintignore
│ ├── public
│ │ ├── css
│ │ │ └── index.css
│ │ ├── js
│ │ │ └── index.js
│ │ └── index.html
│ ├── .eslintrc
│ ├── example.js
│ ├── package.json
│ └── index.js
├── chapter-04-03
│ ├── .eslintignore
│ ├── public
│ │ ├── css
│ │ │ └── index.css
│ │ ├── js
│ │ │ └── index.js
│ │ └── index.html
│ ├── .eslintrc
│ ├── example.js
│ ├── package.json
│ ├── index.js
│ └── send.js
├── chapter-05-01
│ ├── .eslintignore
│ ├── views
│ │ ├── hello.html
│ │ └── index.html
│ ├── .eslintrc
│ ├── example.js
│ ├── index.js
│ └── package.json
├── chapter-05-02
│ ├── .eslintignore
│ ├── .eslintrc
│ ├── example.js
│ ├── index.js
│ └── package.json
├── chapter-05-03
│ ├── .eslintignore
│ ├── .eslintrc
│ ├── example
│ │ ├── index.html
│ │ └── index.js
│ ├── package.json
│ ├── index.js
│ └── lib
│ │ └── read_stream.js
├── chapter-06-01
│ ├── .eslintignore
│ ├── .eslintrc
│ ├── example.js
│ ├── package.json
│ └── index.js
├── chapter-06-02
│ ├── .eslintignore
│ ├── .eslintrc
│ ├── example.js
│ ├── compose.js
│ ├── package.json
│ └── index.js
├── chapter-03-03-01
│ ├── .eslintignore
│ ├── .eslintrc
│ ├── package.json
│ └── index.js
├── chapter-03-03-02
│ ├── .eslintignore
│ ├── .eslintrc
│ ├── index.js
│ └── package.json
└── chapter-middleware
│ ├── .eslintignore
│ ├── .eslintrc
│ ├── example
│ ├── 003.js
│ ├── 001.js
│ ├── view
│ │ ├── 404.html
│ │ └── index.html
│ ├── 002.js
│ └── 004.js
│ └── package.json
├── note
├── chapter01
│ ├── 01.md
│ ├── 04.md
│ ├── 06.md
│ ├── 07.md
│ ├── 03.md
│ ├── 02.md
│ └── 05.md
├── chapter02
│ ├── 03.md
│ ├── 01.md
│ └── 02.md
├── chapter03
│ ├── 01.md
│ ├── 03.md
│ └── 02.md
├── chapter04
│ ├── 01.md
│ ├── 03.md
│ └── 02.md
├── chapter05
│ ├── 02.md
│ ├── 01.md
│ └── 03.md
└── chapter06
│ ├── 02.md
│ └── 01.md
├── LICENSE
├── .gitignore
├── SUMMARY.md
└── README.md
/demo/chapter-01-05/.eslintignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/chapter-01-06/.eslintignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/chapter-01-07/.eslintignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/chapter-03-02/.eslintignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/chapter-04-01/.eslintignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/chapter-04-02/.eslintignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/chapter-04-03/.eslintignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/chapter-05-01/.eslintignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/chapter-05-02/.eslintignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/chapter-05-03/.eslintignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/chapter-06-01/.eslintignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/chapter-06-02/.eslintignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/chapter-03-03-01/.eslintignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/chapter-03-03-02/.eslintignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/chapter-middleware/.eslintignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/chapter-04-02/public/css/index.css:
--------------------------------------------------------------------------------
1 | .hide {
2 | display: none;
3 | }
--------------------------------------------------------------------------------
/demo/chapter-04-03/public/css/index.css:
--------------------------------------------------------------------------------
1 | .hide {
2 | display: none;
3 | }
--------------------------------------------------------------------------------
/demo/chapter-04-02/public/js/index.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | console.log('hello world');
3 | })();
4 |
--------------------------------------------------------------------------------
/demo/chapter-04-03/public/js/index.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | console.log('hello world');
3 | })();
4 |
--------------------------------------------------------------------------------
/demo/chapter-05-01/views/hello.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | hello page
5 |
6 |
--------------------------------------------------------------------------------
/demo/chapter-05-01/views/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | index page
5 |
6 |
--------------------------------------------------------------------------------
/demo/chapter-01-05/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "eslint-config-koa",
3 | "parserOptions": {
4 | "ecmaVersion": 2017
5 | },
6 | "rules": {
7 | "no-unused-vars": 1,
8 | "no-useless-escape": 0
9 | }
10 | }
--------------------------------------------------------------------------------
/demo/chapter-01-06/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "eslint-config-koa",
3 | "parserOptions": {
4 | "ecmaVersion": 2017
5 | },
6 | "rules": {
7 | "no-unused-vars": 1,
8 | "no-useless-escape": 0
9 | }
10 | }
--------------------------------------------------------------------------------
/demo/chapter-01-07/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "eslint-config-koa",
3 | "parserOptions": {
4 | "ecmaVersion": 2017
5 | },
6 | "rules": {
7 | "no-unused-vars": 1,
8 | "no-useless-escape": 0
9 | }
10 | }
--------------------------------------------------------------------------------
/demo/chapter-03-02/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "eslint-config-koa",
3 | "parserOptions": {
4 | "ecmaVersion": 2017
5 | },
6 | "rules": {
7 | "no-unused-vars": 1,
8 | "no-useless-escape": 0
9 | }
10 | }
--------------------------------------------------------------------------------
/demo/chapter-03-03-01/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "eslint-config-koa",
3 | "parserOptions": {
4 | "ecmaVersion": 2017
5 | },
6 | "rules": {
7 | "no-unused-vars": 1,
8 | "no-useless-escape": 0
9 | }
10 | }
--------------------------------------------------------------------------------
/demo/chapter-03-03-02/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "eslint-config-koa",
3 | "parserOptions": {
4 | "ecmaVersion": 2017
5 | },
6 | "rules": {
7 | "no-unused-vars": 1,
8 | "no-useless-escape": 0
9 | }
10 | }
--------------------------------------------------------------------------------
/demo/chapter-04-01/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "eslint-config-koa",
3 | "parserOptions": {
4 | "ecmaVersion": 2017
5 | },
6 | "rules": {
7 | "no-unused-vars": 1,
8 | "no-useless-escape": 0
9 | }
10 | }
--------------------------------------------------------------------------------
/demo/chapter-04-02/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "eslint-config-koa",
3 | "parserOptions": {
4 | "ecmaVersion": 2017
5 | },
6 | "rules": {
7 | "no-unused-vars": 1,
8 | "no-useless-escape": 0
9 | }
10 | }
--------------------------------------------------------------------------------
/demo/chapter-04-03/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "eslint-config-koa",
3 | "parserOptions": {
4 | "ecmaVersion": 2017
5 | },
6 | "rules": {
7 | "no-unused-vars": 1,
8 | "no-useless-escape": 0
9 | }
10 | }
--------------------------------------------------------------------------------
/demo/chapter-05-01/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "eslint-config-koa",
3 | "parserOptions": {
4 | "ecmaVersion": 2017
5 | },
6 | "rules": {
7 | "no-unused-vars": 1,
8 | "no-useless-escape": 0
9 | }
10 | }
--------------------------------------------------------------------------------
/demo/chapter-05-02/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "eslint-config-koa",
3 | "parserOptions": {
4 | "ecmaVersion": 2017
5 | },
6 | "rules": {
7 | "no-unused-vars": 1,
8 | "no-useless-escape": 0
9 | }
10 | }
--------------------------------------------------------------------------------
/demo/chapter-06-01/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "eslint-config-koa",
3 | "parserOptions": {
4 | "ecmaVersion": 2017
5 | },
6 | "rules": {
7 | "no-unused-vars": 1,
8 | "no-useless-escape": 0
9 | }
10 | }
--------------------------------------------------------------------------------
/demo/chapter-06-02/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "eslint-config-koa",
3 | "parserOptions": {
4 | "ecmaVersion": 2017
5 | },
6 | "rules": {
7 | "no-unused-vars": 1,
8 | "no-useless-escape": 0
9 | }
10 | }
--------------------------------------------------------------------------------
/demo/chapter-middleware/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "eslint-config-koa",
3 | "parserOptions": {
4 | "ecmaVersion": 2017
5 | },
6 | "rules": {
7 | "no-unused-vars": 1,
8 | "no-useless-escape": 0
9 | }
10 | }
--------------------------------------------------------------------------------
/demo/chapter-05-03/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "eslint-config-koa",
3 | "parserOptions": {
4 | "ecmaVersion": 2017
5 | },
6 | "rules": {
7 | "no-unused-vars": 1,
8 | "no-useless-escape": 0,
9 | "no-control-regex": 0
10 | }
11 | }
--------------------------------------------------------------------------------
/demo/chapter-04-02/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | hello world
8 |
9 |
10 |
--------------------------------------------------------------------------------
/demo/chapter-04-03/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | hello world
8 |
9 |
10 |
--------------------------------------------------------------------------------
/demo/chapter-04-02/example.js:
--------------------------------------------------------------------------------
1 | const send = require('./index');
2 | const Koa = require('koa');
3 | const app = new Koa();
4 |
5 | app.use(async ctx => {
6 | await send(ctx, ctx.path, { root: `${__dirname}/public` });
7 | });
8 |
9 | app.listen(3000);
10 | console.log('listening on port 3000');
11 |
--------------------------------------------------------------------------------
/demo/chapter-01-07/example.js:
--------------------------------------------------------------------------------
1 | const SimpleKoa = require('./index');
2 |
3 | const app = new SimpleKoa();
4 | const PORT = 3001;
5 |
6 | app.use(async ctx => {
7 | ctx.body = 'this is a body
';
8 | });
9 |
10 | app.listen(PORT, () => {
11 | console.log(`the web server is starting at port ${PORT}`);
12 | });
13 |
--------------------------------------------------------------------------------
/demo/chapter-04-01/example.js:
--------------------------------------------------------------------------------
1 | const Koa = require('koa');
2 | const logger = require('./index');
3 | const app = new Koa();
4 |
5 | app.use(logger);
6 |
7 | app.use(async(ctx, next) => {
8 | ctx.body = 'hello world';
9 | });
10 |
11 | app.listen(3000, () => {
12 | console.log('[demo] is starting at port 3000');
13 | });
--------------------------------------------------------------------------------
/note/chapter01/01.md:
--------------------------------------------------------------------------------
1 | # 学习准备
2 |
3 | ## 知识储备
4 |
5 | - Node.js 的基础知识
6 | - `http` 模块的使用
7 | - `fs` 模块使用
8 | - `path` 模块使用
9 | - `Buffer` 类型
10 | - ES 的基础知识
11 | - `Promise`
12 | - `async/await`
13 | - 其他知识
14 | - `HTTP` 协议
15 | - `Cookie` 原理
16 |
17 | ## 环境准备
18 |
19 | - Linux/Mac 开发环境
20 | - nvm 管理 Node.js 版本
--------------------------------------------------------------------------------
/demo/chapter-04-01/index.js:
--------------------------------------------------------------------------------
1 | const logger = async function(ctx, next) {
2 | let res = ctx.res;
3 |
4 | // 拦截操作请求 request
5 | console.log(`<-- ${ctx.method} ${ctx.url}`);
6 |
7 | await next();
8 |
9 | // 拦截操作响应 request
10 | res.on('finish', () => {
11 | console.log(`--> ${ctx.method} ${ctx.url}`);
12 | });
13 | };
14 |
15 | module.exports = logger
16 |
--------------------------------------------------------------------------------
/demo/chapter-05-02/example.js:
--------------------------------------------------------------------------------
1 | const Koa = require('koa');
2 | const jsonp = require('./index');
3 | const app = new Koa();
4 |
5 | jsonp(app, {});
6 |
7 | app.use(async ctx => {
8 | await ctx.jsonp({
9 | data: 'this is a demo',
10 | success: true
11 | });
12 | });
13 |
14 | app.listen(3000, () => {
15 | console.log('[demo] jsonp is starting at port 3000');
16 | });
17 |
--------------------------------------------------------------------------------
/demo/chapter-05-03/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | example
4 |
5 |
6 |
7 |
form post demo
8 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/demo/chapter-04-03/example.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const Koa = require('koa');
3 | const statics = require('./index');
4 |
5 | const app = new Koa();
6 |
7 | const root = path.join(__dirname, './public');
8 | app.use(statics({ root }));
9 |
10 | app.use(async(ctx, next) => {
11 | if (ctx.path === '/hello') {
12 | ctx.body = 'hello world';
13 | }
14 | });
15 |
16 | app.listen(3000);
17 | console.log('listening on port 3000');
18 |
--------------------------------------------------------------------------------
/demo/chapter-01-06/example.js:
--------------------------------------------------------------------------------
1 | const WebServer = require('./index');
2 |
3 | const app = new WebServer();
4 | const PORT = 3001;
5 |
6 | app.use(ctx => {
7 | ctx.res.write('line 1
');
8 | });
9 |
10 | app.use(ctx => {
11 | ctx.res.write('line 2
');
12 | });
13 |
14 | app.use(ctx => {
15 | ctx.res.write('line 3
');
16 | });
17 |
18 | app.listen(PORT, () => {
19 | console.log(`the web server is starting at port ${PORT}`);
20 | });
21 |
--------------------------------------------------------------------------------
/demo/chapter-05-01/example.js:
--------------------------------------------------------------------------------
1 | const Koa = require('koa');
2 | const path = require('path');
3 | const view = require('./index');
4 | const app = new Koa();
5 |
6 | view(app, {
7 | baseDir: path.join(__dirname, 'views')
8 | });
9 |
10 | app.use(async ctx => {
11 | await ctx.view(`${ctx.path}.html`, {
12 | title: 'index page'
13 | });
14 | });
15 |
16 | app.listen(3000, () => {
17 | console.log('[demo] view is starting at port 3000');
18 | });
19 |
--------------------------------------------------------------------------------
/demo/chapter-05-01/index.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const fs = require('fs');
3 |
4 | function view(app, opts = {}) {
5 | const {baseDir = ''} = opts;
6 |
7 | app.context.view = function(page = '', obj = {}) {
8 | let ctx = this;
9 | let filePath = path.join(baseDir, page);
10 | if (fs.existsSync(filePath)) {
11 | let tpl = fs.readFileSync(filePath, 'binary');
12 | ctx.body = tpl;
13 | } else {
14 | ctx.throw(404);
15 | }
16 | };
17 | }
18 |
19 | module.exports = view;
20 |
--------------------------------------------------------------------------------
/demo/chapter-06-02/example.js:
--------------------------------------------------------------------------------
1 | const mount = require('./index');
2 | const Koa = require('koa');
3 |
4 | const app1 = new Koa();
5 | const app2 = new Koa();
6 |
7 | app1.use(async(ctx, next) => {
8 | await next();
9 | ctx.body = 'app 1';
10 | });
11 |
12 | app2.use(async(ctx, next) => {
13 | await next();
14 | ctx.body = 'app 2';
15 | });
16 |
17 | const app = new Koa();
18 |
19 | app.use(mount('/app1', app1));
20 | app.use(mount('/app2', app2));
21 |
22 | app.listen(3000);
23 | console.log('listening on port 3000');
24 |
--------------------------------------------------------------------------------
/demo/chapter-middleware/example/003.js:
--------------------------------------------------------------------------------
1 | // 003
2 |
3 | const Koa = require('koa');
4 | const app = new Koa();
5 |
6 | const middleware = async function(ctx, next) {
7 | const routes = [
8 | {
9 | method: 'GET',
10 | path: '/index',
11 | middleware: async function(ctx, next){
12 | ctx.body = 'Index Page
'
13 | }
14 | }
15 | ]
16 | };
17 |
18 | app.use(middleware);
19 |
20 | app.use(async(ctx, next) => {
21 | ctx.body = 'hello world';
22 | });
23 |
24 | app.listen(3000, () => {
25 | console.log('[demo] is starting at port 3000');
26 | });
27 |
--------------------------------------------------------------------------------
/demo/chapter-06-01/example.js:
--------------------------------------------------------------------------------
1 |
2 | const Koa = require('koa');
3 | const Router = require('./index');
4 | const app = new Koa();
5 | const router = new Router();
6 |
7 | router.get('/index', async ctx => { ctx.body = 'index page'; });
8 | router.get('/post', async ctx => { ctx.body = 'post page'; });
9 | router.get('/list', async ctx => { ctx.body = 'list page'; });
10 | router.get('/item', async ctx => { ctx.body = 'item page'; });
11 |
12 | app.use(router.routes());
13 |
14 | app.use(async ctx => {
15 | ctx.body = '404';
16 | });
17 |
18 | app.listen(3000);
19 | console.log('listening on port 3000');
20 |
--------------------------------------------------------------------------------
/demo/chapter-03-02/res.js:
--------------------------------------------------------------------------------
1 | const Koa = require('koa');
2 | let app = new Koa();
3 |
4 | const middleware = async function(ctx, next) {
5 | // 中间件 拦截响应
6 | // 把所有响应结果设置成文本类型
7 | ctx.response.type = 'text/plain';
8 | await next();
9 | }
10 |
11 | const page = async function(ctx, next) {
12 | ctx.body = `
13 |
14 |
15 |
16 | ${ctx.path}
17 |
18 |
19 | `;
20 | }
21 |
22 | app.use(middleware);
23 | app.use(page);
24 |
25 | app.listen(3001, function(){
26 | console.log('the demo is start at port 3001');
27 | })
--------------------------------------------------------------------------------
/demo/chapter-05-02/index.js:
--------------------------------------------------------------------------------
1 | function jsonp(app, opts = {}) {
2 | let callback = opts.callback || 'callback';
3 |
4 | app.context.jsonp = function(obj = {}) {
5 | let ctx = this;
6 | if (Object.prototype.toString.call(obj).toLowerCase() === '[object object]') {
7 | let jsonpStr = `;${callback}(${JSON.stringify(obj)})`;
8 |
9 | // 用text/javascript,让请求支持跨域获取
10 | ctx.type = 'text/javascript';
11 |
12 | // 输出jsonp字符串
13 | ctx.body = jsonpStr;
14 | } else {
15 | ctx.throw(500, 'result most be a json');
16 | }
17 | };
18 | }
19 |
20 | module.exports = jsonp;
21 |
--------------------------------------------------------------------------------
/demo/chapter-middleware/example/001.js:
--------------------------------------------------------------------------------
1 | // 001
2 |
3 | const Koa = require('koa');
4 | const app = new Koa();
5 |
6 | const middleware = async function(ctx, next) {
7 | let res = ctx.res;
8 |
9 | // 拦截操作请求 request
10 | console.log(`<-- ${ctx.method} ${ctx.url}`);
11 |
12 | await next();
13 |
14 | // 拦截操作响应 request
15 | res.on('finish', () => {
16 | console.log(`--> ${ctx.method} ${ctx.url}`);
17 | });
18 | };
19 |
20 | app.use(middleware);
21 |
22 | app.use(async(ctx, next) => {
23 | ctx.body = 'hello world';
24 | });
25 |
26 | app.listen(3000, () => {
27 | console.log('[demo] is starting at port 3000');
28 | });
29 |
--------------------------------------------------------------------------------
/demo/chapter-03-02/req.js:
--------------------------------------------------------------------------------
1 | const Koa = require('koa');
2 | let app = new Koa();
3 |
4 | const middleware = async function(ctx, next) {
5 | // 中间件 拦截请求
6 | // 把所有请求不是 /page/ 开头的路径全部抛出500错误
7 | const reqPath = ctx.request.path;
8 | if( reqPath.indexOf('/page/') !== 0 ) {
9 | ctx.throw(500)
10 | }
11 | await next();
12 | }
13 |
14 | const page = async function(ctx, next) {
15 | ctx.body = `
16 |
17 |
18 |
19 | ${ctx.request.path}
20 |
21 |
22 | `;
23 | }
24 |
25 | app.use(middleware);
26 | app.use(page);
27 |
28 | app.listen(3001, function(){
29 | console.log('the demo is start at port 3001');
30 | })
--------------------------------------------------------------------------------
/note/chapter02/03.md:
--------------------------------------------------------------------------------
1 | # HTTP切面流程
2 |
3 |
4 |
5 | ## 任人打扮的HTTP
6 |
7 | - 从HTTP请求从拿到想要的数据
8 | - 从拿到数据处理想要处理的事情
9 | - 给处理后的结果
10 |
11 | 
12 |
13 | ## HTTP生命过程
14 |
15 | - http请求
16 | - 路由操作
17 | - 权限处理
18 | - 数据安全
19 | - 业务操作
20 | - 数据操作
21 | - 书查查询
22 | - http响应
23 | - 响应操作
24 |
25 | 
26 |
27 |
28 | ## Koa.js的HTTP旅程
29 |
30 | - 请求
31 | - 中间件
32 | - 响应
33 |
34 | 
35 |
36 |
--------------------------------------------------------------------------------
/demo/chapter-middleware/example/view/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | example
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
404 Not Found
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/demo/chapter-05-03/example/index.js:
--------------------------------------------------------------------------------
1 | const Koa = require('koa');
2 | const fs = require('fs');
3 | const path = require('path');
4 | const body = require('../index');
5 | const app = new Koa();
6 |
7 | app.use(body());
8 |
9 | app.use(async(ctx, next) => {
10 | if (ctx.url === '/') {
11 | // 当GET请求时候返回表单页面
12 | let html = fs.readFileSync(path.join(__dirname, './index.html'), 'binary');
13 | ctx.body = html;
14 | } else if (ctx.url === '/post' && ctx.method === 'POST') {
15 | // 当POST请求的时候,解析POST表单里的数据,并显示出来
16 | ctx.body = ctx.request.body;
17 | } else {
18 | // 其他请求显示404
19 | ctx.body = '404!!! o(╯□╰)o
';
20 | }
21 |
22 | await next();
23 | });
24 |
25 | app.listen(3000, () => {
26 | console.log('[demo] is starting at port 3000');
27 | });
28 |
--------------------------------------------------------------------------------
/demo/chapter-01-05/example.js:
--------------------------------------------------------------------------------
1 | const compose = require('./index');
2 |
3 | let middleware = [];
4 | let context = {
5 | data: []
6 | };
7 |
8 | middleware.push(async(ctx, next) => {
9 | console.log('action 001');
10 | ctx.data.push(2);
11 | await next();
12 | console.log('action 006');
13 | ctx.data.push(5);
14 | });
15 |
16 | middleware.push(async(ctx, next) => {
17 | console.log('action 002');
18 | ctx.data.push(2);
19 | await next();
20 | console.log('action 005');
21 | ctx.data.push(5);
22 | });
23 |
24 | middleware.push(async(ctx, next) => {
25 | console.log('action 003');
26 | ctx.data.push(2);
27 | await next();
28 | console.log('action 004');
29 | ctx.data.push(5);
30 | });
31 |
32 | const fn = compose(middleware);
33 |
34 | fn(context)
35 | .then(() => {
36 | console.log('end');
37 | console.log('context = ', context);
38 | });
39 |
--------------------------------------------------------------------------------
/demo/chapter-01-05/index.js:
--------------------------------------------------------------------------------
1 | module.exports = compose;
2 |
3 | function compose(middleware) {
4 | if (!Array.isArray(middleware)) {
5 | throw new TypeError('Middleware stack must be an array!');
6 | }
7 |
8 | return function(ctx, next) {
9 | let index = -1;
10 |
11 | return dispatch(0);
12 |
13 | function dispatch(i) {
14 | if (i < index) {
15 | return Promise.reject(new Error('next() called multiple times'));
16 | }
17 | index = i;
18 |
19 | let fn = middleware[i];
20 |
21 | if (i === middleware.length) {
22 | fn = next;
23 | }
24 |
25 | if (!fn) {
26 | return Promise.resolve();
27 | }
28 |
29 | try {
30 | return Promise.resolve(fn(ctx, () => {
31 | return dispatch(i + 1);
32 | }));
33 | } catch (err) {
34 | return Promise.reject(err);
35 | }
36 | }
37 | };
38 | }
39 |
--------------------------------------------------------------------------------
/demo/chapter-03-03-02/index.js:
--------------------------------------------------------------------------------
1 | const Koa = require('koa');
2 | let app = new Koa();
3 |
4 | function indirectMiddleware(path, middleware) {
5 | return async function(ctx, next) {
6 | console.log(ctx.path === path, middleware);
7 | if (ctx.path === path) {
8 | await middleware(ctx, next);
9 | } else {
10 | await next();
11 | }
12 | };
13 | }
14 |
15 | const index = async function(ctx, next) {
16 | ctx.body = 'this is index page';
17 | };
18 |
19 | const hello = async function(ctx, next) {
20 | ctx.body = 'this is hello page';
21 | };
22 |
23 | const world = async function(ctx, next) {
24 | ctx.body = 'this is world page';
25 | };
26 |
27 | app.use(indirectMiddleware('/', index));
28 | app.use(indirectMiddleware('/hello', hello));
29 | app.use(indirectMiddleware('/world', world));
30 |
31 | app.listen(3001, () => {
32 | console.log('the demo is start at port 3001');
33 | });
34 |
--------------------------------------------------------------------------------
/demo/chapter-01-07/compose.js:
--------------------------------------------------------------------------------
1 | module.exports = compose;
2 |
3 | function compose(middleware) {
4 | if (!Array.isArray(middleware)) {
5 | throw new TypeError('Middleware stack must be an array!');
6 | }
7 |
8 | return function(ctx, next) {
9 | let index = -1;
10 |
11 | return dispatch(0);
12 |
13 | function dispatch(i) {
14 | if (i < index) {
15 | return Promise.reject(new Error('next() called multiple times'));
16 | }
17 | index = i;
18 |
19 | let fn = middleware[i];
20 |
21 | if (i === middleware.length) {
22 | fn = next;
23 | }
24 |
25 | if (!fn) {
26 | return Promise.resolve();
27 | }
28 |
29 | try {
30 | return Promise.resolve(fn(ctx, () => {
31 | return dispatch(i + 1);
32 | }));
33 | } catch (err) {
34 | return Promise.reject(err);
35 | }
36 | }
37 | };
38 | }
39 |
--------------------------------------------------------------------------------
/demo/chapter-03-02/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chapter-2-2",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "dev": "./node_modules/.bin/nodemon example.js",
8 | "fix": "./node_modules/.bin/eslint --fix --ext .js ./",
9 | "precommit": "./node_modules/.bin/eslint --ext .jsx,.js ./",
10 | "prepush": "./node_modules/.bin/eslint --ext .jsx,.js ./"
11 | },
12 | "author": "chenshenhai",
13 | "license": "MIT",
14 | "dependencies": {
15 | "koa": "^2.5.0"
16 | },
17 | "devDependencies": {
18 | "eslint": "^4.19.1",
19 | "eslint-config-koa": "^2.0.2",
20 | "eslint-config-standard": "^11.0.0",
21 | "eslint-plugin-import": "^2.10.0",
22 | "eslint-plugin-node": "^6.0.1",
23 | "eslint-plugin-promise": "^3.7.0",
24 | "eslint-plugin-standard": "^3.0.1",
25 | "husky": "^0.14.3",
26 | "nodemon": "^1.17.3"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/demo/chapter-06-02/compose.js:
--------------------------------------------------------------------------------
1 | module.exports = compose;
2 |
3 | function compose(middleware) {
4 | if (!Array.isArray(middleware)) {
5 | throw new TypeError('Middleware stack must be an array!');
6 | }
7 |
8 | return function(ctx, next) {
9 | let index = -1;
10 |
11 | return dispatch(0);
12 |
13 | function dispatch(i) {
14 | if (i < index) {
15 | return Promise.reject(new Error('next() called multiple times'));
16 | }
17 | index = i;
18 |
19 | let fn = middleware[i];
20 |
21 | if (i === middleware.length) {
22 | fn = next;
23 | }
24 |
25 | if (!fn) {
26 | return Promise.resolve();
27 | }
28 |
29 | try {
30 | return Promise.resolve(fn(ctx, () => {
31 | return dispatch(i + 1);
32 | }));
33 | } catch (err) {
34 | return Promise.reject(err);
35 | }
36 | }
37 | };
38 | }
39 |
--------------------------------------------------------------------------------
/note/chapter03/01.md:
--------------------------------------------------------------------------------
1 | # 中间件分类
2 |
3 | ## 前言
4 |
5 | 市面上的大部分Web框架,都提供了很多Web相关的能力支持,例如。
6 | - HTTP服务
7 | - 路由管理
8 | - 模板渲染
9 | - 日志
10 | - 插件/中间件等AOP能力
11 | - 其他能力
12 |
13 | Koa.js 作为一个web框架,总结出来只提供了两种能力
14 | - HTTP服务
15 | - 中间件机制(AOP切面)
16 |
17 | 综上所述,用Koa.js想实现大部分Web功能的话,就需要整合相关功能的中间件。换句话说,Koa.js 说就是中间件的大容器,任何Web所需的能力通过中间件来实现。
18 |
19 | Koa.js 中间件的分类,在我的理解,可以分成以下两种类型。
20 | - 狭义中间件
21 | - 广义中间件
22 |
23 | ## 狭义中间件
24 |
25 | 狭义中间件特点:
26 |
27 | - 中间件内操作请求 `request`
28 | - 中间件内操作响应 `response`
29 | - 中间件内操作上下文 `context`
30 | - 大多数直接被 `app.use()` 加载
31 |
32 | 举个栗子
33 | 例如 中间件`koa-static`主要是靠拦截请求和响应,加载静态资源,中间件`koa-bodyparser`主要是拦截请求后解析出`HTTP`请求体重的POST数据,再挂载到`ctx`上。
34 |
35 |
36 |
37 | ## 广义中间件
38 |
39 | 广义中间件特点
40 |
41 | - 不直接提供中间件
42 | - 通过间接方式提供了中间件或者子中间件
43 | - 间接被 `app.use()` 加载
44 | - 其他方式接入Koa切面
45 |
46 |
47 | 举个例子
48 | 例如中间`koa-router` 是先注册路由后形成多个`子中间件`,后面再封装成一个`父中间件`提供给`app.use()`加载,让所有子中间件加载到`Koa.js`的请求`洋葱模型`中。
--------------------------------------------------------------------------------
/demo/chapter-01-05/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chapter-1-5",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "node example.js",
8 | "dev": "./node_modules/.bin/nodemon example.js",
9 | "fix": "./node_modules/.bin/eslint --fix --ext .js ./",
10 | "precommit": "./node_modules/.bin/eslint --ext .jsx,.js ./",
11 | "prepush": "./node_modules/.bin/eslint --ext .jsx,.js ./"
12 | },
13 | "author": "chenshenhai",
14 | "license": "MIT",
15 | "dependencies": {
16 |
17 | },
18 | "devDependencies": {
19 | "eslint": "^4.19.1",
20 | "eslint-config-koa": "^2.0.2",
21 | "eslint-config-standard": "^11.0.0",
22 | "eslint-plugin-import": "^2.10.0",
23 | "eslint-plugin-node": "^6.0.1",
24 | "eslint-plugin-promise": "^3.7.0",
25 | "eslint-plugin-standard": "^3.0.1",
26 | "husky": "^0.14.3",
27 | "nodemon": "^1.17.3"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/demo/chapter-01-06/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chapter-1-6",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "node example.js",
8 | "dev": "./node_modules/.bin/nodemon example.js",
9 | "fix": "./node_modules/.bin/eslint --fix --ext .js ./",
10 | "precommit": "./node_modules/.bin/eslint --ext .jsx,.js ./",
11 | "prepush": "./node_modules/.bin/eslint --ext .jsx,.js ./"
12 | },
13 | "author": "chenshenhai",
14 | "license": "MIT",
15 | "dependencies": {
16 |
17 | },
18 | "devDependencies": {
19 | "eslint": "^4.19.1",
20 | "eslint-config-koa": "^2.0.2",
21 | "eslint-config-standard": "^11.0.0",
22 | "eslint-plugin-import": "^2.10.0",
23 | "eslint-plugin-node": "^6.0.1",
24 | "eslint-plugin-promise": "^3.7.0",
25 | "eslint-plugin-standard": "^3.0.1",
26 | "husky": "^0.14.3",
27 | "nodemon": "^1.17.3"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/demo/chapter-01-07/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chapter",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "node example.js",
8 | "dev": "./node_modules/.bin/nodemon example.js",
9 | "fix": "./node_modules/.bin/eslint --fix --ext .js ./",
10 | "precommit": "./node_modules/.bin/eslint --ext .jsx,.js ./",
11 | "prepush": "./node_modules/.bin/eslint --ext .jsx,.js ./"
12 | },
13 | "author": "chenshenhai",
14 | "license": "MIT",
15 | "dependencies": {
16 | "koa": "^2.5.0"
17 | },
18 | "devDependencies": {
19 | "eslint": "^4.19.1",
20 | "eslint-config-koa": "^2.0.2",
21 | "eslint-config-standard": "^11.0.0",
22 | "eslint-plugin-import": "^2.10.0",
23 | "eslint-plugin-node": "^6.0.1",
24 | "eslint-plugin-promise": "^3.7.0",
25 | "eslint-plugin-standard": "^3.0.1",
26 | "husky": "^0.14.3",
27 | "nodemon": "^1.17.3"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/demo/chapter-04-02/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chapter",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "node example.js",
8 | "dev": "./node_modules/.bin/nodemon example.js",
9 | "fix": "./node_modules/.bin/eslint --fix --ext .js ./",
10 | "precommit": "./node_modules/.bin/eslint --ext .jsx,.js ./",
11 | "prepush": "./node_modules/.bin/eslint --ext .jsx,.js ./"
12 | },
13 | "author": "chenshenhai",
14 | "license": "MIT",
15 | "dependencies": {
16 | "koa": "^2.5.0"
17 | },
18 | "devDependencies": {
19 | "eslint": "^4.19.1",
20 | "eslint-config-koa": "^2.0.2",
21 | "eslint-config-standard": "^11.0.0",
22 | "eslint-plugin-import": "^2.10.0",
23 | "eslint-plugin-node": "^6.0.1",
24 | "eslint-plugin-promise": "^3.7.0",
25 | "eslint-plugin-standard": "^3.0.1",
26 | "husky": "^0.14.3",
27 | "nodemon": "^1.17.3"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/demo/chapter-04-03/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chapter",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "node example.js",
8 | "dev": "./node_modules/.bin/nodemon example.js",
9 | "fix": "./node_modules/.bin/eslint --fix --ext .js ./",
10 | "precommit": "./node_modules/.bin/eslint --ext .jsx,.js ./",
11 | "prepush": "./node_modules/.bin/eslint --ext .jsx,.js ./"
12 | },
13 | "author": "chenshenhai",
14 | "license": "MIT",
15 | "dependencies": {
16 | "koa": "^2.5.0"
17 | },
18 | "devDependencies": {
19 | "eslint": "^4.19.1",
20 | "eslint-config-koa": "^2.0.2",
21 | "eslint-config-standard": "^11.0.0",
22 | "eslint-plugin-import": "^2.10.0",
23 | "eslint-plugin-node": "^6.0.1",
24 | "eslint-plugin-promise": "^3.7.0",
25 | "eslint-plugin-standard": "^3.0.1",
26 | "husky": "^0.14.3",
27 | "nodemon": "^1.17.3"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/demo/chapter-05-01/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chapter",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "node example.js",
8 | "dev": "./node_modules/.bin/nodemon example.js",
9 | "fix": "./node_modules/.bin/eslint --fix --ext .js ./",
10 | "precommit": "./node_modules/.bin/eslint --ext .jsx,.js ./",
11 | "prepush": "./node_modules/.bin/eslint --ext .jsx,.js ./"
12 | },
13 | "author": "chenshenhai",
14 | "license": "MIT",
15 | "dependencies": {
16 | "koa": "^2.5.0"
17 | },
18 | "devDependencies": {
19 | "eslint": "^4.19.1",
20 | "eslint-config-koa": "^2.0.2",
21 | "eslint-config-standard": "^11.0.0",
22 | "eslint-plugin-import": "^2.10.0",
23 | "eslint-plugin-node": "^6.0.1",
24 | "eslint-plugin-promise": "^3.7.0",
25 | "eslint-plugin-standard": "^3.0.1",
26 | "husky": "^0.14.3",
27 | "nodemon": "^1.17.3"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/demo/chapter-05-02/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chapter",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "node example.js",
8 | "dev": "./node_modules/.bin/nodemon example.js",
9 | "fix": "./node_modules/.bin/eslint --fix --ext .js ./",
10 | "precommit": "./node_modules/.bin/eslint --ext .jsx,.js ./",
11 | "prepush": "./node_modules/.bin/eslint --ext .jsx,.js ./"
12 | },
13 | "author": "chenshenhai",
14 | "license": "MIT",
15 | "dependencies": {
16 | "koa": "^2.5.0"
17 | },
18 | "devDependencies": {
19 | "eslint": "^4.19.1",
20 | "eslint-config-koa": "^2.0.2",
21 | "eslint-config-standard": "^11.0.0",
22 | "eslint-plugin-import": "^2.10.0",
23 | "eslint-plugin-node": "^6.0.1",
24 | "eslint-plugin-promise": "^3.7.0",
25 | "eslint-plugin-standard": "^3.0.1",
26 | "husky": "^0.14.3",
27 | "nodemon": "^1.17.3"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/demo/chapter-06-01/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chapter",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "node example.js",
8 | "dev": "./node_modules/.bin/nodemon example.js",
9 | "fix": "./node_modules/.bin/eslint --fix --ext .js ./",
10 | "precommit": "./node_modules/.bin/eslint --ext .jsx,.js ./",
11 | "prepush": "./node_modules/.bin/eslint --ext .jsx,.js ./"
12 | },
13 | "author": "chenshenhai",
14 | "license": "MIT",
15 | "dependencies": {
16 | "koa": "^2.5.0"
17 | },
18 | "devDependencies": {
19 | "eslint": "^4.19.1",
20 | "eslint-config-koa": "^2.0.2",
21 | "eslint-config-standard": "^11.0.0",
22 | "eslint-plugin-import": "^2.10.0",
23 | "eslint-plugin-node": "^6.0.1",
24 | "eslint-plugin-promise": "^3.7.0",
25 | "eslint-plugin-standard": "^3.0.1",
26 | "husky": "^0.14.3",
27 | "nodemon": "^1.17.3"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/demo/chapter-06-02/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chapter",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "node example.js",
8 | "dev": "./node_modules/.bin/nodemon example.js",
9 | "fix": "./node_modules/.bin/eslint --fix --ext .js ./",
10 | "precommit": "./node_modules/.bin/eslint --ext .jsx,.js ./",
11 | "prepush": "./node_modules/.bin/eslint --ext .jsx,.js ./"
12 | },
13 | "author": "chenshenhai",
14 | "license": "MIT",
15 | "dependencies": {
16 | "koa": "^2.5.0"
17 | },
18 | "devDependencies": {
19 | "eslint": "^4.19.1",
20 | "eslint-config-koa": "^2.0.2",
21 | "eslint-config-standard": "^11.0.0",
22 | "eslint-plugin-import": "^2.10.0",
23 | "eslint-plugin-node": "^6.0.1",
24 | "eslint-plugin-promise": "^3.7.0",
25 | "eslint-plugin-standard": "^3.0.1",
26 | "husky": "^0.14.3",
27 | "nodemon": "^1.17.3"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/demo/chapter-03-03-01/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chapter-2-3",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "node index.js",
8 | "dev": "./node_modules/.bin/nodemon index.js",
9 | "fix": "./node_modules/.bin/eslint --fix --ext .js ./",
10 | "precommit": "./node_modules/.bin/eslint --ext .jsx,.js ./",
11 | "prepush": "./node_modules/.bin/eslint --ext .jsx,.js ./"
12 | },
13 | "author": "chenshenhai",
14 | "license": "MIT",
15 | "dependencies": {
16 | "koa": "^2.5.0"
17 | },
18 | "devDependencies": {
19 | "eslint": "^4.19.1",
20 | "eslint-config-koa": "^2.0.2",
21 | "eslint-config-standard": "^11.0.0",
22 | "eslint-plugin-import": "^2.10.0",
23 | "eslint-plugin-node": "^6.0.1",
24 | "eslint-plugin-promise": "^3.7.0",
25 | "eslint-plugin-standard": "^3.0.1",
26 | "husky": "^0.14.3",
27 | "nodemon": "^1.17.3"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/demo/chapter-04-01/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chapter-04-01",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "node example.js",
8 | "dev": "./node_modules/.bin/nodemon example.js",
9 | "fix": "./node_modules/.bin/eslint --fix --ext .js ./",
10 | "precommit": "./node_modules/.bin/eslint --ext .jsx,.js ./",
11 | "prepush": "./node_modules/.bin/eslint --ext .jsx,.js ./"
12 | },
13 | "author": "chenshenhai",
14 | "license": "MIT",
15 | "dependencies": {
16 | "koa": "^2.5.0"
17 | },
18 | "devDependencies": {
19 | "eslint": "^4.19.1",
20 | "eslint-config-koa": "^2.0.2",
21 | "eslint-config-standard": "^11.0.0",
22 | "eslint-plugin-import": "^2.10.0",
23 | "eslint-plugin-node": "^6.0.1",
24 | "eslint-plugin-promise": "^3.7.0",
25 | "eslint-plugin-standard": "^3.0.1",
26 | "husky": "^0.14.3",
27 | "nodemon": "^1.17.3"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/demo/chapter-middleware/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chapter",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "node example.js",
8 | "dev": "./node_modules/.bin/nodemon example.js",
9 | "fix": "./node_modules/.bin/eslint --fix --ext .js ./",
10 | "precommit": "./node_modules/.bin/eslint --ext .jsx,.js ./",
11 | "prepush": "./node_modules/.bin/eslint --ext .jsx,.js ./"
12 | },
13 | "author": "chenshenhai",
14 | "license": "MIT",
15 | "dependencies": {
16 | "koa": "^2.5.0"
17 | },
18 | "devDependencies": {
19 | "eslint": "^4.19.1",
20 | "eslint-config-koa": "^2.0.2",
21 | "eslint-config-standard": "^11.0.0",
22 | "eslint-plugin-import": "^2.10.0",
23 | "eslint-plugin-node": "^6.0.1",
24 | "eslint-plugin-promise": "^3.7.0",
25 | "eslint-plugin-standard": "^3.0.1",
26 | "husky": "^0.14.3",
27 | "nodemon": "^1.17.3"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/demo/chapter-03-03-02/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chapter-03-03-02",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "node index.js",
8 | "dev": "./node_modules/.bin/nodemon index.js",
9 | "fix": "./node_modules/.bin/eslint --fix --ext .js ./",
10 | "precommit": "./node_modules/.bin/eslint --ext .jsx,.js ./",
11 | "prepush": "./node_modules/.bin/eslint --ext .jsx,.js ./"
12 | },
13 | "author": "chenshenhai",
14 | "license": "MIT",
15 | "dependencies": {
16 | "koa": "^2.5.0"
17 | },
18 | "devDependencies": {
19 | "eslint": "^4.19.1",
20 | "eslint-config-koa": "^2.0.2",
21 | "eslint-config-standard": "^11.0.0",
22 | "eslint-plugin-import": "^2.10.0",
23 | "eslint-plugin-node": "^6.0.1",
24 | "eslint-plugin-promise": "^3.7.0",
25 | "eslint-plugin-standard": "^3.0.1",
26 | "husky": "^0.14.3",
27 | "nodemon": "^1.17.3"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/demo/chapter-05-03/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chapter",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "node example/index.js",
8 | "dev": "./node_modules/.bin/nodemon example/index.js",
9 | "fix": "./node_modules/.bin/eslint --fix --ext .js ./",
10 | "precommit": "./node_modules/.bin/eslint --ext .jsx,.js ./",
11 | "prepush": "./node_modules/.bin/eslint --ext .jsx,.js ./"
12 | },
13 | "author": "chenshenhai",
14 | "license": "MIT",
15 | "dependencies": {
16 | "koa": "^2.5.0"
17 | },
18 | "devDependencies": {
19 | "eslint": "^4.19.1",
20 | "eslint-config-koa": "^2.0.2",
21 | "eslint-config-standard": "^11.0.0",
22 | "eslint-plugin-import": "^2.10.0",
23 | "eslint-plugin-node": "^6.0.1",
24 | "eslint-plugin-promise": "^3.7.0",
25 | "eslint-plugin-standard": "^3.0.1",
26 | "husky": "^0.14.3",
27 | "nodemon": "^1.17.3"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/demo/chapter-01-05/example-simple.js:
--------------------------------------------------------------------------------
1 | let context = {
2 | data: []
3 | };
4 |
5 | async function middleware1(ctx, next) {
6 | console.log('action 001');
7 | ctx.data.push(1);
8 | await next();
9 | console.log('action 006');
10 | ctx.data.push(6);
11 | }
12 |
13 | async function middleware2(ctx, next) {
14 | console.log('action 002');
15 | ctx.data.push(2);
16 | await next();
17 | console.log('action 005');
18 | ctx.data.push(5);
19 | }
20 |
21 | async function middleware3(ctx, next) {
22 | console.log('action 003');
23 | ctx.data.push(3);
24 | await next();
25 | console.log('action 004');
26 | ctx.data.push(4);
27 | }
28 |
29 | Promise.resolve(middleware1(context, async() => {
30 | return Promise.resolve(middleware2(context, async() => {
31 | return Promise.resolve(middleware3(context, async() => {
32 | return Promise.resolve();
33 | }));
34 | }));
35 | }))
36 | .then(() => {
37 | console.log('end');
38 | console.log('context = ', context);
39 | });
40 |
--------------------------------------------------------------------------------
/demo/chapter-06-02/index.js:
--------------------------------------------------------------------------------
1 | const compose = require('./compose');
2 |
3 | function mount(prefix, app) {
4 | let middleware = app.middleware;
5 | let middlewareStream = compose(middleware || []);
6 | if (prefix === '/') {
7 | return middlewareStream;
8 | }
9 |
10 | return async function(ctx, next) {
11 | let mountPath = matchPath(ctx.path);
12 | if (!mountPath) {
13 | await next();
14 | return;
15 | }
16 |
17 | let originPath = ctx.path;
18 | ctx.path = mountPath;
19 |
20 | await middlewareStream(ctx, async() => {
21 | ctx.path = originPath;
22 | await next();
23 | ctx.path = mountPath;
24 | });
25 |
26 | ctx.path = originPath;
27 | };
28 |
29 | function matchPath(originPath) {
30 | if (originPath.indexOf(prefix) < 0) {
31 | return false;
32 | }
33 | const mountPath = originPath.replace(prefix, '') || '/';
34 | if (mountPath[0] !== '/') {
35 | return false;
36 | }
37 | return mountPath;
38 | }
39 | }
40 |
41 | module.exports = mount;
42 |
--------------------------------------------------------------------------------
/demo/chapter-03-03-01/index.js:
--------------------------------------------------------------------------------
1 | const Koa = require('koa');
2 | let app = new Koa();
3 |
4 | class Middleware{
5 | constructor() {
6 | this.stack = [];
7 | }
8 |
9 | get(path, childMiddleware) {
10 | this.stack.push({ path, middleware: childMiddleware })
11 | }
12 |
13 | middlewares() {
14 | let stack = this.stack
15 | return async function(ctx, next) {
16 | let path = ctx.path;
17 | for( let i=0; i { ctx.body = 'page 001' })
30 | middleware.get('/page/002', async(ctx, next) => { ctx.body = 'page 002' })
31 | middleware.get('/page/003', async(ctx, next) => { ctx.body = 'page 003' })
32 |
33 | app.use(middleware.middlewares());
34 |
35 | app.listen(3001, function(){
36 | console.log('the demo is start at port 3001');
37 | })
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018-2020 ChenShenhai
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/note/chapter02/01.md:
--------------------------------------------------------------------------------
1 | # AOP 面向切面编程
2 |
3 | ## 什么是AOP?
4 |
5 | 什么是AOP?中文意思是面向切面编程,听起来感觉很模糊。先举个生产的例子。
6 |
7 | - 农场的水果包装流水线一开始只有 `采摘 - 清洗 - 贴标签`
8 |
9 | 
10 |
11 | - 为了提高销量,想加上两道工序 `分类` 和 `包装` 但又不能干扰原有的流程,同时如果没增加收益可以随时撤销新增工序。
12 |
13 | 
14 |
15 | - 最后在流水线的中的空隙插上两个工人去处理,形成`采摘 - 分类 - 清洗 - 包装 - 贴标签` 的新流程,而且工人可以随时撤回。
16 |
17 | 回到什么是AOP?就是在现有代码程序中,在程序生命周期或者横向流程中 `加入/减去` 一个或多个功能,不影响原有功能。
18 |
19 |
20 | ## Koa.js 的切面
21 |
22 | - 切面由中间件机制实现
23 | - 一个中间件一般有两个切面
24 | - 遵循先进后出的切面执行顺序,类似入栈出栈的顺序
25 |
26 | 
27 |
28 |
29 | ## 参考文章
30 |
31 | - [Intro to Aspect Oriented Programming](http://know.cujojs.com/tutorials/aop/intro-to-aspect-oriented-programming)
32 | - [面向切面编程(AOP)简介](http://bubkoo.com/2014/05/08/intro-to-aspect-oriented-programming/)
33 | - [用AOP改善javascript代码](http://www.alloyteam.com/2013/08/yong-aop-gai-shan-javascript-dai-ma/)
34 |
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (http://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # Typescript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 |
61 | demo/*/node_modules/
62 | demo/*/package-lock.json
63 | _book/
64 |
65 |
66 |
--------------------------------------------------------------------------------
/demo/chapter-middleware/example/002.js:
--------------------------------------------------------------------------------
1 | // 002
2 | const fs = require('fs');
3 | const path = require('path');
4 | const Koa = require('koa');
5 | const app = new Koa();
6 |
7 | const middleware = function() {
8 | return async function(ctx, next) {
9 | if (ctx.render) await next();
10 | ctx.render = function(fileName = '404.html') {
11 | return new Promise((resolve, reject) => {
12 | try {
13 | const fullPath = path.join(__dirname, 'view', fileName);
14 | let content = `[view] ${fileName} is Not Found`;
15 | if (fs.existsSync(fullPath)) {
16 | content = fs.readFileSync(fullPath, 'binary');
17 | }
18 | ctx.body = content;
19 | resolve();
20 | } catch (err) {
21 | reject(err);
22 | }
23 | });
24 | };
25 | await next();
26 | };
27 | };
28 |
29 | // middleware(app)
30 | app.use(middleware());
31 |
32 | app.use(async(ctx, next) => {
33 | if (ctx.path === '/' || ctx.path === '/index' || ctx.path === '/index') {
34 | await ctx.render('index.html');
35 | } else {
36 | await ctx.render('404.html');
37 | }
38 | });
39 |
40 | app.listen(3000, () => {
41 | console.log('[demo] is starting at port 3000');
42 | });
43 |
--------------------------------------------------------------------------------
/demo/chapter-middleware/example/004.js:
--------------------------------------------------------------------------------
1 | const Koa = require('koa');
2 | let app = new Koa();
3 |
4 | const middleware = function(app) {
5 | // 中间件在初始化实例 把getServerInfo方法 挂载代理到上下文
6 | app.context.getServerInfo = function() {
7 | function parseMem( mem = 0 ) {
8 | let memVal = mem / 1024 / 1024;
9 | memVal = memVal.toFixed(2) + 'MB';
10 | return memVal;
11 | }
12 |
13 | function getMemInfo() {
14 | let memUsage = process.memoryUsage();
15 | let rss = parseMem(memUsage.rss);
16 | let heapTotal = parseMem(memUsage.heapTotal);
17 | let heapUsed = parseMem(memUsage.heapUsed);
18 | return {
19 | pid: process.pid,
20 | rss,
21 | heapTotal,
22 | heapUsed
23 | }
24 | }
25 | return getMemInfo()
26 | };
27 | }
28 |
29 | middleware(app);
30 |
31 | const page = async function(ctx, next) {
32 | const serverInfo = ctx.getServerInfo();
33 | ctx.body = `
34 |
35 |
36 |
37 | ${JSON.stringify(serverInfo)}
38 |
39 |
40 | `;
41 | }
42 |
43 | app.use(page);
44 |
45 | app.listen(3001, function(){
46 | console.log('the demo is start at port 3001');
47 | })
--------------------------------------------------------------------------------
/demo/chapter-03-02/ctx.js:
--------------------------------------------------------------------------------
1 | const Koa = require('koa');
2 | let app = new Koa();
3 |
4 | const middleware = async function(ctx, next) {
5 | // 中间件 挂载上下文
6 | // 把所有当前服务的进程PID,内存使用情况方法挂载在ctx上
7 | ctx.getServerInfo = function() {
8 | function parseMem( mem = 0 ) {
9 | let memVal = mem / 1024 / 1024;
10 | memVal = memVal.toFixed(2) + 'MB';
11 | return memVal;
12 | }
13 |
14 | function getMemInfo() {
15 | let memUsage = process.memoryUsage();
16 | let rss = parseMem(memUsage.rss);
17 | let heapTotal = parseMem(memUsage.heapTotal);
18 | let heapUsed = parseMem(memUsage.heapUsed);
19 | return {
20 | pid: process.pid,
21 | rss,
22 | heapTotal,
23 | heapUsed
24 | }
25 | }
26 | return getMemInfo()
27 | };
28 | await next();
29 | }
30 |
31 | const page = async function(ctx, next) {
32 | const serverInfo = ctx.getServerInfo();
33 | ctx.body = `
34 |
35 |
36 |
37 | ${JSON.stringify(serverInfo)}
38 |
39 |
40 | `;
41 | }
42 |
43 | app.use(middleware);
44 | app.use(page);
45 |
46 | app.listen(3001, function(){
47 | console.log('the demo is start at port 3001');
48 | })
--------------------------------------------------------------------------------
/demo/chapter-04-03/index.js:
--------------------------------------------------------------------------------
1 | const {resolve} = require('path');
2 | const send = require('./send');
3 |
4 | function statics(opts = {
5 | root: ''
6 | }) {
7 | opts.root = resolve(opts.root);
8 |
9 | // 是否需要等待其他请求
10 | if (opts.defer !== true) {
11 | // 如果需要等待其他请求
12 | return async function statics(ctx, next) {
13 | let done = false;
14 |
15 | if (ctx.method === 'HEAD' || ctx.method === 'GET') {
16 | try {
17 | await send(ctx, ctx.path, opts);
18 | done = true;
19 | } catch (err) {
20 | if (err.status !== 404) {
21 | throw err;
22 | }
23 | }
24 | }
25 |
26 | if (!done) {
27 | await next();
28 | }
29 | };
30 | } else {
31 | // 如果不需要等待其他请求
32 | return async function statics(ctx, next) {
33 | await next();
34 |
35 | if (ctx.method !== 'HEAD' && ctx.method !== 'GET') {
36 | return;
37 | }
38 |
39 | if (ctx.body != null || ctx.status !== 404) {
40 | return;
41 | }
42 |
43 | try {
44 | await send(ctx, ctx.path, opts);
45 | } catch (err) {
46 | if (err.status !== 404) {
47 | throw err;
48 | }
49 | }
50 | };
51 | }
52 | }
53 |
54 | module.exports = statics;
55 |
--------------------------------------------------------------------------------
/SUMMARY.md:
--------------------------------------------------------------------------------
1 | # Summary
2 | * [Koa.js 设计模式-学习笔记](README.md)
3 | * [1. Koa.js 原理]
4 | * [1.1 学习准备](note/chapter01/01.md)
5 | * [1.2 Promise 使用](note/chapter01/02.md)
6 | * [1.3 async/await 使用](note/chapter01/03.md)
7 | * [1.4 Node.js原生http模块](note/chapter01/04.md)
8 | * [1.5 中间件引擎](note/chapter01/05.md)
9 | * [1.6 普通中间件式HTTP服务实现](note/chapter01/06.md)
10 | * [1.7 最简Koa.js实现](note/chapter01/07.md)
11 | * [2. Koa.js 的AOP设计]
12 | * [2.1 AOP面向切面编程](note/chapter02/01.md)
13 | * [2.2 洋葱模型切面](note/chapter02/02.md)
14 | * [2.3 HTTP切面流程](note/chapter02/03.md)
15 | * [3. Koa.js 中间件]
16 | * [3.1 中间件分类](note/chapter03/01.md)
17 | * [3.2 狭义中间件](note/chapter03/02.md)
18 | * [3.3 广义中间件](note/chapter03/03.md)
19 | * [4. 狭义中间件-请求/响应拦截]
20 | * [4.1 koa-logger 实现](note/chapter04/01.md)
21 | * [4.2 koa-send 实现](note/chapter04/02.md)
22 | * [4.3 koa-static 实现](note/chapter04/03.md)
23 | * [5. 狭义中间件-context代理]
24 | * [5.1 koa-view 实现](note/chapter05/01.md)
25 | * [5.2 koa-jsonp 实现](note/chapter05/02.md)
26 | * [5.3 koa-bodyparser 实现](note/chapter05/03.md)
27 | * [6. 广义中间件-间接中间件处理]
28 | * [6.1 koa-router 实现](note/chapter06/01.md)
29 | * [6.2 koa-mount 实现](note/chapter06/02.md)
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/demo/chapter-05-03/index.js:
--------------------------------------------------------------------------------
1 | const readStream = require('./lib/read_stream');
2 | let strictJSONReg = /^[\x20\x09\x0a\x0d]*(\[|\{)/;
3 |
4 | let jsonTypes = [
5 | 'application/json'
6 | ];
7 |
8 | let formTypes = [
9 | 'application/x-www-form-urlencoded'
10 | ];
11 |
12 | let textTypes = [
13 | 'text/plain'
14 | ];
15 |
16 | function parseQueryStr(queryStr) {
17 | let queryData = {};
18 | let queryStrList = queryStr.split('&');
19 | for (let [ index, queryStr ] of queryStrList.entries()) {
20 | let itemList = queryStr.split('=');
21 | queryData[ itemList[0] ] = decodeURIComponent(itemList[1]);
22 | }
23 | return queryData;
24 | }
25 |
26 | function bodyParser(opts = {}) {
27 | return async function(ctx, next) {
28 | if (!ctx.request.body && ctx.method === 'POST') {
29 | let body = await readStream(ctx.request.req);
30 | let result = body;
31 | if (ctx.request.is(formTypes)) {
32 | result = parseQueryStr(body);
33 | } else if (ctx.request.is(jsonTypes)) {
34 | if (strictJSONReg.test(body)) {
35 | try {
36 | result = JSON.parse(body);
37 | } catch (err) {
38 | ctx.throw(500, err);
39 | }
40 | }
41 | } else if (ctx.request.is(textTypes)) {
42 | result = body;
43 | }
44 |
45 | ctx.request.body = result;
46 | }
47 | await next();
48 | };
49 | }
50 |
51 | module.exports = bodyParser;
52 |
--------------------------------------------------------------------------------
/demo/chapter-06-01/index.js:
--------------------------------------------------------------------------------
1 | const methods = [
2 | 'GET',
3 | 'PUT',
4 | 'PATCH',
5 | 'POST',
6 | 'DELETE'
7 | ];
8 |
9 | class Layer {
10 | constructor(path, methods, middleware, opts) {
11 | this.path = path;
12 | this.methods = methods;
13 | this.middleware = middleware;
14 | this.opts = opts;
15 | }
16 | }
17 |
18 | class Router {
19 | constructor(opts = {}) {
20 | this.stack = [];
21 | }
22 |
23 | register(path, methods, middleware, opts) {
24 | let route = new Layer(path, methods, middleware, opts);
25 | this.stack.push(route);
26 | return this;
27 | }
28 |
29 | routes() {
30 | let stock = this.stack;
31 | return async function(ctx, next) {
32 | let currentPath = ctx.path;
33 | let route;
34 |
35 | for (let i = 0; i < stock.length; i++) {
36 | let item = stock[i];
37 | if (currentPath === item.path && item.methods.indexOf(ctx.method) >= 0) {
38 | route = item.middleware;
39 | break;
40 | }
41 | }
42 |
43 | if (typeof route === 'function') {
44 | route(ctx, next);
45 | return;
46 | }
47 |
48 | await next();
49 | };
50 | }
51 | }
52 |
53 | methods.forEach(method => {
54 | Router.prototype[method.toLowerCase()] = Router.prototype[method] = function(path, middleware) {
55 | this.register(path, [method], middleware);
56 | };
57 | });
58 |
59 | module.exports = Router;
60 |
--------------------------------------------------------------------------------
/note/chapter04/01.md:
--------------------------------------------------------------------------------
1 | # koa-logger 实现
2 |
3 | ## 前言
4 |
5 | 狭义中间件,请求/拦截 最显著的特征是
6 | - 直接被`app.use()`
7 | - 拦截请求
8 | - 操作响应
9 |
10 | 最简单的场景是 Koa.js 官方支持传输静态文件中间件的实现`koa-logger`。
11 |
12 |
13 | > 本节主要以官方的 `koa-logger` 中间件为参考,实现了一个最简单的`koa-logger` 实现,方便原理讲解和后续二次自定义优化开发。
14 |
15 |
16 | ## 实现步骤
17 |
18 | - step 01 拦截请求,打印请求URL
19 | - step 02 操作响应,打印响应URL
20 |
21 |
22 | ## 实现源码
23 |
24 | [https://github.com/chenshenhai/koajs-design-note/tree/master/demo/chapter-04-01](https://github.com/chenshenhai/koajs-design-note/tree/master/demo/chapter-04-01)
25 |
26 |
27 |
28 | ```sh
29 | ## 安装依赖
30 | npm i
31 |
32 | ## 执行 demo
33 | npm run start
34 |
35 | ## 最后启动chrome浏览器访问
36 | ## http://127.0.0.1:3000/hello
37 | ## http://127.0.0.1:3000/world
38 |
39 | ## 控制台显示结果
40 | <-- GET /hello
41 | --> GET /hello
42 | <-- GET /world
43 | --> GET /world
44 | ```
45 |
46 |
47 | ### 解读
48 |
49 | ```js
50 | const logger = async function(ctx, next) {
51 | let res = ctx.res;
52 |
53 | // 拦截操作请求 request
54 | console.log(`<-- ${ctx.method} ${ctx.url}`);
55 |
56 | await next();
57 |
58 | // 拦截操作响应 request
59 | res.on('finish', () => {
60 | console.log(`--> ${ctx.method} ${ctx.url}`);
61 | });
62 | };
63 |
64 | module.exports = logger
65 |
66 | ```
67 |
68 | ### 使用
69 |
70 | ```js
71 | const Koa = require('koa');
72 | const logger = require('./index');
73 | const app = new Koa();
74 |
75 | app.use(logger);
76 |
77 | app.use(async(ctx, next) => {
78 | ctx.body = 'hello world';
79 | });
80 |
81 | app.listen(3000, () => {
82 | console.log('[demo] is starting at port 3000');
83 | });
84 | ```
85 |
86 | ## 附录
87 |
88 | ### 参考
89 |
90 | [https://github.com/koajs/logger](https://github.com/koajs/logger)
91 |
92 |
--------------------------------------------------------------------------------
/demo/chapter-middleware/example/view/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | example
4 |
5 |
6 |
7 |
8 |
9 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
Hello, world!
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/demo/chapter-01-06/index.js:
--------------------------------------------------------------------------------
1 | const http = require('http');
2 | const Emitter = require('events');
3 |
4 | class WebServer extends Emitter {
5 | constructor() {
6 | super();
7 | this.middleware = [];
8 | this.context = Object.create({});
9 | }
10 |
11 | /**
12 | * 服务事件监听
13 | * @param {*} args
14 | */
15 | listen(...args) {
16 | const server = http.createServer(this.callback());
17 | return server.listen(...args);
18 | }
19 |
20 | /**
21 | * 注册使用中间件
22 | * @param {Function} fn
23 | */
24 | use(fn) {
25 | if (typeof fn === 'function') {
26 | this.middleware.push(fn);
27 | }
28 | }
29 |
30 | /**
31 | * 中间件总回调方法
32 | */
33 | callback() {
34 | let that = this;
35 |
36 | if (this.listeners('error').length === 0) {
37 | this.on('error', this.onerror);
38 | }
39 |
40 | const handleRequest = (req, res) => {
41 | let context = that.createContext(req, res);
42 | this.middleware.forEach((cb, idx) => {
43 | try {
44 | cb(context);
45 | } catch (err) {
46 | that.onerror(err);
47 | }
48 |
49 | if (idx + 1 >= this.middleware.length) {
50 | if (res && typeof res.end === 'function') {
51 | res.end();
52 | }
53 | }
54 | });
55 | };
56 | return handleRequest;
57 | }
58 |
59 | /**
60 | * 异常处理监听
61 | * @param {EndOfStreamError} err
62 | */
63 | onerror(err) {
64 | console.log(err);
65 | }
66 |
67 | /**
68 | * 创建通用上下文
69 | * @param {Object} req
70 | * @param {Object} res
71 | */
72 | createContext(req, res) {
73 | let context = Object.create(this.context);
74 | context.req = req;
75 | context.res = res;
76 | return context;
77 | }
78 | }
79 |
80 | module.exports = WebServer;
81 |
--------------------------------------------------------------------------------
/demo/chapter-05-03/lib/read_stream.js:
--------------------------------------------------------------------------------
1 | module.exports = readStream;
2 |
3 | function readStream(req) {
4 | return new Promise((resolve, reject) => {
5 | try {
6 | streamEventListen(req, (data, err) => {
7 | if (data && !isError(err)) {
8 | resolve(data);
9 | } else {
10 | reject(err);
11 | }
12 | });
13 | } catch (err) {
14 | reject(err);
15 | }
16 | });
17 | }
18 |
19 | function isError(err) {
20 | return Object.prototype.toString.call(err).toLowerCase() === '[object error]';
21 | }
22 |
23 | function streamEventListen(req, callback) {
24 | let stream = req.req || req;
25 | let chunk = [];
26 | let complete = false;
27 |
28 | // attach listeners
29 | stream.on('aborted', onAborted);
30 | stream.on('close', cleanup);
31 | stream.on('data', onData);
32 | stream.on('end', onEnd);
33 | stream.on('error', onEnd);
34 |
35 | function onAborted() {
36 | if (complete) {
37 | return;
38 | }
39 | callback(null, new Error('request body parse aborted'));
40 | }
41 |
42 | function cleanup() {
43 | stream.removeListener('aborted', onAborted);
44 | stream.removeListener('data', onData);
45 | stream.removeListener('end', onEnd);
46 | stream.removeListener('error', onEnd);
47 | stream.removeListener('close', cleanup);
48 | }
49 |
50 | function onData(data) {
51 | if (complete) {
52 | return;
53 | }
54 | if (data) {
55 | chunk.push(data.toString());
56 | }
57 | }
58 |
59 | function onEnd(err) {
60 | if (complete) {
61 | return;
62 | }
63 |
64 | if (isError(err)) {
65 | callback(null, err);
66 | return;
67 | }
68 |
69 | complete = true;
70 | let result = chunk.join('');
71 | chunk = [];
72 | callback(result, null);
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/demo/chapter-01-07/index.js:
--------------------------------------------------------------------------------
1 | const http = require('http');
2 | const Emitter = require('events');
3 | const compose = require('./compose');
4 |
5 | /**
6 | * 通用上下文
7 | */
8 | const context = {
9 | _body: null,
10 |
11 | get body() {
12 | return this._body;
13 | },
14 |
15 | set body(val) {
16 | this._body = val;
17 | this.res.end(this._body);
18 | }
19 | };
20 |
21 | class SimpleKoa extends Emitter {
22 | constructor() {
23 | super();
24 | this.middleware = [];
25 | this.context = Object.create(context);
26 | }
27 |
28 | /**
29 | * 服务事件监听
30 | * @param {*} args
31 | */
32 | listen(...args) {
33 | const server = http.createServer(this.callback());
34 | return server.listen(...args);
35 | }
36 |
37 | /**
38 | * 注册使用中间件
39 | * @param {Function} fn
40 | */
41 | use(fn) {
42 | if (typeof fn === 'function') {
43 | this.middleware.push(fn);
44 | }
45 | }
46 |
47 | /**
48 | * 中间件总回调方法
49 | */
50 | callback() {
51 | if (this.listeners('error').length === 0) {
52 | this.on('error', this.onerror);
53 | }
54 |
55 | const handleRequest = (req, res) => {
56 | let context = this.createContext(req, res);
57 | let middleware = this.middleware;
58 | // 执行中间件
59 | compose(middleware)(context).catch(err => this.onerror(err));
60 | };
61 | return handleRequest;
62 | }
63 |
64 | /**
65 | * 异常处理监听
66 | * @param {EndOfStreamError} err
67 | */
68 | onerror(err) {
69 | console.log(err);
70 | }
71 |
72 | /**
73 | * 创建通用上下文
74 | * @param {Object} req
75 | * @param {Object} res
76 | */
77 | createContext(req, res) {
78 | let context = Object.create(this.context);
79 | context.req = req;
80 | context.res = res;
81 | return context;
82 | }
83 | }
84 |
85 | module.exports = SimpleKoa;
86 |
--------------------------------------------------------------------------------
/note/chapter05/02.md:
--------------------------------------------------------------------------------
1 | # jsonp 实现
2 |
3 | > 初始化时候,实例代理上下文context实现
4 |
5 | ## 前言
6 |
7 | 实例代理的还有另外比较有代表性的中间件是官方提供 `koa-safe-jsonp` 中间件,把jsonp的方法挂载在`Koa`实例`app`的`app.context` 属性中。
8 |
9 | 常见实例代理上下文context实现步骤
10 |
11 | - 初始化一个`Koa`实例 `let app = new Koa()`
12 | - 将需要的属性或者方法 `demo` 挂载在 `app.context` 上,`app.context.demo`
13 | - 在`app.use()`中间件直接使用 `ctx.demo` 方法或属性
14 |
15 |
16 | 这里我们实现最简单的模板渲染中间件 `jsonp`,模仿`koa-safe-jsonp`的基本能力。
17 |
18 | ## 实现步骤
19 |
20 |
21 | `jsonp` 的实现步骤
22 |
23 | - step 01 初始化一个`Koa`实例 `let app = new Koa()`
24 | - step 02 将需要的属性或者方法 `jsonp` 挂载在 `app.context` 上,`app.context.jsonp`
25 | - step 03 在`app.use()`中间件直接使用 `ctx.jsonp` 方法或属性渲染模板
26 | - step 04 当前请求响应要返回jsonp数据时候 `ctx.body = ctx.jsonp(result)`
27 |
28 |
29 |
30 | ## 实现源码
31 |
32 | demo源码
33 |
34 | [https://github.com/chenshenhai/koajs-design-note/tree/master/demo/chapter-05-02](https://github.com/chenshenhai/koajs-design-note/tree/master/demo/chapter-05-02)
35 |
36 | ```sh
37 | ## 安装依赖
38 | npm i
39 |
40 | ## 执行 demo
41 | npm run start
42 |
43 | ## 最后启动chrome浏览器访问
44 | ## http://127.0.0.1:3000
45 |
46 | ```
47 |
48 | ### 解读
49 |
50 | ```js
51 | function jsonp(app, opts = {}) {
52 | let callback = opts.callback || 'callback';
53 |
54 | app.context.jsonp = function(obj = {}) {
55 | let ctx = this;
56 | if (Object.prototype.toString.call(obj).toLowerCase() === '[object object]') {
57 | let jsonpStr = `;${callback}(${JSON.stringify(obj)})`;
58 |
59 | // 用text/javascript,让请求支持跨域获取
60 | ctx.type = 'text/javascript';
61 |
62 | // 输出jsonp字符串
63 | ctx.body = jsonpStr;
64 | } else {
65 | ctx.throw(500, 'result most be a json');
66 | }
67 | };
68 | }
69 |
70 | module.exports = jsonp;
71 |
72 | ```
73 |
74 |
75 | ### 使用
76 |
77 | ```js
78 | const Koa = require('koa');
79 | const jsonp = require('./index');
80 | const app = new Koa();
81 |
82 | jsonp(app, {});
83 |
84 | app.use(async ctx => {
85 | await ctx.jsonp({
86 | data: 'this is a demo',
87 | success: true
88 | });
89 | });
90 |
91 | app.listen(3000, () => {
92 | console.log('[demo] jsonp is starting at port 3000');
93 | });
94 |
95 | ```
96 |
97 |
98 |
99 | ## 附录
100 |
101 | ### 参考
102 |
103 | - [https://github.com/koajs/koa-safe-jsonp](https://github.com/koajs/koa-safe-jsonp)
104 |
105 |
106 |
--------------------------------------------------------------------------------
/note/chapter03/03.md:
--------------------------------------------------------------------------------
1 | # 广义中间件
2 |
3 | ## 前言
4 |
5 | - 不直接提供中间件
6 | - 通过间接方式提供了中间件,最常见的是`间接中间件`和`子中间件`
7 | - 间接被 `app.use()` 加载
8 | - 其他方式接入Koa切面
9 |
10 |
11 | ## 间接中间件
12 |
13 | ```js
14 | const Koa = require('koa');
15 | let app = new Koa();
16 |
17 | function indirectMiddleware(path, middleware) {
18 | return async function(ctx, next) {
19 | console.log(ctx.path === path, middleware);
20 | if (ctx.path === path) {
21 | await middleware(ctx, next);
22 | } else {
23 | await next();
24 | }
25 | };
26 | }
27 |
28 | const index = async function(ctx, next) {
29 | ctx.body = 'this is index page';
30 | };
31 |
32 | const hello = async function(ctx, next) {
33 | ctx.body = 'this is hello page';
34 | };
35 |
36 | const world = async function(ctx, next) {
37 | ctx.body = 'this is world page';
38 | };
39 |
40 | app.use(indirectMiddleware('/', index));
41 | app.use(indirectMiddleware('/hello', hello));
42 | app.use(indirectMiddleware('/world', world));
43 |
44 | app.listen(3001, () => {
45 | console.log('the demo is start at port 3001');
46 | });
47 |
48 | ```
49 |
50 |
51 |
52 | ## 子中间件
53 |
54 | 子中间件是广义中间件的一个最有代表场景,主要的特点有
55 |
56 | - 初始化中间件时,内置子中间件列表
57 | - 子中间件列表添加子中间件元素
58 | - 子中间件列表封装成间接中间件,让后被`app.use()`加载
59 |
60 | ```js
61 | const Koa = require('koa');
62 | let app = new Koa();
63 |
64 | class Middleware{
65 | constructor() {
66 | this.stack = [];
67 | }
68 |
69 | get(path, childMiddleware) {
70 | this.stack.push({ path, middleware: childMiddleware })
71 | }
72 |
73 | middlewares() {
74 | let stack = this.stack;
75 | return async function(ctx, next) {
76 | let path = ctx.path;
77 | for( let i=0; i { ctx.body = 'page 001' })
90 | middleware.get('/page/002', async(ctx, next) => { ctx.body = 'page 002' })
91 | middleware.get('/page/003', async(ctx, next) => { ctx.body = 'page 003' })
92 |
93 | app.use(middleware.middlewares());
94 |
95 | app.listen(3001, function(){
96 | console.log('the demo is start at port 3001');
97 | })
98 | ```
99 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/note/chapter05/01.md:
--------------------------------------------------------------------------------
1 | # view实现
2 |
3 | > 初始化实例代理上下文context 实现
4 |
5 | ## 前言
6 |
7 | 狭义中间件区别于请求/响应拦截的另一种方式是上下文context代理。
8 |
9 | 上下文context代理分成两种
10 | - 实例代理上下文context
11 | - 请求过程代理上下文context
12 |
13 | 这里先将第一种代理方式——实例代理上下文context实现步骤,实例代理的比较有代表性的中间件是官方提 `koa-ejs` 中间件,把渲染的方法挂载在`Koa`实例`app`的`app.context` 属性中。
14 |
15 | 常见化实例代理上下文context实现步骤
16 |
17 | - 初始化一个`Koa`实例 `let app = new Koa()`
18 | - 将需要的属性或者方法 `demo` 挂载在 `app.context` 上,`app.context.demo`
19 | - 在`app.use()`中间件直接使用 `ctx.demo` 方法或属性
20 |
21 |
22 | 这里我们实现最简单的模板渲染中间件 `view`,模仿`koa-ejs`的基本能力。
23 |
24 | ## 实现步骤
25 |
26 |
27 | `view` 的实现步骤
28 |
29 | - step 01 初始化一个`Koa`实例 `let app = new Koa()`
30 | - step 02 将需要的属性或者方法 `view` 挂载在 `app.context` 上,`app.context.view`
31 | - step 03 在`app.use()`中间件直接使用 `ctx.view` 方法或属性渲染模板
32 |
33 |
34 |
35 | ## 实现源码
36 |
37 | demo源码
38 |
39 | [https://github.com/chenshenhai/koajs-design-note/tree/master/demo/chapter-05-01](https://github.com/chenshenhai/koajs-design-note/tree/master/demo/chapter-05-01)
40 |
41 | ```sh
42 | ## 安装依赖
43 | npm i
44 |
45 | ## 执行 demo
46 | npm run start
47 |
48 | ## 最后启动chrome浏览器访问
49 | ## http://127.0.0.1:3000/index
50 | ## http://127.0.0.1:3000/hello
51 | ```
52 |
53 | ### 解读
54 |
55 | ```js
56 | const path = require('path');
57 | const fs = require('fs');
58 |
59 | function view(app, opts = {}) {
60 | const {baseDir = ''} = opts;
61 |
62 | // 将需要的属性或者方法 `view` 挂载在 `app.context` 上,`app.context.view`
63 | app.context.view = function(page = '', obj = {}) {
64 | let ctx = this;
65 | let filePath = path.join(baseDir, page);
66 | if (fs.existsSync(filePath)) {
67 | let tpl = fs.readFileSync(filePath, 'binary');
68 | ctx.body = tpl;
69 | } else {
70 | ctx.throw(404);
71 | }
72 | };
73 | }
74 |
75 | module.exports = view;
76 |
77 | ```
78 |
79 | ### 使用
80 |
81 | - 使用目录
82 |
83 | ```sh
84 | .
85 | ├── example.js
86 | ├── index.js
87 | └── views
88 | ├── hello.html
89 | └── index.html
90 | ```
91 |
92 |
93 | ```js
94 | // example.js
95 |
96 | const Koa = require('koa');
97 | const path = require('path');
98 | const view = require('./index');
99 |
100 | // 初始化一个`Koa`实例 `let app = new Koa()`
101 | const app = new Koa();
102 |
103 | // 将需要的属性或者方法 `view` 挂载在 `app.context` 上,`app.context.view`
104 | view(app, {
105 | baseDir: path.join(__dirname, 'views')
106 | });
107 |
108 | app.use(async ctx => {
109 | await ctx.view(`${ctx.path}.html`, {
110 | title: 'index page'
111 | });
112 | });
113 |
114 | app.listen(3000, () => {
115 | console.log('[demo] jsonp is starting at port 3000');
116 | });
117 |
118 | ```
119 |
120 |
121 | ## 附录
122 |
123 | ### 参考
124 |
125 | - [https://github.com/koajs/ejs](https://github.com/koajs/ejs)
126 |
127 |
128 |
--------------------------------------------------------------------------------
/note/chapter01/04.md:
--------------------------------------------------------------------------------
1 | # Node.js原生http模块
2 |
3 | ## 前言
4 |
5 | Koa.js 是基于中间件模式的HTTP服务框架,底层原理是离不开Node.js的`http` 原生模块。
6 |
7 | ## http模块使用
8 |
9 | ```js
10 | const http = require('http');
11 | const PORT = 3001;
12 | const router = (req, res) => {
13 | res.end(`this page url = ${req.url}`);
14 | }
15 | const server = http.createServer(router)
16 | server.listen(PORT, function() {
17 | console.log(`the server is started at port ${PORT}`)
18 | })
19 | ```
20 |
21 | ## http服务构成
22 |
23 | ### 服务容器
24 |
25 | 这里的服务容器,是整个HTTP服务的基石,跟`apache`和`nginx`提供的能力是一致的。
26 |
27 | - 建立了通信连接
28 | - 指定了通信端口
29 | - 提供了可自定内容服务容器,也就是服务的回调函数的容器
30 |
31 | ```js
32 | const http = require('http');
33 | const PORT = 3001;
34 | const server = http.createServer((req, res) => {
35 | // TODO 容器内容
36 | // TODO 服务回调内容
37 | })
38 | server.listen(PORT, function() {
39 | console.log(`the server is started at port ${PORT}`)
40 | })
41 | ```
42 |
43 | ### 服务回调 (内容)
44 | 服务回调,可以理解成服务内容,主要提供服务的功能。
45 | - 解析服务的请求 `req`
46 | - 对请求内容作出响应 `res`
47 |
48 | ```js
49 | const router = (req, res) => {
50 | res.end(`this page url = ${req.url}`);
51 | }
52 | ```
53 |
54 | ### 请求 req
55 |
56 | 是服务回调中的第一个参数,主要是提供了HTTP请求`request`的内容和操作内容的方法。
57 |
58 | 更多操作建议查看 Node.js官方文档
59 |
60 | [https://nodejs.org/dist/latest-v8.x/docs/api/http.html](https://nodejs.org/dist/latest-v8.x/docs/api/http.html)
61 |
62 | [https://nodejs.org/dist/latest-v10.x/docs/api/http.html](https://nodejs.org/dist/latest-v10.x/docs/api/http.html)
63 |
64 |
65 | ### 响应 res
66 |
67 | 是服务回调中的第二个参数,主要是提供了HTTP响应`response`的内容和操作内容的方法。
68 |
69 | 注意:如果请求结束,一定要执行响应 `res.end()`,要不然请求会一直等待阻塞,直至连接断掉页面崩溃。
70 |
71 |
72 | 更多操作建议查看 Node.js官方文档
73 |
74 | [https://nodejs.org/dist/latest-v8.x/docs/api/http.html](https://nodejs.org/dist/latest-v8.x/docs/api/http.html)
75 |
76 |
77 | [https://nodejs.org/dist/latest-v10.x/docs/api/http.html](https://nodejs.org/dist/latest-v10.x/docs/api/http.html)
78 |
79 |
80 | ## 后续
81 |
82 | 通过以上的描述,主要HTTP服务内容是在 “`服务回调`” 中处理的,那我们来根据不同连接拆分一下,就形成了路由`router`,根据路由内容的拆分,就形成了控制器 `controller`。参考代码如下。
83 |
84 | ```js
85 | const http = require('http');
86 | const PORT = 3001;
87 |
88 | // 控制器
89 | const controller = {
90 | index(req, res) {
91 | res.end('This is index page')
92 | },
93 | home(req, res) {
94 | res.end('This is home page')
95 | },
96 | _404(req, res) {
97 | res.end('404 Not Found')
98 | }
99 | }
100 |
101 | // 路由器
102 | const router = (req, res) => {
103 | if( req.url === '/' ) {
104 | controller.index(req, res)
105 | } else if( req.url.startsWith('/home') ) {
106 | controller.home(req, res)
107 | } else {
108 | controller._404(req, res)
109 | }
110 | }
111 |
112 | // 服务
113 | const server = http.createServer(router)
114 | server.listen(PORT, function() {
115 | console.log(`the server is started at port ${PORT}`)
116 | })
117 | ```
--------------------------------------------------------------------------------
/note/chapter06/02.md:
--------------------------------------------------------------------------------
1 | # koa-mount 实现
2 |
3 | ## 前言
4 |
5 | 广义中间件,间接中间件方式实现,还有一个官方的中间件 `koa-mount` ,让多个Koa.js子应用合并成一个父应用,用请求的前缀区分子应用。这里基于第三方中间件 `koa-mount` 用最简单的方式实现 `koa-mount` 最简单功能。
6 |
7 | ## 实现步骤
8 |
9 | - 使用过程
10 | - 初始化子应用Koa.js实例
11 | - 初始化父应用Koa.js实例
12 | - 处理子应用middleware属性,将所有中间件用koa-compose封装成一个子应用中间件
13 | - 用父应用app.use()加载处理后的子应用中间件
14 | - mount实现过程
15 | - 输入子应用的前缀和应用实例
16 | - 获取子应用的中间件集合middleware属性
17 | - 用koa-compose封装子应用的中间件集合
18 | - 返回一个父中间件
19 | - 父中间件里执行compose封装后的子中间件集合
20 | - 执行前把请求path子应用前缀去掉
21 | - 执行后把请求path子应用前缀还原到原始请求path
22 | - 父应用app.use子应用封装后父中间件,(compose封装的子应用所有中间件)
23 |
24 | ## 实现源码
25 |
26 |
27 | [https://github.com/chenshenhai/koajs-design-note/tree/master/demo/chapter-06-02](https://github.com/chenshenhai/koajs-design-note/tree/master/demo/chapter-06-02)
28 |
29 |
30 |
31 | ```sh
32 | ## 安装依赖
33 | npm i
34 |
35 | ## 执行 demo
36 | npm run start
37 |
38 | ## 最后启动chrome浏览器访问
39 | ## http://127.0.0.1:3000/app1
40 | ## http://127.0.0.1:3000/app1
41 | ```
42 |
43 |
44 | ### 依赖
45 |
46 | - `koa-compose`
47 |
48 | ### 解读
49 |
50 | ```js
51 | const path = require('path');
52 | const compose = require('./compose');
53 |
54 | function mount(prefix, app) {
55 | let middleware = app.middleware;
56 | let middlewareStream = compose(middleware || []);
57 | if( prefix === '/' ) {
58 | return middlewareStream;
59 | }
60 |
61 | return async function( ctx, next ) {
62 | let mountPath = matchPath(ctx.path);
63 | if( !mountPath ) {
64 | return await next();
65 | }
66 |
67 | let originPath = ctx.path;
68 | ctx.path = mountPath;
69 |
70 | await middlewareStream(ctx, async () => {
71 | ctx.path = originPath;
72 | await next();
73 | ctx.path = mountPath
74 | });
75 |
76 | ctx.path = originPath;
77 | }
78 |
79 | function matchPath( originPath ) {
80 | if( originPath.indexOf(prefix) < 0 ) {
81 | return false;
82 | }
83 | const mountPath = originPath.replace(prefix, '') || '/';
84 | if( mountPath[0] !== '/' ) {
85 | return false;
86 | }
87 | return mountPath;
88 | }
89 |
90 | }
91 |
92 | module.exports = mount;
93 | ```
94 |
95 | ### 使用
96 |
97 | ```js
98 | const mount = require('./index');
99 | const Koa = require('koa');
100 |
101 | const app1 = new Koa();
102 | const app2 = new Koa();
103 |
104 | app1.use(async (ctx, next) => {
105 | await next()
106 | ctx.body = 'app 1'
107 | })
108 |
109 |
110 | app2.use(async (ctx, next) => {
111 | await next()
112 | ctx.body = 'app 2'
113 | })
114 |
115 | const app = new Koa()
116 |
117 | app.use(mount('/app1', app1))
118 | app.use(mount('/app2', app2))
119 |
120 | app.listen(3000)
121 | console.log('listening on port 3000');
122 |
123 | ```
124 |
125 |
126 | ## 附录
127 |
128 | ### 参考
129 |
130 | - [https://github.com/koajs/mount](https://github.com/koajs/mount)
--------------------------------------------------------------------------------
/note/chapter04/03.md:
--------------------------------------------------------------------------------
1 | # koa-static 实现
2 |
3 | ## 前言
4 |
5 | 狭义中间件 请求/拦截,最典型的场景是 Koa.js 传输静态文件中间件的实现`koa-send`。Koa.js 官方对 `koa-send` 进行二次封装,推出了`koa-static` 中间件,目标是用于做静态服务器或者项目静态资源管理。
6 |
7 |
8 |
9 | > 本节主要以官方的 `koa-static` 中间件为参考,基于上一节实现的最简单`koa-send`, 实现了一个最简单的`koa-static` 中间件,方便原理讲解和后续二次自定义优化开发。
10 |
11 |
12 |
13 | ## 实现步骤
14 |
15 | - step 01 配置静态资源绝对目录地址
16 | - step 02 判断是否支持等待其他请求
17 | - step 03 判断是否为 GET 和 HEAD 类型的请求
18 | - step 04 通过`koa-send` 中间件读取和返回静态文件
19 |
20 |
21 | ## 实现源码
22 |
23 | demo源码
24 |
25 | [https://github.com/chenshenhai/koajs-design-note/tree/master/demo/chapter-04-03](https://github.com/chenshenhai/koajs-design-note/tree/master/demo/chapter-04-03)
26 |
27 | ```sh
28 | ## 安装依赖
29 | npm i
30 |
31 | ## 执行 demo
32 | npm run start
33 |
34 | ## 最后启动chrome浏览器访问
35 | ## http://127.0.0.1:3000/index.html
36 | ```
37 |
38 | ### koa-static 依赖
39 |
40 | `koa-send` 中间件,这里只用了上一节实现的最简单`koa-send`
41 |
42 | ### koa-static 解读
43 |
44 | ```js
45 | const {resolve} = require('path');
46 | const send = require('./send');
47 |
48 | function statics(opts = {
49 | root: ''
50 | }) {
51 | opts.root = resolve(opts.root);
52 |
53 | // 是否需要等待其他请求
54 | if (opts.defer !== true) {
55 | // 如果需要等待其他请求
56 | return async function statics(ctx, next) {
57 | let done = false;
58 |
59 | if (ctx.method === 'HEAD' || ctx.method === 'GET') {
60 | try {
61 | await send(ctx, ctx.path, opts);
62 | done = true;
63 | } catch (err) {
64 | if (err.status !== 404) {
65 | throw err;
66 | }
67 | }
68 | }
69 |
70 | if (!done) {
71 | await next();
72 | }
73 | };
74 | } else {
75 | // 如果不需要等待其他请求
76 | return async function statics(ctx, next) {
77 | await next();
78 |
79 | if (ctx.method !== 'HEAD' && ctx.method !== 'GET') {
80 | return;
81 | }
82 |
83 | if (ctx.body != null || ctx.status !== 404) {
84 | return;
85 | }
86 |
87 | try {
88 | await send(ctx, ctx.path, opts);
89 | } catch (err) {
90 | if (err.status !== 404) {
91 | throw err;
92 | }
93 | }
94 | };
95 | }
96 | }
97 |
98 | module.exports = statics;
99 |
100 | ```
101 |
102 | ### koa-static 使用
103 |
104 | ```js
105 | const path = require('path');
106 | const Koa = require('koa');
107 | const statics = require('./index');
108 |
109 | const app = new Koa();
110 |
111 | const root = path.join(__dirname, './public');
112 | app.use(statics({ root }));
113 |
114 | app.use(async(ctx, next) => {
115 | if (ctx.path === '/hello') {
116 | ctx.body = 'hello world';
117 | }
118 | });
119 |
120 | app.listen(3000);
121 | console.log('listening on port 3000');
122 |
123 | ```
124 |
125 |
126 | ## 附录
127 |
128 | ### 参考
129 |
130 | - [https://github.com/koajs/static](https://github.com/koajs/static)
131 |
132 |
133 |
--------------------------------------------------------------------------------
/note/chapter01/06.md:
--------------------------------------------------------------------------------
1 | # 普通中间件式HTTP服务实现
2 |
3 | ## 前言
4 |
5 | 用过`Express.js`和`Koa.js`的人会发现使用方式很类似,也是基于`中间件`的理念去实现Web服务。
6 |
7 | 直接以`Express.js`回调式的中间件服务比较容易理解。再基于回调式的中间件服务接入`Koa.js`的中间件引擎去处理回调嵌套的处理。
8 |
9 | 这一章主要以原生的Node.js实现纯回调的中间件HTTP服务。
10 |
11 | ## 必要条件
12 |
13 | - 内置中间件队列
14 | - 中间件遍历机制
15 | - 异常处理机制
16 |
17 | ## 最简实现
18 |
19 | - demo源码
20 |
21 |
22 | [https://github.com/chenshenhai/koajs-design-note/tree/master/demo/chapter-01-06](https://github.com/chenshenhai/koajs-design-note/tree/master/demo/chapter-01-06)
23 |
24 | - 服务类封装
25 |
26 | ```js
27 | const http = require('http');
28 | const Emitter = require('events');
29 |
30 | class WebServer extends Emitter {
31 |
32 | constructor() {
33 | super();
34 | this.middleware = [];
35 | this.context = Object.create({});
36 | }
37 |
38 | /**
39 | * 服务事件监听
40 | * @param {*} args
41 | */
42 | listen(...args) {
43 | const server = http.createServer(this.callback());
44 | return server.listen(...args);
45 | }
46 |
47 | /**
48 | * 注册使用中间件
49 | * @param {Function} fn
50 | */
51 | use(fn) {
52 | if (typeof fn === 'function') {
53 | this.middleware.push(fn);
54 | }
55 | }
56 |
57 | /**
58 | * 中间件总回调方法
59 | */
60 | callback() {
61 | let that = this;
62 |
63 | if (this.listeners('error').length === 0) {
64 | this.on('error', this.onerror);
65 | }
66 |
67 | const handleRequest = (req, res) => {
68 | let context = that.createContext(req, res);
69 | this.middleware.forEach((cb, idx) => {
70 | try {
71 | cb(context);
72 | } catch (err) {
73 | that.onerror(err);
74 | }
75 |
76 | if (idx + 1 >= this.middleware.length) {
77 | if (res && typeof res.end === 'function') {
78 | res.end();
79 | }
80 | }
81 | });
82 | };
83 | return handleRequest;
84 | }
85 |
86 | /**
87 | * 异常处理监听
88 | * @param {EndOfStreamError} err
89 | */
90 | onerror(err) {
91 | console.log(err);
92 | }
93 |
94 | /**
95 | * 创建通用上下文
96 | * @param {Object} req
97 | * @param {Object} res
98 | */
99 | createContext(req, res) {
100 | let context = Object.create(this.context);
101 | context.req = req;
102 | context.res = res;
103 | return context;
104 | }
105 | }
106 |
107 | module.exports = WebServer;
108 |
109 | ```
110 |
111 | - 服务使用
112 |
113 | ```js
114 | const WebServer = require('./index');
115 |
116 | const app = new WebServer();
117 | const PORT = 3001;
118 |
119 | app.use(ctx => {
120 | ctx.res.write('line 1
');
121 | });
122 |
123 | app.use(ctx => {
124 | ctx.res.write('line 2
');
125 | });
126 |
127 | app.use(ctx => {
128 | ctx.res.write('line 3
');
129 | });
130 |
131 | app.listen(PORT, () => {
132 | console.log(`the web server is starting at port ${PORT}`);
133 | });
134 |
135 | ```
136 |
--------------------------------------------------------------------------------
/demo/chapter-04-02/index.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const {
4 | basename,
5 | extname
6 | } = path;
7 |
8 | const defaultOpts = {
9 | root: '',
10 | maxage: 0,
11 | immutable: false,
12 | extensions: false,
13 | hidden: false,
14 | brotli: false,
15 | gzip: false,
16 | setHeaders: () => {}
17 | };
18 |
19 | async function send(ctx, urlPath, opts = defaultOpts) {
20 | const { root, hidden, immutable, maxage, brotli, gzip, setHeaders } = opts;
21 | let filePath = urlPath;
22 |
23 | // step 01: normalize path
24 | // 配置静态资源绝对目录地址
25 | try {
26 | filePath = decodeURIComponent(filePath);
27 | // check legal path
28 | if (/[\.]{2,}/ig.test(filePath)) {
29 | ctx.throw(403, 'Forbidden');
30 | }
31 | } catch (err) {
32 | ctx.throw(400, 'failed to decode');
33 | }
34 |
35 | filePath = path.join(root, urlPath);
36 | const fileBasename = basename(filePath);
37 |
38 | // step 02: check hidden file support
39 | // 判断是否支持隐藏文件
40 | if (hidden !== true && fileBasename.startsWith('.')) {
41 | ctx.throw(404, '404 Not Found');
42 | return;
43 | }
44 |
45 | // step 03: stat
46 | // 获取文件或者目录信息
47 | let stats;
48 | try {
49 | stats = fs.statSync(filePath);
50 | if (stats.isDirectory()) {
51 | ctx.throw(404, '404 Not Found');
52 | }
53 | } catch (err) {
54 | const notfound = ['ENOENT', 'ENAMETOOLONG', 'ENOTDIR'];
55 | if (notfound.includes(err.code)) {
56 | ctx.throw(404, '404 Not Found');
57 | return;
58 | }
59 | err.status = 500;
60 | throw err;
61 | }
62 |
63 | let encodingExt = '';
64 | // step 04 check zip
65 | // 判断是否需要压缩
66 | if (ctx.acceptsEncodings('br', 'identity') === 'br' && brotli && (fs.existsSync(filePath + '.br'))) {
67 | filePath = filePath + '.br';
68 | ctx.set('Content-Encoding', 'br');
69 | ctx.res.removeHeader('Content-Length');
70 | encodingExt = '.br';
71 | } else if (ctx.acceptsEncodings('gzip', 'identity') === 'gzip' && gzip && (fs.existsSync(filePath + '.gz'))) {
72 | filePath = filePath + '.gz';
73 | ctx.set('Content-Encoding', 'gzip');
74 | ctx.res.removeHeader('Content-Length');
75 | encodingExt = '.gz';
76 | }
77 |
78 | // step 05 setHeaders
79 | // 设置HTTP头信息
80 | if (typeof setHeaders === 'function') {
81 | setHeaders(ctx.res, filePath, stats);
82 | }
83 |
84 | ctx.set('Content-Length', stats.size);
85 | if (!ctx.response.get('Last-Modified')) {
86 | ctx.set('Last-Modified', stats.mtime.toUTCString());
87 | }
88 | if (!ctx.response.get('Cache-Control')) {
89 | const directives = ['max-age=' + (maxage / 1000 | 0)];
90 | if (immutable) {
91 | directives.push('immutable');
92 | }
93 | ctx.set('Cache-Control', directives.join(','));
94 | }
95 |
96 | const ctxType = encodingExt !== '' ? extname(basename(filePath, encodingExt)) : extname(filePath);
97 | ctx.type = ctxType;
98 |
99 | // step 06 stream
100 | // 静态文件读取
101 | ctx.body = fs.createReadStream(filePath);
102 | }
103 |
104 | module.exports = send;
105 |
--------------------------------------------------------------------------------
/demo/chapter-04-03/send.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const {
4 | basename,
5 | extname
6 | } = path;
7 |
8 | const defaultOpts = {
9 | root: '',
10 | maxage: 0,
11 | immutable: false,
12 | extensions: false,
13 | hidden: false,
14 | brotli: false,
15 | gzip: false,
16 | setHeaders: () => {}
17 | };
18 |
19 | async function send(ctx, urlPath, opts = defaultOpts) {
20 | const { root, hidden, immutable, maxage, brotli, gzip, setHeaders } = opts;
21 | let filePath = urlPath;
22 |
23 | // step 01: normalize path
24 | // 配置静态资源绝对目录地址
25 | try {
26 | filePath = decodeURIComponent(filePath);
27 | // check legal path
28 | if (/[\.]{2,}/ig.test(filePath)) {
29 | ctx.throw(403, 'Forbidden');
30 | }
31 | } catch (err) {
32 | ctx.throw(400, 'failed to decode');
33 | }
34 |
35 | filePath = path.join(root, urlPath);
36 | const fileBasename = basename(filePath);
37 |
38 | // step 02: check hidden file support
39 | // 判断是否支持隐藏文件
40 | if (hidden !== true && fileBasename.startsWith('.')) {
41 | ctx.throw(404, '404 Not Found');
42 | return;
43 | }
44 |
45 | // step 03: stat
46 | // 获取文件或者目录信息
47 | let stats;
48 | try {
49 | stats = fs.statSync(filePath);
50 | if (stats.isDirectory()) {
51 | ctx.throw(404, '404 Not Found');
52 | }
53 | } catch (err) {
54 | const notfound = ['ENOENT', 'ENAMETOOLONG', 'ENOTDIR'];
55 | if (notfound.includes(err.code)) {
56 | ctx.throw(404, '404 Not Found');
57 | return;
58 | }
59 | err.status = 500;
60 | throw err;
61 | }
62 |
63 | let encodingExt = '';
64 | // step 04 check zip
65 | // 判断是否需要压缩
66 | if (ctx.acceptsEncodings('br', 'identity') === 'br' && brotli && (fs.existsSync(filePath + '.br'))) {
67 | filePath = filePath + '.br';
68 | ctx.set('Content-Encoding', 'br');
69 | ctx.res.removeHeader('Content-Length');
70 | encodingExt = '.br';
71 | } else if (ctx.acceptsEncodings('gzip', 'identity') === 'gzip' && gzip && (fs.existsSync(filePath + '.gz'))) {
72 | filePath = filePath + '.gz';
73 | ctx.set('Content-Encoding', 'gzip');
74 | ctx.res.removeHeader('Content-Length');
75 | encodingExt = '.gz';
76 | }
77 |
78 | // step 05 setHeaders
79 | // 设置HTTP头信息
80 | if (typeof setHeaders === 'function') {
81 | setHeaders(ctx.res, filePath, stats);
82 | }
83 |
84 | ctx.set('Content-Length', stats.size);
85 | if (!ctx.response.get('Last-Modified')) {
86 | ctx.set('Last-Modified', stats.mtime.toUTCString());
87 | }
88 | if (!ctx.response.get('Cache-Control')) {
89 | const directives = ['max-age=' + (maxage / 1000 | 0)];
90 | if (immutable) {
91 | directives.push('immutable');
92 | }
93 | ctx.set('Cache-Control', directives.join(','));
94 | }
95 |
96 | const ctxType = encodingExt !== '' ? extname(basename(filePath, encodingExt)) : extname(filePath);
97 | ctx.type = ctxType;
98 |
99 | // step 06 stream
100 | // 静态文件读取
101 | ctx.body = fs.createReadStream(filePath);
102 | }
103 |
104 | module.exports = send;
105 |
--------------------------------------------------------------------------------
/note/chapter06/01.md:
--------------------------------------------------------------------------------
1 | # koa-router 实现
2 |
3 | ## 前言
4 |
5 | 广义中间件,间接中间件方式
6 |
7 | - 不直接提供中间件
8 | - 通过间接方式提供了中间件,最常见的是`间接中间件`和`子中间件`
9 | - 间接被 `app.use()` 加载
10 | - 其他方式接入Koa切面
11 |
12 | 这里 广义中间件,间接中间件方式实现 最代表性是第三方实现的 `koa-router` 中间件,这里基于第三方中间件 `koa-router` 用最简单的方式实现 `koa-router` 最简单功能。
13 |
14 |
15 | ## 实现步骤
16 |
17 | - 初始化路由实例
18 | - 注册路由请求信息缓存到实例中
19 | - 请求类型
20 | - 请求path
21 | - 对应的请求后操作
22 | - 注册的路由操作就是子中间件
23 | - 路由实例输出父中间件
24 | - 返回一个父中间件
25 | - 中间件里对每次请求进行遍历匹配缓存中注册的路由操作
26 | - 匹配上请求类型,路径就执行对应路由子中间件
27 | - app.use()路由实例返回的父中间件
28 |
29 | ## 实现源码
30 |
31 | [https://github.com/chenshenhai/koajs-design-note/tree/master/demo/chapter-06-01](https://github.com/chenshenhai/koajs-design-note/tree/master/demo/chapter-06-01)
32 |
33 |
34 |
35 | ```sh
36 | ## 安装依赖
37 | npm i
38 |
39 | ## 执行 demo
40 | npm run start
41 |
42 | ## 最后启动chrome浏览器访问
43 | ## http://127.0.0.1:3000/index
44 | ## http://127.0.0.1:3000/post
45 | ## http://127.0.0.1:3000/list
46 | ## http://127.0.0.1:3000/item
47 | ```
48 |
49 | ### 解读
50 |
51 | ```js
52 | const methods = [
53 | 'GET',
54 | 'PUT',
55 | 'PATCH',
56 | 'POST',
57 | 'DELETE'
58 | ];
59 |
60 | class Layer {
61 | constructor(path, methods, middleware, opts) {
62 | this.path = path;
63 | this.methods = methods;
64 | this.middleware = middleware;
65 | this.opts = opts;
66 | }
67 | }
68 |
69 | class Router {
70 | constructor(opts = {}) {
71 | this.stack = [];
72 | }
73 |
74 | register(path, methods, middleware, opts) {
75 | let route = new Layer(path, methods, middleware, opts);
76 | this.stack.push(route);
77 | return this;
78 | }
79 |
80 | routes() {
81 |
82 | let stock = this.stack;
83 | return async function(ctx, next) {
84 | let currentPath = ctx.path;
85 | let route;
86 |
87 | for (let i = 0; i < stock.length; i++) {
88 | let item = stock[i];
89 | if (currentPath === item.path && item.methods.indexOf(ctx.method) >= 0) {
90 | route = item.middleware;
91 | break;
92 | }
93 | }
94 |
95 | if (typeof route === 'function') {
96 | route(ctx, next);
97 | return;
98 | }
99 |
100 | await next();
101 | };
102 | }
103 | }
104 |
105 | methods.forEach(method => {
106 | Router.prototype[method.toLowerCase()] = Router.prototype[method] = function(path, middleware) {
107 | this.register(path, [method], middleware);
108 | };
109 | });
110 |
111 | module.exports = Router;
112 |
113 | ```
114 |
115 | ### 使用
116 |
117 | ```js
118 |
119 | const Koa = require('koa');
120 | const Router = require('./index');
121 | const app = new Koa();
122 |
123 | // 初始化路由实例
124 | const router = new Router();
125 |
126 | // 注册路由请求信息缓存到实例中
127 | router.get('/index', async ctx => { ctx.body = 'index page'; });
128 | router.get('/post', async ctx => { ctx.body = 'post page'; });
129 | router.get('/list', async ctx => { ctx.body = 'list page'; });
130 | router.get('/item', async ctx => { ctx.body = 'item page'; });
131 |
132 | // 路由实例输出父中间件 router.routes()
133 | app.use(router.routes());
134 |
135 | app.use(async ctx => {
136 | ctx.body = '404';
137 | });
138 |
139 | app.listen(3000);
140 | console.log('listening on port 3000');
141 |
142 | ```
143 |
144 | ## 附录
145 |
146 | ### 参考
147 |
148 | - [https://github.com/alexmingoia/koa-router](https://github.com/alexmingoia/koa-router)
--------------------------------------------------------------------------------
/note/chapter01/07.md:
--------------------------------------------------------------------------------
1 | # 最简Koa.js实现
2 |
3 | ## 前言
4 |
5 | 从上一章可以看到最简单的中间件式HTTP服务的实现,底层是基于回调嵌套去处理中间件队列。
6 |
7 | ```js
8 | /**
9 | * 中间件总回调方法
10 | */
11 | callback() {
12 | let that = this;
13 |
14 | if (this.listeners('error').length === 0) {
15 | this.on('error', this.onerror);
16 | }
17 |
18 | const handleRequest = (req, res) => {
19 | let context = that.createContext(req, res);
20 | this.middleware.forEach((cb, idx) => {
21 | try {
22 | cb(context);
23 | } catch (err) {
24 | that.onerror(err);
25 | }
26 |
27 | if (idx + 1 >= this.middleware.length) {
28 | if (res && typeof res.end === 'function') {
29 | res.end();
30 | }
31 | }
32 | });
33 | };
34 | return handleRequest;
35 | }
36 | ```
37 |
38 | 但是中间件越多,回调嵌套越深,代码的可读性和可扩展性就很差,所以这时候把回调嵌套转化成 `Promise` + `async/await` ,这个时候就转变成最简单的`Koa.js`实现。
39 |
40 | ## 必要条件
41 | - 通过上下文赋值可代替 `res.end()`
42 | - 洋葱模型的中间件机制
43 |
44 | ## 源码实现
45 |
46 | - demo源码
47 |
48 | [https://github.com/chenshenhai/koajs-design-note/tree/master/demo/chapter-01-07](https://github.com/chenshenhai/koajs-design-note/tree/master/demo/chapter-01-07)
49 |
50 | - 最简Koa.js 实现
51 |
52 | ```js
53 | const http = require('http');
54 | const Emitter = require('events');
55 | // 注意:这里的compose是前几章的中间件引擎源码
56 | const compose = require('./../compose');
57 |
58 | /**
59 | * 通用上下文
60 | */
61 | const context = {
62 | _body: null,
63 |
64 | get body() {
65 | return this._body;
66 | },
67 |
68 | set body(val) {
69 | this._body = val;
70 | this.res.end(this._body);
71 | }
72 | };
73 |
74 | class SimpleKoa extends Emitter {
75 | constructor() {
76 | super();
77 | this.middleware = [];
78 | this.context = Object.create(context);
79 | }
80 |
81 | /**
82 | * 服务事件监听
83 | * @param {*} args
84 | */
85 | listen(...args) {
86 | const server = http.createServer(this.callback());
87 | return server.listen(...args);
88 | }
89 |
90 | /**
91 | * 注册使用中间件
92 | * @param {Function} fn
93 | */
94 | use(fn) {
95 | if (typeof fn === 'function') {
96 | this.middleware.push(fn);
97 | }
98 | }
99 |
100 | /**
101 | * 中间件总回调方法
102 | */
103 | callback() {
104 |
105 | if (this.listeners('error').length === 0) {
106 | this.on('error', this.onerror);
107 | }
108 |
109 | const handleRequest = (req, res) => {
110 | let context = this.createContext(req, res);
111 | let middleware = this.middleware;
112 | // 执行中间件
113 | compose(middleware)(context).catch(err => this.onerror(err))
114 | };
115 | return handleRequest;
116 | }
117 |
118 | /**
119 | * 异常处理监听
120 | * @param {EndOfStreamError} err
121 | */
122 | onerror(err) {
123 | console.log(err);
124 | }
125 |
126 | /**
127 | * 创建通用上下文
128 | * @param {Object} req
129 | * @param {Object} res
130 | */
131 | createContext(req, res) {
132 | let context = Object.create(this.context);
133 | context.req = req;
134 | context.res = res;
135 | return context;
136 | }
137 | }
138 |
139 | module.exports = SimpleKoa;
140 |
141 | ```
142 |
143 | - 执行例子
144 |
145 | ```js
146 | const SimpleKoa = require('./index');
147 |
148 | const app = new SimpleKoa();
149 | const PORT = 3001;
150 |
151 | app.use(async ctx => {
152 | ctx.body = 'this is a body
';
153 | });
154 |
155 | app.listen(PORT, () => {
156 | console.log(`the web server is starting at port ${PORT}`);
157 | });
158 |
159 | ```
160 |
--------------------------------------------------------------------------------
/note/chapter01/03.md:
--------------------------------------------------------------------------------
1 | # async/await 的主要使用
2 |
3 | ## 前言
4 |
5 | 对于回调来讲,`Promise` 的到来看似只解决了回调场景中的状态处理问题,但是`JavaScript`中令人头疼不是`回调`,而是 `回调嵌套`。同时,`Promise`的出现,也不能彻底解决回调嵌套的带来的代码维护和可读性的问题。
6 |
7 | - 原生回调嵌套
8 |
9 | ```js
10 | function increase(num, callback) {
11 | setTimeout(() => {
12 | if( !(num >= 0) ) {
13 | callback(new Error('The parameters must be greater than zero'), null)
14 | } else {
15 | let result = num + 1;
16 | callback(null, result);
17 | }
18 | }, 100)
19 | }
20 |
21 | increase(1, (err, result1) => {
22 | if(!err) {
23 | console.log(`result1 = ${result1}`)
24 |
25 | increase(result1, (err, result2) => {
26 | if(!err) {
27 | console.log(`result2 = ${result2}`)
28 |
29 | increase(result2, (err, result3) => {
30 | if(!err) {
31 | console.log(`result3 = ${result3}`)
32 | } else {
33 | console.log(err)
34 | }
35 | })
36 | } else {
37 | console.log(err)
38 | }
39 | })
40 | } else {
41 | console.log(err)
42 | }
43 | })
44 | // 运行结果
45 | // "result1 = 2"
46 | // "result1 = 3"
47 | // "result1 = 4"
48 | ```
49 |
50 | - Promise 处理回调嵌套
51 |
52 | ```js
53 | function increase(num) {
54 | return new Promise((resolve, reject) => {
55 | setTimeout(() => {
56 | if( !(num >= 0) ) {
57 | reject(new Error('The parameters must be greater than zero'))
58 | } else {
59 | let result = num + 1;
60 | resolve(result);
61 | }
62 | }, 100)
63 | })
64 | }
65 |
66 |
67 | increase(1).then((result1) => {
68 | console.log(`result1 = ${result1}`)
69 |
70 | increase(result1).then((result2) => {
71 | console.log(`result2 = ${result2}`)
72 |
73 | increase(result2).then((result3) => {
74 | console.log(`result3 = ${result3}`)
75 | }).catch(err => console.log(err));
76 |
77 | }).catch(err => console.log(err));
78 |
79 | }).catch(err => console.log(err));
80 | // 运行结果
81 | // "result1 = 2"
82 | // "result1 = 3"
83 | // "result1 = 4"
84 | ```
85 |
86 | 所以这时候,需要一个更优雅处理`Promise 嵌套任务` 的语法,因此,`async/await` 就横空出世,也就是直接或间接解决了 `回调嵌套` 的问题。
87 |
88 | 一句话,`async/await` 的出现是为了解决`回调嵌套`的操作繁琐和可读性差的问题。
89 |
90 |
91 | ## aysnc/await的使用
92 |
93 | - async 是 `声明` 在回调环境函数
94 | - await 是 `运行` 在等待回调结果过程中
95 | - Promise 是封装了回调操作的 `原子任务`
96 |
97 |
98 | 举一个简单的例子
99 |
100 | ```js
101 | // 封装原子任务
102 | function increase(num) {
103 | return new Promise((resolve, reject) => {
104 | setTimeout(() => {
105 | if( !(num >= 0) ) {
106 | reject(new Error('The parameters must be greater than zero'))
107 | } else {
108 | resolve(num + 1)
109 | }
110 |
111 | }, 100);
112 | }).catch(err => console.log(err))
113 |
114 | }
115 |
116 | // 声明任务环境
117 | async function envIncrease() {
118 | let num = 1;
119 | // 等待回调任务结果1返回
120 | let result1 = await increase(num);
121 | console.log(`result1 = ${result1}`);
122 |
123 | // 等待回调任务结果2返回
124 | let result2 = await increase(result1);
125 | console.log(`result2 = ${result2}`);
126 |
127 | // 等待回调任务结果3返回
128 | let result3 = await increase(result2);
129 | console.log(`result3 = ${result3}`);
130 |
131 | return result3
132 | }
133 |
134 | // 声明任务环境
135 | async function env() {
136 | // 等待 环境 Increase 的结果返回
137 | let result = await envIncrease()
138 | console.log(`result = ${result}`);
139 | }
140 |
141 | // 运行环境
142 | env()
143 |
144 |
145 |
146 | // 运行结果
147 | // "result1 = 2"
148 | // "result1 = 3"
149 | // "result1 = 4"
150 | ```
151 |
152 |
153 |
154 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Koa.js 设计模式-学习笔记
2 |
3 | ## 关于本书
4 |
5 | 
6 |
7 | - [关于作者(我)](https://chenshenhai.com/)
8 | - 个人博客阅读入口 [https://chenshenhai.com/koajs-design-note](https://chenshenhai.com/koajs-design-note);
9 | - Github Pages 阅读入口 [https://chenshenhai.github.io/koajs-design-note/](https://chenshenhai.github.io/koajs-design-note/)
10 | - 如有错误或疑问欢迎,提交[issues](https://github.com/chenshenhai/koajs-design-note/issues)或PR
11 |
12 |
13 | 更多前端`技术学习`和`开源学习教程`可关注公众号 `DeepSeaCode` (`大海码` )
14 |
15 | 
16 |
17 |
18 |
19 |
20 | ## 前言
21 |
22 | 之前写过一本 [《Koa2进阶学习笔记》](https://github.com/chenshenhai/koa2-note) 作为Koa的入门教程。很多知识点都是一笔带过,没有深入的讲解。这一本书是通过Koa.js的常用中间件实现原理,举一反三来讲解一些Node.js在Web开发过程中的原理和设计模式。
23 |
24 |
25 | Koa.js 是一个极其精简的Web框架,只提供一下两种功能:
26 |
27 | - HTTP服务
28 | - 处理HTTP请求request
29 | - 处理HTTP响应response
30 | - 中间件容器
31 | - 中间件的加载
32 | - 中间件的执行
33 |
34 | 剩下的其他Web服务所需的能力,就根据开发者的需求去自定义开发,留下了很大的灵活空间,提高了Web服务的开发成本。在我的理解中,Koa.js的灵活度带来的开发成本有以下两种:
35 |
36 | - 框架的设计
37 | - 中间件的选择
38 |
39 | 框架的设计,这一因素比较复杂,后续会新开一本书讲解。本书主要是解析常用的Koa.js中间件,抽象出相关中间件的功能原理和实现方式,用demo让读者理解原理,减少对官方源码的依赖,尽量达到“授人予渔”。
40 |
41 |
42 |
43 | ## 目录
44 |
45 |
46 | * 1. Koa.js 原理
47 | * [1.1 学习准备](https://github.com/chenshenhai/koajs-design-note/tree/master/note/chapter01/01.md)
48 | * [1.2 Promise 使用](https://github.com/chenshenhai/koajs-design-note/tree/master/note/chapter01/02.md)
49 | * [1.3 async/await 使用](https://github.com/chenshenhai/koajs-design-note/tree/master/note/chapter01/03.md)
50 | * [1.4 Node.js原生http模块](https://github.com/chenshenhai/koajs-design-note/tree/master/note/chapter01/04.md)
51 | * [1.5 中间件引擎](https://github.com/chenshenhai/koajs-design-note/tree/master/note/chapter01/05.md)
52 | * [1.6 普通中间件式HTTP服务实现](https://github.com/chenshenhai/koajs-design-note/tree/master/note/chapter01/06.md)
53 | * [1.7 最简Koa.js实现](https://github.com/chenshenhai/koajs-design-note/tree/master/note/chapter01/07.md)
54 | * 2. Koa.js 的AOP设计
55 | * [2.1 AOP面向切面编程](https://github.com/chenshenhai/koajs-design-note/tree/master/note/chapter02/01.md)
56 | * [2.2 洋葱模型切面](https://github.com/chenshenhai/koajs-design-note/tree/master/note/chapter02/02.md)
57 | * [2.3 HTTP切面流程](https://github.com/chenshenhai/koajs-design-note/tree/master/note/chapter02/03.md)
58 | * 3. Koa.js 中间件
59 | * [3.1 中间件分类](https://github.com/chenshenhai/koajs-design-note/tree/master/note/chapter03/01.md)
60 | * [3.2 狭义中间件](https://github.com/chenshenhai/koajs-design-note/tree/master/note/chapter03/02.md)
61 | * [3.3 广义中间件](https://github.com/chenshenhai/koajs-design-note/tree/master/note/chapter03/03.md)
62 | * 4. 狭义中间件-请求/响应拦截
63 | * [4.1 koa-logger 实现](https://github.com/chenshenhai/koajs-design-note/tree/master/note/chapter04/01.md)
64 | * [4.2 koa-send 实现](https://github.com/chenshenhai/koajs-design-note/tree/master/note/chapter04/02.md)
65 | * [4.3 koa-static 实现](https://github.com/chenshenhai/koajs-design-note/tree/master/note/chapter04/03.md)
66 | * 5. 狭义中间件-context代理
67 | * [5.1 koa-view 实现](https://github.com/chenshenhai/koajs-design-note/tree/master/note/chapter05/01.md)
68 | * [5.2 koa-jsonp 实现](https://github.com/chenshenhai/koajs-design-note/tree/master/note/chapter05/02.md)
69 | * [5.3 koa-bodyparser 实现](https://github.com/chenshenhai/koajs-design-note/tree/master/note/chapter05/03.md)
70 | * 6. 广义中间件-间接中间件处理
71 | * [6.1 koa-router 实现](https://github.com/chenshenhai/koajs-design-note/tree/master/note/chapter06/01.md)
72 | * [6.2 koa-mount 实现](https://github.com/chenshenhai/koajs-design-note/tree/master/note/chapter06/02.md)
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/note/chapter02/02.md:
--------------------------------------------------------------------------------
1 | # 洋葱模型切面
2 |
3 | ## 前言
4 |
5 | Koa.js 最为人所知的是基于 `洋葱模型` 的HTTP中间件处理流程。
6 |
7 | 在此,洋葱模式可以拆解成一下几个元素。
8 |
9 | - 生命周期
10 | - 中间件
11 | - 中间件在生命周期中
12 | - 前置操作
13 | - 等待其他中间件操作
14 | - 后置操作
15 |
16 | ## 中间件流程处理
17 |
18 | - 举个代码例子
19 |
20 | ```js
21 | let context = {
22 | data: []
23 | };
24 |
25 | async function middleware1(ctx, next) {
26 | console.log('action 001');
27 | ctx.data.push(1);
28 | await next();
29 | console.log('action 006');
30 | ctx.data.push(6);
31 | }
32 |
33 | async function middleware2(ctx, next) {
34 | console.log('action 002');
35 | ctx.data.push(2);
36 | await next();
37 | console.log('action 005');
38 | ctx.data.push(5);
39 | }
40 |
41 | async function middleware3(ctx, next) {
42 | console.log('action 003');
43 | ctx.data.push(3);
44 | await next();
45 | console.log('action 004');
46 | ctx.data.push(4);
47 | }
48 |
49 | Promise.resolve(middleware1(context, async() => {
50 | return Promise.resolve(middleware2(context, async() => {
51 | return Promise.resolve(middleware3(context, async() => {
52 | return Promise.resolve();
53 | }));
54 | }));
55 | }))
56 | .then(() => {
57 | console.log('end');
58 | console.log('context = ', context);
59 | });
60 |
61 | // 结果显示
62 | // "action 001"
63 | // "action 002"
64 | // "action 003"
65 | // "action 004"
66 | // "action 005"
67 | // "action 006"
68 | // "end"
69 | // "context = { data: [1, 2, 3, 4, 5, 6]}"
70 | ```
71 | - 源码元素解析
72 | - 生命周期就是 `Promise.resolve` 的嵌套
73 | - 中间件就是 `middleware1`、`middleware2`和`middleware3`
74 | - 中间件在生命周期中,就是 `Promise.resolve(middleware)`嵌套中执行中间件
75 | - `middleware1` 前置操作 `action 001`
76 | - 等待嵌套的 `middleware2`
77 | - `middleware2` 前置操作 `action 002`
78 | - 等待嵌套的 `middleware3`
79 | - `middleware3` 前置操作 `action 003`
80 | - `middleware3` 后置操作 `action 004`
81 | - `middleware2` 后置操作 `action 005`
82 | - `middleware1` 后置操作 `action 006`
83 |
84 | ```sh
85 |
86 | +----------------------------------------------------------------------------------+
87 | | |
88 | | middleware 1 |
89 | | |
90 | | +-----------------------------------------------------------+ |
91 | | | | |
92 | | | middleware 2 | |
93 | | | | |
94 | | | +---------------------------------+ | |
95 | | | | | | |
96 | | action | action | middleware 3 | action | action |
97 | | 001 | 002 | | 005 | 006 |
98 | | | | action action | | |
99 | | | | 003 004 | | |
100 | | | | | | |
101 | +---------------------------------------------------------------------------------------------------->
102 | | | | | | |
103 | | | | | | |
104 | | | +---------------------------------+ | |
105 | | +-----------------------------------------------------------+ |
106 | +----------------------------------------------------------------------------------+
107 |
108 |
109 |
110 | ```
111 |
112 |
--------------------------------------------------------------------------------
/note/chapter04/02.md:
--------------------------------------------------------------------------------
1 | # koa-send 实现
2 |
3 | ## 前言
4 |
5 | 狭义中间件,请求/拦截 最显著的特征是
6 | - 直接被`app.use()`
7 | - 拦截请求
8 | - 操作响应
9 |
10 | 最典型的场景是 Koa.js 官方支持传输静态文件中间件的实现`koa-send`。
11 |
12 | 主要实现场景流程是
13 |
14 | - 拦截请求,判断该请求是否请求本地静态资源文件
15 | - 操作响应,返回对应的静态文件文本内容或出错提示
16 |
17 |
18 | > 本节主要以官方的 `koa-send` 中间件为参考,实现了一个最简单的`koa-end` 实现,方便原理讲解和后续二次自定义优化开发。
19 |
20 |
21 | ## 实现步骤
22 |
23 | - step 01 配置静态资源绝对目录地址
24 | - step 02 判断是否支持隐藏文件
25 | - step 03 获取文件或者目录信息
26 | - step 04 判断是否需要压缩
27 | - step 05 设置HTTP头信息
28 | - step 06 静态文件读取
29 |
30 |
31 | ## 实现源码
32 |
33 | demo源码
34 |
35 | [https://github.com/chenshenhai/koajs-design-note/tree/master/demo/chapter-04-02](https://github.com/chenshenhai/koajs-design-note/tree/master/demo/chapter-04-02)
36 |
37 |
38 | ```sh
39 | ## 安装依赖
40 | npm i
41 |
42 | ## 执行 demo
43 | npm run start
44 |
45 | ## 最后启动chrome浏览器访问
46 | ## http://127.0.0.1:3000/index.html
47 | ```
48 |
49 | ### koa-send 源码解读
50 |
51 | ```js
52 | const fs = require('fs');
53 | const path = require('path');
54 | const {
55 | basename,
56 | extname
57 | } = path;
58 |
59 | const defaultOpts = {
60 | root: '',
61 | maxage: 0,
62 | immutable: false,
63 | extensions: false,
64 | hidden: false,
65 | brotli: false,
66 | gzip: false,
67 | setHeaders: () => {}
68 | };
69 |
70 | async function send(ctx, urlPath, opts = defaultOpts) {
71 | const { root, hidden, immutable, maxage, brotli, gzip, setHeaders } = opts;
72 | let filePath = urlPath;
73 |
74 | // step 01: normalize path
75 | // 配置静态资源绝对目录地址
76 | try {
77 | filePath = decodeURIComponent(filePath);
78 | // check legal path
79 | if (/[\.]{2,}/ig.test(filePath)) {
80 | ctx.throw(403, 'Forbidden');
81 | }
82 | } catch (err) {
83 | ctx.throw(400, 'failed to decode');
84 | }
85 |
86 | filePath = path.join(root, urlPath);
87 | const fileBasename = basename(filePath);
88 |
89 | // step 02: check hidden file support
90 | // 判断是否支持隐藏文件
91 | if (hidden !== true && fileBasename.startsWith('.')) {
92 | ctx.throw(404, '404 Not Found');
93 | return;
94 | }
95 |
96 | // step 03: stat
97 | // 获取文件或者目录信息
98 | let stats;
99 | try {
100 | stats = fs.statSync(filePath);
101 | if (stats.isDirectory()) {
102 | ctx.throw(404, '404 Not Found');
103 | }
104 | } catch (err) {
105 | const notfound = ['ENOENT', 'ENAMETOOLONG', 'ENOTDIR']
106 | if (notfound.includes(err.code)) {
107 | ctx.throw(404, '404 Not Found');
108 | return;
109 | }
110 | err.status = 500
111 | throw err
112 | }
113 |
114 | let encodingExt = '';
115 | // step 04 check zip
116 | // 判断是否需要压缩
117 | if (ctx.acceptsEncodings('br', 'identity') === 'br' && brotli && (fs.existsSync(filePath + '.br'))) {
118 | filePath = filePath + '.br';
119 | ctx.set('Content-Encoding', 'br');
120 | ctx.res.removeHeader('Content-Length');
121 | encodingExt = '.br';
122 | } else if (ctx.acceptsEncodings('gzip', 'identity') === 'gzip' && gzip && (fs.existsSync(filePath + '.gz'))) {
123 | filePath = filePath + '.gz';
124 | ctx.set('Content-Encoding', 'gzip');
125 | ctx.res.removeHeader('Content-Length');
126 | encodingExt = '.gz';
127 | }
128 |
129 | // step 05 setHeaders
130 | // 设置HTTP头信息
131 | if (typeof setHeaders === 'function') {
132 | setHeaders(ctx.res, filePath, stats);
133 | }
134 |
135 | ctx.set('Content-Length', stats.size);
136 | if (!ctx.response.get('Last-Modified')) {
137 | ctx.set('Last-Modified', stats.mtime.toUTCString());
138 | }
139 | if (!ctx.response.get('Cache-Control')) {
140 | const directives = ['max-age=' + (maxage / 1000 | 0)];
141 | if (immutable) {
142 | directives.push('immutable');
143 | }
144 | ctx.set('Cache-Control', directives.join(','));
145 | }
146 |
147 | const ctxType = encodingExt !== '' ? extname(basename(filePath, encodingExt)) : extname(filePath);
148 | ctx.type = ctxType;
149 |
150 | // step 06 stream
151 | // 静态文件读取
152 | ctx.body = fs.createReadStream(filePath);
153 | }
154 |
155 | module.exports = send;
156 |
157 | ```
158 |
159 |
160 | ### koa-send 使用
161 |
162 | ```js
163 | const send = require('./index');
164 | const Koa = require('koa');
165 | const app = new Koa();
166 |
167 |
168 | // public/ 为当前项目静态文件目录
169 | app.use(async ctx => {
170 | await send(ctx, ctx.path, { root: `${__dirname}/public` });
171 | });
172 |
173 | app.listen(3000);
174 | console.log('listening on port 3000');
175 |
176 | ```
177 |
178 | ## 附录
179 |
180 | ### 参考
181 |
182 | - [https://github.com/koajs/send](https://github.com/koajs/send)
183 |
184 |
185 |
186 |
187 |
188 |
189 |
--------------------------------------------------------------------------------
/note/chapter03/02.md:
--------------------------------------------------------------------------------
1 | # 狭义中间件
2 |
3 | ## 前言
4 |
5 | 狭义中间件的要素常见要素如下所示。
6 |
7 | - 一切皆中间件
8 | - 中间件内操作请求 `request`
9 | - 请求拦截
10 | - 中间件内操作响应 `response`
11 | - 响应拦截
12 | - 中间件内操作上下文 `context`
13 | - 直接上下文代理,初始化实例时候挂载代理在`app.context`上
14 | - 请求过程上下文代理,请求时候挂载代理在`ctx`上
15 | - 大部分直接被 `app.use()` 加载
16 | - 注意: 初始化实例挂载代理`context`不被`app.use()`
17 |
18 | ## 请求拦截
19 | ```js
20 | const Koa = require('koa');
21 | let app = new Koa();
22 |
23 | const middleware = async function(ctx, next) {
24 | // 中间件 拦截请求
25 | // 把所有请求不是 /page/ 开头的路径全部抛出500错误
26 | const reqPath = ctx.request.path;
27 | if( reqPath.indexOf('/page/') !== 0 ) {
28 | ctx.throw(500)
29 | }
30 | await next();
31 | }
32 |
33 | const page = async function(ctx, next) {
34 | ctx.body = `
35 |
36 |
37 |
38 | ${ctx.request.path}
39 |
40 |
41 | `;
42 | }
43 |
44 | app.use(middleware);
45 | app.use(page);
46 |
47 | app.listen(3001, function(){
48 | console.log('the demo is start at port 3001');
49 | })
50 | ```
51 |
52 |
53 | ## 响应拦截
54 |
55 | ```js
56 | const Koa = require('koa');
57 | let app = new Koa();
58 |
59 | const middleware = async function(ctx, next) {
60 | ctx.response.type = 'text/plain';
61 | await next();
62 | }
63 |
64 | const page = async function(ctx, next) {
65 | ctx.body = `
66 |
67 |
68 |
69 | ${ctx.path}
70 |
71 |
72 | `;
73 | }
74 |
75 | app.use(middleware);
76 | app.use(page);
77 |
78 | app.listen(3001, function(){
79 | console.log('the demo is start at port 3001');
80 | })
81 | ```
82 |
83 |
84 | ## context挂载代理
85 |
86 | - 请求代理注入
87 | - 直接被app.use
88 | - 请求时候才有注入
89 | - 每次请求的注入都不同
90 |
91 | - 初始化实例(应用)代理注入
92 | - 直接注入到app.context
93 | - 初始化应用的时候才注入
94 | - 只注入一次,每次请求都可以使用
95 |
96 |
97 |
98 | ### 请求时挂载代理context
99 |
100 | ```js
101 | const Koa = require('koa');
102 | let app = new Koa();
103 |
104 | const middleware = async function(ctx, next) {
105 | // 中间件 代理/挂载上下文
106 | // 把所有当前服务的进程PID,内存使用情况方法代理/挂载在ctx上
107 | ctx.getServerInfo = function() {
108 | function parseMem( mem = 0 ) {
109 | let memVal = mem / 1024 / 1024;
110 | memVal = memVal.toFixed(2) + 'MB';
111 | return memVal;
112 | }
113 |
114 | function getMemInfo() {
115 | let memUsage = process.memoryUsage();
116 | let rss = parseMem(memUsage.rss);
117 | let heapTotal = parseMem(memUsage.heapTotal);
118 | let heapUsed = parseMem(memUsage.heapUsed);
119 | return {
120 | pid: process.pid,
121 | rss,
122 | heapTotal,
123 | heapUsed
124 | }
125 | }
126 | return getMemInfo()
127 | };
128 | await next();
129 | }
130 |
131 | const page = async function(ctx, next) {
132 | const serverInfo = ctx.getServerInfo();
133 | ctx.body = `
134 |
135 |
136 |
137 | ${JSON.stringify(serverInfo)}
138 |
139 |
140 | `;
141 | }
142 |
143 | app.use(middleware);
144 | app.use(page);
145 |
146 | app.listen(3001, function(){
147 | console.log('the demo is start at port 3001');
148 | })
149 | ```
150 |
151 | ### 初始化实例挂载代理context
152 |
153 | ```js
154 | const Koa = require('koa');
155 | let app = new Koa();
156 |
157 | const middleware = function(app) {
158 | // 中间件在初始化实例 把getServerInfo方法 挂载代理到上下文
159 | app.context.getServerInfo = function() {
160 | function parseMem( mem = 0 ) {
161 | let memVal = mem / 1024 / 1024;
162 | memVal = memVal.toFixed(2) + 'MB';
163 | return memVal;
164 | }
165 |
166 | function getMemInfo() {
167 | let memUsage = process.memoryUsage();
168 | let rss = parseMem(memUsage.rss);
169 | let heapTotal = parseMem(memUsage.heapTotal);
170 | let heapUsed = parseMem(memUsage.heapUsed);
171 | return {
172 | pid: process.pid,
173 | rss,
174 | heapTotal,
175 | heapUsed
176 | }
177 | }
178 | return getMemInfo()
179 | };
180 | }
181 |
182 | middleware(app);
183 |
184 | const page = async function(ctx, next) {
185 | const serverInfo = ctx.getServerInfo();
186 | ctx.body = `
187 |
188 |
189 |
190 | ${JSON.stringify(serverInfo)}
191 |
192 |
193 | `;
194 | }
195 |
196 | app.use(page);
197 |
198 | app.listen(3001, function(){
199 | console.log('the demo is start at port 3001');
200 | })
201 |
202 | ```
--------------------------------------------------------------------------------
/note/chapter01/02.md:
--------------------------------------------------------------------------------
1 | # Promise 的主要使用
2 |
3 | ## 前言
4 |
5 | `回调` 一直是JavaScript编程中比较令人纠结的写法,主要场景是用于处理 “并列”或者“并行”的操作,然后在回调函数中处理操作结果。这样子原生的回调写法就会带来一下的不便。
6 |
7 | - 回调结果状态不便管理
8 | - 回调方式自由松散,没有规范约束
9 |
10 | 例如下面的回调的写法
11 |
12 | ```js
13 | function func(num, callback) {
14 | setTimeout(() => {
15 | try {
16 | let result = 1/num;
17 | callback(result, null);
18 | } catch(err) {
19 | callback(null, err);
20 | }
21 | }, 10)
22 | }
23 |
24 |
25 | func(1, (result, err) => {
26 | if( err ) {
27 | console.log(err)
28 | } else {
29 | console.log(result)
30 | }
31 | })
32 | ```
33 |
34 | 上述代码中,发现如果要处理回调结果 `result`和错误`err` ,后续的所有就必须在回调函数里面处理,而且回调函数里面还需要自己处理异常判断。
35 | 那如果是使用了`Promise`来处理回调操作,就可以用以下写法处理。
36 |
37 | ```js
38 |
39 |
40 | function func(num, callback) {
41 | return new Promise((resolve) => {
42 | setTimeout(() => {
43 | let result = 1/num;
44 | resolve(result);
45 | }, 1000)
46 | })
47 | }
48 |
49 | func(1).then((result) => {
50 | console.log(result)
51 | }).catch((err) => {
52 | console.log(err)
53 | })
54 | ```
55 |
56 |
57 |
58 | ## Promise能力
59 |
60 | Promise 带来的能力是`任务管理`,常用的方式有
61 |
62 | `new Promise(...).then(onResolved, onRejected)`
63 |
64 | - 任务状态管理
65 | - `resolve` 成功状态,对应 `Promise.resolve`
66 | - `reject` 失败状态,对应 `Promise.reject`
67 | - `error` 异常状态, 对应 `Promise.reject` 或 `new Promise().catch(onRejected)`
68 | - `Thenabled`机制提供任务方法链
69 | - `new Promise().then().then().catch()`
70 |
71 | ### resolve
72 |
73 | 处理任务的成功状态
74 |
75 | - 普通方式
76 |
77 | ```js
78 | let p = new Promise((resolve) => {
79 | setTimeout(() => {
80 | let result = 1;
81 | resolve(result);
82 | }, 1000)
83 | })
84 |
85 | p.then((result)=>{ console.log(result) })
86 | ```
87 |
88 | - 快捷方式
89 |
90 | ```js
91 | let p = Promise.resolve(1)
92 |
93 | p.then((result)=>{ console.log(result) })
94 | ```
95 |
96 | ### reject
97 |
98 | 处理任务的失败状态
99 |
100 | - 普通方式
101 |
102 | ```js
103 | let p = new Promise((resolve, reject) => {
104 | setTimeout(() => {
105 | let result = 2;
106 | reject(result);
107 | }, 100)
108 | })
109 | // 有两种方式获取失败状态
110 | // 第一种,通过then 第二个函数参数处理失败状态
111 | p.then((result)=>{
112 | console.log('success:',result);
113 | }, (result)=>{
114 | console.log('fail:',result);
115 | })
116 | // "fail: 2"
117 |
118 | // 第二种,或者通过,catch 获取失败状态
119 | p.then((result)=>{
120 | console.log('success:',result);
121 | }).catch((result)=>{
122 | console.log('error:',result);
123 | })
124 | // "error: 2"
125 |
126 |
127 | // 注意:如果两种方式同时使用的话
128 | // 只会被第一种方式reject操作失败的结果
129 | p.then((result)=>{
130 | console.log('success:',result);
131 | }, (result)=>{
132 | console.log('fail:',result);
133 | }).catch((result)=>{
134 | console.log('error:',result);
135 | })
136 | // "fail: 2"
137 |
138 | ```
139 |
140 | - 快捷方式
141 |
142 | ```js
143 | let p = Promise.reject(2)
144 |
145 | p.then(null, result => console.log('fail:', result))
146 |
147 | // 或
148 | p.then().catch( result => console.log('error:', result))
149 | ```
150 |
151 |
152 | ### catch
153 |
154 | 从上述 `reject` 的使用过程中,会发现, `catch`操作在没有设置 onRejected 处理的时候,会被`catch` 捕获失败处理。同时`catch` 也会捕获 onResolved 和 onRejected中出现的错误。
155 |
156 | - 正常情况下直接捕获`reject`结果
157 |
158 | ```js
159 | let p = new Promise((resolve, reject) => {
160 | reject(3)
161 | });
162 |
163 | p.then((result) => {
164 | console.log('success:', result)
165 | }).catch((result) => {
166 | console.log('error:', result)
167 | })
168 |
169 | // "error: 3"
170 | ```
171 |
172 |
173 | - 捕获 onResolved 中错误异常
174 |
175 | ```js
176 | let p = new Promise((resolve) => {
177 | resolve(3)
178 | });
179 |
180 | p.then((result) => {
181 | throw new Error('custom resolve error!')
182 | console.log('success:', result)
183 | }).catch((err) => {
184 | console.log('Custom error:', err)
185 | })
186 |
187 | // "Custom error: Error: custom resolve error!"
188 |
189 | ```
190 |
191 | - 捕获 onRejected 中错误异常
192 |
193 | ```js
194 | let p = new Promise((resolve) => {
195 | reject(3)
196 | });
197 |
198 | p.then(null, (result) => {
199 | throw new Error('custom reject error!')
200 | console.log('fail:', result)
201 | }).catch((err) => {
202 | console.log('Custom error:', err)
203 | })
204 | // "Custom error: Error: custom reject error!"
205 | ```
206 |
207 |
208 | ## 后记
209 |
210 | 由于本书主要介绍 `Koa.js`的原理,主要涉及到`Promise`的`resolve`、`reject`和`catch` 更多 关于 `Promise` 的原理和使用,请查看一下文档:
211 |
212 | [https://docs.microsoft.com/zh-cn/scripting/javascript/reference/promise-object-javascript](https://docs.microsoft.com/zh-cn/scripting/javascript/reference/promise-object-javascript)
213 |
214 |
215 |
216 |
217 |
--------------------------------------------------------------------------------
/note/chapter01/05.md:
--------------------------------------------------------------------------------
1 | # 中间件引擎
2 |
3 | ## 前言
4 | 在使用Koa.js过程中,会发现中间件的使用都是这样子的,如以下代码所示。
5 |
6 | ```js
7 | const Koa = require('koa');
8 | let app = new Koa();
9 |
10 | const middleware1 = async (ctx, next) => {
11 | console.log(1);
12 | await next();
13 | console.log(6);
14 | }
15 |
16 | const middleware2 = async (ctx, next) => {
17 | console.log(2);
18 | await next();
19 | console.log(5);
20 | }
21 |
22 | const middleware3 = async (ctx, next) => {
23 | console.log(3);
24 | await next();
25 | console.log(4);
26 | }
27 |
28 | app.use(middleware1);
29 | app.use(middleware2);
30 | app.use(middleware3);
31 | app.use(async(ctx, next) => {
32 | ctx.body = 'hello world'
33 | })
34 |
35 | app.listen(3001)
36 |
37 | // 启动访问浏览器
38 | // 控制台会出现以下结果
39 | // 1
40 | // 2
41 | // 3
42 | // 4
43 | // 5
44 | // 6
45 | ```
46 | 为什么会出现以上的结果,
47 | 这个主要是Koa.js的一个中间件引擎 `koa-compose`模块来实现的,也就是Koa.js实现`洋葱模型`的核心引擎。
48 |
49 |
50 | ## 中间件原理
51 |
52 | 洋葱模型可以看出,中间件的在 `await next()` 前后的操作,很像数据结构的一种场景——“栈”,先进后出。同时,又有统一上下文管理操作数据。综上所述,可以总结出一下特性。
53 |
54 | - 有统一 `context`
55 | - 操作先进后出
56 | - 有控制先进后出的机制 `next`
57 | - 有提前结束机制
58 |
59 | 这样子我们可以单纯用 `Promise` 做个简单的实现如下
60 |
61 | ```js
62 | let context = {
63 | data: []
64 | };
65 |
66 | async function middleware1(ctx, next) {
67 | console.log('action 001');
68 | ctx.data.push(1);
69 | await next();
70 | console.log('action 006');
71 | ctx.data.push(6);
72 | }
73 |
74 | async function middleware2(ctx, next) {
75 | console.log('action 002');
76 | ctx.data.push(2);
77 | await next();
78 | console.log('action 005');
79 | ctx.data.push(5);
80 | }
81 |
82 | async function middleware3(ctx, next) {
83 | console.log('action 003');
84 | ctx.data.push(3);
85 | await next();
86 | console.log('action 004');
87 | ctx.data.push(4);
88 | }
89 |
90 | Promise.resolve(middleware1(context, async() => {
91 | return Promise.resolve(middleware2(context, async() => {
92 | return Promise.resolve(middleware3(context, async() => {
93 | return Promise.resolve();
94 | }));
95 | }));
96 | }))
97 | .then(() => {
98 | console.log('end');
99 | console.log('context = ', context);
100 | });
101 |
102 | // 结果显示
103 | // "action 001"
104 | // "action 002"
105 | // "action 003"
106 | // "action 004"
107 | // "action 005"
108 | // "action 006"
109 | // "end"
110 | // "context = { data: [1, 2, 3, 4, 5, 6]}"
111 |
112 | ```
113 |
114 | ## 引擎实现
115 |
116 | 通过上一节中的中间件原理,可以看出,单纯用`Promise` 嵌套可以直接实现中间件流程。虽然可以实现,但是`Promise`嵌套会产生代码的可读性和可维护性的问题,也带来了中间件扩展问题。
117 |
118 | 所以需要把`Promise` 嵌套实现的中间件方式进行高度抽象,达到可以自定义中间件的层数。这时候需要借助前面几章提到的处理 `Promise`嵌套的神器`async/await`。
119 |
120 | 我们先理清楚需要的步骤
121 | - 中间件队列
122 | - 处理中间件队列,并将上下文`context`传进去
123 | - 中间件的流程控制器`next`
124 | - 异常处理
125 |
126 | 根据上一节分析中间的原理,我们可以抽象出
127 | - 每一个中间件需要封装一个 `Promise`
128 | - 洋葱模型的先进后出操作,对应`Promise.resolve`的前后操作
129 |
130 | > 源码地址
131 | [https://github.com/chenshenhai/koajs-design-note/tree/master/demo/chapter-01-05](https://github.com/chenshenhai/koajs-design-note/tree/master/demo/chapter-01-05)
132 |
133 |
134 | ```js
135 | function compose(middleware) {
136 |
137 | if (!Array.isArray(middleware)) {
138 | throw new TypeError('Middleware stack must be an array!');
139 | }
140 |
141 | return function(ctx, next) {
142 | let index = -1;
143 |
144 | return dispatch(0);
145 |
146 | function dispatch(i) {
147 | if (i < index) {
148 | return Promise.reject(new Error('next() called multiple times'));
149 | }
150 | index = i;
151 |
152 | let fn = middleware[i];
153 |
154 | if (i === middleware.length) {
155 | fn = next;
156 | }
157 |
158 | if (!fn) {
159 | return Promise.resolve();
160 | }
161 |
162 | try {
163 | return Promise.resolve(fn(ctx, () => {
164 | return dispatch(i + 1);
165 | }));
166 | } catch (err) {
167 | return Promise.reject(err);
168 | }
169 | }
170 | };
171 | }
172 | ```
173 |
174 | 试用中间件引擎
175 |
176 | ```js
177 | let middleware = [];
178 | let context = {
179 | data: []
180 | };
181 |
182 | middleware.push(async(ctx, next) => {
183 | console.log('action 001');
184 | ctx.data.push(2);
185 | await next();
186 | console.log('action 006');
187 | ctx.data.push(5);
188 | });
189 |
190 | middleware.push(async(ctx, next) => {
191 | console.log('action 002');
192 | ctx.data.push(2);
193 | await next();
194 | console.log('action 005');
195 | ctx.data.push(5);
196 | });
197 |
198 | middleware.push(async(ctx, next) => {
199 | console.log('action 003');
200 | ctx.data.push(2);
201 | await next();
202 | console.log('action 004');
203 | ctx.data.push(5);
204 | });
205 |
206 | const fn = compose(middleware);
207 |
208 | fn(context)
209 | .then(() => {
210 | console.log('end');
211 | console.log('context = ', context);
212 | });
213 |
214 | // 结果显示
215 | // "action 001"
216 | // "action 002"
217 | // "action 003"
218 | // "action 004"
219 | // "action 005"
220 | // "action 006"
221 | // "end"
222 | // "context = { data: [1, 2, 3, 4, 5, 6]}"
223 |
224 | ```
225 |
--------------------------------------------------------------------------------
/note/chapter05/03.md:
--------------------------------------------------------------------------------
1 |
2 | # koa-bodyparser 实现
3 |
4 | > 请求代理上下文context实现
5 |
6 | ## 前言
7 |
8 | 狭义中间件的上下文代理,除了在实例化 `let app = new Koa()` 的时候将属性或者方法挂载到`app.context` 中,供后续中间件使用。另外一种方式是在请求过程中在顶端中间件(一般在第一个中间件)使用,把数据或者方法挂载代理到`ctx` 供下游中间件获取和使用。
9 |
10 | 这里 请求代理上下文实现 最代表性是官方提供的`koa-bodyparser` 中间件,这里基于官方原版用最简单的方式实现`koa-bodyparser`最简单功能。
11 |
12 | 常见请求代理上下文context实现过程
13 |
14 | - 请求代理ctx
15 | - 直接app.use()
16 | - 在请求过程中过载方法或者数据到上下文`ctx`
17 | - 一般在大部分中间件前加载,供下游中间件获取挂载的数据或方法
18 |
19 |
20 | ## 实现步骤
21 |
22 | - step 01 `app.use()`在中间件最顶端
23 | - step 02 拦截post请求
24 | - step 03 等待解析表单信息
25 | - step 04 把表单信息代理到ctx.request.body上
26 | - step 05 下游中间件都可以在ctx.request.body中获取表单数据
27 |
28 |
29 | ## 实现源码
30 |
31 | demo源码
32 |
33 | [https://github.com/chenshenhai/koajs-design-note/tree/master/demo/chapter-05-03](https://github.com/chenshenhai/koajs-design-note/tree/master/demo/chapter-05-03)
34 |
35 | ```sh
36 | ## 安装依赖
37 | npm i
38 |
39 | ## 执行 demo
40 | npm run start
41 |
42 | ## 最后启动chrome浏览器访问
43 | ## http://127.0.0.1:3000
44 | ```
45 |
46 |
47 | ### 依赖
48 |
49 | 请求体数据流解析方法
50 |
51 | ```js
52 | module.exports = readStream;
53 |
54 | function readStream(req) {
55 | return new Promise((resolve, reject) => {
56 | try {
57 | streamEventListen(req, (data, err) => {
58 | if (data && !isError(err)) {
59 | resolve(data);
60 | } else {
61 | reject(err);
62 | }
63 | });
64 | } catch (err) {
65 | reject(err);
66 | }
67 | });
68 | }
69 |
70 | function isError(err) {
71 | return Object.prototype.toString.call(err).toLowerCase() === '[object error]';
72 | }
73 |
74 | function streamEventListen(req, callback) {
75 | let stream = req.req || req;
76 | let chunk = [];
77 | let complete = false;
78 |
79 | // attach listeners
80 | stream.on('aborted', onAborted);
81 | stream.on('close', cleanup);
82 | stream.on('data', onData);
83 | stream.on('end', onEnd);
84 | stream.on('error', onEnd);
85 |
86 | function onAborted() {
87 | if (complete) {
88 | return;
89 | }
90 | callback(null, new Error('request body parse aborted'));
91 | }
92 |
93 | function cleanup() {
94 | stream.removeListener('aborted', onAborted);
95 | stream.removeListener('data', onData);
96 | stream.removeListener('end', onEnd);
97 | stream.removeListener('error', onEnd);
98 | stream.removeListener('close', cleanup);
99 | }
100 |
101 | function onData(data) {
102 | if (complete) {
103 | return;
104 | }
105 | if (data) {
106 | chunk.push(data.toString());
107 | }
108 | }
109 |
110 | function onEnd(err) {
111 | if (complete) {
112 | return;
113 | }
114 |
115 | if (isError(err)) {
116 | callback(null, err);
117 | return;
118 | }
119 |
120 | complete = true;
121 | let result = chunk.join('');
122 | chunk = [];
123 | callback(result, null);
124 | }
125 | }
126 |
127 | ```
128 |
129 | ### 解读
130 |
131 | ```js
132 | const readStream = require('./lib/read_stream');
133 | let strictJSONReg = /^[\x20\x09\x0a\x0d]*(\[|\{)/;
134 |
135 | let jsonTypes = [
136 | 'application/json'
137 | ];
138 |
139 | let formTypes = [
140 | 'application/x-www-form-urlencoded'
141 | ];
142 |
143 | let textTypes = [
144 | 'text/plain'
145 | ];
146 |
147 | function parseQueryStr(queryStr) {
148 | let queryData = {};
149 | let queryStrList = queryStr.split('&');
150 | for (let [ index, queryStr ] of queryStrList.entries()) {
151 | let itemList = queryStr.split('=');
152 | queryData[ itemList[0] ] = decodeURIComponent(itemList[1]);
153 | }
154 | return queryData;
155 | }
156 |
157 | function bodyParser(opts = {}) {
158 | return async function(ctx, next) {
159 | // 拦截post请求
160 | if (!ctx.request.body && ctx.method === 'POST') {
161 | // 解析请求体中的表单信息
162 | let body = await readStream(ctx.request.req);
163 | let result = body;
164 | if (ctx.request.is(formTypes)) {
165 | result = parseQueryStr(body);
166 | } else if (ctx.request.is(jsonTypes)) {
167 | if (strictJSONReg.test(body)) {
168 | try {
169 | result = JSON.parse(body);
170 | } catch (err) {
171 | ctx.throw(500, err);
172 | }
173 | }
174 | } else if (ctx.request.is(textTypes)) {
175 | result = body;
176 | }
177 |
178 | // 将请求体中的信息挂载到山下文的request 属性中
179 | ctx.request.body = result;
180 | }
181 | await next();
182 | };
183 | }
184 |
185 | module.exports = bodyParser;
186 |
187 | ```
188 |
189 | ### 使用
190 |
191 | ```js
192 | const Koa = require('koa');
193 | const fs = require('fs');
194 | const path = require('path');
195 | const body = require('../index');
196 | const app = new Koa();
197 |
198 | app.use(body());
199 |
200 | app.use(async(ctx, next) => {
201 | if (ctx.url === '/') {
202 | // 当GET请求时候返回表单页面
203 | let html = fs.readFileSync(path.join(__dirname, './index.html'), 'binary');
204 | ctx.body = html;
205 | } else if (ctx.url === '/post' && ctx.method === 'POST') {
206 | // 当POST请求的时候,解析POST表单里的数据,并显示出来
207 | ctx.body = ctx.request.body;
208 | } else {
209 | // 其他请求显示404
210 | ctx.body = '404!!! o(╯□╰)o
';
211 | }
212 |
213 | await next();
214 | });
215 |
216 | app.listen(3000, () => {
217 | console.log('[demo] is starting at port 3000');
218 | });
219 |
220 | ```
221 |
222 |
223 | ```html
224 |
225 |
226 | example
227 |
228 |
229 |
230 |
form post demo
231 |
236 |
237 |
238 |
239 | ```
240 |
241 | ## 附录
242 |
243 | ### 参考
244 |
245 | [https://github.com/koajs/bodyparser](https://github.com/koajs/bodyparser)
246 |
247 |
--------------------------------------------------------------------------------