├── .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+"