前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >koa实践及其手撸

koa实践及其手撸

作者头像
一粒小麦
发布2019-07-18 17:57:23
1.1K0
发布2019-07-18 17:57:23
举报
文章被收录于专栏:一Li小麦一Li小麦

使用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;

没什么很难的逻辑。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-06-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 一Li小麦 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 使用koa
    • ctx-上下文
      • 中间件-洋葱模型
        • 常用中间件
          • koa-static
          • koa-router
          • koa-views
          • koa-bodypaser
      • 手撸koa
        • 架构
          • ctx实现
            • 代理原理
            • 实现req,res和ctx
            • 测试
          • 洋葱模型实现
            • 函数嵌套合并
            • compose异步版
            • 整合
          • 手撸常见中间件
            • static中间件
            • router中间件
        相关产品与服务
        消息队列 TDMQ
        消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档