Express version 4.17核心源碼解析

啓動一個Express負責回吐wasm格式文件的服務很是簡單 html

Express的源碼、以及目前如今主流庫已經所有使用TypeScript編寫,呼籲你們全面切換到TypeScript vue

因爲本文是本身項目中的一段服務代碼臨時拼湊而成,因此這裏沒有使用TypeScriptjava

注:不管是javaScript仍是Node.js的框架源碼其實都不難,稍微花點心思就能夠看得很透徹,本文只是在使用wasm中順手一寫,可能不像其餘人分析得那麼專業git

衆所周知,Express引入後,它須要調用纔會得到app對象,那麼能夠得知,咱們引入的Express一開始是一個函數,進入源碼查看github

先分析@types的包  關於TypeScirpt源碼 web

再分析javaScript express

Express初始引入的是一個函數,但是它身上有一些例如express.static的方法,是怎麼回事呢?那麼咱們進入core.Express中查看它的接口數組

初始引入函數遵循的接口繼承了Application服務器

這裏request和response遵循的接口格式應該比較簡單,待會下面在寫websocket

發現Application接口一次性繼承了 EventEmitter  IRouter Express.Application 

系統學習過TypeScript的咱們確定知道,接口是能夠一次繼承多個接口,可是類只能夠經過extends一次繼承一個,要想多個繼承就要連續繼承子類

裏面發現了一些重要的API定義:

經過這裏,咱們能知道這些重要API的參數須要等、

下面開始正式解析Express的javaScript部分源碼


看過@types中的源碼,那麼咱們進來看javaScript部分源碼,簡直輕輕鬆鬆

源碼入口:

確實源碼入口暴露的是一個函數,跟@types中的源碼一致

一塊兒看看createApplication函數作了什麼

{ configurable: true, enumerable: true, writable: true, value: app }

這段代碼是屬性描述符,vue 2.x版本中的get和set和訪問描述符,不懂的去搜下

最重要的初始化,app.init()這段,但是這裏是局部變量,沒有init這個方法啊。上面有調用mixin,聽函數名就知道是混合,不懂的去搜索下,五分鐘包會

進入proto中:

發現初始化,就是在app掛載了四個屬性,初始值都是空對象

發現 app.listen的實現也是依靠http模塊,跟koa差很少

再看static靜態資源服務器實現的模塊

依靠serve-static這個庫實現,小編本人也用原生Node.js寫過靜態資源服務器,感受入門級的Node.js能夠去玩玩~

進入serve-static中發現,默認暴露是一個函數~

module.exports = serveStatic
function serveStatic (root, options) {
    return serveStatic(req,res,next) {
    ...
     if (path === '/' && originalUrl.pathname.substr(-1) !== '/') {
      path = ''
    }
    var stream = send(req, path, opts)
    stream.on('directory', onDirectory)
    if (setHeaders) {
      stream.on('headers', setHeaders)
    }
    if (fallthrough) {
      stream.on('file', function onFile () {
        forwardError = true
      })
    }
    stream.on('error', function error (err) {
      if (forwardError || !(err.statusCode < 500)) {
        next(err)
        return
      }
      next()
    })
    // pipe
    stream.pipe(res)
    }
}

原來調用express-static後會返回一個函數,也是接受請求返回響應~

這段函數代碼其實不少,可是核心跟我返回wasm二進制數據同樣,經過send()方法返回一個可讀流,而後調用pipe導入到res中,返回給客戶端,不一樣的是這裏的pipe方法是本身定義在原型鏈上的

send方法依賴send這個庫

進入查看,發現默認導出

function send (req, path, options) {
  return new SendStream(req, path, options)
}

function SendStream(){
  Stream.call(this)
  ../若干代碼
}

一開始我覺得調用pipe是可讀流的pipe,可是沒有發現SendStream有返回值,後面一看,pipei是本身定義在原型鏈上的方法~

SendStream.prototype.pipe = function pipe (res) {
  //..中間不少容錯處理 頭部處理等
   var path = decode(this.path)
   //若干代碼
   this.sendFile(path)
}

原來返回文件的核心在這裏:

這裏比較繞,須要一點耐心

fs.stat(path, function onstat (err, stat) {
  if (err && err.code === 'ENOENT' && !extname(path) && path[path.length - 1] !== sep) {
    // not found, check extensions
    return next(err)
  }
  if (err) return self.onStatError(err)
  if (stat.isDirectory()) return self.redirect(path)
  self.emit('file', path, stat)
  self.send(path, stat)
})

這裏經過一些容錯機制處理後,把path和文件stat信息對象,傳入this.send中,這裏的send,跟默認暴露的function send不是一個函數,整個源碼這裏是最繞的

