• Express:模板引擎深入研究


     

    深入源码

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

    • view:模板引擎模块,对应 require('./view'),结合 res.render(name) 更好了解些。下面会看下 view 模块。
    • views:模板路径,默认在 views 目录下。
    // default configuration
    this.set('view', View);
    this.set('views', resolve('views'));

    从实例出发

    从官方脚手架生成的代码出发,模板配置如下:

    • 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

  • 相关阅读:
    [译]C++如何切分字符串
    Reverse Words in a String
    excel 获取中文拼音首字母
    jquery 获取css position的值
    excel 截取单元格部分内容(从指定位置截取)
    excel 根据单元格内容自动调整列宽
    excel 如何为列添加指定内容(字符串)
    android_handler(二)
    &quot;Simple Factory&quot; vs &quot;Factory Method&quot; vs &quot;Abstract Factory&quot; vs &quot;Reflect&quot;
    HDU 5074 Hatsune Miku 2014 Asia AnShan Regional Contest dp(水
  • 原文地址:https://www.cnblogs.com/onesea/p/13468076.html
Copyright © 2020-2023  润新知