当前位置: 首页 > 文档资料 > 阅读 express 源码 >

2.4 application.js

优质
小牛编辑
126浏览
2023-12-01

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 里面的等于特定的值,例如 etagquery parsertrust 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)

setPrototypeOfObject.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 插件编写的能力呀~