Express源码系列之模板引擎

1,110 阅读5分钟

本系列目录

1. 全局 点我去看

2. 路由匹配 点我去看

3. 错误处理 点我去看

4. 视图模板 (本文)

5. 设计思想 点我去看


1.关于视图模板引擎

​ 初始听到引擎这个词,感觉又是什么高大上的厉害之物,其实简单来说就是负责将数据注入到静态模板中的工具而已。

模板引擎

(图片来源网络,如侵权联系,马上删除)

​ javascript的模板引擎主要战场是浏览器和node端,两端其实差不多,常用的就是jade,ejs,art-template,

而art-template号称无敌超级霹雳旋风快,有空研究下他们具体实现的异同,现在先不管。

art-template

2. Express与视图模板引擎

​ 差点忘了本文是Express源码系列,还是回到Express,模板引擎已经是存在很久了,一下说不完。

2.1 准备阶段

​ 关于视图模板的初始设置大部分是放在app.settings对象中,相关属性有

  • view 管理视图模板的类,一个实例对应一个试模板文件

  • views 视图模板文件存放的路径

  • view engine 默认的视图模板引擎名称

    还有一个关于视图模板的引擎类型的属性,单独放在app.engines对象中

2.2 视图模板的使用

​ 首先设置view,view engine设置项

app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');

​ 然后调用res.render('模板文件名')直接进行渲染就好,比如渲染并返回error页面

// error handler
app.use(function (err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};
  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

另外还有一个不常用的接口也暴露在app对象上

app.engine = function engine(ext, fn) {
  if (typeof fn !== 'function') {
    throw new Error('callback function required');
  }

  // get file extension
  var extension = ext[0] !== '.' ?
    '.' + ext :
    ext;

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

  return this;
};

​ 其主要作用就是设置对应拓展名的视图模板文件怎么渲染,在文末会稍微写一个简单的视图模板引擎,并利用这个接口来定义渲染操作。

2.3 详细过程

​ 视图模板作为静态html文件的主要生成工具,在Express上唯一暴露的使用接口就是res.render,所以我们就从这个函数看起

res.render = function render(view, options, callback) { //codes... }

​ 可以看到,他的参数有三个,其中view参数是必选的,是要渲染的模板文件的名字,剩下的两个是可选的,内部会根据参数数量进行包装,其中view还是view,opts将传进的options和response对象的locals属性进行了合并,done参数是传入的,可选的,接收模板最后输出的字符串的函数,如果不传入就会简单的包装成如下

 done = done || function (err, str) {
    if (err) return req.next(err);
    self.send(str);
 };

​ 最后会调用app.render并传入包装好的新的三个参数

// render
app.render(view, opts, done);

​ 在app.render中会再次对opts选项进行合并,将一些全局的属性合并进去,最后生成一个renderOptions对象,该对象表示一次模板渲染的所有选项。同时在这里,还会生成模板文件管理实例,也就是前面说过的View的一个实例,这个实例管理这个名字的模板文件和对应解析引擎之间的映射关系,如果这个管理实例有被缓存,就会直接在缓存中取。最后将view实例,renderOptions对象,和包装的done函数传给,tryRender函数

// render
tryRender(view, renderOptions, done);

而tryRender函数只是进行错误捕获,内部直接调用view,render,并传入renderOptions和done

function tryRender(view, options, callback) {
  try {
    view.render(options, callback);
  } catch (err) {
    callback(err);
  }
}

​ 刚才说过,管理实例表示的是对应的模板文件由什么模板引擎来解析渲染,自然在本身上要定义一个render方法,来实现视图模板文件渲染的过程

View.prototype.render = function render(options, callback) {
  debug('render "%s"', this.path);
  this.engine(this.path, options, callback);
};

​ 在以上渲染逻辑中,出现了engine方法,这个方法是每个view实例一个的,主要作用就是定义实际具体的渲染过程,表示为一个函数,这个函数会对传入的path拿到的文件进行渲染,另外这个函数定义在构造函数中

//获取视图模板文件的拓展名
this.ext = extname(name);
if (!this.ext) {
    // get extension from default engine name
    this.ext = this.defaultEngine[0] !== '.'
      ? '.' + this.defaultEngine
      : this.defaultEngine
    fileName += this.ext;
  }
//如果不存在对应拓展名的渲染引擎,就require进来
  if (!opts.engines[this.ext]) {
    // load engine
    var mod = this.ext.substr(1)
    debug('require "%s"', mod)
    // default engine export
    var fn = require(mod).__express
    if (typeof fn !== 'function') {
      throw new Error('Module "' + mod + '" does not provide a view engine.')
    }
    opts.engines[this.ext] = fn
  }
  // store loaded engine
  this.engine = opts.engines[this.ext];

​ 解析渲染的具体过程就是,首先获得要解析渲染的目标文件的拓展名,根据拓展名找到对应的解析渲染引擎,如果找不到就require进来,这就要求,模板引擎实现了__express接口,调用该接口可以将读取模板文件并解析成字符串html,再作为参数传递到engine函数里,最后由callback方法,统一发向浏览器。

​ 整个详细过程可以整理成以下的流程图:

详细流程

3. 自定义简易视图模板引擎并适配Express

​ 因为是简易的写一下,就不写成npm包了,直接使用app.engine接口与Express连接了

3.1 新建一个专属拓展名为vin的模板文件,写入简单字符

// hello.vin
<html>
<head>
    <title>$title$</title>
</head>
input content is $content$;
</html>

3.2 定义转换函数,即引擎核心功能

const fs = require('fs');
const regexp = /(?<=¥).*?(?=¥)/g;
module.exports = function VinParser(path, options, callback) {
    var res;
    try {
        str = fs.readFileSync(path).toString();
        const matchedKeys = str.match(regexp);
        for (let key of matchedKeys) {
            console.log(key, options[key]);
            str = str.replace(`¥${key}¥`, options[key]);
        }
        res = str;
    } catch (err) {
        callback(err);
    }
    callback(null, res);
}

(别骂了别骂了,我正则真的不会,将就着看好了)

3.3 调用app.engine接口定义拓展名为.vin的文件的解析引擎为上面的函数

const VinParser = require('./VinParser.js');
app.engine('vin', VinParser);

3.4 可以使用了🐋

/* GET home page. */
router.get('/', function (req, res, next) {
  res.render('hello', {
    content: 'HelloWorld',
    title: 'vin template'
  });
});

效果如下

效果

本篇没有总结

完🦧