启动一个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核心模块的源码