【Express】源码阅读记录(一)

虽说现在能用户 express 搭建简单的静态服务器,做一些简单的数据交互,但是总是有一种知其然不知其所以然的感觉。本身对nodejs的了解也不够。正好花些时间来阅读以下源码,本文基于Express 4.15.4

Express主要目录架构

项目目录架构选了最主要的一部分列了出来


├── lib
|   ├── middleware        
|   |   ├── init.js
|   |   └── query.js
├── ├── router        //router组件,负责中间件的插入和链式执行
|   |   ├── index.js
|   |   ├── layer.js
|   |   └── route.js
├── ├── application.js
├── ├── express.js
├── ├── request.js   //提供一些方法丰富 request的实例功能
├── ├── reponse.js
├── ├── util.js
├── └── view.js      //提供模板渲染引擎的封装,通过 res.render() 调用引擎渲染网页
|
└── index.js

express和app

在express中出现频率最高的是 expressapp 可见其重要性。举个例子,搭建一个最简单的静态服务器

const express = require('express')
const app = express()

app.get('/',function(req,res){
    res.send('Hello World!')
    res.end()
})
app.listen(3000,function(){
    console.log('服务器已启动...')
})

接下来在源码中分析express和app

/*
 * express.js
 */
//调用express()返回的app其实是一个函数
exports = module.exports = createApplication
//createApplication就相当于express的 main 函数,其中完成了所有创建express实例所需要的动作
function createApplication(){
    var app = function(req,res,next){
        app.handle(req,res,next)
    }
    
    //此处是通过mixin导入
    mixin(app,EventEmitter.protprype,false)
    mixin(app,proto,false)
    
    app.request = Object.create(req,{...})x
    app.response = Object.create(res,{...})
    
    return app
}

上述代码中的app.handle,handle方法是在application.js中定义的。首先看一下application.js的整体架构

//application.js
app.init = function init(){
    this.cache = {}
    this.engines = {}
    this.settings = {}
    
    this.defaultConfiguration()
}

//app.相关方法

app.render = function render(name,options,callback){...}  

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

//容错处理
function logederror(err){...}
function tryRender(view,options,callback){...}

app是一个请求处理函数,作为http.createServer的参数。而express其实是一个工厂函数,用来生成请求处理函数。
为什么说express是一个工厂函数?搭建静态服务器的时候的一行代码可以来证明:

const app = express()

接下来将 app.handle() 单独提出来研究一下:
在express.js中是通过mixin导入的。其作用是将每对[req,res]进行逐级分发,作用在每个定义好的路由及中间件上,直到最后完成

app.handle = function(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)
}

这里引申除了两个关键点 中间件路由

中间件 Middleware

在express中最关键的概念就是中间件了。一个Express应用从本质上来说就是一系列的中间件调用。那么到底什么是中间件?

一个中间件本质上就是一个函数,调用中间件就是对函数的调用。在Express4.x中取消了所有内置的中间件,需要从外部引入。这样的改进是的express 是一个独立的路由和web中间件框架,更新更加方便更加符合unix哲学

中间件一般函数形式如下:

function middleware(req,res,next){
    //...
}

异常处理是异步编程的一个难点,我们不能用常规的 try-catch 语句块去捕获异常。try-catch只能捕获当次事件循环内的异常,对callback执行抛出的异常无能为力。所以Node在处理异常上形成一种约定,将异常作为回调函数的第一个实参传回,如果为空值,则表明没有异常抛出
处理错误中间件函数形式:

function middleware(err,req,res,next){
    //error作为函数的
    //...
}

接下来看一下中间件函数中的参数
忽略req 和 res , next 本身也是一个函数,调用 next() 就会继续执行下一个中间件。请求过程图解如下:

       ↓
---------------
| middleware1 |
---------------
       ↓
---------------
| ... ... ... |
---------------
       ↓
---------------
| middlewareN |
---------------
       ↓

在使用express时我们经常会看到这样的代码:

//匹配所有以/user开始的路径
app.use('/user',function(req,res,next){...})

//精确匹配到/user路径
app.get('/user',function(req,res,next){...})

根据上述代码,中间件大致可以分为两种:普通中间件路由中间件 。注册普通中间件通常是通过 app.use() , 注册路由中间件,通常是通过 app.METHOD()来注册。
而这两种方法其实都是由 app.handle 来处理

路由 Router

