新一代web框架Koa源碼學習

此文已由做者張佃鵬受權網易雲社區發佈。
node

歡迎訪問網易雲社區,瞭解更多網易技術產品運營經驗。git


Koa 就是一種簡單好用的 Web 框架。它的特色是優雅、簡潔、表達力強、自由度高。自己代碼只有1000多行。koa一箇中間件框架,其提供的是一個架子,而幾乎全部的功能都須要由第三方中間件完成,它只是node原生的http的一個封裝,再加入中間件元素,koa 不在內核方法中綁定任何中間件, 它僅僅提供了一個輕量優雅的函數庫,使得編寫 Web 應用變得駕輕就熟github

Koa目前分爲兩個版本:koa 1.0和koa2數組

  • koa 1.0: 依賴generator函數和Promise實現異步處理(ES6)安全

  • koa2: 依賴async函數和Promise實現異步處理(ES7)cookie

如下的關於koa的介紹主要在koa2的基礎上進行分析:app

koa框架的使用

koa框架主要由如下幾個元素組成:框架

app

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

app的主要屬性以下:dom

  • proxy: 表示是否開啓代理信任開關,默認爲false,若是開啓代理信任,對於獲取request請求中的host,protocol,ip分別優先從Header字段中的X-Forwarded-Host,X-Forwarded-Proto,X-Forwarded-For獲取:koa

//如下是koa獲取request對象部分屬性的源碼,都是由app.proxy屬性決定的:{
    get ips() {        const proxy = this.app.proxy;        const val = this.get('X-Forwarded-For');        return proxy && val
          ? val.split(/\s*,\s*/)
          : [];
    },

    get host() {        const proxy = this.app.proxy;        let host = proxy && this.get('X-Forwarded-Host');
        host = host || this.get('Host');        if (!host) return '';        return host.split(/\s*,\s*/)[0];
    },

    get protocol() {        const proxy = this.app.proxy;        if (this.socket.encrypted) return 'https';        if (!proxy) return 'http';        const proto = this.get('X-Forwarded-Proto') || 'http';        return proto.split(/\s*,\s*/)[0];
    },
    get URL() {        if (!this.memoizedURL) {          const protocol = this.protocol;          const host = this.host;          const originalUrl = this.originalUrl || ''; // originalUrl爲req.url
          try {            this.memoizedURL = new URL(`${protocol}://${host}${originalUrl}`);
          } catch (err) {            this.memoizedURL = Object.create(null);
          }
        }        return this.memoizedURL;
    },
    get hostname() {        const host = this.host;        if (!host) return '';        if ('[' == host[0]) return this.URL.hostname || ''; // IPv6
        return host.split(':')[0];
    },
}
  • env:node運行環境

this.env = process.env.NODE_ENV || 'development';
  • keys: app.keys是一個設置簽名的Cookie密鑰的數組,用於生成cookies對象

  • subdomainOffset:表示子域名是從第幾級開始的,這個參數決定了request.subdomains的返回結果,默認值爲2

//好比有netease.youdata.163.com域名app.subdomainOffset = 2;console.log(ctx.request.subdomains);  //返回["youdata", "netease"]app.subdomainOffset = 3;console.log(ctx.request.subdomains);  //返回["netease"]//koa獲取subdomains的源碼get subdomains() {    const offset = this.app.subdomainOffset;    const hostname = this.hostname;    if (net.isIP(hostname)) return [];    return hostname
      .split('.')
      .reverse()
      .slice(offset);
},
  • middleware:app對應的中間件數組,使用app.use函數會將會將中間件加到該數組中

koa使用中間件方式來實現不一樣功能的級聯,當一箇中間件調用next(),則該函數暫停並將控制傳遞給定義的下一個中間件。當在下游沒有更多的中間件執行後,堆棧將展開而且每一箇中間件恢復執行其上游行爲,相似一個入棧出棧的模式,中間件的使用方式以下:

const Koa = require('koa');const app = new Koa();
app.use((ctx, next) => {    console.log('step1-begin');
    next();    console.log('step1-end');
});
app.use((ctx, next) => {    console.log('step2-begin');
    next();    console.log('step2-end');
});

