主题
Express源码解读03 - application
"version": "4.18.1",
1. 初始化应用app.init
1. createApplication
里app.init()
的来源
在express.js
中,创建应用createApplication
方法最后有一个方法我们还没有分析到,就是app.init()
,调用express()
后,内部会自动的执行app.init()
方法。
js
// 创建应用
function createApplication() {
var app = function (req, res, next) {
app.handle(req, res, next);
};
mixin(app, EventEmitter.prototype, false);
mixin(app, proto, false); // false 不合并静态属性,
// expose the prototype that will get set on requests
app.request = Object.create(req, {
app: { configurable: true, enumerable: true, writable: true, value: app },
});
// expose the prototype that will get set on responses
app.response = Object.create(res, {
app: { configurable: true, enumerable: true, writable: true, value: app },
});
app.init();
return app;
}
这节里面主要分析init()
,所以在createApplication
里面,主要看的代理就两行
js
mixin(app, proto, false)
app.init();
mixin(app, proto, false)
上节已经说过,proto
其实就是app
,mixin
的作用就是将两个地方的app
进行对象的合并
mixin
合并了app
后,在app
里面就有了application
里面的属性及方法,因此,下面的app.init()
方法就是./application.js
里面暴露的。(createApplication
里面已经将application
分离出去了)
2. app.init()干了啥?
可以看到,内部在express()
调用的时候,就会执行app.init()
,init
方法的定义在application
里面,具体如下
js
app.init = function init() {
this.cache = {}; // 缓存
this.engines = {}; // 模板引擎
this.settings = {}; // 设置
this.defaultConfiguration(); // 设置一些默认配置,settings里面的东西
};
这里的this
指向的是app
初始化就是声明了三个对象,分别是缓存、模板引擎、配置,均是空对象 然后defaultConfiguration
方法的作用是得到一些默认配置
defaultConfiguration
其实就是利用app.set
一堆设置setting
默认值的操作,跑完defaultConfiguration
方法后,为this.settings
对象设置了如下初始值:
js
settings:{
"x-powered-by": true,
etag: "weak",
"etag fn": function generateETag (body, encoding) {
var buf = !Buffer.isBuffer(body)
? Buffer.from(body, encoding)
: body
return etag(buf, options)
},
env: "development",
"query parser": "extended",
"query parser fn": function parseExtendedQueryString(str) {
return qs.parse(str, {
allowPrototypes: true
});
},
"subdomain offset": 2,
"trust proxy": false,
"trust proxy fn": function trustNone () {
return false
},
}
2. app.set()原理
在express中,提供了 app.set(name,value)
这样的API,可以存储任何想要的值,这在源码中有是这样实现的。
js
app.set = function set(setting, val) {
if (arguments.length === 1) { // 若只有一个参数,是取值操作
// app.get(setting)
var settings = this.settings;
// 根据原型链向上递归查找
while (settings && settings !== Object.prototype) {
if (hasOwnProperty.call(settings, setting)) {
return settings[setting];
}
settings = Object.getPrototypeOf(settings); // 获取settings的原型给settings
}
return undefined;
}
debug('set "%s" to %o', setting, val); // 输出
// set value
this.settings[setting] = val;
// trigger matched settings
switch (setting) {
case "etag":
this.set("etag fn", compileETag(val));
break;
case "query parser":
this.set("query parser fn", compileQueryParser(val));
break;
case "trust proxy":
this.set("trust proxy fn", compileTrust(val));
// trust proxy inherit back-compat
Object.defineProperty(this.settings, trustProxyDefaultSymbol, {
configurable: true,
value: false,
});
break;
}
return this;
};
可以看出,源码里面app.set()
方法,不仅做了存值操作,也做了取值操作,就是根据判断有几个参数来实现的。取值会根据原型链层层向上查找,最终都找不到,才会返回undefined
并且,若是设置etag
、query parser
、trust proxy
的值,还会当独设置一个对应的处理函数在settings
里面
对boolean
类型的值时候进行设置值和取值的时候,为了简化操作,源码里面还提供四个方法,app.enabled
、app.enable
、app.disabled
、app.disable
,具体如下:
js
// 取值操作
app.enabled = function enabled(setting) { // 取值,返回布尔值
return Boolean(this.set(setting));
};
app.disabled = function disabled(setting) { // 取值, 返回相反的布尔值
return !this.set(setting);
};
// 设置值
app.enable = function enable(setting) { // 设置setting为true
return this.set(setting, true);
};
app.disable = function disable(setting) { // 设置setting为false
return this.set(setting, false);
};
带ed
的为取值,否则为设置值
取值还可以通过app.get()
和app.get
来操作,但是他们都只能传一个参数
那么问题来了,app.get
不就和路由的get
请求冲突了吗?是的,确实会与GET
请求冲突,所以,源码里面是做了这样的操作
在路由匹配里面做了如下判断,来区分是去取值还是真正的路由。
js
if (method === "get" && arguments.length === 1) {
// app.get(setting)
return this.set(path);
}
3. 路由匹配
3.1 GET、POST、PUT、DELETE...匹配
路由在express
里面也是内置的功能,不用单独安装,我们平时开发时,路由会有很多方法,比如GET
、POST
、DELETE
、PUT
等等,那么在express
里面,是如来根据不同的路由来返回给客户端不同资源的呢?
路由系统在
Express
里面占了很大的比重,这里仅仅只是分析根据不同请求方法来匹配的方式,后面会有章节专门分析Express里面的路由。
在application.js
里面,也做了路由的匹配,源码如下:
js
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;
};
});
其中,methods
是一个第三方的包,里面定义了所有请求方式的动词
可以看出,Express
里面是通过循环的方式,将请求方式一个个的挂载在app
上面的,当请求进来的时候,会自己去走请求方式对应的方法,至于其他的很多有关路由匹配的解释,这里暂时不做过多解释,后面文章会详细说到。
lazyrouter()
方法是加载路由的,见名知意就是懒加载路由,由于express
是自带路由系统的,正常应该是创建应用,就会加载路由,但是为了提高性能考虑,并非一创建应用就会加载路由系统,而是在用到了路由的时候,才会去加载。因此称作懒加载。
3.2 路由all方法
Express
里面有这样的一种路由,app.all()
方法,无论什么请求方式,只要请求的路径能匹配到all
里面的path
,就会走对应的路由处理函数,原理就是比路由方法匹配少了一个方法的匹配,直接走的路径匹配。
js
app.all = function all(path) {
this.lazyrouter();
var route = this._router.route(path);
var args = slice.call(arguments, 1);
for (var i = 0; i < methods.length; i++) {
route[methods[i]].apply(route, args);
}
return this;
};
4. listen方法
每个WEB应用程序要与外界通信,都需要有一个端口,这里的端口指的是虚拟端口(其它还有物理端口,可以理解为插口),listen
方法是用来监听端口的
Express
对http模块
进行了封装,当调用app.listen()
方法就会返回一个 http.Server
对象,所以可以直接使用app.listen(port)
来监听端口并启动应用程序。
js
app.listen = function listen() {
var server = http.createServer(this); // 创建http Serve对象,传入的是应用app
return server.listen.apply(server, arguments); // 返回原生Nodejs的应用程序
};
可以看到,上面做的一系列有关app
的操作,最终都是为创建http服务器对象做准备的
也就是说,只要一运行app.listen
,就会启动一个http服务器,express()
顶多就是创建一个应用程序,但是这个应用程序并没有启动,不能对外提供服务。