2.4 application.js
2.4.1 app.init
首先找到 app.init
方法, cache、settings、engines 是存放缓存、设置、以及引擎的对象,具体这个引擎是啥,目前来说我们是不知道的。当然这里的 this 其实就是 app 对象。并且 app 也是我们导出的一个空对象,之后再在 app 对象上面添加方法,添加了方法之后,app 当然就不会是空的了。
var app = exports = module.exports = {};
app.init = function init() {
this.cache = {};
this.engines = {};
this.settings = {};
this.defaultConfiguration();
};
2.4.2 app.defaultConfiguration
再转到 defaultConfiguration 方法,我们先忽略 onmount 方法。
app.defaultConfiguration = function defaultConfiguration() {
var env = process.env.NODE_ENV || 'development';
// default 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);
// trust proxy inherit back-compat
Object.defineProperty(this.settings, trustProxyDefaultSymbol, {
configurable: true,
value: true
});
debug('booting in %s mode', env);
this.on('mount', function onmount(parent) {
// inherit trust proxy
if (this.settings[trustProxyDefaultSymbol] === true
&& typeof parent.settings['trust proxy fn'] === 'function') {
delete this.settings['trust proxy'];
delete this.settings['trust proxy fn'];
}
// inherit protos
setPrototypeOf(this.request, parent.request)
setPrototypeOf(this.response, parent.response)
setPrototypeOf(this.engines, parent.engines)
setPrototypeOf(this.settings, parent.settings)
});
// 创建 locals 变量
this.locals = Object.create(null);
// 浏览器首页的URL
this.mountpath = '/';
// 默认在 locals 上面保存 setting
this.locals.settings = this.settings;
// 默认配置
this.set('view', View); // View 是从 view.js 导入
this.set('views', resolve('views')); // 指定视图目录的路径
this.set('jsonp callback name', 'callback');
if (env === 'production') {
this.enable('view cache'); // 正式上线时,开启模板缓存
}
// 增加 app.router 已经删除的提示消息
Object.defineProperty(this, 'router', {
get: function() {
throw new Error('\'app.router\' is deprecated!\nPlease see the 3.x to 4.x migration guide for details on how to update your app.');
}
});
};
process.env.NODE_ENV
可以拿到系统环境变量 NODE_ENV
, 在 linux、unix 系统中可以通过 SET NODE_ENV=production
来指定。拿到当前的环境,好判断是否开启 debug 调试模式,默认是开发环境 development
.
var env = process.env.NODE_ENV || 'development';
2.4.3 set 与 setting
上面所调用的 enable 与 set 等都是跟配置对象 setting 有关的方法,所以再来看看 set 方法。
app.set = function set(setting, val) {
if (arguments.length === 1) {
// app.get(setting)
return this.settings[setting];
}
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;
};
判断参数的个数,只有一个说明是获取的,直接就返回对象里面的值就好。
arguments.length === 1
正常运行的话,也就是2个参数的是,在 setting 对象里面设置相应的值即可。
this.settings[setting] = val;
而 switch (setting)
就是为了让 settings 里面的等于特定的值,例如 etag
、query parser
、trust proxy
的时候,触发一下各自的 fn 方法,并且把返回值再继续存在 setting 上面。
再看依赖于 set 的一些便捷方法
// 启动某一配置项
app.enable = function enable(setting) {
return this.set(setting, true);
};
// 关闭某一配置项
app.disable = function disable(setting) {
return this.set(setting, false);
};
// 是否启动了某一配置项
app.enabled = function enabled(setting) {
return Boolean(this.set(setting));
};
// 是否关闭了某一配置项
app.disabled = function disabled(setting) {
return !this.set(setting);
};
2.4.4 回到 defaultConfiguration 方法的 onmount
this.on('mount', function onmount(parent) {
// inherit trust proxy
if (this.settings[trustProxyDefaultSymbol] === true
&& typeof parent.settings['trust proxy fn'] === 'function') {
delete this.settings['trust proxy'];
delete this.settings['trust proxy fn'];
}
// inherit protos
setPrototypeOf(this.request, parent.request)
setPrototypeOf(this.response, parent.response)
setPrototypeOf(this.engines, parent.engines)
setPrototypeOf(this.settings, parent.settings)
});
这里之所以可以使用事件的 on 方法,是因为在 express.js 文件中
mixin(app, EventEmitter.prototype, false);
mixin(app, proto, false);
在混合 app 与 proto 之前,先与事件监听 EventEmitter.prototype 进行了混合。并且这个函数主要是进行了以下作为。
setPrototypeOf(this.request, parent.request)
setPrototypeOf(this.response, parent.response)
setPrototypeOf(this.engines, parent.engines)
setPrototypeOf(this.settings, parent.settings)
setPrototypeOf 是 Object.setPrototypeOf() 的 polyfill,就是降级叠片处理库(兼容到 IE8)。
相当于 this.request.__proto__ = parent.request
。 也就是说 mount 是触发更换原型链的一个事件,通过触发 mount 我们可以安装和增加一些属性。
2.4.5 app.lazyrouter
现在呢,我们从上往下阅读,看看哪些我们是没有看到的方法。
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));
}
};
这其实是一个单例设计模式,也可以理解为缓存某一个对象。
在注释上面,给出了这是一个私有方法,也是一个缓存路由的方法,假如不存在 this._router 就根据配置项新建它,,这个 Router 是 router/index.js 文件而不是 Router/route.js 文件,然后调用 use 把 query 与 middleware 传递进去。 query 和 middleware 都是 middleware文件夹里面的模块,也就是 express 默认的路由中间件,现在暂不赘述中间件里面的模块。
2.4.6 app.handle
finalhandler 是一个处理请求错误的函数,它的返回值为 done,当我们调用 done 的时候就会返回 404,或者从 head、body 里面提取的其他错误。router.handle 先放一放。
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);
};
2.4.6 app.route
用来指定一个路径的函数,它会放回一个 router/route.js 文件里面的 Route 对象,代理的 this._router.route 方法。
app.route = function route(path) {
this.lazyrouter();
return this._router.route(path);
};
2.4.7 app.engine
app.engine 用于指定,视图模板的后缀名,与进行处理的回调 fn
app.engine = function engine(ext, fn) {
if (typeof fn !== 'function') {
throw new Error('callback function required');
}
// get file extension
var extension = ext[0] !== '.'
? '.' + ext
: ext;
// store engine
this.engines[extension] = fn;
return this;
};
2.4.8 app.param
代理的 this._router.param , name 参数支持传递数组的形式。
app.param = function param(name, fn) {
this.lazyrouter();
if (Array.isArray(name)) {
for (var i = 0; i < name.length; i++) {
this.param(name[i], fn);
}
return this;
}
this._router.param(name, fn);
return this;
};
2.4.9 app.path
假如有 this.parent 就把它的 path 给加上。
app.path = function path() {
return this.parent
? this.parent.path() + this.mountpath
: '';
};
2.4.10 绑定路由方法
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 的源码其实非常简单,就是 http.METHODS
里面支持的方法全部转成小写。给 app 里面添加所有 Node 支持的 http 方法函数,假如是 get 且只有一个参数,说明调用的是 app.set 拿到配置项,否则通过 this._router.route 设定 path ,再调用 route 对应的方法。
类似于这样( _ 以下划线开头的说明都是私有方法,我们不应该直接调用,这里只是为了说明):
app.get('/user', function(){.....})
app._router.route('/user').get(function(){...})
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;
};
这个就比较霸道了,给所以支持的方法上面,为这个 path 路径添加相同的回调处理函数。
app.del = deprecate.function(app.delete, 'app.del: Use app.delete instead');
假如使用的是 app.del 会给出彩色提示,叫你用 app.delete 替换掉,尽管有提示(警告提示),但是可以正常运行。
2.4.11 app.render
这个就是渲染模板的函数。
app.render = function render(name, options, callback) {
var cache = this.cache;
var done = callback;
var engines = this.engines;
var opts = options;
var renderOptions = {};
var view;
// support callback function as second arg
if (typeof options === 'function') {
done = options;
opts = {};
}
// merge app.locals
merge(renderOptions, this.locals);
// merge options._locals
if (opts._locals) {
merge(renderOptions, opts._locals);
}
// merge options
merge(renderOptions, opts);
// set .cache unless explicitly provided
if (renderOptions.cache == null) {
renderOptions.cache = this.enabled('view cache');
}
// primed cache
if (renderOptions.cache) {
view = cache[name];
}
// view
if (!view) {
var View = this.get('view');
view = new View(name, {
defaultEngine: this.get('view engine'),
root: this.get('views'),
engines: engines
});
if (!view.path) {
var dirs = Array.isArray(view.root) && view.root.length > 1
? 'directories "' + view.root.slice(0, -1).join('", "') + '" or "' + view.root[view.root.length - 1] + '"'
: 'directory "' + view.root + '"'
var err = new Error('Failed to lookup view "' + name + '" in views ' + dirs);
err.view = view;
return done(err);
}
// prime the cache
if (renderOptions.cache) {
cache[name] = view;
}
}
// render
tryRender(view, renderOptions, done);
};
if (typeof options === 'function') {
done = options;
opts = {};
}
这是为了支持第二个参数为回调的时候,让 opts 默认为 {}
。接下来还 merge 了几个选项而已,而这个 View 就是导入的 view.js 模块了,暂时先放一放。缓存选项打开的话,就保存到缓存 cache 选项上面去,假如缓存上面有的话,就直接拿不用再 new 了。假如路径啥的出错了,通过 done(err) 交给回调就是了,这个回调 done 是我们自己传递的。
这一段的代码缓存比较清楚,是不是我们又 get 到了一个技能呢?
tryRender 就是用 try/catch 包裹了一下 render
2.4.12 app.listen
app.listen = function listen() {
var server = http.createServer(this);
return server.listen.apply(server, arguments);
};
之前这个我就没细说,我感觉大家应该会懂,毕竟已经用过 express了,这里我稍微提一下,arguments 就是 listen 所有的参数,它是一个数组,server.listen.apply(server, arguments);
通过这种方式可以把所有的参数直接传给 server.listen。
你可能会问,诶,app 不是对象吗,怎么可以作为 http.createServer 的参数。
是的,app 是一个对象,还是一个函数对象,可能你忘记了,在 express.js 里面有这么一段代码。
var app = function(req, res, next) {
app.handle(req, res, next);
};
2.4.13 app.use
这个 app.use 我为什么放到最后面说呢,因为相对其他的来说,这个算是 express 最核心的功能了,它是扩展 express 的方法。
首先我们看一下flatten 是干嘛用的,readme 给出下面一段示例,非常明显,通过它可以把数组的维度降低。
> flatten([1, [2, 3], [4, 5, 6], [7, [8, 9]], 10])
[ 1,
2,
3,
4,
5,
6,
7,
8,
9,
10 ]
> flatten([1, [2, [3, [4, [5]]]]], 2)
[ 1,
2,
3,
[ 4, [ 5 ] ] ]
app.use = function use(fn) {
var offset = 0;
var path = '/';
// default path to '/'
// 以这种方式传递 app.use([fn])
if (typeof fn !== 'function') {
var arg = fn;
while (Array.isArray(arg) && arg.length !== 0) {
arg = arg[0]; // 拿到数组第一个项保存为 arg
}
// 第一个 arg 是路径 app.use('some', fn)
if (typeof arg !== 'function') { // 假如 arg 还不是函数,而是 path 路径
offset = 1; // 设置 偏移量为1
path = fn; // path 就等于我们参数的第一项 fn
}
}
var fns = flatten(slice.call(arguments, offset)); // 先把第一项 path 忽略掉,降低数组维度
if (fns.length === 0) { // 参数传递不正确
throw new TypeError('app.use() requires middleware functions');
}
// 启动一下路由
this.lazyrouter();
var router = this._router;
fns.forEach(function (fn) {
// 假如 fn 并不符合一个插件规范,则说明是注册路由
if (!fn || !fn.handle || !fn.set) {
return router.use(path, fn);
}
debug('.use app under %s', path);
fn.mountpath = path; // 当前插件挂载路径
fn.parent = this; // 保存当前插件上一次 app 的实例
// restore .app property on req and res
router.use(path, function mounted_app(req, res, next) {
var orig = req.app; // 先拿到 当前 req 上面的 app 实例
fn.handle(req, res, function (err) { // 给 fn.handle 传入 req,res,插件会给 req res 添加方法、属性等。
setPrototypeOf(req, orig.request) // 把修改后的 req 的 __proto__ 设置为当前 request
setPrototypeOf(res, orig.response)
next(err); // 假如有 err 传递下去即可,最后会被 finalHandle 捕获的
});
});
// mounted an app
fn.emit('mount', this); // 触发 mount 时间,更新 request、response、settings、engines 对象
}, this);
return this;
};
要想更清楚的了解 use 的参数,我们可以看一下 app.use API
2.4.14 cookie-parser 是如何工作的
之后,我们再来查看一个插件是如何写的 cookie-parser
module.exports = cookieParser
function cookieParser (secret, options) {
return function cookieParser (req, res, next) {
if (req.cookies) {
return next()
}
var cookies = req.headers.cookie
var secrets = !secret || Array.isArray(secret)
? (secret || [])
: [secret]
req.secret = secrets[0]
req.cookies = Object.create(null)
req.signedCookies = Object.create(null)
// no cookies
if (!cookies) {
return next()
}
req.cookies = cookie.parse(cookies, options)
// parse signed cookies
if (secrets.length !== 0) {
req.signedCookies = signedCookies(req.cookies, secrets)
req.signedCookies = JSONCookies(req.signedCookies)
}
// parse JSON cookies
req.cookies = JSONCookies(req.cookies)
next()
}
}
cookieParser 其实是一个柯里化函数,利用了 js 的闭包特性。简单点说就是2层函数,好处就是,我们可以在第二层函数里面使用 secret 与 options ,提前传递这2个变量,这属于函数编程的范畴。
if (req.cookies) {
return next()
}
假如 req 上面已经有了 cookies 说明该中间件已经执行过了,直接跳出函数且执行 next 即可。var cookies = req.headers.cookie
首先拿到 headers 上面的 cookie 请求头,之后。
var secrets = !secret || Array.isArray(secret)
? (secret || [])
: [secret]
secret 不存在或者是一个数组,就返回 空数组 或者 secret,否则用数组包裹一下。
req.secret = secrets[0]
req.cookies = Object.create(null)
req.signedCookies = Object.create(null)
// no cookies
if (!cookies) {
return next()
}
给 req 上面添加一些初始化变量,再次判断 headers 上面是否有 cookies 请求头,没有同样跳出函数,并执行 next。
req.cookies = cookie.parse(cookies, options)
if (secrets.length !== 0) {
req.signedCookies = signedCookies(req.cookies, secrets)
req.signedCookies = JSONCookies(req.signedCookies)
}
cookie 是导入的一个叫 cookie 的 package 包,通过 parse 方法解析 headers 上面的 cookies 请求头,解析为 js 对象。然后判断签名秘钥 secrets 不为空,通过 signedCookies 验证 req.cookies,保证不被篡改,被篡改了之后就删除该属性,并赋给req.signedCookies,之后通过 JSONCookies 编码一下 req.signedCookies,转为 JS 对象。
req.cookies = JSONCookies(req.cookies)
就是解析普通的不加密的 cookie。所以说这个插件非常的简单,假如有设置加密串secrets, 则为 req 添加一个signedCookies,没有设置则添加一个 cookies 的功能。
照着这个插件一改,是不是又 get 到一个 express 插件编写的能力呀~