Node二:KOA

KOA

# 安装 koa
npm i koa
# 或者
yarn add koa

https://koa.bootcss.com/

创建WebServer

const Koa = require('koa');
// 初始化一个 koa 对象
const app = new Koa();
// 使用 app 对象来创建一个 webserver
/**
 * http.createServer((req, res) => {})
 *
 * (new Http.Server().on('request', (req, res) => {}))
 */
app.listen(8888);

中间件

Koa 中,中间件 本质上就是用来完成各种具体业务逻辑的函数,通过 use 方法来进行注册

const Koa = require('koa');
const app = new Koa();
app.use(() => {
  console.log('中间件 - 1');
});
app.listen(8888);

上下文对象

KoaRequestResponse 进行了二次封装,提供了更多的特性和方法来完成工作。同时又封装了一个 Context 对象,KoaApplicationRequestResponse 对象后续其它一些特性方法挂载到该对象上,并在 中间件 执行过程中通过第一个参数进行传入:

const Koa = require('koa');
const app = new Koa();

app.use((ctx) => {
  console.log('中间件 - 1');
  //只需要把要返回的内容赋值给ctx.body即可,Koa会在最后进行一些处理并end。
  ctx.body = ' 这是返回的内容';
});

app.listen(8888);

Compose

通常,我们的业务是由一系列的完成不同任务的中间件函数组合而成

const Koa = require('koa');
const app = new Koa();

app.use((ctx) => {
  console.log('中间件 - 1');
  ctx.body = ' 11111';
});
app.use((ctx) => {
  console.log('中间件 - 2');
  ctx.body = ' 2222';
});
app.listen(8888);

上述代码执行的结果是:只有第一个中间件被执行了 。原因在于 Koa 并非直接通过循环调用的方式去执行每一个中间件,而是在执行当前中间件的时候会把下一个中间件函数作为第二个参数传入。这样,我们就可以在中间件中控制下一个中间件逻辑是否可以继续进行

const Koa = require('koa');
const app = new Koa();

// 模拟用户登录数据
let user = null;
// let user = {id: 1, username: 'zMouse'};

app.use((ctx, next) => {
  if (!user) {
      ctx.body = '没有权限';
  } else {
    next();
    ctx.body = `<h1>${ctx.body}</h1>`;
  }
});
app.use((ctx, next) => {
  ctx.body = '用户有权限';
});

app.listen(8888);

异步中间件

有的时候,我们的中间件逻辑中会包含一些异步任务:

const Koa = require('koa');
const app = new Koa();

// 模拟用户登录数据
let user = null;
// let user = {id: 1, username: 'zMouse'};

app.use(async (ctx, next) => {
  if (!user) {
      ctx.body = '没有权限';
  } else {
    //使用await  
    await next();
    ctx.body = `<h1>${ctx.body}</h1>`;
  }
});
app.use((ctx, next) => {
  // 比如读取数据库(异步)
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      ctx.body = '用户有权限';
      resolve();
    }, 1000)
  });
});

app.listen(8888);

部分实用中间件介绍

静态文件代理

npm i koa-static-cache
const Koa = require('koa');
const koaStaticCache = require('koa-static-cache'); 
const app = new Koa();

app.use( koaStaticCache({
  // url 中的前缀
  prefix: '/public',
  // url 对应的静态资源存放目录
  dir: './public',
  // 启用 gzip 传输压缩
  gzip: true,
  // 监听静态资源文件变化
  dynamic: true
}) );

app.listen(8888);

//输入如下链接就能访问到public目录下对应的资源
// http://localhost:8888/public/index.html
//http://localhost:8888/public/user.html

路由

npm i koa-router
const Koa = require('koa');
const KoaRouter = require('koa-router'); 
const app = new Koa();

const router = new KoaRouter();

// 路由函数注册
router.get('/', async (ctx, next) => {
  ctx.body = '首页';
});

router.get('/users', async (ctx, next) => {
  ctx.body = '用户列表';
});

// 注册中间件
app.use( router.routes() );

app.listen(8888);

//输入如下链接就能访问到对应的路由返回
//http://localhost:8888
//http://localhost:8888/users

post请求body内容解析

npm i koa-body
const Koa = require('koa');
const KoaRouter = require('koa-router');
const koaBody = require('koa-body');
const fs = require('fs');

const app = new Koa();

const router = new KoaRouter();

// 路由函数注册
router.get('/', async (ctx, next) => {
    ctx.body = fs.readFileSync('./users.html').toString();
});

// 处理 post 提交的数据
router.post('/add', koaBody(), async (ctx, next) => {
    console.log( ctx.request.body);//{ username: 'xxx' }
     ctx.body = '获取成功';
});

// 注册中间件
app.use(router.routes());

app.listen(8888);

users.html文件
<body>
    <form action="/add" method="post">
        <input type="text" name='username'>
        <button>提交</button>
    </form>
</body>

KOA源码

Application 对象

通过该对象来初始化并创建 WebServer

constructor(options) {
  super();
  options = options || {};
  ...
  this.middleware = [];
  this.context = Object.create(context);
  this.request = Object.create(request);
  this.response = Object.create(response);
  ...
}

