Express:模板引擎深入研究

深入源碼

首先,看下express模板默認配置。

  • view:模板引擎模塊,對應 require(‘./view’),結合 res.render(name) 更好了解些。下面會看下 view 模塊。
  • views:模板路徑,默認在 views 目錄下。
// default configuration
this.set('view', View);
this.set('views', resolve('views'));

騰訊IVWEB前端團隊招前端工程師,2年以上工作經驗,本科以上學歷,有意者可私信、留言,或者郵箱聯繫 2377488447@qq.com,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);
};

源碼開頭有 cacheengines 兩個屬性,它們在 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');

有兩個關於模版引擎的配置:

  1. views:模版文件放在哪裡,默認是在項目根目錄下。舉個例子:app.set('views', './views')
  2. view engine:使用什麼模版引擎,舉例:app.set('view engine', 'jade')

可以看到,默認是用jade做模版的。如果不想用jade怎麼辦呢?下面會提供一些模板引擎選擇的思路。

選擇標準

需要考慮兩點:實際業務需求、個人偏好。

首先考慮業務需求,需要支持以下幾點特性。

  • 支持模版繼承(extend)
  • 支持模版擴展(block)
  • 支持模版組合(include)
  • 支持預編譯

對比了下,jadenunjunks都滿足要求。個人更習慣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 的值 ,進行如下操作

  1. 確定模板的路徑
  2. 根據模板的擴展性確定採用哪個渲染引擎
  3. 加載渲染引擎

重複調用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就會根據擴展名加載相應的模板引擎。比如:

  1. index.jade:加載引擎jade
  2. 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年以上工作經驗,本科以上學歷,有意者可私信、留言,或者郵箱聯繫 2377488447@qq.com,JD可參考這裏

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師"嚨底家"!!

您可能也會喜歡…