app.listen(3000);/*輸出結果爲:
    step1-begin
    step2-begin
    step2-end
    step1-end
*/
  • context:這個是建立中間件中使用的「ctx」的原型,直接使用app.context意義不大,並且app.context上不少屬性實際上是爲ctx準備的,直接用app.context調用會報錯:

//如下context.js中的部分源碼:toJSON() {    return {
      request: this.request.toJSON(),   //若是直接使用app.context調用這個會報錯,由於這個時候this.request是undefined,只有在中間件裏使用ctx調用纔不會報錯
      response: this.response.toJSON(),
      app: this.app.toJSON(),
      originalUrl: this.originalUrl,
      req: '<original node req>',
      res: '<original node res>',
      socket: '<original node socket>'
    };
  },

context主要有如下用途:

//咱們能夠在context對象上加一些全局路由裏公用的屬性,這樣就不須要每次請求都在中間件裏賦值const Koa = require('koa');const app = new Koa();
app.context.datasourceConfig = {    "connectionLimit": 100,    "database": "development",    "host": "10.165.124.134",    "port": 3360,    "user": "sup_bigviz",    "password": "123456",    "multipleStatements": true};
app.use((ctx, next) => {    console.log('datasourceConfig:', ctx.datasourceConfig); //這裏能夠打印出全局配置
    next();
});
  • request: 這個是建立ctx.request的原型,直接使用app.context.request幾乎沒有意義,不少屬性都會報錯,不過和app.context同樣,能夠給app.context添加一些ctx.request中用到的公共屬性

  • response: 這個是建立ctx.response的原型,直接使用app.context.response幾乎沒有意義,不少屬性都會報錯,不過和app.context同樣,能夠給app.context添加一些ctx.request中用到的公共屬性

app的主要函數以下:

  • use函數: use函數主要做用是給app.middleware數組中添加中間件

let koa = require('koa');
koa.use(async (ctx, next) => {    //before do something...    next();    //after await do something...
})
  • listen函數:app.listen函數是建立服務的入口,只有調用app.listen函數之後,全部的中間件纔會被使用

//app.listen實際上是http.createServer的語法糖,源碼實現以下:function listen(...args) {
    debug('listen');    const server = http.createServer(this.callback()); //最終全部路由處理是在app..callback中實現的
    return server.listen(...args);
 }
  • callback函數:返回一個函數供http.createServer() 方法的回調函數來處理請求。你也可使用此回調函數將koa應用程序掛載到Connect/Express應用程序中

//koa的callback函數實現源碼function callback() {    const fn = compose(this.middleware);   //koa-compose包負責講多箇中間件組裝成一箇中間件
    if (!this.listeners('error').length) this.on('error', this.onerror);    const handleRequest = (req, res) => {      const ctx = this.createContext(req, res);  //這個函數負責生成中間件接收器ctx,綁定一些對象的關聯關係
      return this.handleRequest(ctx, fn);  //使用中間件函數fn處理路由請求
    };    return handleRequest;
}//handleRequest函數的源碼實現也很簡單,執行中間件函數,並作一些返回處理和異常處理function handleRequest(ctx, fnMiddleware) {    const res = ctx.res;
    res.statusCode = 404;    const onerror = err => ctx.onerror(err);    const handleResponse = () => respond(ctx);
    onFinished(res, onerror);    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }

ctx

ctx是中間件中的上下文環境,也是koa框架中最經常使用最重要的對象,每一個請求都會根據app.context建立一個新的ctx,並在中間件中做爲接收器引用

ctx對象上會綁定app,request,response等對象

//生成ctx的源碼function createContext(req, res) {    const context = Object.create(this.context);   //由上文中講解的app.context生成
    const request = context.request = Object.create(this.request);  //由上文中講解的app.request生成
    const response = context.response = Object.create(this.response); //由上文中講解的app.response生成
    context.app = request.app = response.app = this;
    context.req = request.req = response.req = req;   //req是node的req,儘可能避免使用,而是使用ctx.request;
    context.res = request.res = response.res = res;   //res是node的res,儘可能避免使用,而是應該使用ctx.response;
    request.ctx = response.ctx = context;
    request.response = response;
    response.request = request;
    context.originalUrl = request.originalUrl = req.url;
    context.cookies = new Cookies(req, res, {       //生成cookies,是由[cookie模塊生成的](https://github.com/pillarjs/cookies):
      keys: this.keys,
      secure: request.secure   //secure是根據域名是否是https返回的結果
    });
    request.ip = request.ips[0] || req.socket.remoteAddress || '';   //客戶端訪問ip
    context.accept = request.accept = accepts(req);  //
    context.state = {};   //這個給用戶使用,用於存放用戶在多箇中間件中用到的一些屬性或者函數
    return context;
}

