本文共 8330 字,大约阅读时间需要 27 分钟。
koa的使用非常简单,引入依赖后编写
const Koa = require('koa')let app = new Koa()app.use((ctx, next) => { console.log(ctx)})app.listen(4000)
然后在浏览器端打开http://127.0.0.1:4000
即可访问
若没有指定返回body
,koa默认处理成了Not Found
再进一步扩展代码,看看ctx上面有哪些东西
// ... console.log(ctx) console.log('native req ----') // node原生的req console.log(ctx.req.url) console.log(ctx.request.req.url) console.log('koa request ----') // koa封装了request console.log(ctx.url) console.log(ctx.request.url) // native req ---- // / // / // koa request ---- // / // /// ...
以上,自取。
在有说明在ctx挂载了一系列request
和response
的属性别名。
ctx = {}ctx.request = {}ctx.response = {}ctx.req = ctx.request.req = reqctx.res = ctx.response.res = res// ctx.url 代理了 ctx.request.url
以下,自取。
使用next看看作用
const Koa = require('koa')let app = new Koa()app.use((ctx, next) => { console.log(1) next() console.log(2)})app.use((ctx, next) => { console.log(3) next() console.log(4)})app.use((ctx, next) => { console.log(5) next() console.log(6)})app.listen(4000)// 1// 3// 5// 6// 4// 2
从上面代码打印结果可以看出,next的作用就是做一个占位符。可以看成以下形式
app.use((ctx, next) => { console.log(1) app.use((ctx, next) => { console.log(3) app.use((ctx, next) => { console.log(5) next() console.log(6) }) console.log(4) }) console.log(2)})
这即是洋葱模型。
如果某个中间件有异步代码呢?
const Koa = require('koa')let app = new Koa()// 异步函数const logger = () => { return new Promise((resolve, reject) => { setTimeout(_ => { console.log('logger') resolve() }, 1000) })}app.use((ctx, next) => { console.log(1) next() console.log(2)})app.use(async (ctx, next) => { console.log(3) await logger() next() console.log(4)})app.use((ctx, next) => { console.log(5) next() console.log(6)})app.listen(4000)// 1// 3// 2// 等待1s// logger// 5// 6// 4
此时打印结果并不是我们预期的结果,我们期望的是1 -> 3 -> 1s logger -> 5-> 6-> 4 ->2
此时我们需要在next
前面加一个await
// ...app.use(async (ctx, next) => { console.log(1) await next() console.log(2)})// ...
koa
致力于成为一个更小、更富有表现力、更健壮的web
开发框架。
其源码也是非常轻量且易读。
核心文件四个
application.js
:简单封装http.createServer()
并整合context.js
context.js
:代理并整合request.js
和response.js
request.js
:基于原生req
封装的更好用response.js
:基于原生res
封装的更好用下面涉及到的代码存放到仓库中,需要的自取。
koa是用ES6实现的,主要是两个核心方法app.listen()
和app.use((ctx, next) =< { ... })
先来在application.js
中实现app.listen()
const http = require('http')class Koa { constructor () { // ... } // 处理用户请求 handleRequest (req, res) { // ... } listen (...args) { let server = http.createServer(this.handleRequest.bind(this)) server.listen(...args) } }module.exports = Koa
从上面的简单使用中可以看出
ctx = {}ctx.request = {}ctx.response = {}ctx.req = ctx.request.req = reqctx.res = ctx.response.res = resctx.xxx = ctx.request.xxxctx.yyy = ctx.response.yyy
我们需要以上几个对象,最终都代理到ctx
对象上。
创建context.js/request.js/response.js
三个文件
request.js
内容
const url = require('url')let request = {}module.exports = request
response.js
内容
let response = {}module.exports = response
context.js
内容
let context = {}module.exports = context
在application.js
中引入上面三个文件并放到实例上
const context = require('./context')const request = require('./request')const response = require('./response')class Koa extends Emitter{ constructor () { super() // Object.create 切断原型链 this.context = Object.create(context) this.request = Object.create(request) this.response = Object.create(response) }}
由于不能直接用等号为其赋值,不然在修改变量属性时会直接篡改原始变量,因为对象引用了同一内存空间。
所以使用Object.create
方法切断依赖,此方法相当于
function create (parentPrototype) { function F () {} F.prototype = parentPrototype return new F()}
然后处理用户请求并在ctx
上代理request / response
// 创建上下文 createContext (req, res) { let ctx = this.context // 请求 ctx.request = this.request ctx.req = ctx.request.req = req // 响应 ctx.response = this.response ctx.res = ctx.response.res = res return ctx } handleRequest (req, res) { let ctx = this.createContext(req, res) return ctx }
在context.js
中,使用__defineGetter__ / __defineSetter__
实现代理,他是Object.defineProperty()
方法的变种,可以单独设置get/set
,不会覆盖设置。
let context = {}// 定义获取器function defineGetter (key, property) { context.__defineGetter__ (property, function () { return this[key][property] })}// 定义设置器function defineSetter (key, property) { context.__defineSetter__ (property, function (val) { this[key][property] = val })}// 代理 requestdefineGetter('request', 'path')defineGetter('request', 'url')defineGetter('request', 'query')// 代理 responsedefineGetter('response', 'body')defineSetter('response', 'body')module.exports = context
在request.js
中,使用ES5提供的属性访问器实现封装
const url = require('url')let request = { get url () { return this.req.url // 此时的this为调用的对象 ctx.request }, get path () { let { pathname } = url.parse(this.req.url) return pathname }, get query () { let { query } = url.parse(this.req.url, true) return query } // ...更多待完善}module.exports = request
在response.js
中,使用ES5提供的属性访问器实现封装
let response = { set body (val) { this._body = val }, get body () { return this._body // 此时的this为调用的对象 ctx.response } // ...更多待完善}module.exports = response
以上实现了封装request/response
并代理到ctx
上
ctx = {}ctx.request = {}ctx.response = {}ctx.req = ctx.request.req = reqctx.res = ctx.response.res = resctx.xxx = ctx.request.xxxctx.yyy = ctx.response.yyy
接下来实现koa中第二个方法app.use((ctx, next) =< { ... })
use中存放着一个个中间件,如cookie、session、static...
等等一堆处理函数,并且以洋葱式
的形式执行。
constructor () { // ... // 存放中间件数组 this.middlewares = [] } // 使用中间件 use (fn) { this.middlewares.push(fn) }
当处理用户请求时,期望执行所注册的一堆中间件
// 组合中间件 compose (middlewares, ctx) { function dispatch (index) { // 迭代终止条件 取完中间件 // 然后返回成功的promise if (index === middlewares.length) return Promise.resolve() let middleware = middlewares[index] // 让第一个函数执行完,如果有异步的话,需要看看有没有await // 必须返回一个promise return Promise.resolve(middleware(ctx, () => dispatch(index + 1))) } return dispatch(0) } // 处理用户请求 handleRequest (req, res) { let ctx = this.createContext(req, res) this.compose(this.middlewares, ctx) return ctx }
以上的dispatch
迭代函数在很多地方都有运用,比如递归删除目录
,也是koa
的核心。
返回的promise主要是为了处理中间件中含有异步代码的情况
在所有中间件执行完毕后,需要渲染页面
// 处理用户请求 handleRequest (req, res) { let ctx = this.createContext(req, res) res.statusCode = 404 // 默认404 当设置body再做修改 let ret = this.compose(this.middlewares, ctx) ret.then(_ => { if (!ctx.body) { // 没设置body res.end(`Not Found`) } else if (ctx.body instanceof Stream) { // 流 res.setHeader('Content-Type', 'text/html;charset=utf-8') ctx.body.pipe(res) } else if (typeof ctx.body === 'object') { // 对象 res.setHeader('Content-Type', 'text/josn;charset=utf-8') res.end(JSON.stringify(ctx.body)) } else { // 字符串 res.setHeader('Content-Type', 'text/html;charset=utf-8') res.end(ctx.body) } }) return ctx }
需要考虑多种情况做兼容。
通过以上代码进行以下测试
执行结果将是
// 1 => 3 =>1s,logger => 4// => 3 =>1s,logger => 4 => 2
并不满足我们的预期
因为执行过程如下
在第 2 步中, 传入的 i 值为 1, 因为还是在第一个中间件函数内部, 但是 compose 内部的 index 已经是 2 了, 所以 i < 2, 所以报错了, 可知在一个中间件函数内部不允许多次调用 next 函数。
解决方法就是使用flag作为洋葱模型的记录已经运行的函数中间件的下标, 如果一个中间件里面运行两次 next, 那么 index 是会比 flag 小的。
/** * 组合中间件 * @param {Array} middlewares * @param {context} ctx */ compose (middlewares, ctx) { let flag = -1 function dispatch (index) { // 3)flag记录已经运行的中间件下标 // 3.1)若一个中间件调用两次next那么index会小于flag // if (index <= flag) return Promise.reject(new Error('next() called multiple times')) flag = index // 2)迭代终止条件:取完中间件 // 2.1)然后返回成功的promise if (index === middlewares.length) return Promise.resolve() // 1)让第一个函数执行完,如果有异步的话,需要看看有没有await // 1.1)必须返回一个promise let middleware = middlewares[index] return Promise.resolve(middleware(ctx, () => dispatch(index + 1))) } return dispatch(0) }
如何处理在中间件中出现的异常呢?
Node
是以事件驱动的,所以我们只需继承events
模块即可
const Emitter = require('events')class Koa extends Emitter{ // ... // 处理用户请求 handleRequest (req, res) { // ... let ret = this.compose(this.middlewares, ctx) ret.then(_ => { // ... }).catch(err => { // 处理程序异常 this.emit('error', err) }) return ctx } }
然后在上面做捕获异常,使用时如下就好
const Koa = require('./src/index')let app = new Koa()app.on('error', err => { console.log(err)})
测试用例代码存放在中,需要的自取。
通过以上我们实现了一个简易的KOA
,request/response.js
文件还需扩展支持更多属性。
完整代码以及测试用例存放在,感兴趣可前往调试。
转载地址:http://jsqni.baihongyu.com/