koa实践及其手撸

2019-07-18 17:57:23 浏览数 (1)

使用koa

Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。

Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。

必修的 hello world 应用:

代码语言:javascript复制
const Koa = require('koa');
const app = new Koa();

app.use(async ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);
ctx-上下文

和express直接调用 reqres不同,app.use用的是一个上下文对象 ctx

Ctx 将 node 的 requestresponse 对象封装到单个对象中,为编写 Web 应用程序和 API 提供了许多有用的方法。 这些操作在 HTTP 服务器开发中频繁使用,它们被添加到此级别而不是更高级别的框架,这将强制中间件重新实现此通用功能。

每个 请求都将创建一个 Context,并在中间件中作为接收器引用,或者 ctx 标识符,如以下代码片段所示:

代码语言:javascript复制
app.use(async ctx => {
  ctx; // 这是 Context
  ctx.request; // 这是 koa Request
  ctx.response; // 这是 koa Response
  ctx.cookies; // 获取和设置cookie ctx.cookies(name,value,[oprions])
  ctx.throw; // 比如返回500错误
});

如果你想返回数据:

代码语言:javascript复制
app.use(async ctx => {
  ctx.body = [{
      name:'djtao',
      job:'coder'
  }];
});

如果返回html:

代码语言:javascript复制
app.use(async ctx => {
    if(ctx.url==='/html'){
        let person={
            name:'djtao',
            job:'coder'
        };
        ctx.type='text/html;charset=utf-8';
        ctx.body=`<div>
            <p>名字:${person.name}</p>
            <p>职业:${person.job}</p>
        </div>`
    }   
  });

为方便起见,许多上下文的访问器和方法直接委托给它们的 ctx.requestctx.response,不然的话它们是相同的。 例如 ctx.typectx.length 委托给 response 对象, ctx.pathctx.method 委托给 request

中间件-洋葱模型

中间件是一个简单函数,参数除了ctx外,还有一个参数就是next——它会把流程的控制权交给下一个中间件。

koa中间件的业务逻辑依赖的是著名的洋葱模型:

代码语言:javascript复制
const Koa = require('koa');
const app = new Koa();

// #1
app.use(async (ctx, next)=>{
    console.log(1)
    await next();
    console.log(1)
});
// #2
app.use(async (ctx, next) => {
    console.log(2)
    await next();
    console.log(2)
})

app.use(async (ctx, next) => {
    console.log(3)
})

app.listen(3000);

打印的顺序是:12321。类似栈,也就是说,是#1嵌套#2,#2再嵌套#3的逻辑。

在每个中间件的操作中,ctx都得到了继承。

常用中间件

koa的好处在于,把非核心的业务全部交给中间件去做。

以下介绍常用中间件:

koa-static

我想调用一个静态html时:

代码语言:javascript复制
const koaStatic= require('koa-static');
app.use(koaStatic(__dirname '/public'));

那么,直接访问的是根目录下的/public/index.html

koa-router

路由:

代码语言:javascript复制
const Router=require('koa-router');
const router=new Router();

router.get('/string',async (ctx,next)=>{
    ctx.body='string';
})

router.get('/json',async (ctx,next)=>{
    ctx.body=[{
        name:'djtao',
        job:'coder'
    }]
})
app
  .use(router.routes()).use(router.allowedMethods())

问题来了:如果是渲染静态html呢?

koa-views

koa-views对需要进行视图模板渲染的应用是个不可缺少的中间件,支持ejs, nunjucks等众多模板引擎。

代码语言:javascript复制
const views=require('koa-views');

// 必须在router之前使用
app.use(views(__dirname   '/public', {
    // map: {
    //   html: 'underscore' 这里是渲染的模板引擎,为空时默认渲染html。
    // }
}));

app.use(koaStatic(__dirname '/public'));
router.get('/html',async (ctx,next)=>{
    await ctx.render('index'); // 渲染index文件
});

这样就完成了静态资源加载。图片标签也可以正常显示。

koa-bodypaser

这个中间件可以把post请求解析道ctx.request.body上。

代码语言:javascript复制
const bodyparser=require('koa-bodyparser')
app.use(bodyparser())

router.post('/post', async (ctx,next)=>{
    console.log(ctx.request.body)
    ctx.body=ctx.request.body;
})

在命令行模拟登录请求:

代码语言:javascript复制
curl -d "username=djtao&password=1234" http://localhost:3000/post

返回结果为:

!image-20190614214321477

手撸koa

koa的使用体会

  • ctx太好用了,集成了很多功能(res,req)。
  • 中间件(洋葱模型)

如果手撸一个名为Doa的框架,核心就是实现以上功能。

先搭建架构。

架构

万事先起架构。

代码语言:javascript复制
// doa
const http=require('http')
const server=(callback)=>{
    return http.createServer(callback);
}

class Doa{
    constructor(props){
        // super(props)
        this.middleweres=[];
    }

