前言
又是一周过去了,常规学习不能断!但是选择什么主题呢?一时间不知道选什么好,于是又想起简单的 koajs 非常愉快的就选择他了 https://koajs.com/ ,了解一下?
他是个什么东西呢?
Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。
hello world
首先新建一个 node 项目,其实很简单,只需要一个 package.json 文件,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 { "name": "koa-hello", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "node src/index.js" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "koa": "^2.7.0" } }
然后执行
代码 index.js 文件,新建一个 koa 实例,使用 app.use 写一个 async 方法,设置 ctx.body 的值就可以了。最后使用 app.listen 启动。
1 2 3 4 5 6 7 8 const Koa = require('koa'); const app = new Koa(); app.use(async ctx => { ctx.body = 'Hello World'; }); app.listen(3000);
这样的话,一个 web 服务器就搭建好了,访问 http://localhost:3000/ 就会得到 hello world 返回结果了。你可以尝试更改字段从而得到不同的返回结果。
源码解析
koa 的源码只有四个文件,不包含其他引用的话
1 2 3 4 5 6 7 8 9 10 . ├── History.md ├── LICENSE ├── Readme.md ├── lib │ ├── application.js │ ├── context.js │ ├── request.js │ └── response.js └── package.json
主入口可以在 package.json 的 main 中得到,是 application.js,暂时先知道 middleware 是中间接,通常一个请求过来就会依次执行中间件的方法。
构造函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 module.exports = class Application extends Emitter { /** * Initialize a new `Application`. * * @api public */ constructor() { super(); this.proxy = false; this.middleware = []; this.subdomainOffset = 2; this.env = process.env.NODE_ENV || 'development'; this.context = Object.create(context); this.request = Object.create(request); this.response = Object.create(response); if (util.inspect.custom) { this[util.inspect.custom] = this.inspect; } } }
app.use 其实就是添加一个中间件,我们通常使用 async 的函数,generator 被抛弃了!
1 2 3 4 5 6 7 8 9 10 11 12 use(fn) { if (typeof fn !== 'function') throw new TypeError('middleware must be a function!'); if (isGeneratorFunction(fn)) { deprecate('Support for generators will be removed in v3. ' + 'See the documentation for examples of how to convert old middleware ' + 'https://github.com/koajs/koa/blob/master/docs/migration.md'); fn = convert(fn); } debug('use %s', fn._name || fn.name || '-'); this.middleware.push(fn); return this; }
app.listen 创建一个服务器,监听 3000 端口,http.createServer 是 node 的服务器。
1 2 3 4 5 listen(...args) { debug('listen'); const server = http.createServer(this.callback()); return server.listen(...args); }
callback 是提供一个函数,所有请求都会走到这个函数里面进行处理。每次请求过来都会调用这个函数,所以,我们可以看到,每次请求都会创建一个 ctx 的对象。
compose 的作用就是将所有的中间件整合成一个函数,使用 next 函数继续调用下一个函数。
1 2 3 4 5 6 7 8 9 10 11 12 callback() { const fn = compose(this.middleware); if (!this.listenerCount('error')) this.on('error', this.onerror); const handleRequest = (req, res) => { const ctx = this.createContext(req, res); return this.handleRequest(ctx, fn); }; return handleRequest; }
初始化 ctx 对象,这里 this.request 将会把原生的 request 参数进行解析,方便我们进行相关参数获取。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 /** * Initialize a new context. * * @api private */ createContext(req, res) { const context = Object.create(this.context); const request = context.request = Object.create(this.request); const response = context.response = Object.create(this.response); context.app = request.app = response.app = this; context.req = request.req = response.req = req; context.res = request.res = response.res = res; request.ctx = response.ctx = context; request.response = response; response.request = request; context.originalUrl = request.originalUrl = req.url; context.state = {}; return context; }
比如我们之后就可以使用
** ctx.query.key ** 来获取 http://localhost:3000?key=value,为什么可以使用 ctx.query 又可以获取参数呢,这个要靠 Object.create 的本事了,它相当于创造了一个对象,继承了原来的对象,而 this.request 有 query 的参数,而最为重要的是 this.context = Object.create(context); context 委托(使用了 Delegator)了这些 request 的相关属性和方法。【第一次体会到 js 委托,以前知识听说不知道是啥】
1 2 3 4 5 6 7 8 9 10 /** * Request delegation. */ delegate(proto, 'request') .access('method') .access('query') .access('path') .access('url') ....... // 省略
handleRequest 请求处理,fnMiddleware 就是所有的中间件,
1 2 3 4 5 6 7 8 handleRequest(ctx, fnMiddleware) { const res = ctx.res; res.statusCode = 404; const onerror = err => ctx.onerror(err); const handleResponse = () => respond(ctx); onFinished(res, onerror); return fnMiddleware(ctx).then(handleResponse).catch(onerror); }
调用完中间件以后,就执行 handleResponse 将数据返回,返回数据也就是将 ctx.body 拿出来,使用 response.end 返回数据,返回时,会对数据进行处理,在最后面可以体会到~
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 /** * Response helper. */ function respond(ctx) { // allow bypassing koa if (false === ctx.respond) return; if (!ctx.writable) return; const res = ctx.res; let body = ctx.body; const code = ctx.status; // ignore body if (statuses.empty[code]) { // strip headers ctx.body = null; return res.end(); } if ('HEAD' == ctx.method) { if (!res.headersSent && isJSON(body)) { ctx.length = Buffer.byteLength(JSON.stringify(body)); } return res.end(); } // status body if (null == body) { if (ctx.req.httpVersionMajor >= 2) { body = String(code); } else { body = ctx.message || String(code); } if (!res.headersSent) { ctx.type = 'text'; ctx.length = Buffer.byteLength(body); } return res.end(body); } // responses if (Buffer.isBuffer(body)) return res.end(body); if ('string' == typeof body) return res.end(body); if (body instanceof Stream) return body.pipe(res); // body: json body = JSON.stringify(body); if (!res.headersSent) { ctx.length = Buffer.byteLength(body); } res.end(body); }
到这里,基本的请求已经清楚了~~
End
再来看一眼最简单的 http server 代码,对比一下,比 koa 代码的 hello world 相比并没有多复杂
1 2 3 4 5 6 var http = require('http'); http.createServer(function (req, res) { res.writeHead(200, {'Content-Type': 'text/plain'}); res.write('Hello World!'); res.end(); }).listen(8080);
但是,获取参数,使用路由等等插件,koa 生态做了很多,非常方便,快来体验吧!