JAVA程序员如何转node_05

前言

这一篇应该就是这个系列的最后一篇了。之后的文章里我会分享node的其他内容,作为node的入门文章来说我觉得这几个足以了。不放前置文章了,大家想看的自己往前翻就是了。


koa

前面写了那么些,我们需要明确的一点是:koa这个框架到底做了些什么事情?

为了说明这个问题,让我们再次比较一下,原始node和koa是怎么写http的

// node 
var http = require('http');
http.createServer((request, response)=> {response.end('Hello World\n');}).listen(8888);
console.log('Server running at http://127.0.0.1:8888/');

//koa
const Koa = require('koa');
const app = new Koa();
app.use((context,next)=>{context.body='hello world';);
app.listen(8888);
console.log('Server running at http://127.0.0.1:8888/');

我的理解是:1、他将中间件组织了起来以便程序对他们进行依次调用,2、他将请求(request)和响应(response)封装成了一个上下文(context),使context可以使用request和response的方法和属性。

分析

我们直接从源码的角度进行分析。

在一个目录下cmd输入 npm install --save koa ,就会下载koa的相关包,这时候查看node_modules中,koa的源代码只有四个:koa、koa-compose、koa-convert、koa-is-json

其中koa-is-json只有这么一点代码 忽略掉

function isJSON(body) {
    if (!body) return false;
    if ('string' == typeof body) return false;
    if ('function' == typeof body.pipe) return false;
    if (Buffer.isBuffer(body)) return false;
    return true;
}

我们首先来看看koa是如何组织中间件的。我提前说一下结论,首先先在koa包中的app类中编写一个方法use()将中间件添加到数组中,然后使用koa-compose包的中的函数将该数组中的函数组织成中间件。

//koa包  lib/application.js
use(fn) {
  if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
  if (isGeneratorFunction(fn)) {
    deprecate('Support for generators will be removed in v3\. ' +
    'See the documentation for examples of how to convert old middleware ' +
    'https://github.com/koajs/koa/tree/v2.x#old-signature-middleware-v1x---deprecated');
    fn = convert(fn);
  }
  debug('use %s', fn._name || fn.name || '-');
  this.middleware.push(fn);
  return this;
}

我们知道,在node中,函数是作为第一等公民的,所以函数是可以作为数组中的一个成员的。当我们编写了函数function func1(ctx,next){}并使用app.use(func1)的时候,实际上就是将func1这个函数放入了一个数组中。

接下来,你可以继续添加,当你一旦使用app.listen(port)对端口进行监听的时候,这时候koa就会使用koa-compose对数组中的函数进行组织。

// koa-compose包 index.js
function compose (middleware) {
    if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
    for (const fn of middleware) {
        if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
    }
/**
* @param {Object} context
* @return {Promise}
* @api public
*/
    return function (context, next) {
        let index = -1
        return dispatch(0)
        function dispatch (i) {
            if (i <= index) return Promise.reject(new Error('next() called multiple times'))
            index = i
            let fn = middleware[i]
            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)
            }
        }
    }
}

returne返回的这个函数有些难以看懂,其实他就是用promise写一个处理逻辑,递归调用dispatch,按照middleware数组的顺序往下一层一层地调用next来执行中间件(回调函数);

可以用async来改写。

function compose(middleware) {
  return dispatch(0);
  async function dispatch(i) {
    let fn = middleware[i];
    try {
      await fn(dispatch.bind(null, i + 1));
    } catch (err) {
      return err;
    }
  }
}

可以看出在调用逻辑上中间件和async调用没有什么本质上的差别。通过这种形式,我们就将顺序操作组织为层级操作。


再来看看koa对request和response的封装。

在node_modules中打开koa包,可以看到他有四个文件(application.js、context.js、request.js、response.js),主要来看看application的实现。

首先在new了一个Koa实例出来后,application(app)构造函数

// application.js
constructor() {
  super();
  this.proxy = false;
  this.middleware = [];
  this.subdomainOffset = 2;
  this.env = process.env.NODE_ENV || 'development';
  this.context = Object.create(context);
  this.request = Object.create(request);
  this.response = Object.create(response);
}

并没有做什么实质性的工作,只是根据另外三个文件创建了三个对象。

接下来,app.listen(端口号)。

执行了这一步,koa就会把服务器实例正式运行起来(包括刚才对中间件的组织),具体代码如下

// application.js
listen() {
    const server = http.createServer(this.callback()); //这个http.createServer就是上文node的那种创建方式
    return server.listen.apply(server, arguments);  //这里是用js的语法更改一下this的指向和传入参数并执行
}
callback() {
    const fn = compose(this.middleware);  // 这个函数调用的就是上文所说的中间件组织
    if (!this.listeners('error').length) this.on('error', this.onerror);
    return (req, res) => {               //返回一个参数为request和response的函数给http.createServer()
        res.statusCode = 404;
        const ctx = this.createContext(req, res);  //将request和response封装成一个context
        const onerror = err => ctx.onerror(err);
        onFinished(res, onerror);
        fn(ctx).then(() => respond(ctx)).catch(onerror); // 依次执行中间件
    };
}
createContext(req, res) {
    const context = Object.create(this.context);  //每次传入来一个请求,都会复制出来一个新的context对象、request和response对象,让他们拥有指向彼此的指针
    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.cookies = new Cookies(req, res, {
        keys: this.keys,
        secure: request.secure
    });
    request.ip = request.ips[0] || req.socket.remoteAddress || '';
    context.accept = request.accept = accepts(req);
    context.state = {};
    return context;
}

由于http模块的作用,每次server在收到一个有效request请求之后,会产生一个request对象和response对象(上一篇讲到的,不记得的可以回去看),这时候koa层面就会把这个request和response做一个合并的处理,让他们都在ctx(context)对象中进行操作。

从代码里面可以看出,ctx里保存着res(response)和req(request)对象的引用,而res对其他两个也是如此。其实我个人认为这样只是单纯有利于调试,对于代码的组织来说似乎没什么作用。我们在java中也习惯了httpRequest处理请求,httpResponse返回消息的操作。

image

此外值得注意的一点是koa包中context.js文件

// context.js

delegate(proto, 'response')
  .method('attachment')
  .method('redirect')
  .method('remove')
  .method('vary')
  .method('has')
  .method('set')
  .method('append')
  .method('flushHeaders')
  ...

delegate(proto, 'request')
  .method('acceptsLanguages')
  .method('acceptsEncodings')
  .method('acceptsCharsets')
  .method('accepts')
  .method('get')
  .method('is')
  .access('querystring')
  .access('idempotent')
  .access('socket')
  ...

这里只需要知道他是使用了delegate委托的方法,将request和response中的方法代理到context中去,这就够了。

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

推荐阅读更多精彩内容