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返回消息的操作。

此外值得注意的一点是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中去,这就够了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值