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中去,這就夠了。

發佈了16 篇原創文章 · 獲贊 3 · 訪問量 1711
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章