Koa2框架原理及实现 Koa2 是一个基于Node实现的Web框架,特点是优雅、简洁、健壮、体积小、表现力强。它所有的功能通过插件的形式来实现。
下面自己实现一个简单的Koa,通过这种方式来深入理解Koa原理,尤其是中间件部分的理解。Koa的具体实现可以看的koa的源码 。
1 2 3 4 5 6 7 8 9 10 11 12 13 import Koa from 'koa' const app = new Koa ()const port = 3000 app.use ((ctx ) => { ctx.body = 'hello koa' }) app.listen (port, () => { console .log (`server is running at Http://localhost${port} ` ) })
启动应用,浏览器中打开 http://localhost:3000/
通过上面的代码,如果要实现koa,需要实现三个模块,分别是http的封装,ctx对象的构建,中间件机制的实现,当然koa还实现了错误捕获和错误处理。
封装http模块 通过阅读Koa2的源码 可知Koa是通过封装原生的node http模块。
1 2 3 4 5 6 7 8 9 10 11 import http from 'http' const port = 3000 const app = http .createServer ((req, res ) => { res.end ('hello nodejs' ) }) .listen (port, () => { console .log (`server is running at http://localhost:${port} ` ) })
以上是使用Node.js创建一个HTTP服务的代码片段,关键是使用http模块中的createServer()方法,接下来对上面这面这部分过程进行一个封装。
创建application.js,并创建一个Application类用于创建Koa实例。
通过创建use()方法来注册中间件和回调函数。
通过listen()方法开启服务监听实例,并传入use()方法注册的回调函数。
如下代码所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import http from 'http' export default class Application { constructor ( ) { this .callback = () => {} } use (callback ) { this .callback = callback return this } listen (...args ) { http .createServer ((req, res ) => { this .callback (req, res) }) .listen (...args) } }
接下来创建一个server.js,引入application.js进行测试
1 2 3 4 5 6 7 8 9 10 11 import MiniKoa from './application.js' const port = 3000 const app = new MiniKoa ()app .use ((req, res ) => { res.end ('hello world' ) }) .listen (port, () => { console .log (`server is running at http://localhost:${port} ` ) })
启动后,在浏览器中输入 http://localhost:3000/ 就能看到显示”hello world”。这样就完成http server的简单封装了。
构造ctx对象 Koa 的 Context 把 Node 的 Request 对象和 Response 对象封装到单个对象中,并且暴露给中间件等回调函数。比如获取 url,封装之前通过req.url的方式获取,封装之后只需要ctx.url就可以获取。因此需要达到以下效果:
1 2 3 4 5 app.use (async ctx => { ctx ctx.request ctx.response })
JavaScript 的 getter 和 setter 在此之前,需要了解 setter 和 getter 属性,通过 setter 和 getter 属性,可以自定义属性的特性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 let person = { _name : 'old name' , get name () { return this ._name }, set name (val ) { console .log ('---修改name的值---' ) this ._name = val } } console .log (person.name ) person.name = 'new name' console .log (person.name )
构造 context 使用 getter 和 setter 来构造 context:
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 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 import http from 'http' export default class Application { constructor ( ) { this .context = context this .request = request this .response = response } use (callback ) { this .callback = callback return this } listen (...args ) { http .createServer (async (req, res) => { const ctx = this .createCtx (req, res) await this .callback (ctx) ctx.res .end (ctx.body ) }) .listen (...args) } createCtx (req, res ) { const ctx = Object .create (this .context ) ctx.request = Object .create (this .request ) ctx.response = Object .create (this .response ) ctx.req = ctx.request .req = req ctx.res = ctx.response .res = res return ctx } } const request = { get url () { return this .req .url } } const response = { get body () { return this ._body }, set body (val ) { this ._body = val } } const context = { get url () { return this .request .url }, get body () { return this .response .body }, set body (val ) { this .response .body = val } }
这时,就可以通过 ctx 来获取 url 了:
1 2 3 4 5 6 7 8 9 10 11 12 import MiniKoa from './application.js' const port = 3000 const app = new MiniKoa ()app .use (async (ctx) => { ctx.body = 'hello world!' }) .listen (port, () => { console .log (`server is running at http://localhost:${port} ` ) })
Koa中间件及洋葱圈模型的理解与实现 koa的中间件机制是一个洋葱圈模型,通过use()注册多个中间件放入数组中,然后从外层开始往内执行,遇到next()后进入下一个中间件,当所有中间件执行完后,开始返回,依次执行中间件中未执行的部分,如上图所示。
在实现之前,先来了解一下中间件的原理,根据中间件的原理可知,要层层递进执行多个函数,比如下面的例子:
1 2 3 4 5 6 7 8 const add = (x, y ) => x + yconst double = (z ) => z * 2 const res1 = add (1 , 2 )const res2 = double (res1)console .log (res2)
上面的例子中,把add()函数的结果传入double()中,把函数作为参数,这样最终就会先执行add()然后执行double(),下面把这种模式编写成一个通用的compose()函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const add = (x, y ) => x + yconst double = (z ) => z * 2 const compose = (middleware ) => { return (...args ) => { let res = middleware[0 ](...args) for (let i = 1 ; i < middleware.length ; i++) { res = middleware[i](res) } return res } } const fn = compose ([add, double])const res = fn (1 , 2 )console .log (res)
上面的compose()函数还有一个缺点,它是一个同步的方法,并没有异步的等待,如果要使用异步,直接使用for循环是不行的,它不能等待异步执行完毕。
koa 对外暴露了next()方法来实现异步等待,它是一个Promise,当执行到它时就执行下一个中间件。
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 55 56 57 58 async function fn1 (next ) { console .log ('fn1' ) await next () console .log ('end fn1' ) } async function fn2 (next ) { console .log ('fn2' ) await delay () await next () console .log ('end fn2' ) } async function fn3 (next ) { console .log ('fn3' ) } function delay ( ) { return new Promise ((resolve, reject ) => { setTimeout (() => { resolve () }, 2000 ) }) } function compose (middleware ) { return () => { return dispatch (0 ) function dispatch (i ) { let fn = middleware[i] if (!fn) return Promise .resolve () return Promise .resolve (fn (function next ( ) { return dispatch (i + 1 ) }) ) } } } const middleware = [fn1, fn2, fn3]const finalFn = compose (middleware)finalFn ()
上面已经实现一个了一个简单的中间件示例,接下来把它整合到 Application 类中:
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 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 import http from 'http' export default class Application { constructor ( ) { this .context = context this .request = request this .response = response this .middleware = [] } use (callback ) { this .middleware .push (callback) return this } compose (middleware ) { return function (context ) { return dispatch (0 ) function dispatch (i ) { let fn = middleware[i] if (!fn) return Promise .resolve () return Promise .resolve ( fn (context, function next ( ) { return dispatch (i + 1 ) }) ) } } } listen (...args ) { http .createServer (async (req, res) => { const ctx = this .createCtx (req, res) const fn = this .compose (this .middleware ) await fn (ctx) ctx.res .end (ctx.body ) }) .listen (...args) } createCtx (req, res ) { const ctx = Object .create (this .context ) ctx.request = Object .create (this .request ) ctx.response = Object .create (this .response ) ctx.req = ctx.request .req = req ctx.res = ctx.response .res = res return ctx } } const request = { get url () { return this .req .url } } const response = { get body () { return this ._body }, set body (val ) { this ._body = val } } const context = { get url () { return this .request .url }, get body () { return this .response .body }, set body (val ) { this .response .body = val } }
测试它是否好用:
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 import MiniKoa from './application.js' const port = 3000 const app = new MiniKoa ()function delay ( ) { return new Promise ((resolve, reject ) => { setTimeout (() => { resolve () }, 2000 ) }) } app .use (async (ctx, next) => { ctx.body = '(fn1) ' await next () ctx.body += '(end fn1) ' }) .use (async (ctx, next) => { ctx.body += '(fn2) ' await delay () await next () ctx.body += '(end fn2) ' }) .use (async (ctx, next) => { ctx.body += '(fn3) ' }) .listen (port, () => { console .log (`server is running at http://localhost:${port} ` ) })
总结 到此为止,一个简单的 Koa 就实现了,但是这里还缺少了异常处理,更详细的实现方式请查看 Koa 源码,无非也只是一些工具函数以及一些功能点的细化,其基本原理大概就是如此了。其中的难点是中间件原理,通过这个例子彻底理解中间件原理后,以后再使用起这个框架来,就更加得心应手了。