├── .nvmrc
├── static
├── script.js
├── style.css
└── index.html
├── .gitignore
├── package.json
├── views
└── tiny.dot
├── README.md
├── .editorconfig
├── .vscode
└── launch.json
├── test
└── browser-xhr.js
├── src
├── middleware
│ ├── tiny-koa-cookie.js
│ ├── tiny-koa-cors.js
│ ├── tiny-koa-static.js
│ ├── tiny-koa-body.js
│ ├── tiny-koa-router.js
│ └── tiny-koa-views.js
└── tiny-koa.js
└── app.js
/.nvmrc:
--------------------------------------------------------------------------------
1 | 8.9.1
2 |
--------------------------------------------------------------------------------
/static/script.js:
--------------------------------------------------------------------------------
1 | console.log('Tiny Koa');
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | source-koa
3 | node.sh
4 | node_modules
5 |
--------------------------------------------------------------------------------
/static/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | background: #fefefe;
3 | }
4 | h1 {
5 | color: lightsalmon;
6 | text-align: center;
7 | }
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tiny-koa",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "app.js",
6 | "scripts": {
7 | "dev": "nodemon app.js"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC",
12 | "devDependencies": {
13 | "nodemon": "^1.12.1"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/views/tiny.dot:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{=it.title}}
8 |
9 |
10 | {{=it.body}}
11 |
12 |
13 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Tiny Koa
2 |
3 | Koa is already very mini framework for HTTP, but somes read the source code still hard, I wrote a lite version for Koa.
4 | With some middlewares.
5 |
6 | Help you learn Koa.
7 | 💪
8 |
9 | ## Run
10 |
11 | Use Node.js 8.9.1+
12 |
13 | ```
14 | $ npm install
15 | $ npm run dev
16 | ```
17 |
18 | open `http://localhost:8600/index.html`
19 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: http://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | # Unix-style newlines with a newline ending every file
7 | [*]
8 | end_of_line = lf
9 | insert_final_newline = true
10 |
11 | # Matches multiple files with brace expansion notation
12 | # Set default charset
13 | [*.js]
14 | charset = utf-8
15 | indent_style = space
16 | indent_size = 2
17 |
18 | [{package.json,.travis.yml}]
19 | indent_style = space
20 | indent_size = 2
21 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // 使用 IntelliSense 了解相关属性。
3 | // 悬停以查看现有属性的描述。
4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "node",
9 | "request": "launch",
10 | "name": "Launch Program",
11 | "program": "${workspaceFolder}/app.js",
12 | "runtimeExecutable": "${env:HOME}/.nvm/versions/node/v8.9.1/bin/node"
13 | }
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/test/browser-xhr.js:
--------------------------------------------------------------------------------
1 | var xhr = new XMLHttpRequest();
2 | xhr.open('post', 'http://localhost:5678/api', true);
3 | xhr.withCredentials = true;
4 | xhr.setRequestHeader('content-type', 'application/json');
5 | xhr.send(null);
6 |
7 | fetch('http://localhost:5678/api', {
8 | method: 'post',
9 | mode: 'cors',
10 | headers: {
11 | 'Content-Type': 'application/json'
12 | },
13 | body: JSON.stringify({
14 | name: 'Hubot',
15 | login: 'hubot',
16 | })
17 | });
18 |
19 | fetch('http://localhost:5678/api', {
20 | method: 'post',
21 | mode: 'cors',
22 | headers: {
23 | 'Content-Type': 'application/x-www-form-urlencoded'
24 | },
25 | body: 'name=user&login=pwd'
26 | });
27 |
--------------------------------------------------------------------------------
/src/middleware/tiny-koa-cookie.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = (config) => {
4 | return (ctx, next) => {
5 | let {cookie=''} = ctx.req.headers;
6 | let cookieObj = {};
7 | let cookieArr = cookie.split(';');
8 |
9 | cookieArr.forEach(item => {
10 | let itemSplit = item.split('=');
11 | cookieObj[itemSplit[0]] = unescape(itemSplit[1]);
12 | });
13 |
14 | ctx.cookies = {
15 | get: (key) => {
16 | if (key === undefined) {
17 | return cookieObj;
18 | }
19 | return cookieObj[key];
20 | },
21 | set: (key, value) => {
22 | // TODO cookie其他字段
23 | ctx.res.setHeader('Set-Cookie', `${key}=${value}`);
24 | }
25 | };
26 | return next();
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/middleware/tiny-koa-cors.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = (config) => {
4 | return (ctx, next) => {
5 | let setHeaders = () => {
6 | if (ctx.req.headers.origin) {
7 | ctx.res.setHeader('Access-Control-Allow-Origin', ctx.req.headers.origin);
8 | ctx.res.setHeader('Access-Control-Allow-Credentials', 'true');
9 | ctx.res.setHeader('Access-Control-Allow-Headers', ctx.req.headers['access-control-request-headers'] || 'content-type' );
10 | ctx.res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, PUT, OPTIONS');
11 | }
12 | };
13 |
14 | setHeaders();
15 | if (ctx.req.method.toUpperCase() === 'OPTIONS') {
16 | ctx.res.writeHead(200);
17 | } else {
18 | return next();
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/middleware/tiny-koa-static.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const fs = require('fs');
3 | const Stream = require('stream');
4 |
5 | module.exports = (config) => {
6 | let staticDir = null;
7 | if (typeof config === 'string') {
8 | staticDir = config;
9 | }
10 |
11 | return async (ctx, next) => {
12 | let statInfo = false;
13 | let filePath = staticDir + ctx.path;
14 | try {
15 | statInfo = fs.statSync(filePath);
16 | } catch (err) {}
17 |
18 | if (statInfo && statInfo.isFile()) {
19 | // TODO 应该用Accept更准确点
20 | let index = filePath.lastIndexOf(".");
21 | let ext = filePath.substr(index+1);
22 | switch(ext) {
23 | case 'html':
24 | ctx.res.setHeader('Content-Type', 'text/html; charset=utf-8');
25 | break;
26 | case 'js':
27 | ctx.res.setHeader('Content-Type', 'text/javascript; charset=utf-8');
28 | break;
29 | case 'css':
30 | ctx.res.setHeader('Content-Type', 'text/css; charset=utf-8');
31 | break;
32 | }
33 |
34 | let rs = fs.createReadStream(filePath);
35 | ctx.body = rs;
36 | } else {
37 | return next();
38 | }
39 | }
40 | };
41 |
--------------------------------------------------------------------------------
/src/middleware/tiny-koa-body.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = (config) => {
4 | const jsonTypes = 'application/json';
5 | const formTypes = 'application/x-www-form-urlencoded';
6 | const textTypes = 'text/plain';
7 |
8 | return async (ctx, next) => {
9 | let reqStr = await new Promise((resolve, reject) => {
10 | let data = '';
11 | ctx.req.on('data', chunk => {
12 | data += chunk;
13 | });
14 | ctx.req.on('end', () => {
15 | resolve(data);
16 | })
17 | });
18 |
19 | if (ctx.method.toUpperCase() === 'options') {
20 | return next();
21 | }
22 |
23 | let curTypes = ctx.req.headers['content-type'] || textTypes;
24 | if (curTypes.includes(jsonTypes)) {
25 | ctx.req.body = JSON.parse(reqStr || "null"); // TODO try
26 | } else if (curTypes.includes(formTypes)) {
27 | let formArr = reqStr.split('&');
28 | let formObj = {};
29 | formArr.forEach((item) => {
30 | let formItem = item.split('=');
31 | formItem = formItem.map(_=>decodeURIComponent(_));
32 | formObj[formItem[0]] = formItem[1];
33 | });
34 | ctx.req.body = formObj;
35 | } else {
36 | ctx.req.body = reqStr;
37 | }
38 |
39 | return next();
40 | }
41 | };
42 |
--------------------------------------------------------------------------------
/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Tiny Koa
8 |
9 |
14 |
15 |
16 | Tiny Koa
17 |
18 |
19 | http://localhost:8600/api/foo/123
20 |
21 | http://localhost:8600/api/bar
22 |
23 | http://localhost:8600/page/tpl
24 |
25 |
26 |
41 |
42 |
--------------------------------------------------------------------------------
/src/tiny-koa.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const http = require('http');
3 | const url = require('url');
4 | const URL = url.URL;
5 | const Stream = require('stream');
6 |
7 | module.exports = class TinyKoa {
8 | constructor() {
9 | this.middleware = [];
10 | this.body = '';
11 | }
12 |
13 | use(fn) {
14 | this.middleware.push(fn);
15 | }
16 |
17 | compose (ctx) {
18 | let dispatch = (i) => {
19 | if (i === this.middleware.length) {
20 | return Promise.resolve();
21 | }
22 |
23 | let midFn = this.middleware[i];
24 |
25 | let midFnWrap = () => {
26 | return midFn(ctx, () => {
27 | return dispatch(i+1);
28 | });
29 | };
30 |
31 | return Promise.resolve(midFnWrap());
32 | };
33 |
34 | return dispatch(0);
35 | }
36 |
37 | listen(port, cb) {
38 | const server = http.createServer((req, res) => {
39 | res.statusCode = 404;
40 |
41 | // TODO ctx
42 | let ctx = {};
43 | ctx.req = req;
44 | ctx.res = res;
45 | ctx.path = url.parse(req.url).pathname;
46 | ctx.method = req.method;
47 |
48 | let middlewareCompose = this.compose(ctx);
49 | middlewareCompose.then(() => {
50 | // TODO 其他数据类型
51 | let body = ctx.body;
52 |
53 | if (ctx.res.headersSent) {
54 | res.end();
55 | } else {
56 | res.statusCode = 200;
57 | }
58 | if (body === undefined) {
59 | res.statusCode = 404;
60 | }
61 |
62 | if (body instanceof Stream) {
63 | return body.pipe(res);
64 | }
65 |
66 | if (typeof body !== 'string') {
67 | body = JSON.stringify(body);
68 | }
69 |
70 | res.end(body || 'not found');
71 | }).catch(err => {
72 | console.log(err);
73 | })
74 | });
75 | server.listen(port, cb);
76 | }
77 | };
78 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | const TinyKoa = require('./src/tiny-koa.js');
2 | const app = new TinyKoa();
3 | const port = 8600;
4 |
5 | const Router = require('./src/middleware/tiny-koa-router.js');
6 | const router = new Router();
7 |
8 | const cors = require('./src/middleware/tiny-koa-cors.js');
9 | const bodyparser = require('./src/middleware/tiny-koa-body.js');
10 | const server = require('./src/middleware/tiny-koa-static.js');
11 | const koaCookies = require('./src/middleware/tiny-koa-cookie.js');
12 | const views = require('./src/middleware/tiny-koa-views.js');
13 | let render = views({
14 | root: __dirname + '/views'
15 | });
16 |
17 | app.use(cors());
18 | app.use(koaCookies());
19 | app.use(bodyparser());
20 |
21 | app.use(async function ck (ctx, next) {
22 | console.log('set cookie', ctx.path);
23 | ctx.cookies.set('path', decodeURIComponent(ctx.path));
24 | await next ();
25 | });
26 |
27 | // curl http://localhost:8600/api/foo/123
28 | router.all('/api/foo/:id', (ctx, next) => {
29 | // console.log('/api/foo/:id');
30 | ctx.body = {
31 | param: {
32 | id: ctx.params.id
33 | },
34 | route: '/api/foo/:id'
35 | };
36 | // return next(); // if match not to next
37 | });
38 |
39 | // curl http://localhost:8600/api/ccc
40 | router.all('/api/bar', (ctx, next) => {
41 | // console.log('/api/bar');
42 | ctx.body = {
43 | route: '/api/bar'
44 | };
45 | // return next(); // if match not to next
46 | });
47 |
48 | // doT测试
49 | router.all('/page/tpl', async (ctx, next) => {
50 | ctx.body = await render('tiny', {title: 'tiny dot template', body: 'powered by doT'});
51 | });
52 |
53 | let routeMiddleware = router.routes();
54 | app.use(routeMiddleware);
55 |
56 | app.use(async (ctx, next) => {
57 | console.log('body test', ctx.req.body);
58 | if (ctx.path === '/api/fetch') {
59 | ctx.body = ctx.req.body;
60 | } else {
61 | return next();
62 | }
63 | });
64 |
65 | app.use(server(__dirname + '/static'));
66 |
67 | app.listen(port, () => {
68 | console.log(`listening ${port}`);
69 | });
70 |
--------------------------------------------------------------------------------
/src/middleware/tiny-koa-router.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Route = class {
4 | constructor(path, method, route) {
5 | this.path = path;
6 | this.method = method.toUpperCase();
7 | this.route = (ctx, next) => {
8 | ctx.params = this.params;
9 | route(ctx, next);
10 | };
11 | this.params = {};
12 | }
13 |
14 | match(reqPath) {
15 | let paramsObj = {};
16 |
17 | let routePathArr = this.path.split('/').filter(_=>_!=='');
18 | let reqPathArr = reqPath.split('/').filter(_=>_!=='');
19 |
20 | if (routePathArr.length !== reqPathArr.length) {
21 | return false;
22 | }
23 |
24 | for (let i = 0, len = routePathArr.length; i < len; i++) {
25 | let route = routePathArr[i];
26 | let isParam = route.startsWith(':');
27 |
28 | if (isParam) {
29 | let paramKey = route.slice(1);
30 | paramsObj[paramKey] = reqPathArr[i];
31 | } else if(route !== reqPathArr[i]) {
32 | return false;
33 | }
34 | }
35 | this.params = paramsObj;
36 |
37 | return true;
38 | }
39 | };
40 |
41 | const TinyKoaRouter = class {
42 | constructor() {
43 | this.routeStack = [];
44 | this.methods = ['get', 'post'];
45 | this.methods.forEach((method) => {
46 | TinyKoaRouter.prototype[method] = (path, route) => {
47 | this.routeStack.push(new Route(path, method, route));
48 | }
49 | });
50 | }
51 |
52 | all(path, route) {
53 | this.routeStack.push(new Route(path, 'all', route));
54 | }
55 |
56 | getMatchRoutes(reqPath) {
57 | return this.routeStack.filter((item) => {
58 | return item.match(reqPath);
59 | });
60 | }
61 |
62 | routes() {
63 | return async (ctx, next) => {
64 | let routePath = ctx.path;
65 |
66 | let matchRouts = this.getMatchRoutes(routePath);
67 | if (matchRouts.length === 0) {
68 | return next();
69 | }
70 |
71 | let dispatch = (i) => {
72 | if (i === matchRouts.length) {
73 | return next(); // to next middleware
74 | }
75 | let route = matchRouts[i].route;
76 |
77 | let routeWrap = () => {
78 | return route(ctx, () => {
79 | return dispatch(i+1);
80 | });
81 | };
82 |
83 | return Promise.resolve(routeWrap());
84 | };
85 |
86 | return dispatch(0);
87 | }
88 | }
89 | };
90 |
91 | module.exports = TinyKoaRouter;
92 |
--------------------------------------------------------------------------------
/src/middleware/tiny-koa-views.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const fs = require('fs');
5 |
6 | let D = null;
7 | // doT.js
8 | // 2011-2014, Laura Doktorova, https://github.com/olado/doT
9 | // Licensed under the MIT license.
10 |
11 | (function () {
12 | "use strict";
13 |
14 | var doT = {
15 | name: "doT",
16 | version: "1.1.1",
17 | templateSettings: {
18 | evaluate: /\{\{([\s\S]+?(\}?)+)\}\}/g,
19 | interpolate: /\{\{=([\s\S]+?)\}\}/g,
20 | encode: /\{\{!([\s\S]+?)\}\}/g,
21 | use: /\{\{#([\s\S]+?)\}\}/g,
22 | useParams: /(^|[^\w$])def(?:\.|\[[\'\"])([\w$\.]+)(?:[\'\"]\])?\s*\:\s*([\w$\.]+|\"[^\"]+\"|\'[^\']+\'|\{[^\}]+\})/g,
23 | define: /\{\{##\s*([\w\.$]+)\s*(\:|=)([\s\S]+?)#\}\}/g,
24 | defineParams:/^\s*([\w$]+):([\s\S]+)/,
25 | conditional: /\{\{\?(\?)?\s*([\s\S]*?)\s*\}\}/g,
26 | iterate: /\{\{~\s*(?:\}\}|([\s\S]+?)\s*\:\s*([\w$]+)\s*(?:\:\s*([\w$]+))?\s*\}\})/g,
27 | varname: "it",
28 | strip: true,
29 | append: true,
30 | selfcontained: false,
31 | doNotSkipEncoded: false
32 | },
33 | template: undefined, //fn, compile template
34 | compile: undefined, //fn, for express
35 | log: true
36 | }, _globals;
37 |
38 | doT.encodeHTMLSource = function(doNotSkipEncoded) {
39 | var encodeHTMLRules = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'", "/": "/" },
40 | matchHTML = doNotSkipEncoded ? /[&<>"'\/]/g : /&(?!#?\w+;)|<|>|"|'|\//g;
41 | return function(code) {
42 | return code ? code.toString().replace(matchHTML, function(m) {return encodeHTMLRules[m] || m;}) : "";
43 | };
44 | };
45 |
46 | _globals = (function(){ return this || (0,eval)("this"); }());
47 |
48 | /* istanbul ignore else */
49 | if (typeof module !== "undefined" && module.exports) {
50 | module.exports = doT;
51 | } else if (typeof define === "function" && define.amd) {
52 | define(function(){return doT;});
53 | } else {
54 | _globals.doT = doT;
55 | }
56 |
57 | var startend = {
58 | append: { start: "'+(", end: ")+'", startencode: "'+encodeHTML(" },
59 | split: { start: "';out+=(", end: ");out+='", startencode: "';out+=encodeHTML(" }
60 | }, skip = /$^/;
61 |
62 | function resolveDefs(c, block, def) {
63 | return ((typeof block === "string") ? block : block.toString())
64 | .replace(c.define || skip, function(m, code, assign, value) {
65 | if (code.indexOf("def.") === 0) {
66 | code = code.substring(4);
67 | }
68 | if (!(code in def)) {
69 | if (assign === ":") {
70 | if (c.defineParams) value.replace(c.defineParams, function(m, param, v) {
71 | def[code] = {arg: param, text: v};
72 | });
73 | if (!(code in def)) def[code]= value;
74 | } else {
75 | new Function("def", "def['"+code+"']=" + value)(def);
76 | }
77 | }
78 | return "";
79 | })
80 | .replace(c.use || skip, function(m, code) {
81 | if (c.useParams) code = code.replace(c.useParams, function(m, s, d, param) {
82 | if (def[d] && def[d].arg && param) {
83 | var rw = (d+":"+param).replace(/'|\\/g, "_");
84 | def.__exp = def.__exp || {};
85 | def.__exp[rw] = def[d].text.replace(new RegExp("(^|[^\\w$])" + def[d].arg + "([^\\w$])", "g"), "$1" + param + "$2");
86 | return s + "def.__exp['"+rw+"']";
87 | }
88 | });
89 | var v = new Function("def", "return " + code)(def);
90 | return v ? resolveDefs(c, v, def) : v;
91 | });
92 | }
93 |
94 | function unescape(code) {
95 | return code.replace(/\\('|\\)/g, "$1").replace(/[\r\t\n]/g, " ");
96 | }
97 |
98 | doT.template = function(tmpl, c, def) {
99 | c = c || doT.templateSettings;
100 | var cse = c.append ? startend.append : startend.split, needhtmlencode, sid = 0, indv,
101 | str = (c.use || c.define) ? resolveDefs(c, tmpl, def || {}) : tmpl;
102 |
103 | str = ("var out='" + (c.strip ? str.replace(/(^|\r|\n)\t* +| +\t*(\r|\n|$)/g," ")
104 | .replace(/\r|\n|\t|\/\*[\s\S]*?\*\//g,""): str)
105 | .replace(/'|\\/g, "\\$&")
106 | .replace(c.interpolate || skip, function(m, code) {
107 | return cse.start + unescape(code) + cse.end;
108 | })
109 | .replace(c.encode || skip, function(m, code) {
110 | needhtmlencode = true;
111 | return cse.startencode + unescape(code) + cse.end;
112 | })
113 | .replace(c.conditional || skip, function(m, elsecase, code) {
114 | return elsecase ?
115 | (code ? "';}else if(" + unescape(code) + "){out+='" : "';}else{out+='") :
116 | (code ? "';if(" + unescape(code) + "){out+='" : "';}out+='");
117 | })
118 | .replace(c.iterate || skip, function(m, iterate, vname, iname) {
119 | if (!iterate) return "';} } out+='";
120 | sid+=1; indv=iname || "i"+sid; iterate=unescape(iterate);
121 | return "';var arr"+sid+"="+iterate+";if(arr"+sid+"){var "+vname+","+indv+"=-1,l"+sid+"=arr"+sid+".length-1;while("+indv+"