js 模板引擎

实例

挑选了三个模板引擎,语法几乎一样

1. ejs结合express
//ejshtml.html
<html>  
    <header></header>
    <body>
        <p> <%= message %></p>
    </body>
</html>

// ejshtml.js
var express = require('express');  
var ejs = require('ejs');  
var app = express();

app.engine('html', ejs.__express);  
app.set('view engine', 'html');  
app.set('views', __dirname)

app.use('/ejshtml', function(req, res) {  
  res.render('ejshtml.html', {message : 'hello'})
})
app.listen(3000)  
2. atrtemplate结合express
// arttemplate.html
<html>  
    <header></header>
    <body>
        <p> <%= message %></p>
    </body>
</html>

//artemplate.js
var express = require('./lib/express');  
var app = express();

var template = require('art-template');  
template.config('base', '');  
template.config('extname', '.html');  
app.engine('.html', template.__express);  
app.set('view engine', 'html');

app.use('/arttemplate', function(req, res) {  
  res.render('arttemplate.html', {message : 'hello'})
})

app.listen(3000)  
3. underscore.js
// underscore.js
'use strict';

var underscore = require('underscore');  
var compiled = underscore.template("hello <%= name %>");  
console.log(compiled({name:'mike'}))  
分析

可以注意到一件事,本质上模板引擎其实跟html关系不大,只是对字符串的分析

不过据说pug(前身jade)内含大量针对html优化的部分

重点来看一下ejs结合express的渲染流程
ejs 拥有以下标签

<% %>流程控制标签  
<%= %>输出内容标签(原文输出HTML标签)  
<%- %>输出标签(HTML会被浏览器解析)  
<%# %>注释标签  
% 对标记进行转义
<%- include(path) %> 引入 path 代表你引入其他模板的路径  

参考上述的例子看一下<%= %>标签
整个ejshtml.html被处理,实际上是被有限自动机切割成几个token

模板引擎.png

如果对html部分做解析,那就需要更小的token颗粒

代码分析

express 4.15.5版本做代码分析

  if (!opts.engines[this.ext]) {
    // load engine
    var mod = this.ext.substr(1)
    debug('require "%s"', mod)
    opts.engines[this.ext] = require(mod).__express
  }

  // store loaded engine
  this.engine = opts.engines[this.ext];


这里面的__express就是外面ejshtml.js中利用app.use传入的

__express对传入的数据处理代码调用栈如下

code-flow.png

最终核心逻辑在Template.compile
其中509行查看

注意ejs有两个项目,一个是tj的,一个是mde的;现在项目是由mde维护的

this.source 和 src 变量

this.source : ""

    ||
    ||  this.generateSource
    \/

this.source: 

"    ; __append("<html>\r\n    <header></header>\r\n    <body>\r\n        <p> ")
    ; __line = 4
    ; __append(escapeFn( message ))
    ; __append("</p>\r\n    </body>\r\n</html>")
    ; __line = 6
"

   ||
   ||  ejs.js line 510 ~ 519 
   \/

this.source:

"  var __output = [], __append = __output.push.bind(__output);
  with (locals || {}) {
    ; __append("<html>\r\n    <header></header>\r\n    <body>\r\n        <p> ")
    ; __line = 4
    ; __append(escapeFn( message ))
    ; __append("</p>\r\n    </body>\r\n</html>")
    ; __line = 6
  }
  return __output.join("");
"

   || 
   || compileDebug == true 
   \/

local src :   

"var __line = 1
  , __lines = "<html>\r\n    <header></header>\r\n    <body>\r\n        <p> <%= message %></p>\r\n    </body>\r\n</html>"
  , __filename = "c:\\Users\\chainhelen\\Desktop\\what you want\\express\\ejshtml.html";
try {  
  var __output = [], __append = __output.push.bind(__output);
  with (locals || {}) {
    ; __append("<html>\r\n    <header></header>\r\n    <body>\r\n        <p> ")
    ; __line = 4
    ; __append(escapeFn( message ))
    ; __append("</p>\r\n    </body>\r\n</html>")
    ; __line = 6
  }
  return __output.join("");
} catch (e) {
  rethrow(e, __lines, __filename, __line, escapeFn);
}
"

 ||
 ||  skip opts.client code
 \/

 ||
 ||  fn = new Function(opts.localsName + ', escapeFn, include, rethrow', src);
 \/

fn: 

"function anonymous(locals, escapeFn, include, rethrow
/*``*/) {
    var __line = 1
      , __lines = "<html>\r\n    <header></header>\r\n    <body>\r\n        <p> <%= message %></p>\r\n    </body>\r\n</html>"
      , __filename = "c:\\Users\\chainhelen\\Desktop\\what you want\\express\\ejshtml.html";
    try {
      var __output = [], __append = __output.push.bind(__output);
      with (locals || {}) {
        ; __append("<html>\r\n    <header></header>\r\n    <body>\r\n        <p> ")
        ; __line = 4
        ; __append(escapeFn( message ))
        ; __append("</p>\r\n    </body>\r\n</html>")
        ; __line = 6
      }
      return __output.join("");
    } catch (e) {
      rethrow(e, __lines, __filename, __line, escapeFn);
    }
}"  

可以看见,实际上原始数据{message: hello}是通过Fn执行,变量locals带入的
escapeFn 是将数据进行html转码

整体逻辑需要理清楚的两个步骤
this.generateSource

genrateSource

在ejs.js第597行

// Slurp spaces and tabs before <%_ and after _%>
this.templateText =  
    this.templateText.replace(/[ \t]*<%_/gm, '<%_').replace(/_%>[ \t]*/gm, '_%>');

先尝试把类特定token的部分前后空白去除掉

在ejs.js第598行

var matches = this.parseTemplateText();  

主要看就看parseTemplateText()函数

  parseTemplateText: function () {
    var str = this.templateText;
    var pat = this.regex;
    var result = pat.exec(str);
    var arr = [];
    var firstPos;

    while (result) {
      firstPos = result.index;

      if (firstPos !== 0) {
        arr.push(str.substring(0, firstPos));
        str = str.slice(firstPos);
      }

      arr.push(result[0]);
      str = str.slice(result[0].length);
      result = pat.exec(str);
    }

    if (str) {
      arr.push(str);
    }

    return arr;
  },

这个函数会把原始数据进行拆分成上图一效果,数组arr都是token字符串

这里有个可能可以优化的点 => 正则 参考vary 提速

紧接着ejs605行进入不断循环状态机,对于不同状态的不同处理在ejs652行,函数scanLine

对于model来说,状态如下

Template.modes = {  
  EVAL: 'eval',
  ESCAPED: 'escaped',
  RAW: 'raw',
  COMMENT: 'comment',
  LITERAL: 'literal'
};
其实还有null这一其实状态  


比方说,扫描到了<%,那么就进入Template.modes.ESCAPED状态,随后的匹配token分为两种

  1. token不是结束符%>、_%>、-%>等,那么就按ESCAPED状态处理,即ejs763行
  2. token是结束符,那么成为起始状态null,即ejs753行