1. 第六次迭代

本小节是第六次迭代,主要的目的是介绍一下 Express 是如何集成现有的渲染引擎的。与渲染引擎有关的事情涉及到下面几个方面:

  • 如何开发或绑定一个渲染引擎。
  • 如何注册一个渲染引擎。
  • 如何指定模板路径。
  • 如何渲染模板引擎。

Express 通过 app.engine(ext, callback) 方法即可创建一个你自己的模板引擎。其中,ext 指的是文件扩展名、callback 是模板引擎的主函数,接受文件路径、参数对象和回调函数作为其参数。

//下面的代码演示的是一个非常简单的能够渲染 “.ntl” 文件的模板引擎。

var fs = require('fs'); // 此模板引擎依赖 fs 模块
app.engine('ntl', function (filePath, options, callback) { // 定义模板引擎
  fs.readFile(filePath, function (err, content) {
    if (err) return callback(new Error(err));
    // 这是一个功能极其简单的模板引擎
    var rendered = content.toString().replace('#title#', '<title>'+ options.title +'</title>')
    .replace('#message#', '<h1>'+ options.message +'</h1>');
    return callback(null, rendered);
  })
});

为了让应用程序可以渲染模板文件,还需要做如下设置:

//views, 放模板文件的目录
app.set('views', './views')
//view engine, 模板引擎
app.set('view engine', 'ntl')

一旦 view engine 设置成功,就不需要显式指定引擎,或者在应用中加载模板引擎模块,Express 已经在内部加载。下面是如何渲染页面的方法:

app.get('/', function (req, res) {
  res.render('index', { title: 'Hey', message: 'Hello there!'});
});

要想实现上述功能,首先在 Application 类中定义两个变量,一个存储 app.setapp.get 这两个方法存储的值,另一个存储模板引擎中扩展名和渲染函数的对应关系。

然后是实现 app.set 函数:

Application.prototype.set = function(setting, val) {
  	if (arguments.length === 1) {
	  // app.get(setting)
	  return this.settings[setting];
	}
  
	this.settings[setting] = val;
	return this;
};

代码中不仅仅实现了设置,如何传入的参数只有一个等价于 get 函数。

接着实现 app.get 函数。因为现在已经有了一个 app.get 方法用来设置路由,所以需要在该方法上进行重载。

http.METHODS.forEach(function(method) {
    method = method.toLowerCase();
    Application.prototype[method] = function(path, fn) {
    	if(method === 'get' && arguments.length === 1) {
    		// app.get(setting)
    		return this.set(path);
    	}

		...
    };
});

最后实现 app.engine 进行扩展名和引擎函数的映射。

Application.prototype.engine = function(ext, fn) {
	// get file extension
	var extension = ext[0] !== '.'
	  ? '.' + ext
	  : ext;

	// store engine
	this.engines[extension] = fn;

	return this;
};

扩展名当做 key,统一添加 “.”。到此设置模板引擎相关信息的函数算是完成,接下来就是最重要的渲染引擎函数的实现。

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

	//如果定义回调,则返回,否则渲染
	done = done || function(err, str) {
		if(err) {
			return self.req.next(err);
		}

		self.send(str);
	};

	//渲染
	app.render(view, opts, done);
};

渲染函数一共有三个参数,view 表示模板的名称,options 是模板渲染的变量,callback 是渲染成功后的回调函数。函数内部直接调用 render 函数进行渲染,渲染完成后调用 done 回调。这里有两个地方需要注意下,第一个是 this.req.app 变量,另一个是 self.req.next 函数,二者目前都没有实现。

这里先定义 req.app 变量,这个变量初始化需要 application 对象,方法很多,这里使用最简单的方法,直接在 expressInit 中赋值:

exports.init = function (app) {
	return function expressInit(req, res, next) {
		//request文件可能用到res对象
		req.res = res;

		//response文件可能用到req对象
		res.req = req;

		//赋值
		req.app = app;

		//修改原始req和res原型
		Object.setPrototypeOf(req, request);
		Object.setPrototypeOf(res, response);

		//继续
		next();
	};
};

然后修改 Application.prototype.lazyrouter 函数,传入 app 变量:

Application.prototype.lazyrouter = function () {
	if (!this._router) {
		this._router = new Router();

		this._router.use(middleware.init(this));
	}
};

接着定义 req.next 变量:

