💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
- 框架目录 - 初识 - ctx - use与中间件 - ctx.body - 请求体 - static - 关于错误捕获 - 获取demo代码 [TOC] ## 框架目录 ``` koa/ | | - context.js | | - request.js | | - response.js | ·- application.js ``` ## 初识 ### 介绍 首先我们通过`Koa`包导入的是一个类(Express中是一个工厂函数),我们可以通过`new`这个类来创建一个app ``` let Koa = require('koa'); let app = new Koa(); ``` 这个`app`对象上就两个方法 `listen` 用来启动一个http服务器 ``` app.listen(8080); ``` `use`用来注册一个中间件 ``` app.use((ctx,next)=>{ ... }) // 一般我们将(ctx,next)=>{}包装成一个异步函数 //async (ctx,next)=>{} ``` 可以发现这个use方法接收一个函数作为参数,这个函数又接收两个参数`ctx`、`next`, 其中ctx是koa自己封装的一个上下文对象,这个对象你可以看做是原生http中req和res的集合。 而next和Express中的next一样,可以在注册的函数中调用用以执行下一个中间件。 ### 框架搭建 ``` /* application.js */ class Koa extends EventEmitter{ constructor(){ super(); this.middlewares = []; this.context = context; this.request = request; this.response = response; } //监听&&启动http服务器 listen(){ const server = http.createServer(this.handleRequest()); return server.listen(...arguments); } //注册中间件 use(fn){ this.middlewares.push(fn); } //具体的请求处理方法 handleRequest(){ return (req,res)=>{...} } //创建上下文对象 createContext(req,res){ ... } //将中间件串联起来的方法 compose(ctx,middlewares){ ... } } ``` ## ctx ### 用法 ctx,即context,大多数人称之为上下文对象。 这个对象下有4个主要的属性,它们分别是 - ctx.req:原生的req对象 - ctx.res:原生的res对象 - ctx.request:koa自己封装的request对象 - ctx.response:koa自己封装的response对象 其中koa自己封装的和原生的最大的区别在于,koa自己封装的请求和响应对象的内容不仅囊括原生的还有一些其独有的东东 ``` ... console.log(ctx.query); //原生中需要经过url.parse(p,true).query才能得到的query对象 console.log(ctx.path); //原生中需要经过url.parse(p).pathname才能得到的路径(url去除query部分) ... ``` 除此之外,ctx本身还代理了ctx.request和ctx.response身上的属性,So以上还能简化为 ``` ... console.log(ctx.query); console.log(ctx.path); ... ``` ### 原理 首先我们要创建三个模块来代表三个对象 ctx对象/模块 ``` //context.js let proto = {}; module.exports = proto; ``` 请求对象/模块 ``` let request = {}; module.export = request; ``` 响应对象/模块 ``` let response = {}; module.exports = response; ``` 然后在`application.js`中引入 ``` let context = require('./context'); let request = require('./request'); let response = require('./response'); ``` 并在constructor中挂载 ``` this.context = context; this.request = request; this.response = response; ``` 接下来我们来理一理流程,`ctx.request/response`是koa自己封装的,那么什么时候生成的呢?肯定是得到原生的req、res之后才能进行加工吧。 So,我们在专门处理请求的`handleRequest`方法中来创建我们的ctx ``` handleRequest(){ return (req,res)=>{ let ctx = this.createContext(req,res); ... } } ``` #### createContext 为了使我们的每次请求都拥有一个**全新的`ctx`对象**,我们在createContext方法中采用`Object.create`来创建一个**继承**自`this.context`的对象。 这样即使我们在每一次请求中改变了ctx,例如`ctx.x = xxx`,那么也只会在本次的ctx中创建一个**私有**属性而不会影响到下一次请求中的ctx。(response也是同理) ``` createContext(req,res){ let ctx = Object.create(this.context); //ctx.__proto__ = this.context ctx.response = Object.create(this.response); } ``` 呃,说回我们最初的目的,我们要创建一个ctx对象,这个ctx对象下有4个主要的属性:`ctx.req`、`ctx.res`、`ctx.request`、`ctx.response`。 其中`ctx.request/response`囊括`ctx.req/res`的所有属性,那么我们要怎么将原本req和res下的属性赋给koa自己创建的请求和响应对象呢?这么多属性,难道要一个一个for过去吗?显然这样操作太重了。 我们能不能想个办法当我们访问ctx.request.xx属性的时候其实就是访问ctx.req.xx属性呢? #### get/set of coures,we can! ``` //application.js createContext(req,res){ ... ctx.req = ctx.request.req = req; ctx.res = ctx.response.res = res; return ctx; } // --- --- --- //request.js let request = { get method(){ return this.req.method } } ``` 通过以上代码,我们在访问`ctx.response.method`的时候其实访问的就是`ctx.req.method`,而ctx.req.method其实就是req.method。 其中的`get method(){}`这样的语法时es5里的特性,当我们访问该对象下的method属性时就会执行该方法并以这个方法中的返回值作为我们访问到的值。 我们还能通过在get中做一些处理来为`ctx.request`创建一些原生的req对象没有的属性 ``` let request = { ... get query(){ return url.parse(this.req.url,true).query; } }; ``` #### delateGetter 除了通过`ctx.request.query`拿到query对象,我们还能通过`ctx.query`这样简写的方式直接拿到原本在request下的所有属性。这又是怎么实现的呢? 很简单,我们只需要用ctx来代理ctx.request即可 ``` // context.js ... function delateGetter(property,name){ proto.__defineGetter__(name,function(){ return this[property][name]; }); } delateGetter('request','query'); ... ``` 通过`proto.__defineGetter__(name,function(){})`代理(和上一节所展示的get/set是一样的功能) 当我们访问`proto.name`的时候其实就是访问的`proto.property.name`。 也就是说`ctx.query`的值即为`ctx.request.query`的值。 >**注意:** 这里get/set,delateGetter/Setter都只演示了一两个属性,想要更多,就得添加更多的get()/set(),delateGetter/Setter(),嗯源码就这么干的。 ## use与中间件 我们通过`use`方法注册中间件,这些中间件会根据注册时的先后顺序,被依次注册到一个数组当中,并且当一个请求来临时,这些中间件会按照注册时的顺序依次执行。 但这些中间件并不是自动依次执行的,我们需要在`中间件callback`中手动调用`next`方法执行下一个`中间件callback`(和express中一样),并且最后的显示的结果是有点微妙的。 >**注意:** 这一节内容需要对异步的发展有清楚的认知, > 对于异步发展还不大清楚的同学可以参考我的这盘文章 > [异步发展简明指北](https://juejin.im/post/5a6212386fb9a01ca5608de3) ### next与洋葱模型 ![](https://user-gold-cdn.xitu.io/2018/4/16/162cdbb1df1f0c87?w=478&h=435&f=webp&s=27312) 我们来看下面这样一个栗子 ``` app.use(async (ctx,next)=>{ console.log(1); await next(); console.log(2); }); app.use(async (ctx,next)=>{ console.log(3); await next(); console.log(4); }); <<< 1 3 4 2 ``` 嗯,第一次接触koa的同学肯定很纳闷,what the fk???这是什么鬼? 嗯,我们先记住这个现象先不急探究,再接着往下看看中间件其它需要注意的事项。 ### 中间件与异步 我们在注册中间件时,通常会将回调包装成一个`async`函数,这样,假若我们的回调中存在异步代码,就能不写那冗长的回调而通过`await`关键字像写同步代码一样写异步回调。 ``` app.use(async (ctx,next)=>{ let result = await read(...); //promisify的fs.read console.log(result); }) ``` #### 包装成promise 需要补充的一点时,要让await有效,就需要将异步函数包装成一个promise,通常我们直接使用promisify方法来promise化一个异步函数。 #### next也要使用await 还需要注意的是假若下一个要执行的中间件回调中也存在异步函数,我们就需要在调用next时也使用`await`关键字 ``` app.use(async (ctx,next)=>{ let result = await read(...); //promisify的fs.read console.log(result); await next(); //本身async函数也是一个promise对象,故使用await有效 console.log('1'); }) ``` 不使用awiat的话,假若下一个中间件中存在异步就不会等待这个异步执行完就会打印`1`。 ### 原理 接下来我们来看怎么实现中间件洋葱模型。 如果一个中间件回调中没有异步的话其实很简单 ``` let fns = [fn1,fn2,fn3]; function dispatch(index){ let middle = fns[index]; if(fns.length === index)return; middle(ctx,()=>dispatch(index+1)); } ``` 我们只需要有一个`dispatch`方法来遍历存放中间件回调函数的数组。并将这个dispatch方法作为next参数传给本次执行的中间件回调。 这样我们就能在一个回调中通过调用next来执行下一次遍历(dispatch)。 但一个中间件回调中往往存在异步代码,如果我们像上面这样写是达不到我们想要的效果的。 那么,要怎样做呢?我们需要借助promise的力量,将每个中间件回调串联起来。 ``` handleRequest(){ ... let composeMiddleWare = this.compose(ctx,this.middlewares) ... } ``` ``` compose(ctx,middlewares){ function dispatch(index){ let middleware = middlewares[index]; if(middlewares.length === index)return Promise.resolve(); return Promise.resolve(middleware(ctx,()=>dispatch(index+1))); } return dispatch(0); } ``` 其中一个`middleware`即是一个`async fn`,而每一个`async fn`都是一个promise, 在上面的代码中我们让这个promise转换为成功态后才会去遍历下一个middleware,而什么时候promise才会转为成功态呢? 嗯,只有当一个`async fn`执行完毕后,`async fn`这个promise才会转为成功态,而每一个`async fn`在内部若存在异步函数的话又可以使用await, SO,我们就这样将各个`middleware`串联了起来,即使其内部存在异步代码,也会按照洋葱模型执行。 ## ctx.body ### 使用 `ctx.body`即是koa中对于原生res的封装。 ``` app.use(async (ctx,next)=>{ ctx.body = 'hello'; }); <<< hello ``` 需要注意的是,`ctx.body`可以被多次连续调用,但只有最后被调用的会生效 ``` ... ctx.body = 'hello'; ctx.body = 'world'; ... <<< world ``` `ctx.body`支持以流、object作为响应值。 ``` ctx.body = {...} ``` ``` ctx.body = require('fs').createReadStream(...); ``` ### 原理 我们调用ctx.body实际上调用的是ctx.response.body(参考ctx代理部分),并且我们只是给这个属性赋值,这仅仅是个属性并不会立马调用res.end等来进行响应 而我们真正响应的时候是在所有中间件都执行完毕以后 ``` //application.js handleRequest(){ let composeMiddleWare = this.compose(ctx,this.middlewares); composeMiddleWare.then(function(){ let body = ctx.body; if(body == undefined){ return res.end('Not Found'); } if(body instanceof Stream){ //如果ctx.body是一个流 return body.pipe(res); } if(typeof body === 'object'){ //如果ctx.body是一个对象 return res.end(JSON.stringify(body)); } res.end(ctx.body); //ctx.body是字符串和buffer }) } ``` ## 请求体 上面我们说过在`async fn`中我们能使用`await`来"同步"异步方法。 其实除了一些异步方法需要await外,请求体的接收也需要await ``` app.use(async (ctx,next)=>{ ctx.req.on('data',function(data){ //异步的 buffers.push(data); }); ctx.req.on('end',function(){ console.log(Buffer.concat(buffers)); }); }); app.use(async (ctx,next)=>{ console.log(1); }) ``` 像上面这样的例子`1`是会被先打印的,这意味着如果我们想要在一个中间件中获取完请求体并在下一个中间件中使用它,是做不到。 那么要怎样才能达到我们预期的效果呢?在await一节中我们讲过,我们可以将代码封装成一个promise然后再去await就能达到同步的效果。 我们可以通过npm下载到这样的一个库——koa-bodyparser ``` let bodyparser = require('koa-bodyparser'); app.use(bodyparser()); ``` 这样,我们就能在任何中间件回调中通过`ctx.request.body`获取到请求体 ``` app.use(async (ctx,next)=>{ console.log(ctx.request.body); }) ``` 但需要注意的是,`koa-bodyparser`并不支持文件上传,如果要支持文件上传,可以使用`better-body-parser`这个包。 ### body-parser 实现 ``` function bodyParser(options={}){ let {uploadDir} = options; return async (ctx,next)=>{ await new Promise((resolve,reject)=>{ let buffers = []; ctx.req.on('data',function(data){ buffers.push(data); }); ctx.req.on('end',function(){ let type = ctx.get('content-type'); // console.log(type);//multipart/form-data; boundary=----WebKitFormBoundary8xKcmy8E9DWgqZT3 let buff = Buffer.concat(buffers); let fields = {}; if(type.includes('multipart/form-data')){ //有文件上传的情况 }else if(type === 'application/x-www-form-urlencoded'){ // a=b&&c=d fields = require('querystring').parse(buff.toString()); }else if(type === 'application/json'){ fields = JSON.parse(buff.toString()); }else{ // 是个文本 fields = buff.toString(); } ctx.request.fields = fields; resolve(); }); }); await next(); }; } ``` 可以发现 `bodyParser`本身即是一个`async fn`,它将`on data on end`接收请求体部分代码封装成了一个promise,并且`await`这个promise,这意味着只有当这个promise转换为成功态时,才会走`next`(遍历下一个中间件)。 而我们什么时候将这个promise转换为成功态的呢?是在将请求体解析完毕封装成一个`fields`对象并挂载到`ctx.request.fields`之后,我们才resolve了这个promise。 以上就是bodyParser实现的大体思路,还有一点我们没有详细解释的部分既是有文件上传的情况。 当我们将`enctype`设置为`multipart/form-data`,我们就可以通过表单上传文件了,此时请求体的样子是长这样的 ![](https://box.kancloud.cn/226f14757c62a84dc616b29e6a887a4f_698x396.png) 嗯。。。其实接下来要干的的事情即是对这个请求体进行拆分拼接。。一顿字符串操作,这里就不再展开啦 有兴趣的朋友可以到我的仓库中查看完整代码示例[点我~](https://github.com/fancierpj0/iKoa/blob/master/better-body.js) ## static Koa中为我们提供了静态服务器的功能,不过需要额外引一个包 ``` let static = require('koa-static'); let path = require('path'); app.use(static(path.join(__dirname,'public'))); app.listen(8000); ``` 只需三行代码,咳咳,静态服务器你值得拥有。 ### 原理 原理也很简单啦,`static`首先它也是一个`async fn` ``` function static(p){ return async(ctx,next)=>{ try{ p = path.join(p,'.'+ctx.path); let statObj = await stat(p); if(statObj.isDirectory()){ ... }else{ ctx.body = fs.createReadStream(p); //在body上挂载可读流,会在所有中间件执行完毕后以pipe形式输出到客户端 } }catch(e) { await next(); } } } ``` ## 关于错误捕获 最后,koa还允许我们在一个`async fn`中抛出一个异常,此时它会返回个客户端一串字符串`Internal Server Error`,并且它还会触发一个`error`事件 ``` app.use(async (ctx,next)=>{ throw Error('something wrong'); }); app.on('error',function(err){ console.log('e',err); }); ``` ### 原理 ``` // application.js handleRequest(){ ... composeMiddleWare.then(function(){ ... }).catch(e=>{ this.emit('error',e); res.end('Internal Server Error'); }) ... } ``` ## 获取demo代码 > 仓库:[点我](https://github.com/fancierpj0/iKoa)