node框架express的研究

lhyt發表於2018-07-06

0.前言

在node中,express可以說是node中的jQuery了,簡單粗暴,容易上手,用過即會,那麼我們來試一下怎麼實現。下面我們基於4.16.2版本進行研究

1. 從入口開始

1.1入口

主入口是index.js,這個檔案僅僅做了require引入express.js這一步,而express.js暴露的主要的函式createApplication,我們平時的var app = express();就是呼叫了這個函式。

function createApplication() {
  var app = function(req, res, next) {
    app.handle(req, res, next);
  };

//var EventEmitter = require('events').EventEmitter;
//var mixin = require('merge-descriptors');
//用了merge-descriptors這個包混合兩個物件(包括set、get),也可用assign
  mixin(app, EventEmitter.prototype, false); 
  mixin(app, proto, false);

  // expose the prototype that will get set on requests
  app.request = Object.create(req, { //在app加上一個屬性,它的值是一個物件,繼承於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;
}
複製程式碼

1.2 proto

我們也看見有proto這個東西,其實他是var proto = require('./application');這樣來的,而這個檔案就是給app這個物件寫上一些方法:

var app = exports = module.exports = {};
app.init = function (){}
app.handle = function (){}
app.use = function (){}
app.route = function (){}
//此外,下面的還有listen,render,all,disable,enable,disabled,set,param,engine等方法
複製程式碼

上面我們已經把這個application.js的app物件和express.js裡面的app物件混合,也就是express.js這個檔案裡面的app.handle、app.init也是呼叫了這個檔案的

1.2.1 app.init方法

其實就是初始化

app.init = function init() {
  this.cache = {};
  this.engines = {};
  this.settings = {}; //存放配置
  this.defaultConfiguration(); //預設配置
};
複製程式碼

defaultConfiguration預設配置:已省略一些程式碼

app.defaultConfiguration = function defaultConfiguration() {
  // 預設設定
  this.enable('x-powered-by');
  this.set('etag', 'weak');
  this.set('env', env);
    // 讓app繼承父的同名屬性
    setPrototypeOf(this.request, parent.request)
    setPrototypeOf(this.response, parent.response)
    setPrototypeOf(this.engines, parent.engines)
    setPrototypeOf(this.settings, parent.settings)
  });

  // setup locals
  this.locals = Object.create(null);

  // top-most app is mounted at /
  this.mountpath = '/';

  // default locals
  this.locals.settings = this.settings;

  // default configuration
  this.set('view', View);
  this.set('views', resolve('views'));
  this.set('jsonp callback name', 'callback');
};
複製程式碼

我們再看app.set

app.set = function set(setting, val) {
  if (arguments.length === 1) {
    // 只傳一個引數直接返回結果
    return this.settings[setting];
  }

  //對settings設定值
  this.settings[setting] = val;

  // 值的匹配,具體過程略過
  switch (setting) {
    case 'etag':
      break;
    case 'query parser':
      break;
    case 'trust proxy':
      break;
  }
  return this;
};
複製程式碼

1.2.2 app.handle方法

把回撥函式先寫好

app.handle = function handle(req, res, callback) {
  var router = this._router;
  // 路由匹配成功觸發回撥
  var done = callback || finalhandler(req, res, {
    env: this.get('env'),
    onerror: logerror.bind(this)
  });
  // 沒有路由
  if (!router) {
    done();
    return;
  }
  router.handle(req, res, done);
};
複製程式碼

那麼,我們的this._router來自哪裡?

1.2.3 每一個method的處理

我們的this._router來自 this.lazyrouter()方法

//methods是常見的http請求以及其他一些方法名字的字串陣列
methods.forEach(function(method){
//app.get app.post等等我們常用的api
  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)); //就是常用的route.get('/page',callback)
    return this;
  };
});
複製程式碼

1.2.4 app.lazyrouter產生this._router

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));
  }
};
複製程式碼

2. Router(下文講到router和route,注意區分)

實際上就是var Router = require('./router');,於是我們開啟router這個資料夾。

  • index.js: Router類,他的stack用於儲存中介軟體陣列,處理所有的路由
  • layer.js 中介軟體實體Layer類,處理各層路由中介軟體或者普通中介軟體;
  • route.js Route類,用於處理子路由和不同方法(get、post)的路由中介軟體

2.1 index.js檔案

上面我們也看見了new一個新路由的過程,index.js用於處理儲存中介軟體陣列。在router資料夾下的index.js裡面,暴露的是proto,我們require引入的Router也是proto:

