Express:模板引擎深入研究
深入原始碼
首先,看下express模板預設配置。
- view:模板引擎模組,對應 require('./view'),結合 res.render(name) 更好了解些。下面會看下 view 模組。
- views:模板路徑,預設在 views 目錄下。
// default configuration
this.set('view', View);
this.set('views', resolve('views'));
騰訊IVWEB前端團隊招前端工程師,2年以上工作經驗,本科以上學歷,有意者可私信、留言,或者郵箱聯絡 [email protected],JD可參考這裡
從例項出發
從官方腳手架生成的程式碼出發,模板配置如下:
- views:模板檔案在 views 目錄下;
- view engine:用jade這個模板引擎進行模板渲染;
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
假設此時有如下程式碼呼叫,內部邏輯是如何實現的?
res.render('index');
res.render(view)
完整的 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);
};
app.render(view)
完整原始碼如下:
/**
* 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模組原始碼
看下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
- nunjunks
配置說明
先看程式碼。
// view engine setup
app.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
的風格,於是敲定。那麼,怎麼樣使用呢?
支援nunjucks
首先,安裝依賴
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');
res.render(view [, locals] [, callback])
引數說明:
- 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 client
res.render('index');
// if a callback is specified, the rendered HTML string has to be sent explicitly
res.render('index', function(err, html) {
res.send(html);
});
// pass a local variable to the view
res.render('user', { name: 'Tobi' }, function(err, html) {
// ...
});
關於view cache
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 Express
http://expressjs.com/en/guide/using-template-engines.html
res.render 方法使用說明
http://expressjs.com/en/4x/api.html#res.render
騰訊IVWEB前端團隊招前端工程師,2年以上工作經驗,本科以上學歷,有意者可私信、留言,或者郵箱聯絡 [email protected],JD可參考這裡