//如果定义回调,则返回,否则渲染
done = done || function (err, str) {
	if (err) {
		return self.req.next(err);
	}
	self.send(str);
};

req.next 函数默认是没有定义的,这里需要赋值一下,在 Router.handle 函数中,可以保存 next 函数:

proto.handle = function(req, res, done) {
	
	//...

	//保存原始路径
	req.orginalUrl = req.orginalUrl || req.url;
	
	// setup next layer
	req.next = next;

	//....
}

接下来创建一个 view.js 文件,主要功能是负责各种模板引擎和框架间的隔离,保持对内接口的统一性。

function View(name, options) {
	var opts = options || {};

	this.defaultEngine = opts.defaultEngine;
	this.root = opts.root;

	this.ext = path.extname(name);
	this.name = name;


	var fileName = name;
	if (!this.ext) {
	  // get extension from default engine name
	  this.ext = this.defaultEngine[0] !== '.'
	    ? '.' + this.defaultEngine
	    : this.defaultEngine;

	  fileName += this.ext;
	}


	// store loaded engine
	this.engine = opts.engines[this.ext];

	// lookup path
	this.path = this.lookup(fileName);
}

View 类内部定义了很多属性,主要包括引擎、根目录、扩展名、文件名等等,为了以后的渲染做准备。

View.prototype.render = function render(options, callback) {
	this.engine(this.path, options, callback);
};

View 的渲染函数内部就是调用一开始注册的引擎渲染函数。了解了 View 的定义,接下来实现 app.render 模板渲染函数。

Application.prototype.render = function(name, options, callback) {
	var done = callback;
	var engines = this.engines;
	var opts = options;

	view = new View(name, {
	  defaultEngine: this.get('view engine'),
	  root: this.get('views'),
	  engines: engines
	});


	if (!view.path) {
      var err = new Error('Failed to lookup view "' + name + '"');
      return done(err);
    }


	try {
	  view.render(options, callback);
	} catch (e) {
	  callback(e);
	}
};

view.js 文件还有一些细节没有在教程中展示出来,可以参考 github 上传的案例代码。

运行 node test/index.js,查看效果。

上面的代码是自己注册的引擎,如果想要和现有的模板引擎结合还需要在回调函数中引用模板自身的渲染方法,当然为了方便,Express 框架内部提供了一个默认方法,如果模板引擎导出了该方法,则表示该模板引擎支持 Express 框架,无需使用 app.engine 再次封装。

该方法声明如下:

 __express(filePath, options, callback)

可以参考 ejs 模板引擎的代码,看看它们是如何写的:

//该行代码在lib/ejs.js文件的355行左右
exports.__express = exports.renderFile;

Express 框架是如何实现这个默认加载的功能的呢?很简单,只需要在 View 的构造函数中加一个判断即可。

if (!opts.engines[this.ext]) {
  // load engine
  var mod = this.ext.substr(1);
  opts.engines[this.ext] = require(mod).__express;
}

代码逻辑很简单,如果没有找到引擎对应的渲染函数,那就尝试加载 __express 函数。

2. 后记

至此,算是结束本篇文章了。这是第三次修改本文,并没有修改整体框架,主要是细微的排版,和修改过去记录不太准确的地方。

简单的说一下还有哪里没有介绍。

  • 关于 Application。
    如果稍微看过 expross 代码的人都会发现,Application 并不是像我写的这样是一个类,而是一个中间件,一个对象,该对象使用了 mixin 方法的多继承手段,express.js 文件中的 createApplication 函数算是整个框架的切入点。

  • 关于 Router.handle
    这个函数可以说是整个 Express 框架的核心,如果理解了该函数,整个框架基本上就掌握了。我在仿制的时候舍弃了很多细节,在这里个函数里面内部有两个个关键点没说。一、处理 URL 形式的参数,这里涉及对 params 参数的提取过程。其中有一个 restore 函数使用高阶函数的方法做了缓存,仔细体会很有意思。二、setImmediate 异步返回,之所以要使用异步处理,是因为下面的代码需要运行,包括路径相关的参数,这些参数在下一个处理函数中可能会用到,这是一种常见的异步迭代手法。

  • 关于其它函数。
    太多函数了,不一一列举,前文已经提到,涉及的细节太多,正则表达式,HTTP 协议层,nodejs 本身函数的使用,对于整个框架的理解帮助不大,全部舍弃。不过大多数函数都是自成体系,很好理解。