    listen(port,callback){
        const _server=server((req,res)=>{
            this.middleweres[0](req,res)
        });

        _server.listen(port,(err)=>{            
            callback(err)
        })
    }

    // 安装中间件
    use(middlewere){
        this.middleweres.push(middlewere); 
    }

}

module.exports=Doa;

测试用例

代码语言:javascript复制
const Doa=require('./index.js')
const app=new Doa();

app.use((ctx,next)=>{
    console.log(ctx)
})

app.listen('3000',()=>{
    console.log('aaa')
})

那么listen就实现了。

ctx实现
代理原理

res和req在某种情况下建立了绑定关系。

首先了解下get和set:我有一个对象,内有info字段,如果我要用一个name方法代理到设置或者是读取info的name字段——也就是说用Djtao.name访问读写info.name:

代码语言:javascript复制
const djtao={
  info:{name:'djtao',desc:'666'},
  get name(){
    return info.name;
  }
  set name(val){
    return this.info.name=val;
  }
}
console.log(djtao.name)
djtao.name=`dangjingtao`;
console.log(djtao.name)

同理,res,req都可以代理到一个属性。一个可读写的字段对应两个方法(get/set)。

实现req,res和ctx

koa的req实现 https://github.com/koajs/koa/blob/master/lib/

从源码来看,koa几个lib文件写的可读性相当高。也是一个字段对应get和set个方法。

所以新建request.js/response.js/ctx.js,回忆相应对象的常用方法:

  • req=>对应常用方法是: method/ url,而且是只读的。
代码语言:javascript复制
// request.js
module.exports={
    get url(){
        return this.req.url
    },

    get method(){
        return this.req.method.toLowerCase()
    }
}
  • res=>对应常用方法是: body,他是可读写的。
代码语言:javascript复制
// response.js
module.exports={
    get body(){
        return this._body
    },

    set body(val){
        this._body=val
    }
}
  • ctx=>对应常用的是 body(可读写)/ url(只读)/ method(只读)
代码语言:javascript复制
// context.js

module.exports={
    get url(){
        return this.request.url;
    },

    get body(){
        return this.request.body;
    },

    set body(val){
        this.request.body=val
    },

    get method(){
        return this.request.method
    }

}
  • 然后在koa.js中引入上述三个文件,创建新的方法:
代码语言:javascript复制
// doa.js
const context=require('./context');
const request=require('./request');
const response=require('./response');

class Doa{
        // ...
    // 自创的方法
    createCtx(req,res){
        // Object.create(对象):实现一个新的对象,原型链为参数。
        const ctx=Object.create(context);
        ctx.request=Object.create(request);
        ctx.response=Object.create(response);

        // 把原生的res和req代理到上下文的req和res
        ctx.req=ctx.request.req=req;
        ctx.res=ctx.request.res=res;
        return ctx;
    }

    // 在上下文环境中创建ctx
      listen(port,callback){
        const _server=server((req,res)=>{
            // 创建上下文环境
            const ctx=this.createCtx(req,res);
            this.middlewere(ctx);
            res.end(ctx.body);
        });

        _server.listen(port,(err)=>{            
            callback(err)
        })
    }
}
测试

经过上面的一系列神操作,就把核心的方法api实现了。

测试用例:

代码语言:javascript复制
const Doa=require('./index.js')

const app=new Doa();

app.use((ctx,next)=>{
    console.log(ctx)
    ctx.body='aaa'
})

app.listen('3000',()=>{
    console.log('aaa')
})
洋葱模型实现
函数嵌套合并

合并函数是设计模式里的一个常见概念:

操作:

代码语言:javascript复制
const add=(x,y)=>x y;
const square=z=>z*z;

// const fn=(x,y)==>square(add(x,y)); 比较low,过于硬核
const compose=(fn1,fn2)=>(...args)=>fn2(fn1(...args));
const fn=compose(add,square);
console.log(fn(1,2)) //(1 2)*(1 2)=9

上述代码只能适应两个变量,如果是需要多个函数合并,有更加优雅的操作,通过for循环,把解构出来的数组迭代:

代码语言:javascript复制
const compose=(...[first,...other])
// compose(a,b,c)=>>
// first=>>a
// other=>>[b,c]

所以完整的写法是:

代码语言:javascript复制
const add=(x,y)=>x y;
const square=z=>z*z;
const compose=(...[first,...other])=>(...args)=>{
    let ret=first(...args);
    other.forEach(fn=>{
        ret=fn(ret);
    })
    return ret;
}
const fn=compose(add,square);
console.log(fn(1,2)) //(1 2)*(1 2)=9

中间件是一个数列;

compose异步版

假设我有这样一组函数fn1-3:

代码语言:javascript复制
async function fn1(next){
    console.log('<fn1>')
    await next();
    console.log('</fn1>')
  }

async function fn2(next){
    console.log('<fn2>')
    await delay(); //
    await next();
    console.log('</fn2>')
  }