var proto = module.exports = function(options) {
  var opts = options || {};

  function router(req, res, next) {
    router.handle(req, res, next);
  }

  // router也獲得了proto 的方法了
  setPrototypeOf(router, proto)

  router.params = {};
  router._params = [];
  router.caseSensitive = opts.caseSensitive;
  router.mergeParams = opts.mergeParams;
  router.strict = opts.strict;
  router.stack = []; //棧存放中介軟體

  return router;
};
//接下來是proto的一些方法:proto.param 、proto.handle 、proto.process_params、proto.use、proto.route
//後面是對於methods加上一個all方法,進行和上面methods類似的操作
methods.concat('all').forEach(function(method){
  proto[method] = function(path){ //route.all, route.get, router.post
    var route = this.route(path)
    route[method].apply(route, slice.call(arguments, 1));
    return this;
  };
});
複製程式碼

this.route方法

proto.route = function route(path) {
  var route = new Route(path);
  var layer = new Layer(path, { //layer是中介軟體實體
    sensitive: this.caseSensitive,
    strict: this.strict,
    end: true
  }, route.dispatch.bind(route));

  layer.route = route; //注意這裡

  this.stack.push(layer); //中介軟體實體壓入Router路由棧
  return route;
};
複製程式碼

2.2 layer.js

function Layer(path, options, fn) {
  if (!(this instanceof Layer)) {
    return new Layer(path, options, fn);//無new呼叫
  }

  var opts = options || {};
  this.handle = fn;
  this.name = fn.name || '<anonymous>';
  this.params = undefined;
  this.path = undefined;
  this.regexp = pathRegexp(path, this.keys = [], opts);

  // 路徑匹配相關設定
  this.regexp.fast_star = path === '*'
  this.regexp.fast_slash = path === '/' && opts.end === false
}

Layer.prototype.handle_request = function handle(req, res, next) {
  var fn = this.handle;

  if (fn.length > 3) {//引數是req,res,next,長度是3,這時候有next了
    return next();
  }
  try {
    fn(req, res, next);
  } catch (err) {
    next(err);
  }
};
複製程式碼

Layer.prototype後面接著handle_error(處理錯誤)、match(正則匹配路由)、decode_param(decodeURIComponent封裝)方法,具體不解釋了

3. 中介軟體

我們平時這麼用的:

app.use((req,res,next)=>{
  //做一些事情
  //next() 無next或者res.end,將會一直處於掛起狀態
});
app.use(middlewareB);
app.use(middlewareC);
複製程式碼

3.1 app.use

使用app.use(middleware)後,傳進來的中介軟體實體(一個函式,引數是req,res,next)壓入路由棧,執行完畢後呼叫next()方法執行棧的下一個函式。中介軟體有app中介軟體和路由中介軟體,其實都是差不多的,我們繼續回到路由proto物件(也就是Router物件):

proto.use = function use(fn) {
  var offset = 0; //表示從第幾個開始
  var path = '/';//預設是/

//如果第一個引數不是函式,app.use('/page',(req,res,next)=>{})
  if (typeof fn !== 'function') {
    var arg = fn;

//考慮到第一個引數是陣列
    while (Array.isArray(arg) && arg.length !== 0) { //一直遍歷,直到arg不是陣列
      arg = arg[0];
    }

    // first arg is the path
    if (typeof arg !== 'function') {
      offset = 1; //如果第一個引數不是函式,從第二個開始
      path = fn; //app.use('/page',(req,res,next)=>{}),第一個引數是路徑
    }
  }

  var callbacks = flatten(slice.call(arguments, offset)); //陣列扁平化與回撥函式集合
  for (var i = 0; i < callbacks.length; i++) {
    var fn = callbacks[i];
    // 增加中介軟體
    var layer = new Layer(path, {
      sensitive: this.caseSensitive,
      strict: false,
      end: false
    }, fn);

    layer.route = undefined;  //如果是路由中介軟體就是一個Route物件,否則就是undefined。
//如果是路由中介軟體,在index.js的proto.route方法裡面,給layer例項定義layer.route = route 
    this.stack.push(layer);//壓入Router物件的棧中
  }
  return this;
};
複製程式碼

3.2 route.js檔案對methods陣列處理

這個檔案是用於處理不同method的,後面有一段與前面類似的對methods關鍵程式碼:

