啓動一個Express負責回吐wasm格式文件的服務很是簡單 html
Express
的源碼、以及目前如今主流庫已經所有使用TypeScript編寫,呼籲你們全面切換到TypeScript
vue
因爲本文是本身項目中的一段服務代碼臨時拼湊而成,因此這裏沒有使用TypeScript
java
注:不管是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, ' ') 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核心模塊的源碼