發現進入這個函數後,最終調用this.stream  

到如今已經繞了三個庫,將近2000行代碼了,仍是沒有返回響應,可是Node.js裏面就是那幾個原生API能夠返回響應,此次應該到了返回響應的時候了

進入this.stream中,發現頭部就返回了響應

原來繞了這麼久,仍是小編開頭的那段代碼返回了響應,只是因爲遵循commonJS模塊化規範,把不少屬性都掛載到了每一個模塊的prototype和this上,致使了閱讀難度提高~

至此,靜態資源服務器源碼和app.listen源碼模塊源碼解析完畢

小編的靜態資源服務器,源碼更容易閱讀~

https://github.com/JinJieTan/util-static-server

app.get原理解析:

函數首先針對get方法只有一個參數時做出了定義,此時get方法返回app的設定屬性,跟咱們沒有關係。

this.lazyrouter()爲app實例初始化了基礎router對象,並調用router.use方法爲這個router添加了兩個基礎層,回調函數分別爲query和middleware.init。咱們不去管這個過程。

下一句var route = this._router.route(path)就以第一個參數path調用了router.route方法(router在lazyrouter初始化)。router在router目錄中index.js文件中聲明,它的屬性stack存儲了以layer描述的各個中間層。route方法定義在proto.route函數中,代碼以下:

能夠看到,首先建立了一個新的route實例;而後將route.dispatch函數做爲回調函數建立了一個新的layer實例,並將layer的route屬性設置爲這個route實例以後,將這個layer推入router(this.stack的this是router)的stack中。

形象地說,這個過程就是新建了一個layer做爲中間層放入了router的stack數組中。這個layer的回調爲route.dispatch。

執行完這個router.route方法後,又經過route[method].apply(route, slice.call(arguments, 1));讓生成的這個route(不是router)調用了route.get。route.get中的關鍵流以下:

到此,程序就完成了對get方法的加載。咱們簡短地回顧下這個過程:首先爲app實例化一個router對象,這個對象的stack屬性是一個數組,保存了app的不一樣中間層。一箇中間層以一個layer實例表徵,這個layer的handle屬性引用了回調函數。對於get等方法建立的layer,它的handle爲route.dispatch函數,而在get方法中自定義的回調函數是存放在route的stack中的。若是例程中繼續爲app添加其餘路由,則router對象會繼續生成新的layer存儲這些中間件,並放入本身的stack中。


app.use,添加中間件源碼:

一樣第一次都會調用,初始化一個 new Layer 中間層

app.use = function use(fn) {
var offset = 0;
var path = '/';
var fns = flatten(slice.call(arguments, offset));
this.lazyrouter();
var router = this._router;

fns.forEach(function (fn) {
  router.use(path, function mounted_app(req, res, next) {
    var orig = req.app;
    fn.handle(req, res, function (err) {
      setPrototypeOf(req, orig.request)
      setPrototypeOf(res, orig.response)
      next(err);
    });
  });
  fn.emit('mount', this);
}, this);

return this;
};

lazyrouter,每次初始化都會生成一個新的Layer

app.lazyrouter = function lazyrouter() {
  if (!this._router) {
    this._router = new Router({
      caseSensitive: this.enabled('case sensitive routing'),
      strict: this.enabled('strict routing')
    });

    this._router.use(query(this.get('query parser fn')));
    this._router.use(middleware.init(this));
  }
};

上面省掉了不少的容錯處理,這裏有一個flatten函數,扁平化數組的

依賴一個獨立的第三方庫,裏面代碼也很簡單

function flattenForever (array, result) {
  for (var i = 0; i < array.length; i++) {
    var value = array[i]

    if (Array.isArray(value)) {
      flattenForever(value, result)
    } else {
      result.push(value)
    }
  }

  return result
}

這裏也是很巧妙,forEach時候傳入了this的值給函數,我之前不知道forEach能傳兩個值,

而後傳入相應回調函數

app.handle = function handle(req, res, callback) {
  var router = this._router;

  // final handler
  var done = callback || finalhandler(req, res, {
    env: this.get('env'),
    onerror: logerror.bind(this)
  });
  // no routes
  if (!router) {
    debug('no routes defined on app');
    done();
    return;
  }

  router.handle(req, res, done);};

先取出第一層,判斷與request的path是否match。第1、二層是router初始化時的query函數和middleware.init函數,它們都會進入執行trim_prefix(layer, layerError, layerPath, path);的分支,並調用其中的layer.handle_request(req,res, next);,這個next就是router.handle函數裏的閉包next。執行了這兩層後,繼續回調next函數。