应用代码

const Koa = require('koa');
const app = new Koa();

listen 方法

WebServer 并不是在 Application 对象创建的时候就被创建的,而是在调用了 Application 下的 listen 方法的时候在创建。其本质还是通过 NodeJS 内置的 http 模块的 createServer 方法创建的 Server 对象。并且把 this.callback() 执行后的结果(函数)作为后续请求的回调函数。

listen(...args) {
  debug('listen');
  const server = http.createServer(this.callback());
  return server.listen(...args);
}

应用代码

const Koa = require('koa');
const app = new Koa();
app.listen(8888);

中间件

所谓的中间件函数, Application 对象中会提供一个属性 this.middleware = []; ,用来存储处理的各种业务函数。这些业务函数通过 use 方法进行注册。

constructor() {
  ...
  this.middleware = [];
  ...
}
use(fn) {
  ...
  this.middleware.push(fn);
  return this;
}

中间件的执行流程

callback() {
    //compose 函数将middleware处理为一个函数
    const fn = compose(this.middleware);
    // 有请求时触发,node原生的req和res对象
    const handleRequest = (req, res) => {
        //会调用createContext方法对req和res进行包装,生成Koa另外一个核心对象:Context
        const ctx = this.createContext(req, res);
        return this.handleRequest(ctx, fn);
    };

    return handleRequest;
}

handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    onFinished(res, onerror);
    //这里的 fnMiddleware就是一系列中间件函数执行后结果(一个 Promise 对象),当所有中间件函数执行完成以后,会通过then调用handleResponse也就是调用了 respond这个方法。
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}

//响应处理
function respond(ctx) {
     //各种响应处理
     ...
    //发送响应
    res.end(body);
}

function compose(middleware) {
    //返回一个函数接受context, next(调用时没传入参,默认是undefined)
    return function (context, next) {
        return dispatch(0)
        function dispatch(i) {
            let fn = middleware[i]
            //最后一个middleware函数没有next,赋值为undefined
            if (i === middleware.length) fn = next
            if (!fn) return Promise.resolve()
            try {
                // 每次执行时将下一个函数作为第二个参数传入
                return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
            } catch (err) {
                return Promise.reject(err)
            }
        }
    }
}

应用代码

const Koa = require('koa');
const app = new Koa();

app.use(async (ctx, next) => {
   console.log('a - start');
   next();
   console.log('a - end');
});

app.use(async (ctx, next) => {
    console.log('b - start');
    next();
    console.log('b - end');
 });

app.use(async (ctx, next) => {
    console.log('c - start');
    next();
    console.log('c - end');
 });

app.listen(8888);

输出
a - start
b - start
c - start
c - end
b - end
a - end

洋葱模型

image.png

Context 对象

提供一些基础方法,同时对 RequestResponse 对象做了代理访问

context 对象的初始化

Application 对象初始化的时候,会创建一个 Context 对象,并挂载到 Applicationcontext 属性下。同时在callback执行的时候,还会对这个 context 进行包装,并把包装后的 context 对象作为中间件函数的第一个参数。

//接受node原生的req和res对象
createContext(req, res) {
  const context = Object.create(this.context);
  const request = context.request = Object.create(this.request);
  const response = context.response = Object.create(this.response);
  context.app = request.app = response.app = this;
  context.req = request.req = response.req = req;
  context.res = request.res = response.res = res;
  request.ctx = response.ctx = context;
  request.response = response;
  response.request = request;
  context.originalUrl = request.originalUrl = req.url;
  context.state = {};
  return context;
}
一个简单代理示例
const delegate = require('delegates');
class test {
    constructor() {
        this.req = {
          nodeHeaders: 'node headers'
        }
        this.resquest = {
            get header() {
                return this.req.nodeHeaders;
            },

            set header(val) {
                this.req.nodeHeaders = val;
            },
        }
        this.context = {
            utils() {
                console.log('一些工具函数');
            }
        }
        // 通过代理,直接用context访问resquest下的属性
        delegate(this.context, 'resquest')
        .access('header')
    }

    createContext() {
        const context = Object.create(this.context);
        const resquest = context.resquest = Object.create(this.resquest);
        context.req = resquest.req = this.req;
        return context
    }
}

const a = new test()
const context = a.createContext()

console.log(context.resquest.header);//node headers
console.log(context.header);//node headers

ctx.state 属性

有的时候,我们需要在多个中间件中传递共享数据,虽然可以通过 context 对象进行挂载,但是这样会对 context 对象造成污染, context 对象提供了一个专门用来共享这类用户数据的命名空间( context.state = {} )。

应用代码

const Koa = require('koa');
const app = new Koa();

app.use(async (ctx, next) => {
  ctx.state.user = {id: 1, name: 'zMouse'};
});
app.use(async (ctx, next) => {
  ctx.body = `Hello, ${ctx.state.user.name}`;
});

app.listen(8888);
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,616评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,020评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,078评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,040评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,154评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,265评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,298评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,072评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,491评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,795评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,970评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,654评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,272评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,985评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,815评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,852评论 2 351

推荐阅读更多精彩内容