Express version 4.17核心源码解析

2020-05-09 17:29:29 浏览数 (1)

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


0 人点赞