首先,看下express模板默认配置。
view:模板引擎模块,对应 require(’./view’),结合 res.render(name) 更好了解些。下面会看下 view 模块。
views:模板路径,默认在 views 目录下。
// default configurationthis.set('view', View);this.set('views', resolve('views'));
从官方脚手架生成的代码出发,模板配置如下:
views:模板文件在 views 目录下;
view engine:用jade这个模板引擎进行模板渲染;
// view engine setupapp.set('views', path.join(__dirname, 'views'));app.set('view engine', 'jade');
假设此时有如下代码调用,内部逻辑是如何实现的?
res.render('index');
完整的 render
方法代码如下:
/** * Render `view` with the given `options` and optional callback `fn`. * When a callback function is given a response will _not_ be made * automatically, otherwise a response of _200_ and _text/html_ is given. * * Options: * * - `cache` boolean hinting to the engine it should cache * - `filename` filename of the view being rendered * * @public */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; // support callback function as second arg if (typeof options === 'function') { done = options; opts = {}; } // merge res.locals opts._locals = self.locals; // default callback to respond done = done || function (err, str) { if (err) return req.next(err); self.send(str); }; // render app.render(view, opts, done);};
核心代码就一句,调用了 app.render(view) 这个方法。
res.render = function (name, options, callback) { var app = this.req.app; app.render(view, opts, done);};
完整源码如下:
/** * Render the given view `name` name with `options` * and a callback accepting an error and the * rendered template string. * * Example: * * app.render('email', { name: 'Tobi' }, function(err, html){ * // ... * }) * * @param {String} name * @param {String|Function} options or fn * @param {Function} callback * @public */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);};
源码开头有 cache
、engines
两个属性,它们在 app.int()
阶段就初始化了。
this.cache = {};this.engines = {};
看下View模块的源码:
/** * Initialize a new `View` with the given `name`. * * Options: * * - `defaultEngine` the default template engine name * - `engines` template engine require() cache * - `root` root path for view lookup * * @param {string} name * @param {object} options * @public */function View(name, options) { var opts = options || {}; this.defaultEngine = opts.defaultEngine; this.ext = extname(name); this.name = name; this.root = opts.root; if (!this.ext && !this.defaultEngine) { throw new Error('No default engine was specified and no extension was provided.'); } var fileName = name; if (!this.ext) { // get extension from default engine name this.ext = this.defaultEngine[0] !== '.' ? '.' + this.defaultEngine : this.defaultEngine; fileName += this.ext; } if (!opts.engines[this.ext]) { // load engine opts.engines[this.ext] = require(this.ext.substr(1)).__express; } // store loaded engine this.engine = opts.engines[this.ext]; // lookup path this.path = this.lookup(fileName);}
模板引擎大家不陌生了,关于express模板引擎的介绍可以参考官方文档。
下面主要讲下使用配置、选型等方面的内容。
包括但不限于如下模板引擎
jade
ejs
dust.js
dot
mustache
handlerbar
先看代码。
// view engine setupapp.set('views', path.join(__dirname, 'views'));app.set('view engine', 'jade');
有两个关于模版引擎的配置:
views
:模版文件放在哪里,默认是在项目根目录下。举个例子:app.set('views', './views')
view engine
:使用什么模版引擎,举例:app.set('view engine', 'jade')
可以看到,默认是用jade
做模版的。如果不想用jade
怎么办呢?下面会提供一些模板引擎选择的思路。
需要考虑两点:实际业务需求、个人偏好。
首先考虑业务需求,需要支持以下几点特性。
支持模版继承(extend)
支持模版扩展(block)
支持模版组合(include)
支持预编译
对比了下,jade
、nunjunks
都满足要求。个人更习惯nunjunks
的风格,于是敲定。那么,怎么样使用呢?
首先,安装依赖
npm install --save nunjucks
然后,添加如下配置
var nunjucks = require('nunjucks');nunjucks.configure('views', { autoescape: true, express: app});app.set('view engine', 'html');
看下views/layout.html
<!DOCTYPE html><html><head> <title> {% block title %} layout title {% endblock %} </title></head><body><h1> {% block appTitle %} layout app title {% endblock %}</h1><p>正文</p></body></html>
看下views/index.html
{% extends "layout.html" %} {% block title %}首页{% endblock %} {% block appTitle %}首页{% endblock %}
通过app.engine(engineExt, engineFunc)
来注册模板引擎。其中
engineExt:模板文件后缀名。比如jade
。
engineFunc:模板引擎核心逻辑的定义,一个带三个参数的函数(如下)
// filepath: 模板文件的路径 // options:渲染模板所用的参数 // callback:渲染完成回调 app.engine(engineExt, function(filepath, options, callback){ // 参数一:渲染过程的错误,如成功,则为null // 参数二:渲染出来的字符串 return callback(null, 'Hello World'); });
比如下面例子,注册模板引擎 + 修改配置一起,于是就可以愉快的使用后缀为tmpl
的模板引擎了。
app.engine('tmpl', function(filepath, options, callback){ // 参数一:渲染过程的错误,如成功,则为null // 参数二:渲染出来的字符串 return callback(null, 'Hello World'); }); app.set('views', './views'); app.set('view engine', 'tmpl');
参数说明:
view:模板的路径。
locals:对象类型。渲染模板时传进去的本地变量。
callback:回调函数。如果声明了的话,当渲染工作完成时被调用,参数为两个,分别是错误(如果出错的话)、渲染好的字符串。在这种情况下,response不会自动完成。当错误发生时,内部会自动调用 next(err)
view参数说明:
可以是相对路径(相对于views
设置的目录),或者绝对路径;
如果没有声明文件后缀,则以view engine
设置为准;
如果声明了文件后缀,那么Express会根据文件后缀,通过 require() 加载对应的模板引擎来完成渲染工作(通过模板引擎的 __express 方法完成渲染)。
locals参数说明:
locals.cache 启动模板缓存。在生产环境中,模板缓存是默认启用的。在开发环境,可以通过将 locals.cache 设置为true来启用模板缓存。
例子:
// send the rendered view to the clientres.render('index'); // if a callback is specified, the rendered HTML string has to be sent explicitlyres.render('index', function(err, html) { res.send(html); }); // pass a local variable to the viewres.render('user', { name: 'Tobi' }, function(err, html) { // ... });
The local variable cache enables view caching. Set it to true, to cache the view during development; view caching is enabled in production by default. render(view, opt, callback) 这个方法调用时,Express会根据 view 的值 ,进行如下操作
确定模板的路径
根据模板的扩展性确定采用哪个渲染引擎
加载渲染引擎
重复调用render()方法,如果 cache === false
那么上面的步骤每次都会重新做一遍;如果 cache === true
,那么上面的步骤会跳过;
关键源代码:
if (renderOptions.cache) { view = cache[name]; }
此外,在 view.render(options, callback)
里,options
也会作为参数传入this.engine(this.path, options, callback)
。也就是说,渲染引擎(比如jade
)也会读取到options.cache
这个配置。根据options.cache
的值,渲染引擎内部也可能会进行缓存操作。(比如为true时,jade
读取模板后会缓存起来,如果为false,每次都会重新从文件系统读取)
View.prototype.render = function render(options, callback) { debug('render "%s"', this.path); this.engine(this.path, options, callback); };
备注:cache
配置对渲染引擎的影响是不确定的,因此实际需要用到某个渲染引擎时,需确保对渲染引擎足够了解。
以jade为例,在开发阶段,NODE_ENV !== 'production'
,cahce默认是false。因此每次都会从文件系统读取模板,再进行渲染。因此,在开发阶段,可以动态修改模板内容来查看效果。
当 NODE_ENV === 'production'
,cache 默认是true,此时会缓存模板,提升性能。
根据对源码的分析,实现很简单。只要带上文件扩展名,Express就会根据扩展名加载相应的模板引擎。比如:
index.jade
:加载引擎jade
index.ejs
:加载引擎ejss
// 混合使用多种模板引擎 var express = require('express'); var app = express(); app.get('/index.jade', function (req, res, next) { res.render('index.jade', {title: 'jade'}); }); app.get('/index.ejs', function (req, res, next) { res.render('index.ejs', {title: 'ejs'}); });app.listen(3000);
比如模板引擎是jade
,但是因为一些原因,扩展名需要采用.tpl
// 同样的模板引擎,不同的扩展名 var express = require('express'); var app = express();// 模板采用 tpl 扩展名 app.set('view engine', 'tpl');// 对于以 tpl 扩展名结尾的模板,采用 jade 引擎 app.engine('tpl', require('jade').__express); app.get('/index', function (req, res, next) { res.render('index', {title: 'tpl'}); }); app.listen(3000);
Using template engines with Expresshttp://expressjs.com/en/guide/using-template-engines.html
res.render 方法使用说明http://expressjs.com/en/4x/api.html#res.render