要想了解请求处理的详细过程,首先需要了解Router。先来看一下Routerde 目录结构:

├── router      
|   ├── index.js
|   ├── layer.js
|   └── route.js

简单来说,Router(源码在router/index.js)就是一个中间件的容器。事实上,Router是Express中一个非常核心的东西,基本上就是一个简化版的Express框架。app的很多API,例如:app.use(),app.param(),app.handle()等,事实上都是对Router的API的一个简单包装。可以通过app._router来访问默认的Router对象。

app.get()是如何实现的

methods.forEach(function(method){
    //在JavaScript可以通过 '[]' 或 '.'来访问对象属性.
    //当我们不只属性内容,或者属性内容是个变量时可以用'[]'
    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
    }
})

通过上述代码我们可以发下,Express对 METHOD 方法的添加都是动态的。一个forEach循环搞定了所有method函数的定义。
看下函数的内部实现,如果函数参数长度为 1 ,那么直接返回this.set(path)。查看Express API可以发现:app.get() 实现了两个功能,如果长度为1,则返回app.set()定义的变量,如果参数长度大于1 ,则进行路由处理。

接着, this.lazyrouter() ,定位到源码

app.lazyrouter = function lazyrouter(){
    if(!this._router){    //如果_router不存在,就new一个Router出来
        this._router = new Router({
            caseSensitive: this.enabled('case sensitive routing'),
            strict: this.enabled('strict routing')
        })
        this._router.use(query(this.get('query parse fn')))
        this._router.use(middleware.init(this))
    }
}

这个new出来的Router其本质上是一个中间件容器。并且 Router是Express中一个非常核心的东西,基本上就是一个简化版的Express框架 。app的很多API,例如:app.use(),app.param(),app.handle()等,事实上都是对Router的API的一个简单包装。可以通过app._router来访问默认的Router对象。

返回之前的 methods.forEach ,发现当执行完 this.lazyrouter() 之后
Route可以简单的理解为存放路由处理函数的容器,它有一个 stack 属性为一个数组,其中的每一项是一个Layer对象,是对路由处理函数的包装。

var route = this._router.route(path)

根据上述代码可以定位到 router/index.js

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
}

这里new了一个 route 对象和一个 layer 对象,然后将route 对象赋值给 layer.route

Route模块对应的是route.js,主要是来处理路由信息的,每条路由都会生成一个Route实例。而Router模块对应的是index.js,Router是一个路由的集合,在Router模块下可以定义多个路由,也就是说,一个Router模块会包含多个Route模块。通过上边的代码我们已经知道,每个express创建的实例都会懒加载一个_router来进行路由处理,这个_router就是一个Router模块。

那么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];

      if (typeof handle !== 'function') {
        var type = toString.call(handle);
        var msg = 'Route.' + method + '() requires callback functions but got a ' + type;
        throw new Error(msg);
      }

      debug('%s %o', method, this.path)

      var layer = Layer('/', {}, handle);
      layer.method = method;

      this.methods[method] = true;
      this.stack.push(layer);
    }

    return this;
  };
});

和application.js中处理方式是相同的。当调用 route.MEHOD() 的时候,新建一个layer 放在 route.stack

通过上面的分析可以发现,Router其实是一个二维的结构。例如,一个可能的router.stack结构如下所示:

----------------
|    layer1    |
----------------
        ↓
---------------- layer2.route.stack  ------------   ------------   ------------
|    layer2    | ------------------> | layer2-1 |-->| layer2-2 |-->| layer2-3 |
----------------                     ------------   ------------   ------------
        ↓
---------------- layer3.route.stack  ------------   ------------
|    layer3    | ------------------> | layer3-1 |-->| layer3-2 |
----------------                     ------------   ------------
        ↓
----------------
|    ......    |
----------------
        ↓
----------------
|    layerN    |
----------------

综上所述,我们发现一个路由中间件注册的过程大致为:app.METHOD-->router.route-->route.METHOD
而其中最重要的是通过 router.route() 来创建一条新路由,然后调用 route.METHOD()来注册相关函数

其他参考链接

源码地址

Express 4.15.4

参考链接4

  1. 前端乱炖
  2. 从express源码探析其路由机制
  3. 解读EXPRESS 4.X源码
  4. 朴零 《深入浅出Node.js》
  5. express源码分析之Router
    原文作者:昵称不能随便取
    原文地址: https://segmentfault.com/a/1190000011049112
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