博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Express 源码分析1-(服务启动和请求服务过程)
阅读量:6849 次
发布时间:2019-06-26

本文共 7195 字,大约阅读时间需要 23 分钟。

前言

关于Node JS 的后端框架,不管是Express,Koa, 甚至Eggjs(Eggjs 是基于Koa 底层封装的框架),都是基于NodeJS 的http模块进行处理的,其最重要的是方法是

const server = http.createServer((rep, res) => {    res.end('hello world')})server.listen(prot, () => {    console.log('Server Started. Port: '+ prot)})复制代码

无论那个框架的中间件, 路由等的处理,都是从这里是一个入口,对服务器的资源的任何访问都会先进入http.createServer方法的回调函数,也就是下面的方法

(rep, res) => {    res.end('hello world')}复制代码

之前我们已经分析过了Eggjs 框架,但是没有分析Nodejs实现后端框架实现的底层原理,因为Eggjs 是基于Koa 实现的一个上层框架, 我们这次来通过Express来分析下Nodejs 实现后端框架的底层原理。

源码结构

我们先从 clone 一份源码,其对应的lib 文件夹就是express框架的整个源码

其最重要的几个文件是:

  1. express.js (项目的入口文件,暴露除了很多对象,其中最重要的是一个createApplication 方法)(重点)
  2. application.js (最核心的一个文件,但是是对上面createApplication方法,返回的app 对象去挂载很多方法)(重点)
  3. request.js 和response.js 两个文件,主要是对http.createServer 方法中的rep 和 res 进行相应的封装处理
  4. utils.js 只是封装了一些帮助方法
  5. View.js 模版引擎的相关的方法
  6. router 文件夹,是express实现的关键,也就是路由的处理,我们的任何一个请求,其实对应的就是一个路由, 然后返回相应的资源(重点)
  7. middleware, 是定义中间件的文件夹,不过其中只有两个很简单的内置中间件, 因为Express的很多中间件都是第三方的库

我们下面根据启动服务* 和 ** 访问服务 两个流程来分析express, 会针对上面标注为(重点)的相应的文件,进行详细的分析.

启动服务

express.js

我们先从怎么使用开始,作为入口,下面是一个简单的express的demo.

const express = require('./lib/express')const app = module.exports = express()app.get('/', (req, res) => {  res.end('hello world')})if (!module.parent) {  app.listen(3000);  console.log('Express started on port 3000')}复制代码

上面一段简单的代码,我们就已经搭建好了一个后端服务,当我们用浏览器打开http://localhost:3000/时,就会显示hello world.,下面我们就来看看是怎么实现的.

const app = module.exports = express() 可以看出express()应该是express.js 文件里面暴露出来的一个方法, 其对应的脚本是: exports = module.exports = createApplication; createApplication方法如下:

function createApplication() {  var app = function(req, res, next) {    app.handle(req, res, next);  };  mixin(app, EventEmitter.prototype, false);// 合并prototype  mixin(app, proto, false);// 合并proto  app.request = Object.create(req, {    app: { configurable: true, enumerable: true, writable: true, value: app }  })  app.response = Object.create(res, {    app: { configurable: true, enumerable: true, writable: true, value: app }  })  app.init();  return app;}复制代码

这个方法,返回一个app 对象, 这个对象相当于继承与EventEmitter.prototype 和一个proto对象原型, 然后执行了app.init()方法,这个方法主要是做一些初始化工作并清空cache, engines, settings, 并且去初始化一些配置,比如说:

this.enable('x-powered-by');  this.set('etag', 'weak');  this.set('env', env);  this.set('query parser', 'extended');  this.set('subdomain offset', 2);  this.set('trust proxy', false);复制代码

this.set设置的值是保存在settings中的, 比如我们我们可以this.settings['x-powered-by'] 可以在应用中任何的地方去调用.所以这里有一个扩展出一个应用:

const express = require('./lib/express');const app = module.exports = express();app.set('config', {  url: 'http://localhost:8080',  userInfo: {    name: 'ivan fan',    age: 18  }})app.get('/', (req, res) => {  const config = app.get('config')  console.log(config)  res.end('hello world')})if (!module.parent) {  app.listen(3000);  console.log('Express started on port 3000');}复制代码

上面我们通过app.set 去设置一个config的值,我们在其他的地方可以通过app.get去获取这个值,这样看起来感觉没有什么用途,因为我们可以直接定义一个变量就可以,没必要通过app.set, 但是如果我们的应用很大的时候,我们将项目拆分成了很多单独的文件,我们只是共享了app对象,但是在多个js文件中可能需要公用一个全局的配置,我们可以创建一个config.json文件,在不同的页面都去import 进来,但是如果如果我们在app.js中将这个配置注入到app中其他的地方,只要通过app.get就可以达到共享的作用。

总结:

  1. express.js 只是暴露除了一个createApplication方法, 并且返回了一个app对象
  2. 给app对象的原型做了相应的处理
  3. 给app 进行初始化设置

application.js

上面我们已经分析了express.js文件,知道其返回了一个app对象,但是我们至今位置没有看到哪里定义了listenget方法。

我们在上面分析发现,执行了mixin(app, proto, false);方法,这个是在app 原型上去添加了另外一个原型,而proto指向的就是application.js文件, 下面我们就来具体分析这个文件。

listen

首先我们找到listen 方法,其代码如下:

app.listen = function listen() {  var server = http.createServer(this);  return server.listen.apply(server, arguments);};复制代码

这个就是我们在一开始说的,所有的Nodejs 后端框架都是基于http 这个模块的实现的,所以这个里我们就已经实现了一个后端的服务。

get

在我们的demo 中,我们有调用一个app.get方法,其代码如下:

app.get('/', (req, res) => {  const config = app.get('config')  console.log(config)  res.end('hello world')})复制代码

但是我们找遍了整个application.js文件,都没有找到这个方法在哪里实现的, get只是http请求众多方法的其中一个, http方法,还有'post','put','delete'等一些列方法,为了简洁,express 引用了第三方库methods, 这个库几乎涵盖了http 请求的常见方法,所以通过循环去给app挂载不同的方法(Koa 也是这样处理)

methods.forEach(function(method){  app[method] = function(path){    if (method === 'get' && arguments.length === 1) {      // app.get(setting)      return this.set(path);    }    this.lazyrouter();    var route = this._router.route(path);    route[method].apply(route, slice.call(arguments, 1));    return this;  };});复制代码

首先this.lazyrouter(); 方法是去给app对象挂载一个_router的路由(Router)属性, 然后我们在看下route方法:

proto.route = function route(path) {  var route = new Route(path);  var layer = new Layer(path, {    sensitive: this.caseSensitive,    strict: this.strict,    end: true  }, route.dispatch.bind(route));  layer.route = route;  this.stack.push(layer);  return route;};复制代码

从上面的代码可知,var route = this._router.route(path);, route也就是this.stack 中的layer中的reoute, 所以最后回调函数是挂载在stack 的layer 上面的。

route[method].apply(route, slice.call(arguments, 1));app.get 的回调函数挂载在route属性上面,其代码如下(删除异常处理代码):

methods.forEach(function(method){  Route.prototype[method] = function(){    var handles = flatten(slice.call(arguments));    for (var i = 0; i < handles.length; i++) {      var handle = handles[i];       var layer = Layer('/', {}, handle);      layer.method = method;      this.methods[method] = true;      this.stack.push(layer);    }    return this;  };});复制代码

我们先不具体分析代码逻辑, 我们可以根据上面的图片,分析下app对象下的一个数据结构:

  1. 在app 上面挂载一个_router属性, (router/index.js)
  2. _router下面有一个stack 的属性,其是一个数组
  3. stack数组中,保存的都是一个Layer类型的对象
  4. Layer 对象中又挂载了一个route(Route)的对象
  5. route对象保存了path(path:/abc), methods, 同样也有一个stack的属性,也是一个数组, 同样里面保存的也是一个Layer 对象
  6. Layer里面挂载了一个重要的属性handle, 其实从现在的分析看,这个handle就是我们app.get方法的第二个回调函数参数.

上面我们已经分析了express启动的过程,下面我们来分析访问服务express处理的过程,也就是我们访问http://localhost:3000/时,express到底做了些什么.

访问服务

从一开始,我们就知道,对服务器的方法,首先都会进入http.createServer的回调函数,而且express是通过listen方法,执行这个方法的

app.listen = function listen() {  var server = http.createServer(this);  return server.listen.apply(server, arguments);};复制代码

其中this就是app实例,也就是在express.js文件中定义的,如下:

var app = function(req, res, next) {    app.handle(req, res, next);  };复制代码

在这个方法中,会调用handle方法,下面我们来分析下这个方法

handle

handle的代码如下:

app.handle = function handle(req, res, callback) {  var router = this._router;  var done = callback || finalhandler(req, res, {    env: this.get('env'),    onerror: logerror.bind(this)  });  if (!router) {    debug('no routes defined on app');    done();    return;  }  router.handle(req, res, done);};复制代码

然后会执行router.handle(req, res, done);, 在上面我们已经得知, this._router指向的是router/index.js这个文件夹的对象,下面我们进入到这个handle方法中, 这个方法很长,但是其实就是根据我们访问的路径来查找对应的Layer, 其关键代码是:

while (match !== true && idx < stack.length) {      layer = stack[idx++];      match = matchLayer(layer, path);      route = layer.route;    ...    }复制代码

通过matchLayer(layer, path);去匹配layer. 找到Layer后,然后去执行layer.handle_request(req, res, next);

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);  }};复制代码

var fn = this.handle; 这个fn 其实指向的就是app.get里面的第二参数,也就是回调函数,

app.get('/', (req, res) => {  res.end('hello world')})复制代码

然后就相当于请求完成了。

总结

上面我们已经分析了,Express 在启动的整个过程,主要是进行数据的一些加载处理和路由的处理,而且也分析了我们在请求Server时的整个过程。

后续我会继续分析use 的用法,并且针对express.static 源码来分析Express 中间件的处理和总结中间件的使用方式,以及express.static对缓存的处理(Etag, Last-Modified)

转载地址:http://xwgul.baihongyu.com/

你可能感兴趣的文章
OSChina 周六乱弹 —— 历史总是惊人的相似
查看>>
MySQL 大小写
查看>>
Lync 2013部署图片赏析-证书服务安装配置
查看>>
HTML5 本地缓存 (web存储)
查看>>
tomcat redis session共享(包含redis安全设置)
查看>>
iptables中DNAT、SNAT和MASQUERADE的作用
查看>>
kvm命令学习记录
查看>>
小菜鸡进阶之路-First week
查看>>
ORACLE 10g SYSAUX表空间快速增长之WRH$_ACTIVE_SESSION_HISTORY篇
查看>>
我的友情链接
查看>>
我的友情链接
查看>>
子数组的和的最大值(包括升级版的首尾相连数组)
查看>>
LeetCode - Nth Highest Salary
查看>>
9.ORM数据访问
查看>>
href=“javascript:”vs href=“javascript:void(0)”
查看>>
win10文件夹无法打开,双击闪屏
查看>>
【学习笔记14】全局类型转换器
查看>>
Spring Boot学习记录手册<1>
查看>>
在Word2007和Word2010中插入视频文件,并自动在word中播放
查看>>
javascript设置http请求的头信息
查看>>