methods.forEach(function(method){
  Route.prototype[method] = function(){    //app.get('/page',(req,res,next)=>{})就這樣子來的
    var handles = flatten(slice.call(arguments));
    for (var i = 0; i < handles.length; i++) {
      var handle = handles[i];
      var layer = Layer('/', {}, handle);
      layer.method = method;

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


    return this;
  };
});
複製程式碼

3.3 中介軟體種類

普通與路由中介軟體

  • 普通中介軟體:app.use,不管是什麼請求方法,只要路徑匹配就執行回撥函式
  • 路由中介軟體:根據HTTP請求方法的中介軟體,路徑匹配和方法匹配才執行 所以有兩種Layer:
  • 普通中介軟體Layer,儲存了name,回撥函式已經undefined的route變數。
  • 路由中介軟體Layer,儲存name和回撥函式,route還會建立一個route物件 還有,中介軟體有父子之分:
    1

Router與Route

Router類的Layer例項物件layer.route為undefined表示這個layer為普通中介軟體;如果layer.route是Route例項物件,這個layer為路由中介軟體,但沒有method物件。而route物件的Layer例項layer是沒有route變數的,有method物件,儲存了HTTP請求型別,也就是帶了請求方法的路由中介軟體。

所以Router類中的Layer例項物件是儲存普通中介軟體的例項或者路由中介軟體的路由,而Route例項物件route中的Layer例項layer是儲存路由中介軟體的真正例項。

node框架express的研究

  • Route類用於建立路由中介軟體,並且建立擁有多個方法(多個方法是指app.get('/page',f1,f2...)中的那堆回撥函式f1、f2...)的layer(對於同一個路徑app.get、app.post就是兩個layer)儲存stack中去。
  • Router類的主要作用是建立一個普通中介軟體或者路由中介軟體的引導(layer.route = route),然後將其儲存到stack中去。
  • Route類例項物件的stack陣列儲存的是中介軟體的方法的資訊(get,post等等),Router類例項物件的stack陣列儲存的是路徑(path)

4. 模板引擎

我們平時這樣做的:

app.set('views', path.join(__dirname, 'views')); //設定檢視資料夾
app.set('view engine', 'jade'); //使用什麼模板引擎

//在某個請求裡面,使用render
res.render('index');  //因為設定了app.set('view engine', 'jade'); ,所以我們不用res.render('index.jade');
複製程式碼

set方法前面講過,給setting物件加上key-value。然後我們開始呼叫render函式

4.1 從res.render開始

我們來到response.js,找到這個方法:

res.render = function render(view, options, callback) {
  var app = this.req.app;
  var done = callback;
  var opts = options || {};
  var req = this.req;
  var self = this;
  if (typeof options === 'function') {
    done = options;
    opts = {};
  }

  opts._locals = self.locals;
  done = done || function (err, str) { //我們不寫callback的時候,如res.render('index',{key:1}); 
    if (err) return req.next(err);
    self.send(str); //把物件轉字串傳送
  };
  app.render(view, opts, done);
};
複製程式碼

我們發現最後來到了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;
//對renderOptions混合 this.locals、opts._locals、opts
  merge(renderOptions, this.locals);
  if (opts._locals) {
    merge(renderOptions, opts._locals);
  }
  merge(renderOptions, opts);

  if (!view) {//第一次進,如果沒有設定檢視
    var View = this.get('view');
    view = new View(name, { //引用了view.js的View類
      defaultEngine: this.get('view engine'),
      root: this.get('views'),
      engines: engines
    });

  tryRender(view, renderOptions, done); //渲染函式,內部呼叫view.render(options, callback);
};
複製程式碼

4.2 view.js

view.render方法在此檔案中找到,實際上它內部再執行了this.engine(this.path, options, callback)。而engine方法是在View建構函式裡面設定:

function View(name, options) {
  var opts = options || {};
  this.defaultEngine = opts.defaultEngine;
  this.ext = extname(name);
  this.name = name;
  this.root = opts.root;
  var fileName = name;
  if (!this.ext) {
    this.ext = this.defaultEngine[0] !== '.'
      ? '.' + this.defaultEngine
      : this.defaultEngine;
    fileName += this.ext;
  }

  if (!opts.engines[this.ext]) {
    var mod = this.ext.substr(1) //獲取字尾 ejs、jade
 // 模板引擎對應express的處理函式,具體內容大概是把模板轉為正常的html,這裡不研究了
    var fn = require(mod).__express 

    if (typeof fn !== 'function') { //如果模板引擎不支援express就報錯
      throw new Error('Module "' + mod + '" does not provide a view engine.')
    }
    opts.engines[this.ext] = fn 
    
    //當然,application.js也有類似的設定,現在是無opts.engines[this.ext]的情況下的設定
    //若app.engine設定過了就不會來到這裡了
  }

  this.engine = opts.engines[this.ext];  // 設定模板引擎對應於express的編譯函式
  this.path = this.lookup(fileName);// 尋找路徑
}
複製程式碼

那麼this.engine(this.path, options, callback)實際上就是require(mod).__express(this.path, options, callback),如果那個模板引擎支援express,那就按照他的規則走

看見一些文章說中介軟體用connect模組做的,我看了一下connect的確是可以,而且形參一模一樣,但是我看原始碼裡面壓根就沒有connect的影子。connect應該算是早期的express吧

相關文章