新一代web框架Koa原始碼學習

網易雲社群發表於2018-11-02

此文已由作者張佃鵬授權網易雲社群釋出。

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


Koa 就是一種簡單好用的 Web 框架。它的特點是優雅、簡潔、表達力強、自由度高。本身程式碼只有1000多行。koa一箇中介軟體框架,其提供的是一個架子,而幾乎所有的功能都需要由第三方中介軟體完成,它只是node原生的http的一個封裝,再加入中介軟體元素,koa 不在核心方法中繫結任何中介軟體, 它僅僅提供了一個輕量優雅的函式庫,使得編寫 Web 應用變得得心應手

Koa目前分為兩個版本:koa 1.0和koa2

  • koa 1.0: 依賴generator函式和Promise實現非同步處理(ES6)

  • koa2: 依賴async函式和Promise實現非同步處理(ES7)

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

koa框架的使用

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

app

const Koa = require('koa');const app = new Koa();複製程式碼

app的主要屬性如下:

  • proxy: 表示是否開啟代理信任開關,預設為false,如果開啟代理信任,對於獲取request請求中的host,protocol,ip分別優先從Header欄位中的X-Forwarded-Host,X-Forwarded-Proto,X-Forwarded-For獲取:

//以下是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);複製程式碼


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

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




相關文章:
【推薦】 使用者研究如何獲取更為真實的使用者資訊


相關文章