async function fn3(next){
    console.log('<fn3>')
    // await next(); fn3是最里层
    console.log('</fn3>')
  }

  const delay=()=>{
    return Promise.resolve(resolve=>{
      setTimout(()=>{
        resolve()
      },2000)
    })
  }

  const middleweres=[fn1,fn2,fn3];
  const finalfn=compose(middleweres);

那么,compose应当如何写呢?

代码语言:javascript复制
function compose(middleweres){
  return function(){
    // 如果有下一个,返回下一个带参数的promise,否则结束。
    function dispatch(i){
      let fn=middleweres[i];
      if(!fn){
        return Promise.resolve();
      }
      // 注意此处,把下个中间件作为next的函数内容。
      return Promise.resolve(fn(function next(){
        return dispatch(i 1)
      }))
    }
    return dispatch(0);
  }
}

打印结果如下:

整合

整合上述内容到doa中:

代码语言:javascript复制
compose(middleweres) {
        return function (ctx) {
            // 如果有下一个,返回下一个带参数的promise,否则结束。
            function dispatch(i) {
                let fn = middleweres[i];
                if (!fn) {
                    return Promise.resolve();
                }
                return Promise.resolve(ctx,fn(function next() {
                    return dispatch(i   1)
                }))
            }
            return dispatch(0);
        }
    }

参考阅读:Express 中间件实现 https://github.com/nanjixiong218/analys-middlewares/tree/master/src

手撸常见中间件

中间件就是一个异步函数。koa中间件的规范:

  • 一个async函数 接收ctx和next两个参数
  • 任务结束需要执行next

中间件常见任务:

  • 请求拦截
  • 路由
  • 日志
  • 静态文件服务
static中间件

static的实现需求是:访问 /public,则直接解析静态资源或列表。

代码语言:javascript复制
module.exports = (dirPath = "./public") => {
    return async (ctx, next) => {
        if (ctx.url.indexOf("/public") === 0) {
            // public开头 读取文件
            const url = path.resolve(__dirname, dirPath);
            const fileBaseName = path.basename(url);
            const filepath = url   ctx.url.replace("/public", ""); 
                console.log(filepath);
            // console.log(ctx.url,url, filepath, fileBaseName) 
            try {
                stats = fs.statSync(filepath);
                if (stats.isDirectory()) {
                    const dir = fs.readdirSync(filepath);
                    const ret = ['<div style="padding-left:20px">'];
                    dir.forEach(filename => {
                        console.log(filename);
                        // 简单认为不带小数点的格式,就是文件夹,实际应该用statSync 
                        if (filename.indexOf(".") > -1) {
                            ret.push(
                                `<p><a style="color:black" href="${
                                ctx.url
                                }/${filename}">${filename}</a></p>`
                            );
                        } else {
                            // 文件
                            ret.push(
                                `<p><a href="${ctx.url}/${filename}">${filename}</a></p>`
                            );
                        }
                    });
                    ret.push("</div>");
                    ctx.body = ret.join("");
                } else {
                    console.log("文件");
                    const content = fs.readFileSync(filepath);
                    ctx.body = content;
                }
            } catch (e) {
                // 报错了 文件不存在
                ctx.body = "404, not found";
            }
        } else {
            // 否则不是静态资源,直接去下一个中间件
            await next();
        }
    }
}

测试用例

代码语言:javascript复制
// 使用
const static = require('./static') 
app.use(static(__dirname   '/public'));

访问/public路由,就看到结果了。

router中间件

看需求:

代码语言:javascript复制
const Doa = require('./doa')
const Router = require('./router')
const app = new Doa()
const router = new Router();
router.get('/index', async ctx => { ctx.body = 'index page'; });
router.get('/post', async ctx => { ctx.body = 'post page'; });
router.get('/list', async ctx => { ctx.body = 'list page'; });
router.post('/index', async ctx => { ctx.body = 'post page'; });
// 路由实例输出父中间件 
router.routes() app.use(router.routes());

那么和手撸express一样:

代码语言:javascript复制
class Router {
    constructor() {
        this.stack = [];
    }
    // 每次定义一个路由,都注册一次
    register(path, methods, middleware) {
        let route = { path, methods, middleware }
        this.stack.push(route);
    }
    // 现在只支持get和post,其他的同理 
    get(path, middleware) {
        this.register(path, 'get', middleware);
    }
    post(path, middleware) {
        this.register(path, 'post', middleware);
    }
      //调用
    routes() {
        let stock = this.stack;
        return async function (ctx, next) {
            let currentPath = ctx.url;
            let route;
            for (let i = 0; i < stock.length; i  ) {
                let item = stock[i];
                if (currentPath === item.path && item.methods.indexOf(ctx.method) >= 0) {
                    // 判断path和method
                    route = item.middleware; break;
                }
            }
            if (typeof route === 'function') {
                route(ctx, next);
                return;
            }
            await next();
        };
    }
}

module.exports = Router;

没什么很难的逻辑。

0 人点赞