while (match !== true && idx < stack.length) {
  layer = stack[idx++];
  match = matchLayer(layer, path);
  route = layer.route;
  //...若干d代碼
  trim_prefix(layer, layerError, layerPath, path);
   function trim_prefix(layer, layerError, layerPath, path) {
    if (layerPath.length !== 0) {
  // Validate path breaks on a path separator
  var c = path[layerPath.length]
  if (c && c !== '/' && c !== '.') return next(layerError)

  // Trim off the part of the url that matches the route
  // middleware (.use stuff) needs to have the path stripped
  debug('trim prefix (%s) from url %s', layerPath, req.url);
  removed = layerPath;
  req.url = protohost + req.url.substr(protohost.length + removed.length);

  // Ensure leading slash
  if (!protohost && req.url[0] !== '/') {
    req.url = '/' + req.url;
    slashAdded = true;
  }

  // Setup base URL (no trailing slash)
  req.baseUrl = parentUrl + (removed[removed.length - 1] === '/'
    ? removed.substring(0, removed.length - 1)
    : removed);
}

debug('%s %s : %s', layer.name, layerPath, req.originalUrl);

if (layerError) {
  layer.handle_error(layerError, req, res, next);
} else {
  layer.handle_request(req, res, next);
}
}
  
}

這時就執行到了加載時生成的route所在的層,判斷request路徑是否匹配,這裏的匹配執行的是嚴格匹配,好比這層的regexp屬性(從加載時的路由肯定)是'/',那麼'/a'也不能匹配。

若路徑不匹配,while循環會直接跳過當此循環,對router.stack的下一層進行匹配;若是path與這個route的regexp匹配,就會執行layer.handle_request(req, res, next);。

layer.handle_request函數:

Layer.prototype.handle_request = function handle(req, res, next) {
  var fn = this.handle;

  if (fn.length > 3) {
    // not a standard request handler
    return next();
  }

  try {
    fn(req, res, next);
  } catch (err) {
    next(err);
  }
};

這裏很是巧妙,也是最繞的,咱們知道調用red.end就會返回響應結束匹配,不然express就會逐個路由匹配執行,這裏肯定執行全部的匹配請求後,就會調用finalhandler(最終的處理),返回響應

finalhandler是另一個獨立的第三方庫,專門用來處理響應的

裏面核心函數:

if (isFinished(req)) {
  write()
  return
}

  function write () {
  // response body
  var body = createHtmlDocument(message)

  // response status
  res.statusCode = status
  res.statusMessage = statuses[status]

  // response headers
  setHeaders(res, headers)

  // security headers
  res.setHeader('Content-Security-Policy', "default-src 'none'")
  res.setHeader('X-Content-Type-Options', 'nosniff')

  // standard headers
  res.setHeader('Content-Type', 'text/html; charset=utf-8')
  res.setHeader('Content-Length', Buffer.byteLength(body, 'utf8'))

  if (req.method === 'HEAD') {
    res.end()
    return
  }
  res.end(body, 'utf8')
}

經過如下函數判斷: 

function isFinished(msg) {
  var socket = msg.socket
  if (typeof msg.finished === 'boolean') {
    // OutgoingMessage
    return Boolean(msg.finished || (socket && !socket.writable))
  }

  if (typeof msg.complete === 'boolean') {
    // IncomingMessage
    return Boolean(msg.upgrade || !socket || !socket.readable || (msg.complete && !msg.readable))
  }
  // don't know
  return undefined
}

判斷有沒有協議升級事件(例如websocket的第一次握手時)、有沒有socket對象、socket是否是可讀等

最終調用createHtmlDocument拼裝數據,返回響應~

function createHtmlDocument (message) {
  var body = escapeHtml(message)
    .replace(NEWLINE_REGEXP, '<br>')
    .replace(DOUBLE_SPACE_REGEXP, ' &nbsp;')

  return '<!DOCTYPE html>\n' +
    '<html lang="en">\n' +
    '<head>\n' +
    '<meta charset="utf-8">\n' +
    '<title>Error</title>\n' +
    '</head>\n' +
    '<body>\n' +
    '<pre>' + body + '</pre>\n' +
    '</body>\n' +
    '</html>\n'
}

至此,花費4000字解析了express的核心全部API,感受有一點繞,這裏特別是get路由的觸發,是整個源碼的核心。

express目前的地位仍是不能夠撼動,koa更像是一個玩具,源碼很是輕量級,能夠先看koa,再看express,再接着看Node.js核心模塊的源碼


相關文章
相關標籤/搜索