如果您正在为平台node.js开发,那么您可能听说过
express.js 。 这是用于为节点创建Web应用程序的最流行的轻量级框架之一。

该材料的作者(我们今天将发布其翻译版本)通过分析源代码的源代码并考虑其使用示例来研究Express框架的内部结构特征。 他认为,对流行的开源库背后的机制的研究有助于加深对它们的了解,消除它们的“神秘感”,并有助于基于它们创建更好的应用程序。
您可能会发现,在阅读本材料时,保持便捷的源代码很方便。
在此使用
此版本 。 您可以在不打开明确代码的情况下阅读本文,因为在适当情况下,在此给出了该库的代码片段。 在那些代码缩写的地方,
// ...
形式的注释
使用快递的基本示例
首先,让我们看一下“ Hello World!”,它是掌握新计算机技术的传统示例。 可以在框架的官方网站上找到它,它将作为我们研究的起点。
const express = require('express') const app = express() app.get('/', (req, res) => res.send('Hello World!')) app.listen(3000, () => console.log('Example app listening on port 3000!'))
此代码在端口3000上启动新的HTTP服务器并发送
Hello World!
GET /
路由上收到的请求。 如果您不详细介绍信息,那么我们可以区分正在发生的四个阶段,我们可以对其进行分析:
- 创建一个新的快速申请。
- 创建一条新路线。
- 从指定的端口号启动HTTP服务器。
- 处理到服务器的传入请求。
创建一个新的快递申请
var app = express()
命令允许您创建一个新的express应用程序。 来自
lib / express.js文件的
createApplication
函数是默认的导出函数;是我们通过调用
express()
函数来访问它的函数。 以下是您应注意的一些重要事项:
// ... var mixin = require('merge-descriptors'); var proto = require('./application'); // ... function createApplication() { // , . // : `function(req, res, next)` var app = function(req, res, next) { app.handle(req, res, next); }; // ... // `mixin` `proto` `app` // - `get`, . mixin(app, proto, false); // ... return app; }
从此函数返回的
app
对象是我们的应用程序代码中使用的对象之一。 使用
merge-descriptors库的
mixin
函数添加
app.get
方法,该函数负责分配
proto
声明的
app
方法。
proto
对象本身是从
lib / application.js导入的。
建立新路线
现在,让我们看一下负责从示例中创建
app.get
方法的
代码 。
var slice = Array.prototype.slice; // ... /** * `.VERB(...)` `router.VERB(...)`. */ // `methods` HTTP, ( ['get','post',...]) methods.forEach(function(method){ // app.get app[method] = function(path){ // // var route = this._router.route(path); // route[method].apply(route, slice.call(arguments, 1)); // `app`, return this; }; });
有趣的是,除了语义特征之外,在功能方面,可以将实现HTTP操作的所有方法(例如
app.get
,
app.post
,
app.put
等)视为相同。 如果简化上面的代码,将其简化为仅一种
get
方法的实现,则会得到如下所示的内容:
app.get = function(path, handler){ // ... var route = this._router.route(path); route.get(handler) return this }
尽管上面的函数有2个参数,但它与函数
app[method] = function(path){...}
类似。 第二个参数
handler
是通过调用
slice.call(arguments, 1)
。
简而言之,
app.<method>
只是使用其
route
方法将路由保存在应用程序路由器中,然后将
handler
传递给
route.<method>
。
在
lib / router / index.js中声明
route()
路由器方法:
// proto - `_router` 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; };
毫不奇怪,
lib / router / route.js中 route.get
方法的声明类似于
app.get的声明:
methods.forEach(function (method) { Route.prototype[method] = function () { // `flatten` , [1,[2,3]], var handles = flatten(slice.call(arguments)); for (var i = 0; i < handles.length; i++) { var handle = handles[i]; // ... // , , Layer, // var layer = Layer('/', {}, handle); // ... this.stack.push(layer); } return this; }; });
每个路由可以具有多个处理程序;在每个处理程序的基础上,构造一个
Layer
类型的变量,该变量是一个数据处理层,然后进入堆栈。
图层对象
_router
和
route
使用
Layer
类型的对象。 为了理解这样一个对象的本质,让我们看一下它的
构造函数 :
function Layer(path, options, fn) { // ... this.handle = fn; this.regexp = pathRegexp(path, this.keys = [], opts); // ... }
在创建类型为
Layer
对象时
Layer
会为它们提供路径,某些参数和一个函数。 就我们的路由器而言,此功能是
route.dispatch
(
route.dispatch
,我们将在下面进一步讨论该功能,旨在将请求传输到单独的路由)。 对于路由本身,此函数是在我们的示例代码中声明的处理程序函数。
每个类型为
Layer
对象都有一个
handle_request方法,该方法负责执行初始化对象时传递的函数。
回想一下使用
app.get
方法创建路线时发生的情况:
- 在应用程序的路由器(
this._router
)中创建一条路由。 dispatch
路由方法被分配为相应的Layer
对象的处理程序方法,并且将该对象压入路由器堆栈。- 请求处理程序作为处理程序方法传递给
Layer
对象,然后将该对象压入路由堆栈。
结果,所有处理程序都以路由堆栈内部的
Layer
类型对象的形式存储在
app
实例内部,其
dispatch
方法分配给路由器堆栈中的
Layer
对象:
在路由器堆栈和路由堆栈上分层对象传入的HTTP请求将根据此逻辑进行处理。 我们将在下面讨论它们。
HTTP服务器启动
配置路由后,需要启动服务器。 在我们的示例中,我们转到
app.listen
方法,将端口号和回调函数作为参数传递给它。 为了了解此方法的功能,我们可以参考
lib / application.js文件:
app.listen = function listen() { var server = http.createServer(this); return server.listen.apply(server, arguments); };
app.listen
只是
http.createServer
的包装。 这样的观点是有道理的,因为如果您回忆起初我们所讨论的内容,则
app
只是具有签名
function(req, res, next) {...}
的函数,它与
http.createServer
必需的参数兼容
http.createServer
(此方法的签名是
function (req, res) {...}
)。
在意识到这一点之后,express.js给我们的一切最终都可以简化为一个非常智能的函数处理程序,该框架不再像以前那样复杂和神秘。
HTTP请求处理
现在我们知道该
app
只是一个请求处理程序,我们将遵循HTTP请求在快速应用程序内部传递的路径。 该路径将其引向我们声明的处理程序。
首先,请求转到
createApplication
函数(
lib / express.js ):
var app = function(req, res, next) { app.handle(req, res, next); };
然后转到
app.handle
方法(
lib / application.js ):
app.handle = function handle(req, res, callback) { // `this._router` - , , `app.get` var router = this._router; // ... // `handle` router.handle(req, res, done); };
router.handle
方法在
lib / router / index.js中声明 :
proto.handle = function handle(req, res, out) { var self = this; //... // self.stack - , // Layer ( ) var stack = self.stack; // ... next(); function next(err) { // ... // var path = getPathname(req); // ... var layer; var match; var route; while (match !== true && idx < stack.length) { layer = stack[idx++]; match = matchLayer(layer, path); route = layer.route; // ... if (match !== true) { continue; } // ... HTTP, } // ... // process_params , self.process_params(layer, paramcalled, req, res, function (err) { // ... if (route) { // `layer.handle_request` // `next` // , `next` , // , `next` , return layer.handle_request(req, res, next); } // ... }); } };
简而言之,
router.handle
函数遍历堆栈上的所有层,直到找到与请求中指定的路径匹配的层。 然后,将调用
handle_request
图层方法,该方法将执行预定义的处理函数。 此处理程序函数是
dispatch
路由方法,在
lib / route / route.js中声明 :
Route.prototype.dispatch = function dispatch(req, res, done) { var stack = this.stack; // ... next(); function next(err) { // ... var layer = stack[idx++]; // ... layer.handle_request(req, res, next); // ... } };
就像路由器一样,在处理每个路由期间,将枚举此路由所具有的层,并调用它们的执行层处理程序方法的
handle_request
方法。 在我们的例子中,这是一个请求处理程序,在应用程序代码中声明。
最后,这里的HTTP请求属于我们应用程序的代码区域。
快速申请中的请求路径总结
在这里,我们仅检查了express.js库的基本机制,这些基本机制负责Web服务器的操作,但是该库还具有许多其他功能。 我们没有在请求到达处理程序之前就停止检查;我们没有谈论使用
res
和
req
变量时可用的辅助方法。 最后,我们没有涉及Express最强大的功能之一。 它包含中间件的使用,中间件的目标是解决几乎所有问题-从解析请求到实现完整的身份验证系统。
我们希望该材料可以帮助您了解Express设备的主要功能,现在,如有必要,您可以通过独立分析该库中您感兴趣的部分源代码来了解其他所有内容。
亲爱的读者们! 您是否使用express.js?
