前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Express version 4.17核心源码解析

Express version 4.17核心源码解析

作者头像
Peter谭金杰
发布2020-05-09 17:29:29
4920
发布2020-05-09 17:29:29
举报

启动一个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函数做了什么

代码语言:javascript
复制
{ 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中发现,默认暴露是一个函数~

代码语言:javascript
复制
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这个库

进入查看,发现默认导出

代码语言:javascript
复制
function send (req, path, options) {
  return new SendStream(req, path, options)
}

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

一开始我以为调用pipe是可读流的pipe,但是没有发现SendStream有返回值,后面一看,pipei是自己定义在原型链上的方法~

代码语言:javascript
复制
SendStream.prototype.pipe = function pipe (res) {
  //..中间很多容错处理 头部处理等
   var path = decode(this.path)
   //若干代码
   this.sendFile(path)
}

原来返回文件的核心在这里:

这里比较绕,需要一点耐心

代码语言:javascript
复制
 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源码模块源码解析完毕

小编的静态资源服务器,源码更容易阅读~

代码语言:javascript
复制
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 中间层

代码语言:javascript
复制
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

代码语言:javascript
复制
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函数,扁平化数组的

依赖一个独立的第三方库,里面代码也很简单

代码语言:javascript
复制
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能传两个值,

然后传入相应回调函数

代码语言:javascript
复制
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函数。

代码语言:javascript
复制
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函数:

代码语言:javascript
复制
 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是另外一个独立的第三方库,专门用来处理响应的

里面核心函数:

代码语言:javascript
复制
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')
}

通过以下函数判断:

代码语言:javascript
复制
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拼装数据,返回响应~

代码语言:javascript
复制
 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核心模块的源码


本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
消息队列 TDMQ
消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档