├── 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 |
9 | data 10 |
11 | 12 |
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 | ![koa2-77 014](https://user-images.githubusercontent.com/8216630/42408394-9db3991e-81fe-11e8-8a32-940941ad4480.jpeg) 12 | 13 | ## HTTP生命过程 14 | 15 | - http请求 16 | - 路由操作 17 | - 权限处理 18 | - 数据安全 19 | - 业务操作 20 | - 数据操作 21 | - 书查查询 22 | - http响应 23 | - 响应操作 24 | 25 | ![koa2-77 019](https://user-images.githubusercontent.com/8216630/42408395-9efe19ca-81fe-11e8-9a6e-3dc5b1896dca.jpeg) 26 | 27 | 28 | ## Koa.js的HTTP旅程 29 | 30 | - 请求 31 | - 中间件 32 | - 响应 33 | 34 | ![koa2-77 013](https://user-images.githubusercontent.com/8216630/42408401-ada72fca-81fe-11e8-9f05-c5a93bb15670.jpeg) 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 | ![example](https://user-images.githubusercontent.com/8216630/42586566-0014a912-856b-11e8-8e96-6aa54db8f60c.png) 10 | 11 | - 为了提高销量,想加上两道工序 `分类` 和 `包装` 但又不能干扰原有的流程,同时如果没增加收益可以随时撤销新增工序。 12 | 13 | ![example](https://user-images.githubusercontent.com/8216630/42586569-0113afe8-856b-11e8-9580-4238053ddc60.png) 14 | 15 | - 最后在流水线的中的空隙插上两个工人去处理,形成`采摘 - 分类 - 清洗 - 包装 - 贴标签` 的新流程,而且工人可以随时撤回。 16 | 17 | 回到什么是AOP?就是在现有代码程序中,在程序生命周期或者横向流程中 `加入/减去` 一个或多个功能,不影响原有功能。 18 | 19 | 20 | ## Koa.js 的切面 21 | 22 | - 切面由中间件机制实现 23 | - 一个中间件一般有两个切面 24 | - 遵循先进后出的切面执行顺序,类似入栈出栈的顺序 25 | 26 | ![example](https://user-images.githubusercontent.com/8216630/42587672-084c4402-856e-11e8-8fb4-dde31009baad.png) 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 |
44 |

© Company 2017-2018

45 |
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 | ![koajs-design-note mini](https://user-images.githubusercontent.com/8216630/42525467-ceea9f42-84a5-11e8-9f3f-9ce358952a51.png) 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 | ![qrcode_for_gh_959d1c4d729a_258](https://user-images.githubusercontent.com/8216630/43264303-495bf52c-9118-11e8-85cd-4ec6fcc6d066.jpg) 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 |
232 | data 233 |
234 | 235 |
236 |
237 | 238 | 239 | ``` 240 | 241 | ## 附录 242 | 243 | ### 参考 244 | 245 | [https://github.com/koajs/bodyparser](https://github.com/koajs/bodyparser) 246 | 247 | --------------------------------------------------------------------------------