启动一个Express负责回吐wasm格式文件的服务非常简单
Express
的源码、以及目前现在主流库已经全部使用TypeScript编写,呼吁大家全面切换到TypeScript
由于本文是自己项目中的一段服务代码临时拼凑而成,所以这里没有使用TypeScript
注:无论是javaScript还是Node.js的框架源码其实都不难,稍微花点心思就可以看得很透彻,本文只是在使用wasm中顺手一写,可能不像其他人分析得那么专业
众所周知,Express引入后,它需要调用才会获得app对象,那么可以得知,我们引入的Express一开始是一个函数,进入源码查看
先分析@types的包 关于TypeScirpt源码
再分析javaScript
Express初始引入的是一个函数,可是它身上有一些例如express.static的方法,是怎么回事呢?那么我们进入core.Express中查看它的接口
初始引入函数遵循的接口继承了Application
这里request和response遵循的接口格式应该比较简单,待会下面在写
发现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。第一、二层是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核心模块的源码
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。