ctx會代理ctx.response和ctx.request上的一些屬性和函數(這個代理邏輯是在ctx.response和ctx.request的原型上實現的)

//如下是koa源碼(method表示代理方法,access表示代理屬性可讀可寫,getter表示代理屬性可讀):delegate(proto, 'response')
  .method('attachment') //將Content-Disposition 設置爲 「附件」 以指示客戶端提示下載
  .method('redirect') //返回重定向,若是沒有code設置,默認設置code爲302
  .method('remove')   //刪除響應頭的某個屬性
  .method('vary')  //設置Vary響應頭
  .method('set') //設置響應頭,能夠傳遞對象,數組,單個值的形式
  .method('append') //給response.headers中的某個key值追加其它value
  .method('flushHeaders')  //執行this.res.flushHeaders()
  .access('status')  //http返回code碼,優先選擇用戶的設置,若是用戶沒有主動設置,而設置了ctx.body的值, 若是設置值爲null,則返回204,若是設置值不爲null,那麼返回200,不然默認狀況下是404
  .access('message')  //獲取響應的狀態消息. 默認狀況下, response.message 與 response.status 關聯
  .access('body')   //response的返回結果
  .access('length')  //response的headers的Content-Length,能夠本身設置,默認根據body二進制大小設置
  .access('type')   //設置響應的content-type
  .access('lastModified')  //設置響應頭Last-Modified
  .access('etag')  //設置包含 " 包裹的 ETag 響應頭
  .getter('headerSent')  //檢查是否已經發送了一個響應頭。 用於查看客戶端是否可能會收到錯誤通知
  .getter('writable');   //返回是否能夠繼續寫入delegate(proto, 'request')
  .method('acceptsLanguages')
  .method('acceptsEncodings')
  .method('acceptsCharsets')
  .method('accepts')        //accepts函數用於判斷客戶端請求是否接受某種返回類型
  .method('get')   //獲取請求頭中的某個屬性值
  .method('is')  //判斷請求頭但願返回什麼類型
  .access('querystring') //獲取原始查詢字符串
  .access('idempotent')
  .access('socket') //返回請求套接字
  .access('search') //搜索字符串
  .access('method')  //請求方法
  .access('query')  //獲取請求的查詢字符串對象
  .access('path')  //獲取請求路徑名
  .access('url')  //請求的url,該url能夠被重寫
  .getter('origin')  //獲取url的來源:包括 protocol 和 host(http://example.com)
  .getter('href') //獲取完整的請求URL,包括 protocol,host 和 url(http://example.com/foo/bar?q=1)
  .getter('subdomains') //獲取請求的子域名
  .getter('protocol') //返回請求協議
  .getter('host') //獲取當前主機的host(hostname:port)
  .getter('hostname') //獲取當前主機的host
  .getter('URL') //獲取 WHATWG 解析的 URL 對象
  .getter('header') //返回請求頭對象
  .getter('headers')  //返回請求頭對象
  .getter('secure') //經過 ctx.protocol == "https" 來檢查請求是否經過 TLS 發出
  .getter('stale')
  .getter('fresh')
  .getter('ips')  //當 X-Forwarded-For 存在而且 app.proxy 被啓用時,這些 ips 的數組被返回
  .getter('ip'); //請求遠程地址//好比如下操做是等價的:ctx.body = {
    code: 200,
    result: {
        nick: "zhangdianpeng"
    }
}

ctx.response.body = {
    code: 200,
    result: {
        nick: "zhangdianpeng"
    }
}console.log('ctx.method:', ctx.method);console.log('ctx.request.method:', ctx.request.method);


免費體驗雲安全(易盾)內容安全、驗證碼等服務

更多網易技術、產品、運營經驗分享請點擊




相關文章:
【推薦】 網易杭研易盾實習心得(4)
【推薦】 如何玩轉基於風險的測試
【推薦】 AndroidTVOverscan

相關文章
相關標籤/搜索